From f5df36d3b8385b5e426718fa86b18ad8601b3262 Mon Sep 17 00:00:00 2001 From: mariadforgeflow Date: Mon, 27 Feb 2023 15:07:54 +0100 Subject: [PATCH 1/2] [IMP] rma_purchase: add test return_and_refund_diff_price --- .../tests/test_rma_stock_account_purchase.py | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/rma_purchase/tests/test_rma_stock_account_purchase.py b/rma_purchase/tests/test_rma_stock_account_purchase.py index c74842f2d..3fed8591c 100644 --- a/rma_purchase/tests/test_rma_stock_account_purchase.py +++ b/rma_purchase/tests/test_rma_stock_account_purchase.py @@ -17,6 +17,10 @@ def setUpClass(cls): cls.rma_operation_supplier_refund = cls.env.ref( "rma_account.rma_operation_supplier_refund" ) + acc_type = cls._create_account_type("expense", "other") + cls.account_price_diff = cls._create_account( + acc_type, "Refund Price Difference Expense", "rpde", cls.company, False + ) def test_01_cost_from_po_move(self): """ @@ -142,3 +146,77 @@ def test_02_return_and_refund_ref_po(self): ) self.assertEqual(sum(grni_amls.mapped("balance")), 0.0) self.assertTrue(all(grni_amls.mapped("reconciled"))) + + def test_03_return_and_refund_diff_price(self): + """ + Purchase a product. + Then create an RMA to return it and get the refund from the supplier with + a different price than the original purchase price + """ + self.product_fifo_1.categ_id.update( + { + "property_account_creditor_price_difference_categ": self.account_price_diff + } + ) + self.product_fifo_1.standard_price = 1234 + po = self.po_model.create( + { + "partner_id": self.partner_id.id, + } + ) + pol_1 = self.pol_model.create( + { + "name": self.product_fifo_1.name, + "order_id": po.id, + "product_id": self.product_fifo_1.id, + "product_qty": 10.0, + "product_uom": self.product_fifo_1.uom_id.id, + "price_unit": 100.0, + "date_planned": Datetime.now(), + } + ) + po.button_confirm() + self._do_picking(po.picking_ids) + self.product_fifo_1.standard_price = 1234 # this should not be taken + supplier_view = self.env.ref("rma_purchase.view_rma_line_form") + rma_line = Form( + self.rma_line.with_context(supplier=1).with_user(self.rma_basic_user), + view=supplier_view.id, + ) + rma_line.partner_id = po.partner_id + rma_line.purchase_order_line_id = pol_1 + rma_line.price_unit = 4356 + rma_line.operation_id = self.rma_operation_supplier_refund + rma_line = rma_line.save() + rma_line.action_rma_to_approve() + self._deliver_rma(rma_line) + + with Form( + self.env["account.move"].with_context(default_move_type="in_refund") + ) as bill_form: + bill_form.partner_id = rma_line.partner_id + bill_form.invoice_date = Date.today() + bill_form.add_rma_line_id = rma_line + bill = bill_form.save() + bill_form = Form(bill) + with bill_form.invoice_line_ids.edit(0) as line_form: + line_form.price_unit = 110 + bill = bill_form.save() + bill.action_post() + self.assertEqual(len(bill.invoice_line_ids), 1) + self.assertEqual(bill.invoice_line_ids.rma_line_id, rma_line) + grni_amls = self.env["account.move.line"].search( + [ + ("account_id", "=", self.account_grni.id), + ("rma_line_id", "=", rma_line.id), + ] + ) + self.assertEqual(sum(grni_amls.mapped("balance")), 0.0) + self.assertTrue(all(grni_amls.mapped("reconciled"))) + price_diff_amls = self.env["account.move.line"].search( + [ + ("account_id", "=", self.account_price_diff.id), + ("rma_line_id", "=", rma_line.id), + ] + ) + self.assertEqual(sum(price_diff_amls.mapped("balance")), -100) From 933b24b15e53c2381cff903c0e278bba1d7946d0 Mon Sep 17 00:00:00 2001 From: mariadforgeflow Date: Mon, 27 Feb 2023 13:28:52 +0100 Subject: [PATCH 2/2] [FIX] rma_purchase: write-off differences in price between rma line and vendor refund --- rma_purchase/models/__init__.py | 1 + rma_purchase/models/account_move.py | 173 +++++++++++++++++- rma_purchase/models/account_move_line.py | 20 ++ .../tests/test_rma_stock_account_purchase.py | 2 +- 4 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 rma_purchase/models/account_move_line.py diff --git a/rma_purchase/models/__init__.py b/rma_purchase/models/__init__.py index 39c026827..25f674834 100644 --- a/rma_purchase/models/__init__.py +++ b/rma_purchase/models/__init__.py @@ -5,3 +5,4 @@ from . import rma_operation from . import procurement from . import account_move +from . import account_move_line diff --git a/rma_purchase/models/account_move.py b/rma_purchase/models/account_move.py index c173139fd..4f615f2a9 100644 --- a/rma_purchase/models/account_move.py +++ b/rma_purchase/models/account_move.py @@ -1,7 +1,8 @@ # Copyright 2017-22 ForgeFlow S.L. # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) -from odoo import models +from odoo import fields, models +from odoo.tools.float_utils import float_compare class AccountMove(models.Model): @@ -40,3 +41,173 @@ def action_post(self): ) amls.reconcile() return res + + def _stock_account_prepare_anglo_saxon_in_lines_vals(self): + lines_vals_list_rma = [] + rma_refunds = self.env["account.move"] + price_unit_prec = self.env["decimal.precision"].precision_get("Product Price") + for move in self: + if ( + move.move_type != "in_refund" + or not move.company_id.anglo_saxon_accounting + ): + continue + move = move.with_company(move.company_id) + for line in move.invoice_line_ids.filtered(lambda l: l.rma_line_id): + # Filter out lines being not eligible for price difference. + # Moreover, this function is used for standard cost method only. + if ( + line.product_id.type != "product" + or line.product_id.valuation != "real_time" + ): + continue + + # Retrieve accounts needed to generate the price difference. + debit_expense_account = line._get_price_diff_account() + if not debit_expense_account: + continue + # Retrieve stock valuation moves. + valuation_stock_moves = ( + self.env["stock.move"].search( + [ + ("rma_line_id", "=", line.rma_line_id.id), + ("state", "=", "done"), + ("product_qty", "!=", 0.0), + ] + ) + if line.rma_line_id + else self.env["stock.move"] + ) + + if line.product_id.cost_method != "standard" and line.rma_line_id: + if move.move_type == "in_refund": + valuation_stock_moves = valuation_stock_moves.filtered( + lambda stock_move: stock_move._is_out() + ) + else: + valuation_stock_moves = valuation_stock_moves.filtered( + lambda stock_move: stock_move._is_in() + ) + + if not valuation_stock_moves: + continue + + ( + valuation_price_unit_total, + valuation_total_qty, + ) = valuation_stock_moves._get_valuation_price_and_qty( + line, move.currency_id + ) + valuation_price_unit = ( + valuation_price_unit_total / valuation_total_qty + ) + valuation_price_unit = line.product_id.uom_id._compute_price( + valuation_price_unit, line.product_uom_id + ) + else: + # Valuation_price unit is always expressed in invoice currency, + # so that it can always be computed with the good rate + price_unit = line.product_id.uom_id._compute_price( + line.product_id.standard_price, line.product_uom_id + ) + price_unit = ( + -price_unit + if line.move_id.move_type == "in_refund" + else price_unit + ) + valuation_date = ( + valuation_stock_moves + and max(valuation_stock_moves.mapped("date")) + or move.date + ) + valuation_price_unit = line.company_currency_id._convert( + price_unit, + move.currency_id, + move.company_id, + valuation_date, + round=False, + ) + + price_unit = line._get_gross_unit_price() + + price_unit_val_dif = abs(price_unit) - valuation_price_unit + relevant_qty = line.quantity + price_subtotal = relevant_qty * price_unit_val_dif + # We consider there is a price difference if the subtotal is not zero. In case a + # discount has been applied, we can't round the price unit anymore, and hence we + # can't compare them. + if ( + not move.currency_id.is_zero(price_subtotal) + and float_compare( + line["price_unit"], + line.price_unit, + precision_digits=price_unit_prec, + ) + == 0 + ): + # Add price difference account line. + vals = { + "name": line.name[:64], + "move_id": move.id, + "partner_id": line.partner_id.id + or move.commercial_partner_id.id, + "currency_id": line.currency_id.id, + "product_id": line.product_id.id, + "product_uom_id": line.product_uom_id.id, + "quantity": relevant_qty, + "price_unit": price_unit_val_dif, + "price_subtotal": relevant_qty * price_unit_val_dif, + "amount_currency": relevant_qty + * price_unit_val_dif + * line.move_id.direction_sign, + "balance": line.currency_id._convert( + relevant_qty + * price_unit_val_dif + * line.move_id.direction_sign, + line.company_currency_id, + line.company_id, + fields.Date.today(), + ), + "account_id": debit_expense_account.id, + "analytic_distribution": line.analytic_distribution, + "display_type": "cogs", + "rma_line_id": line.rma_line_id.id, + } + + lines_vals_list_rma.append(vals) + + # Correct the amount of the current line. + vals = { + "name": line.name[:64], + "move_id": move.id, + "partner_id": line.partner_id.id + or move.commercial_partner_id.id, + "currency_id": line.currency_id.id, + "product_id": line.product_id.id, + "product_uom_id": line.product_uom_id.id, + "quantity": relevant_qty, + "price_unit": -price_unit_val_dif, + "price_subtotal": relevant_qty * -price_unit_val_dif, + "amount_currency": relevant_qty + * -price_unit_val_dif + * line.move_id.direction_sign, + "balance": line.currency_id._convert( + relevant_qty + * -price_unit_val_dif + * line.move_id.direction_sign, + line.company_currency_id, + line.company_id, + fields.Date.today(), + ), + "account_id": line.account_id.id, + "analytic_distribution": line.analytic_distribution, + "display_type": "cogs", + "rma_line_id": line.rma_line_id.id, + } + lines_vals_list_rma.append(vals) + rma_refunds |= move + lines_vals_list = super( + AccountMove, self - rma_refunds + )._stock_account_prepare_anglo_saxon_in_lines_vals() + lines_vals_list += lines_vals_list_rma + return lines_vals_list diff --git a/rma_purchase/models/account_move_line.py b/rma_purchase/models/account_move_line.py new file mode 100644 index 000000000..a838457cc --- /dev/null +++ b/rma_purchase/models/account_move_line.py @@ -0,0 +1,20 @@ +from odoo import models + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + + def _get_price_diff_account(self): + # force the price difference account to be taken from the price + # different properties as they was in the previous Odoo versions + self.ensure_one() + if self.product_id.cost_method != "standard": + debit_pdiff_account = ( + self.product_id.property_account_creditor_price_difference + or self.product_id.categ_id.property_account_creditor_price_difference_categ + ) + debit_pdiff_account = self.move_id.fiscal_position_id.map_account( + debit_pdiff_account + ) + return debit_pdiff_account + return super()._get_price_diff_account() diff --git a/rma_purchase/tests/test_rma_stock_account_purchase.py b/rma_purchase/tests/test_rma_stock_account_purchase.py index 3fed8591c..1d7306c7b 100644 --- a/rma_purchase/tests/test_rma_stock_account_purchase.py +++ b/rma_purchase/tests/test_rma_stock_account_purchase.py @@ -17,7 +17,7 @@ def setUpClass(cls): cls.rma_operation_supplier_refund = cls.env.ref( "rma_account.rma_operation_supplier_refund" ) - acc_type = cls._create_account_type("expense", "other") + acc_type = "expense" cls.account_price_diff = cls._create_account( acc_type, "Refund Price Difference Expense", "rpde", cls.company, False )