From 3bcfac7d99580c9767f023f1644b00bfa71d6cda Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Wed, 27 Apr 2016 16:18:08 +0200 Subject: [PATCH] [FIX] sale_mrp, sale_stock: _get_anglo_saxon_price_unit with a kit When a kit is set with a valuation = "real_time" and invoice_policy = "delivery", the price unit of the kit computed for the Expense Account in the the journal of entries must be the sum of the average price of each component of this kit. Used case: Create a kit001 with: *General information: -Product Type=Stockable Product -Invoicing Policy=Delivered Quantities -Sale Price=100$ -Cost Price=80$ *Inventory: -Route=Manufacture -Internal Category=All/Sealable *its product category set with -Expense Account=210000 Cost of Goods Sold -Costing Method=Standard Price -Inventory Valuation=Perpetual(automated) *a bom of two components child001 and child002 Create child001 with: *General information: -Product Type=Stockable Product -Invoicing Policy=Delivered Quantities -Sale Price=100$ -Cost Price=70$ *Inventory: -Route=Buy -Internal Category=All *its product category set with -Expense Account=210000 Cost of Goods Sold -Costing Method=Real Price -Inventory Valuation=Perpetual(automated) Create child002 with: *General information: -Product Type=Stockable Product -Invoicing Policy=Delivered Quantities -Sale Price=100$ -Cost Price=10$ *Inventory: -Route=Buy -Internal Category=All *its product category set with -Expense Account=210000 Cost of Goods Sold -Costing Method=Real Price -Inventory Valuation=Perpetual(automated) Create a SO with: -one line with the kit001 Confirm the SO Validate the Delivery Create and view the invoices -per invoicable lines(deduct down payments) Validate the invoice Result: Before the fix: in the entry created for this invoice, the debit for the kit001 in Cost of Goods Sold was equal to 70 instead of 80. After the fix: the debit for the kit001 in Cost of Goods Sold was equal to 80(the sum of the average price of each component of the kit001). opw:674608 --- addons/sale_mrp/sale_mrp.py | 62 +++++++++++++++++++++++++++++++++ addons/sale_stock/sale_stock.py | 38 +++++++++++--------- 2 files changed, 83 insertions(+), 17 deletions(-) diff --git a/addons/sale_mrp/sale_mrp.py b/addons/sale_mrp/sale_mrp.py index 35bb2c539a8..8dc89814a3c 100644 --- a/addons/sale_mrp/sale_mrp.py +++ b/addons/sale_mrp/sale_mrp.py @@ -60,6 +60,31 @@ class SaleOrderLine(models.Model): return 0.0 return super(SaleOrderLine, self)._get_delivered_qty() + @api.multi + def _get_bom_component_qty(self, bom): + product_uom_qty_bom = self.env['product.uom']._compute_qty_obj(self.product_uom, self.product_uom_qty, bom.product_uom) + bom_exploded = self.env['mrp.bom']._bom_explode(bom, self.product_id, product_uom_qty_bom)[0] + components = {} + for bom_line in bom_exploded: + product = bom_line['product_id'] + uom = bom_line['product_uom'] + qty = bom_line['product_qty'] + if components.get(product, False): + if uom != components[product]['uom']: + from_uom_id = uom + to_uom_id = components[product]['uom'] + qty = self.env['product.uom']._compute_qty(from_uom_id, qty, to_uom_id=to_uom_id) + components[product]['qty'] += qty + else: + # To be in the uom reference of the product + to_uom = self.env['product.product'].browse([product])[0].uom_id.id + if uom != to_uom: + qty = self.env['product.uom']._compute_qty(uom, qty, to_uom_id=to_uom) + components[product] = {'qty': qty, 'uom': to_uom} + return components + + + @api.multi def _prepare_order_line_procurement(self, group_id=False): vals = super(SaleOrderLine, self)._prepare_order_line_procurement(group_id=group_id) @@ -85,3 +110,40 @@ class StockMove(models.Model): """ property_ids = move.procurement_id.sale_line_id.property_ids.ids return super(StockMove, self.with_context(property_ids=property_ids))._action_explode(move) + +class AccountInvoiceLine(models.Model): + _inherit = "account.invoice.line" + + def _get_anglo_saxon_price_unit(self): + price_unit = super(AccountInvoiceLine, self)._get_anglo_saxon_price_unit() + # in case of anglo saxon with a product configured as invoiced based on delivery, with perpetual + # valuation and real price costing method, we must find the real price for the cost of good sold + uom_obj = self.env['product.uom'] + if self.product_id.invoice_policy == "delivery": + for s_line in self.sale_line_ids: + # qtys already invoiced + qty_done = sum([uom_obj._compute_qty_obj(x.uom_id, x.quantity, x.product_id.uom_id) for x in s_line.invoice_lines if x.invoice_id.state in ('open', 'paid')]) + quantity = uom_obj._compute_qty_obj(self.uom_id, self.quantity, self.product_id.uom_id) + # Put moves in fixed order by date executed + moves = self.env['stock.move'] + for procurement in s_line.procurement_ids: + moves |= procurement.move_ids + moves.sorted(lambda x: x.date) + # Go through all the moves and do nothing until you get to qty_done + # Beyond qty_done we need to calculate the average of the price_unit + # on the moves we encounter. + bom = s_line.product_id.product_tmpl_id.bom_ids and s_line.product_id.product_tmpl_id.bom_ids[0] + if bom.type == 'phantom': + average_price_unit = 0 + components = s_line._get_bom_component_qty(bom) + for product_id in components.keys(): + factor = components[product_id]['qty'] + prod_moves = [m for m in moves if m.product_id.id == product_id] + prod_qty_done = factor * qty_done + prod_quantity = factor * quantity + average_price_unit += factor * self._compute_average_price(prod_qty_done, prod_quantity, prod_moves) + price_unit = average_price_unit or price_unit + price_unit = uom_obj._compute_qty_obj(self.uom_id, price_unit, self.product_id.uom_id, round=False) + return price_unit + else: + return price_unit diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py index f91e1fc40cd..5c5e789f63a 100644 --- a/addons/sale_stock/sale_stock.py +++ b/addons/sale_stock/sale_stock.py @@ -327,27 +327,31 @@ class AccountInvoiceLine(models.Model): # Go through all the moves and do nothing until you get to qty_done # Beyond qty_done we need to calculate the average of the price_unit # on the moves we encounter. - average_price_unit = 0 - qty_delivered = 0 - invoiced_qty = 0 - for move in moves: - if move.state != 'done': - continue - invoiced_qty += move.product_qty - if invoiced_qty <= qty_done: - continue - qty_to_consider = move.product_qty - if invoiced_qty - move.product_qty < qty_done: - qty_to_consider = invoiced_qty - qty_done - qty_to_consider = min(qty_to_consider, quantity - qty_delivered) - qty_delivered += qty_to_consider - average_price_unit = (average_price_unit * (qty_delivered - qty_to_consider) + move.price_unit * qty_to_consider) / qty_delivered - if qty_delivered == quantity: - break + average_price_unit = self._compute_average_price(qty_done, quantity, moves) price_unit = average_price_unit or price_unit price_unit = uom_obj._compute_qty_obj(self.uom_id, price_unit, self.product_id.uom_id, round=False) return price_unit + def _compute_average_price(self, qty_done, quantity, moves): + average_price_unit = 0 + qty_delivered = 0 + invoiced_qty = 0 + for move in moves: + if move.state != 'done': + continue + invoiced_qty += move.product_qty + if invoiced_qty <= qty_done: + continue + qty_to_consider = move.product_qty + if invoiced_qty - move.product_qty < qty_done: + qty_to_consider = invoiced_qty - qty_done + qty_to_consider = min(qty_to_consider, quantity - qty_delivered) + qty_delivered += qty_to_consider + average_price_unit = (average_price_unit * (qty_delivered - qty_to_consider) + move.price_unit * qty_to_consider) / qty_delivered + if qty_delivered == quantity: + break + return average_price_unit + class ProductProduct(models.Model): _inherit = 'product.product'