From 822b5f88e716cb8a8118c80c23f3b12c1bcac9a0 Mon Sep 17 00:00:00 2001
From: "Walid HANNICHE (waha)" <waha@odoo.com>
Date: Fri, 30 Sep 2022 20:21:00 +0000
Subject: [PATCH] [FIX] purchase_stock: stock valuation foreign currency
 Journal Entry

Steps to reproduce:
- Setup product (
    cost 10$
    Storeable Product
    Standard Price Accounting
    automated valuation
    add account for price difference
)

- Purchase product in foreign currency (set a different price)
- Receive the product
- Create and validate Bill

Bug:
The reception JE is for the cost amount set on the product ($10)
debit and credit value are correct but the amount in currency
is wrong (purchase cost)

After creating the bill the Journal items are not correctly matched
since reconciliation is done on currency amount in the case fo foreign
currency transaction

Fix:
Set the correct amount (cost configured on product page)
in currency amount if costing method is standard

opw-2822366

closes odoo/odoo#101809

Signed-off-by: William Henrotin (whe) <whe@odoo.com>
---
 addons/purchase_stock/models/stock.py         |  23 ++-
 .../tests/test_stockvaluation.py              | 180 ++++++++++++++++++
 2 files changed, 196 insertions(+), 7 deletions(-)

diff --git a/addons/purchase_stock/models/stock.py b/addons/purchase_stock/models/stock.py
index 5281b06e215e..34e18c75625e 100644
--- a/addons/purchase_stock/models/stock.py
+++ b/addons/purchase_stock/models/stock.py
@@ -67,13 +67,22 @@ class StockMove(models.Model):
         if self.purchase_line_id:
             purchase_currency = self.purchase_line_id.currency_id
             if purchase_currency != self.company_id.currency_id:
-                # Do not use price_unit since we want the price tax excluded. And by the way, qty
-                # is in the UOM of the product, not the UOM of the PO line.
-                purchase_price_unit = (
-                    self.purchase_line_id.price_subtotal / self.purchase_line_id.product_uom_qty
-                    if self.purchase_line_id.product_uom_qty
-                    else self.purchase_line_id.price_unit
-                )
+                if(self.purchase_line_id.product_id.cost_method == 'standard'):
+                    purchase_price_unit = self.purchase_line_id.product_id.cost_currency_id._convert(
+                        self.purchase_line_id.product_id.standard_price,
+                        purchase_currency,
+                        self.company_id,
+                        self.date,
+                        round=False,
+                    )
+                else:
+                    # Do not use price_unit since we want the price tax excluded. And by the way, qty
+                    # is in the UOM of the product, not the UOM of the PO line.
+                    purchase_price_unit = (
+                        self.purchase_line_id.price_subtotal / self.purchase_line_id.product_uom_qty
+                        if self.purchase_line_id.product_uom_qty
+                        else self.purchase_line_id.price_unit
+                    )
                 currency_move_valuation = purchase_currency.round(purchase_price_unit * abs(qty))
                 rslt['credit_line_vals']['amount_currency'] = rslt['credit_line_vals']['credit'] and -currency_move_valuation or currency_move_valuation
                 rslt['credit_line_vals']['currency_id'] = purchase_currency.id
diff --git a/addons/purchase_stock/tests/test_stockvaluation.py b/addons/purchase_stock/tests/test_stockvaluation.py
index e47a7b58d97b..c8aee9707714 100644
--- a/addons/purchase_stock/tests/test_stockvaluation.py
+++ b/addons/purchase_stock/tests/test_stockvaluation.py
@@ -527,6 +527,186 @@ class TestStockValuationWithCOA(AccountTestInvoicingCommon):
         # has gone to the stock account, and must be reflected in inventory valuation
         self.assertEqual(self.product1.value_svl, 150)
 
