From 12b4a24b9731df67fdaec34de8708d71336fd73c Mon Sep 17 00:00:00 2001 From: Jigar Vaghela Date: Tue, 2 Jun 2020 14:12:48 +0000 Subject: [PATCH] [FIX] stock_account/sale_stock: correctly valuate the product in below use case Below is the case of product with automated valuation: - You bought 8 units @ 10$. - Then you deliver 10 units of the product. - You assumed the missing 2 should go out at a value of 10$ but you are not sure yet as it hasn't been bought in Odoo yet. - Afterwards, you buy missing 2 units of the same product at 12$. In this case the product has been undervalued when delivered without stock, the vacuum entry is the following one (this entry already takes place): Account | Debit | Credit =================================================== Stock Valuation | 0.00 | 4.00 Stock Interim (Delivered) | 4.00 | 0.00 So, on delivering product with different price, We should create additional journal items like: Account | Debit | Credit =================================================== Stock Interim (Delivered) | 0.00 | 4.00 Expenses Revaluation | 4.00 | 0.00 closes odoo/odoo#52396 Task: 2224025 Closes: #52396 Signed-off-by: Simon Lejeune (sle) --- .../tests/test_anglo_saxon_valuation.py | 62 +++++++++++++++++++ addons/stock_account/models/product.py | 56 +++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/addons/sale_stock/tests/test_anglo_saxon_valuation.py b/addons/sale_stock/tests/test_anglo_saxon_valuation.py index 3b9625d5492..fa2b578c669 100644 --- a/addons/sale_stock/tests/test_anglo_saxon_valuation.py +++ b/addons/sale_stock/tests/test_anglo_saxon_valuation.py @@ -1009,3 +1009,65 @@ class TestAngloSaxonValuation(SavepointCase): cogs_aml = amls.filtered(lambda aml: aml.account_id == self.expense_account) self.assertEqual(cogs_aml.debit, 56) self.assertEqual(cogs_aml.credit, 0) + + def test_fifo_delivered_invoice_post_delivery_4(self): + """Receive 8@10. Sale order 10@12. Deliver and also invoice it without receiving the 2 missing. + Now, receive 2@12. Make sure price difference is correctly reflected in expense account.""" + self.product.categ_id.property_cost_method = 'fifo' + self.product.invoice_policy = 'delivery' + self.product.standard_price = 10 + + in_move_1 = self.env['stock.move'].create({ + 'name': 'a', + 'product_id': self.product.id, + 'location_id': self.env.ref('stock.stock_location_suppliers').id, + 'location_dest_id': self.stock_location.id, + 'product_uom': self.product.uom_id.id, + 'product_uom_qty': 8, + 'price_unit': 10, + }) + in_move_1._action_confirm() + in_move_1.quantity_done = 8 + in_move_1._action_done() + + # Create and confirm a sale order for 10@12 + sale_order = self.env['sale.order'].create({ + 'partner_id': self.customer.id, + 'order_line': [ + (0, 0, { + 'name': self.product.name, + 'product_id': self.product.id, + 'product_uom_qty': 10.0, + 'product_uom': self.product.uom_id.id, + 'price_unit': 12, + 'tax_id': False, # no love taxes amls + })], + }) + sale_order.action_confirm() + + # Deliver 10 + sale_order.picking_ids.move_lines.quantity_done = 10 + sale_order.picking_ids.button_validate() + + # Invoice the sale order. + invoice = sale_order.with_context(default_journal_id=self.journal_sale.id)._create_invoices() + invoice.post() + + # Make the second receipt + in_move_2 = self.env['stock.move'].create({ + 'name': 'a', + 'product_id': self.product.id, + 'location_id': self.env.ref('stock.stock_location_suppliers').id, + 'location_dest_id': self.stock_location.id, + 'product_uom': self.product.uom_id.id, + 'product_uom_qty': 2, + 'price_unit': 12, + }) + in_move_2._action_confirm() + in_move_2.quantity_done = 2 + in_move_2._action_done() + + # check the last anglo saxon move line + revalued_anglo_expense_amls = sale_order.picking_ids.mapped('move_lines.stock_valuation_layer_ids')[-1].stock_move_id.account_move_ids[-1].mapped('line_ids') + revalued_cogs_aml = revalued_anglo_expense_amls.filtered(lambda aml: aml.account_id == self.expense_account) + self.assertEqual(revalued_cogs_aml.debit, 4, 'Price difference should have correctly reflected in expense account.') diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py index fa954ec8f50..d8d8b113221 100644 --- a/addons/stock_account/models/product.py +++ b/addons/stock_account/models/product.py @@ -437,6 +437,62 @@ class ProductProduct(models.Model): vacuum_svl.stock_move_id._account_entry_move( vacuum_svl.quantity, vacuum_svl.description, vacuum_svl.id, vacuum_svl.value ) + # Create the related expense entry + self._create_fifo_vacuum_anglo_saxon_expense_entry(vacuum_svl, svl_to_vacuum) + + def _create_fifo_vacuum_anglo_saxon_expense_entry(self, vacuum_svl, svl_to_vacuum): + """ When product is delivered and invoiced while you don't have units in stock anymore, there are chances of that + product getting undervalued/overvalued. So, we should nevertheless take into account the fact that the product has + already been delivered and invoiced to the customer by posting the value difference in the expense account also. + Consider the below case where product is getting undervalued: + + You bought 8 units @ 10$ -> You have a stock valuation of 8 units, unit cost 10. + Then you deliver 10 units of the product. + You assumed the missing 2 should go out at a value of 10$ but you are not sure yet as it hasn't been bought in Odoo yet. + Afterwards, you buy missing 2 units of the same product at 12$ instead of expected 10$. + In case the product has been undervalued when delivered without stock, the vacuum entry is the following one (this entry already takes place): + + Account | Debit | Credit + =================================================== + Stock Valuation | 0.00 | 4.00 + Stock Interim (Delivered) | 4.00 | 0.00 + + So, on delivering product with different price, We should create additional journal items like: + Account | Debit | Credit + =================================================== + Stock Interim (Delivered) | 0.00 | 4.00 + Expenses Revaluation | 4.00 | 0.00 + """ + if not vacuum_svl.company_id.anglo_saxon_accounting or not svl_to_vacuum.stock_move_id._is_out(): + return False + AccountMove = self.env['account.move'].sudo() + account_move_lines = svl_to_vacuum.account_move_id.line_ids + # Find related customer invoice where product is delivered while you don't have units in stock anymore + reconciled_line_ids = list(set(account_move_lines._reconciled_lines()) - set(account_move_lines.ids)) + account_move = AccountMove.search([('line_ids','in', reconciled_line_ids)], limit=1) + # If delivered quantity is not invoiced then no need to create this entry + if not account_move: + return False + accounts = svl_to_vacuum.product_id.product_tmpl_id.get_product_accounts(fiscal_pos=account_move.fiscal_position_id) + if not accounts.get('stock_output') or not accounts.get('expense'): + return False + description = "Expenses %s" % (vacuum_svl.description) + move_lines = vacuum_svl.stock_move_id._prepare_account_move_line( + vacuum_svl.quantity, vacuum_svl.value * -1, + accounts['stock_output'].id, accounts['expense'].id, + description) + new_account_move = AccountMove.sudo().create({ + 'journal_id': accounts['stock_journal'].id, + 'line_ids': move_lines, + 'date': self._context.get('force_period_date', fields.Date.context_today(self)), + 'ref': description, + 'stock_move_id': vacuum_svl.stock_move_id.id, + 'move_type': 'entry', + }) + new_account_move.post() + to_reconcile_account_move_lines = vacuum_svl.account_move_id.line_ids.filtered(lambda l: not l.reconciled and l.account_id == accounts['stock_output'] and l.account_id.reconcile) + to_reconcile_account_move_lines += new_account_move.line_ids.filtered(lambda l: not l.reconciled and l.account_id == accounts['stock_output'] and l.account_id.reconcile) + return to_reconcile_account_move_lines.reconcile() @api.model def _svl_empty_stock(self, description, product_category=None, product_template=None):