purchase_report.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. #
  4. # Please note that these reports are not multi-currency !!!
  5. #
  6. import re
  7. from odoo import api, fields, models, _
  8. from odoo.exceptions import UserError
  9. from odoo.osv.expression import AND, expression
  10. class PurchaseReport(models.Model):
  11. _name = "purchase.report"
  12. _description = "Purchase Report"
  13. _auto = False
  14. _order = 'date_order desc, price_total desc'
  15. date_order = fields.Datetime('Order Date', readonly=True)
  16. state = fields.Selection([
  17. ('draft', 'Draft RFQ'),
  18. ('sent', 'RFQ Sent'),
  19. ('to approve', 'To Approve'),
  20. ('purchase', 'Purchase Order'),
  21. ('done', 'Done'),
  22. ('cancel', 'Cancelled')
  23. ], 'Status', readonly=True)
  24. product_id = fields.Many2one('product.product', 'Product', readonly=True)
  25. partner_id = fields.Many2one('res.partner', 'Vendor', readonly=True)
  26. date_approve = fields.Datetime('Confirmation Date', readonly=True)
  27. product_uom = fields.Many2one('uom.uom', 'Reference Unit of Measure', required=True)
  28. company_id = fields.Many2one('res.company', 'Company', readonly=True)
  29. currency_id = fields.Many2one('res.currency', 'Currency', readonly=True)
  30. user_id = fields.Many2one('res.users', 'Purchase Representative', readonly=True)
  31. delay = fields.Float('Days to Confirm', digits=(16, 2), readonly=True, group_operator='avg', help="Amount of time between purchase approval and order by date.")
  32. delay_pass = fields.Float('Days to Receive', digits=(16, 2), readonly=True, group_operator='avg',
  33. help="Amount of time between date planned and order by date for each purchase order line.")
  34. avg_days_to_purchase = fields.Float(
  35. 'Average Days to Purchase', digits=(16, 2), readonly=True, store=False, # needs store=False to prevent showing up as a 'measure' option
  36. help="Amount of time between purchase approval and document creation date. Due to a hack needed to calculate this, \
  37. every record will show the same average value, therefore only use this as an aggregated value with group_operator=avg")
  38. price_total = fields.Float('Total', readonly=True)
  39. price_average = fields.Float('Average Cost', readonly=True, group_operator="avg")
  40. nbr_lines = fields.Integer('# of Lines', readonly=True)
  41. category_id = fields.Many2one('product.category', 'Product Category', readonly=True)
  42. product_tmpl_id = fields.Many2one('product.template', 'Product Template', readonly=True)
  43. country_id = fields.Many2one('res.country', 'Partner Country', readonly=True)
  44. fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', readonly=True)
  45. commercial_partner_id = fields.Many2one('res.partner', 'Commercial Entity', readonly=True)
  46. weight = fields.Float('Gross Weight', readonly=True)
  47. volume = fields.Float('Volume', readonly=True)
  48. order_id = fields.Many2one('purchase.order', 'Order', readonly=True)
  49. untaxed_total = fields.Float('Untaxed Total', readonly=True)
  50. qty_ordered = fields.Float('Qty Ordered', readonly=True)
  51. qty_received = fields.Float('Qty Received', readonly=True)
  52. qty_billed = fields.Float('Qty Billed', readonly=True)
  53. qty_to_be_billed = fields.Float('Qty to be Billed', readonly=True)
  54. @property
  55. def _table_query(self):
  56. ''' Report needs to be dynamic to take into account multi-company selected + multi-currency rates '''
  57. return '%s %s %s %s' % (self._select(), self._from(), self._where(), self._group_by())
  58. def _select(self):
  59. select_str = """
  60. SELECT
  61. po.id as order_id,
  62. min(l.id) as id,
  63. po.date_order as date_order,
  64. po.state,
  65. po.date_approve,
  66. po.dest_address_id,
  67. po.partner_id as partner_id,
  68. po.user_id as user_id,
  69. po.company_id as company_id,
  70. po.fiscal_position_id as fiscal_position_id,
  71. l.product_id,
  72. p.product_tmpl_id,
  73. t.categ_id as category_id,
  74. c.currency_id,
  75. t.uom_id as product_uom,
  76. extract(epoch from age(po.date_approve,po.date_order))/(24*60*60)::decimal(16,2) as delay,
  77. extract(epoch from age(l.date_planned,po.date_order))/(24*60*60)::decimal(16,2) as delay_pass,
  78. count(*) as nbr_lines,
  79. sum(l.price_total / COALESCE(po.currency_rate, 1.0))::decimal(16,2) * currency_table.rate as price_total,
  80. (sum(l.product_qty * l.price_unit / COALESCE(po.currency_rate, 1.0))/NULLIF(sum(l.product_qty/line_uom.factor*product_uom.factor),0.0))::decimal(16,2) * currency_table.rate as price_average,
  81. partner.country_id as country_id,
  82. partner.commercial_partner_id as commercial_partner_id,
  83. sum(p.weight * l.product_qty/line_uom.factor*product_uom.factor) as weight,
  84. sum(p.volume * l.product_qty/line_uom.factor*product_uom.factor) as volume,
  85. sum(l.price_subtotal / COALESCE(po.currency_rate, 1.0))::decimal(16,2) * currency_table.rate as untaxed_total,
  86. sum(l.product_qty / line_uom.factor * product_uom.factor) as qty_ordered,
  87. sum(l.qty_received / line_uom.factor * product_uom.factor) as qty_received,
  88. sum(l.qty_invoiced / line_uom.factor * product_uom.factor) as qty_billed,
  89. case when t.purchase_method = 'purchase'
  90. then sum(l.product_qty / line_uom.factor * product_uom.factor) - sum(l.qty_invoiced / line_uom.factor * product_uom.factor)
  91. else sum(l.qty_received / line_uom.factor * product_uom.factor) - sum(l.qty_invoiced / line_uom.factor * product_uom.factor)
  92. end as qty_to_be_billed
  93. """
  94. return select_str
  95. def _from(self):
  96. from_str = """
  97. FROM
  98. purchase_order_line l
  99. join purchase_order po on (l.order_id=po.id)
  100. join res_partner partner on po.partner_id = partner.id
  101. left join product_product p on (l.product_id=p.id)
  102. left join product_template t on (p.product_tmpl_id=t.id)
  103. left join res_company C ON C.id = po.company_id
  104. left join uom_uom line_uom on (line_uom.id=l.product_uom)
  105. left join uom_uom product_uom on (product_uom.id=t.uom_id)
  106. left join {currency_table} ON currency_table.company_id = po.company_id
  107. """.format(
  108. currency_table=self.env['res.currency']._get_query_currency_table({'multi_company': True, 'date': {'date_to': fields.Date.today()}}),
  109. )
  110. return from_str
  111. def _where(self):
  112. return """
  113. WHERE
  114. l.display_type IS NULL
  115. """
  116. def _group_by(self):
  117. group_by_str = """
  118. GROUP BY
  119. po.company_id,
  120. po.user_id,
  121. po.partner_id,
  122. line_uom.factor,
  123. c.currency_id,
  124. l.price_unit,
  125. po.date_approve,
  126. l.date_planned,
  127. l.product_uom,
  128. po.dest_address_id,
  129. po.fiscal_position_id,
  130. l.product_id,
  131. p.product_tmpl_id,
  132. t.categ_id,
  133. po.date_order,
  134. po.state,
  135. line_uom.uom_type,
  136. line_uom.category_id,
  137. t.uom_id,
  138. t.purchase_method,
  139. line_uom.id,
  140. product_uom.factor,
  141. partner.country_id,
  142. partner.commercial_partner_id,
  143. po.id,
  144. currency_table.rate
  145. """
  146. return group_by_str
  147. @api.model
  148. def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
  149. """ This is a hack to allow us to correctly calculate the average of PO specific date values since
  150. the normal report query result will duplicate PO values across its PO lines during joins and
  151. lead to incorrect aggregation values.
  152. Only the AVG operator is supported for avg_days_to_purchase.
  153. """
  154. avg_days_to_purchase = next((field for field in fields if re.search(r'\bavg_days_to_purchase\b', field)), False)
  155. if avg_days_to_purchase:
  156. fields.remove(avg_days_to_purchase)
  157. if any(field.split(':')[1].split('(')[0] != 'avg' for field in [avg_days_to_purchase] if field):
  158. raise UserError(_("Value: 'avg_days_to_purchase' should only be used to show an average. If you are seeing this message then it is being accessed incorrectly."))
  159. if 'price_average:avg' in fields:
  160. fields.extend(['aggregated_qty_ordered:array_agg(qty_ordered)'])
  161. fields.extend(['aggregated_price_average:array_agg(price_average)'])
  162. res = []
  163. if fields:
  164. res = super(PurchaseReport, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
  165. if 'price_average:avg' in fields:
  166. qties = 'aggregated_qty_ordered'
  167. special_field = 'aggregated_price_average'
  168. for data in res:
  169. if data[special_field] and data[qties]:
  170. total_unit_cost = sum(float(value) * float(qty) for value, qty in zip(data[special_field], data[qties]) if qty and value)
  171. total_qty_ordered = sum(float(qty) for qty in data[qties] if qty)
  172. data['price_average'] = (total_unit_cost / total_qty_ordered) if total_qty_ordered else 0
  173. del data[special_field]
  174. del data[qties]
  175. if not res and avg_days_to_purchase:
  176. res = [{}]
  177. if avg_days_to_purchase:
  178. self.check_access_rights('read')
  179. query = """ SELECT AVG(days_to_purchase.po_days_to_purchase)::decimal(16,2) AS avg_days_to_purchase
  180. FROM (
  181. SELECT extract(epoch from age(po.date_approve,po.create_date))/(24*60*60) AS po_days_to_purchase
  182. FROM purchase_order po
  183. WHERE po.id IN (
  184. SELECT "purchase_report"."order_id" FROM %s WHERE %s)
  185. ) AS days_to_purchase
  186. """
  187. subdomain = AND([domain, [('company_id', '=', self.env.company.id), ('date_approve', '!=', False)]])
  188. subtables, subwhere, subparams = expression(subdomain, self).query.get_sql()
  189. self.env.cr.execute(query % (subtables, subwhere), subparams)
  190. res[0].update({
  191. '__count': 1,
  192. avg_days_to_purchase.split(':')[0]: self.env.cr.fetchall()[0][0],
  193. })
  194. return res