+    def test_standard_valuation_multicurrency(self):
+        company = self.env.user.company_id
+        company.anglo_saxon_accounting = True
+        company.currency_id = self.usd_currency
+
+        date_po = '2019-01-01'
+
+        self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+        self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
+        self.product1.property_account_creditor_price_difference = self.price_diff_account
+        self.product1.standard_price = 10
+
+        # SetUp currency and rates   1$ = 2 Euros
+        self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", (self.usd_currency.id, company.id))
+        self.env['res.currency.rate'].search([]).unlink()
+        self.env['res.currency.rate'].create({
+            'name': date_po,
+            'rate': 1.0,
+            'currency_id': self.usd_currency.id,
+            'company_id': company.id,
+        })
+
+        self.env['res.currency.rate'].create({
+            'name': date_po,
+            'rate': 2,
+            'currency_id': self.eur_currency.id,
+            'company_id': company.id,
+        })
+
+        # Create PO
+        po = self.env['purchase.order'].create({
+            'currency_id': self.eur_currency.id,
+            'partner_id': self.partner_id.id,
+            'order_line': [
+                (0, 0, {
+                    'name': self.product1.name,
+                    'product_id': self.product1.id,
+                    'product_qty': 1.0,
+                    'product_uom': self.product1.uom_po_id.id,
+                    'price_unit': 100.0, # 50$
+                    'date_planned': date_po,
+                }),
+            ],
+        })
+        po.button_confirm()
+
+        # Receive the goods
+        receipt = po.picking_ids[0]
+        receipt.move_lines.quantity_done = 1
+        receipt.button_validate()
+
+        # Create a vendor bill
+        inv = self.env['account.move'].with_context(default_move_type='in_invoice').create({
+            'move_type': 'in_invoice',
+            'invoice_date': date_po,
+            'date': date_po,
+            'currency_id': self.eur_currency.id,
+            'partner_id': self.partner_id.id,
+            'invoice_line_ids': [(0, 0, {
+                'name': 'Test',
+                'price_unit': 100.0,
+                'product_id': self.product1.id,
+                'purchase_line_id': po.order_line.id,
+                'quantity': 1.0,
+                'account_id': self.stock_input_account.id,
+            })]
+        })
+
+        inv.action_post()
+
+        # Check what was posted in stock input account
+        input_amls = self.env['account.move.line'].search([('account_id', '=', self.stock_input_account.id)])
+        self.assertEqual(len(input_amls), 3, "Only three lines should have been generated in stock input account: one when receiving the product, one when making the invoice.")
+        invoice_amls = input_amls.filtered(lambda l: l.move_id == inv)
+        picking_aml = input_amls - invoice_amls
+        payable_aml = invoice_amls.filtered(lambda l: l.amount_currency > 0)
+        diff_aml = invoice_amls - payable_aml
+
+        # check USD
+        self.assertAlmostEqual(payable_aml.debit, 50, "Total debit value should be equal to the original PO price of the product.")
+        self.assertAlmostEqual(picking_aml.credit, 10, "credit value for stock should be equal to the standard price of the product.")
+        self.assertAlmostEqual(diff_aml.credit, 40, "credit value for price difference")
+
+        # check EUR
+        self.assertAlmostEqual(payable_aml.amount_currency, 100, "Total debit value should be equal to the original PO price of the product.")
+        self.assertAlmostEqual(picking_aml.amount_currency, -20, "credit value for stock should be equal to the standard price of the product.")
+        self.assertAlmostEqual(diff_aml.amount_currency, -80, "credit value for price difference")
+
+    def test_valuation_multicurecny_with_tax(self):
+        """ Check that a tax without account will increment the stock value.
+        """
+
+        company = self.env.user.company_id
+        company.anglo_saxon_accounting = True
+        company.currency_id = self.usd_currency
+
+        date_po = '2019-01-01'
+
+        self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
+        self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
+        self.product1.property_account_creditor_price_difference = self.price_diff_account
+
+        # SetUp currency and rates 1$ = 2Euros
+        self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", (self.usd_currency.id, company.id))
+        self.env['res.currency.rate'].search([]).unlink()
+        self.env['res.currency.rate'].create({
+            'name': date_po,
+            'rate': 1.0,
+            'currency_id': self.usd_currency.id,
+            'company_id': company.id,
+        })
+
+        self.env['res.currency.rate'].create({
+            'name': date_po,
+            'rate': 2,
+            'currency_id': self.eur_currency.id,
+            'company_id': company.id,
+        })
+
+        tax_with_no_account = self.env['account.tax'].create({
+            'name': "Tax with no account",
+            'amount_type': 'fixed',
+            'amount': 5,
+            'sequence': 8,
+            'price_include': True,
+        })
+
+        # Create PO
+        po = self.env['purchase.order'].create({
+            'currency_id': self.eur_currency.id,
+            'partner_id': self.partner_id.id,
+            'order_line': [
+                (0, 0, {
+                    'name': self.product1.name,
+                    'product_id': self.product1.id,
+                    'product_qty': 1.0,
+                    'product_uom': self.product1.uom_po_id.id,
+                    'price_unit': 100.0, # 50$
+                    'taxes_id': [(4, tax_with_no_account.id)],
+                    'date_planned': date_po,
+                }),
+            ],
+        })
+
+        po.button_confirm()
+
+        # Receive the goods
+        receipt = po.picking_ids[0]
+        receipt.move_lines.quantity_done = 1
+        receipt.button_validate()
+
+        # Create a vendor bill
+        inv = self.env['account.move'].with_context(default_move_type='in_invoice').create({
+            'move_type': 'in_invoice',
+            'invoice_date': date_po,
+            'date': date_po,
+            'currency_id': self.eur_currency.id,
+            'partner_id': self.partner_id.id,
+            'invoice_line_ids': [(0, 0, {
+                'name': 'Test',
+                'price_unit': 100.0,
+                'product_id': self.product1.id,
+                'purchase_line_id': po.order_line.id,
+                'quantity': 1.0,
+                'account_id': self.stock_input_account.id,
+            })]
+        })
+
+        inv.action_post()
+
+        # Check what was posted in stock input account
+        input_amls = self.env['account.move.line'].search([('account_id', '=', self.stock_input_account.id)])
+        self.assertEqual(len(input_amls), 2, "Only two lines should have been generated in stock input account: one when receiving the product, one when making the invoice.")
+        invoice_aml = input_amls.filtered(lambda l: l.move_id == inv)
+        picking_aml = input_amls - invoice_aml
+
+        # check EUR
+        self.assertAlmostEqual(invoice_aml.amount_currency, 100, "Total debit value should be equal to the original PO price of the product.")
+        self.assertAlmostEqual(picking_aml.amount_currency, -95, "credit value for stock should be equal to the untaxed price of the product.")
+
     def test_average_realtime_anglo_saxon_valuation_multicurrency_same_date(self):
         """
         The PO and invoice are in the same foreign currency.
-- 
GitLab