summaryrefslogtreecommitdiff
path: root/addons/purchase_stock/tests/test_stockvaluation.py
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/purchase_stock/tests/test_stockvaluation.py
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/purchase_stock/tests/test_stockvaluation.py')
-rw-r--r--addons/purchase_stock/tests/test_stockvaluation.py1328
1 files changed, 1328 insertions, 0 deletions
diff --git a/addons/purchase_stock/tests/test_stockvaluation.py b/addons/purchase_stock/tests/test_stockvaluation.py
new file mode 100644
index 00000000..06cfabd6
--- /dev/null
+++ b/addons/purchase_stock/tests/test_stockvaluation.py
@@ -0,0 +1,1328 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import time
+from datetime import datetime
+from unittest.mock import patch
+
+from odoo import fields
+from odoo.tests import Form
+from odoo.tests.common import TransactionCase, tagged
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
+
+
+class TestStockValuation(TransactionCase):
+ def setUp(self):
+ super(TestStockValuation, self).setUp()
+ self.supplier_location = self.env.ref('stock.stock_location_suppliers')
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ self.partner_id = self.env['res.partner'].create({
+ 'name': 'Wood Corner Partner',
+ 'company_id': self.env.user.company_id.id,
+ })
+ self.product1 = self.env['product.product'].create({
+ 'name': 'Large Desk',
+ 'standard_price': 1299.0,
+ 'list_price': 1799.0,
+ 'type': 'product',
+ })
+ Account = self.env['account.account']
+ self.stock_input_account = Account.create({
+ 'name': 'Stock Input',
+ 'code': 'StockIn',
+ 'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
+ 'reconcile': True,
+ })
+ self.stock_output_account = Account.create({
+ 'name': 'Stock Output',
+ 'code': 'StockOut',
+ 'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
+ 'reconcile': True,
+ })
+ self.stock_valuation_account = Account.create({
+ 'name': 'Stock Valuation',
+ 'code': 'Stock Valuation',
+ 'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
+ })
+ self.stock_journal = self.env['account.journal'].create({
+ 'name': 'Stock Journal',
+ 'code': 'STJTEST',
+ 'type': 'general',
+ })
+ self.product1.categ_id.write({
+ 'property_stock_account_input_categ_id': self.stock_input_account.id,
+ 'property_stock_account_output_categ_id': self.stock_output_account.id,
+ 'property_stock_valuation_account_id': self.stock_valuation_account.id,
+ 'property_stock_journal': self.stock_journal.id,
+ })
+
+ def test_change_unit_cost_average_1(self):
+ """ Confirm a purchase order and create the associated receipt, change the unit cost of the
+ purchase order before validating the receipt, the value of the received goods should be set
+ according to the last unit cost.
+ """
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
+ po1 = self.env['purchase.order'].create({
+ 'partner_id': self.partner_id.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': self.product1.name,
+ 'product_id': self.product1.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.product1.uom_po_id.id,
+ 'price_unit': 100.0,
+ 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
+ }),
+ ],
+ })
+ po1.button_confirm()
+
+ picking1 = po1.picking_ids[0]
+ move1 = picking1.move_lines[0]
+
+ # the unit price of the purchase order line is copied to the in move
+ self.assertEqual(move1.price_unit, 100)
+
+ # update the unit price on the purchase order line
+ po1.order_line.price_unit = 200
+
+ # the unit price on the stock move is not directly updated
+ self.assertEqual(move1.price_unit, 100)
+
+ # validate the receipt
+ res_dict = picking1.button_validate()
+ wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
+ wizard.process()
+
+ # the unit price of the valuationlayer used the latest value
+ self.assertEqual(move1.stock_valuation_layer_ids.unit_cost, 200)
+
+ self.assertEqual(self.product1.value_svl, 2000)
+
+ def test_standard_price_change_1(self):
+ """ Confirm a purchase order and create the associated receipt, change the unit cost of the
+ purchase order and the standard price of the product before validating the receipt, the
+ value of the received goods should be set according to the last standard price.
+ """
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+
+ # set a standard price
+ self.product1.product_tmpl_id.standard_price = 10
+
+ po1 = self.env['purchase.order'].create({
+ 'partner_id': self.partner_id.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': self.product1.name,
+ 'product_id': self.product1.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.product1.uom_po_id.id,
+ 'price_unit': 11.0,
+ 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
+ }),
+ ],
+ })
+ po1.button_confirm()
+
+ picking1 = po1.picking_ids[0]
+ move1 = picking1.move_lines[0]
+
+ # the move's unit price reflects the purchase order line's cost even if it's useless when
+ # the product's cost method is standard
+ self.assertEqual(move1.price_unit, 11)
+
+ # set a new standard price
+ self.product1.product_tmpl_id.standard_price = 12
+
+ # the unit price on the stock move is not directly updated
+ self.assertEqual(move1.price_unit, 11)
+
+ # validate the receipt
+ res_dict = picking1.button_validate()
+ wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
+ wizard.process()
+
+ # the unit price of the valuation layer used the latest value
+ self.assertEqual(move1.stock_valuation_layer_ids.unit_cost, 12)
+
+ self.assertEqual(self.product1.value_svl, 120)
+
+ def test_change_currency_rate_average_1(self):
+ """ Confirm a purchase order in another currency and create the associated receipt, change
+ the currency rate, validate the receipt and then check that the value of the received goods
+ is set according to the last currency rate.
+ """
+ self.env['res.currency.rate'].search([]).unlink()
+ usd_currency = self.env.ref('base.USD')
+ self.env.company.currency_id = usd_currency.id
+
+ eur_currency = self.env.ref('base.EUR')
+
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
+
+ # default currency is USD, create a purchase order in EUR
+ po1 = self.env['purchase.order'].create({
+ 'partner_id': self.partner_id.id,
+ 'currency_id': eur_currency.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': self.product1.name,
+ 'product_id': self.product1.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.product1.uom_po_id.id,
+ 'price_unit': 100.0,
+ 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
+ }),
+ ],
+ })
+ po1.button_confirm()
+
+ picking1 = po1.picking_ids[0]
+ move1 = picking1.move_lines[0]
+
+ # convert the price unit in the company currency
+ price_unit_usd = po1.currency_id._convert(
+ po1.order_line.price_unit, po1.company_id.currency_id,
+ self.env.company, fields.Date.today(), round=False)
+
+ # the unit price of the move is the unit price of the purchase order line converted in
+ # the company's currency
+ self.assertAlmostEqual(move1.price_unit, price_unit_usd, places=2)
+
+ # change the rate of the currency
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y-%m-%d'),
+ 'rate': 2.0,
+ 'currency_id': eur_currency.id,
+ 'company_id': po1.company_id.id,
+ })
+ eur_currency._compute_current_rate()
+ price_unit_usd_new_rate = po1.currency_id._convert(
+ po1.order_line.price_unit, po1.company_id.currency_id,
+ self.env.company, fields.Date.today(), round=False)
+
+ # the new price_unit is lower than th initial because of the rate's change
+ self.assertLess(price_unit_usd_new_rate, price_unit_usd)
+
+ # the unit price on the stock move is not directly updated
+ self.assertAlmostEqual(move1.price_unit, price_unit_usd, places=2)
+
+ # validate the receipt
+ res_dict = picking1.button_validate()
+ wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
+ wizard.process()
+
+ # the unit price of the valuation layer used the latest value
+ self.assertAlmostEqual(move1.stock_valuation_layer_ids.unit_cost, price_unit_usd_new_rate)
+
+ self.assertAlmostEqual(self.product1.value_svl, price_unit_usd_new_rate * 10, delta=0.1)
+
+ def test_extra_move_fifo_1(self):
+ """ Check that the extra move when over processing a receipt is correctly merged back in
+ the original move.
+ """
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
+ po1 = self.env['purchase.order'].create({
+ 'partner_id': self.partner_id.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': self.product1.name,
+ 'product_id': self.product1.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.product1.uom_po_id.id,
+ 'price_unit': 100.0,
+ 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
+ }),
+ ],
+ })
+ po1.button_confirm()
+
+ picking1 = po1.picking_ids[0]
+ move1 = picking1.move_lines[0]
+ move1.quantity_done = 15
+ picking1.button_validate()
+
+ # there should be only one move
+ self.assertEqual(len(picking1.move_lines), 1)
+ self.assertEqual(move1.price_unit, 100)
+ self.assertEqual(move1.stock_valuation_layer_ids.unit_cost, 100)
+ self.assertEqual(move1.product_qty, 15)
+ self.assertEqual(self.product1.value_svl, 1500)
+
+ def test_backorder_fifo_1(self):
+ """ Check that the backordered move when under processing a receipt correctly keep the
+ price unit of the original move.
+ """
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
+ po1 = self.env['purchase.order'].create({
+ 'partner_id': self.partner_id.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': self.product1.name,
+ 'product_id': self.product1.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.product1.uom_po_id.id,
+ 'price_unit': 100.0,
+ 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
+ }),
+ ],
+ })
+ po1.button_confirm()
+
+ picking1 = po1.picking_ids[0]
+ move1 = picking1.move_lines[0]
+ move1.quantity_done = 5
+ res_dict = picking1.button_validate()
+ self.assertEqual(res_dict['res_model'], 'stock.backorder.confirmation')
+ wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id')).with_context(res_dict['context'])
+ wizard.process()
+
+ self.assertEqual(len(picking1.move_lines), 1)
+ self.assertEqual(move1.price_unit, 100)
+ self.assertEqual(move1.product_qty, 5)
+
+ picking2 = po1.picking_ids.filtered(lambda p: p.backorder_id)
+ move2 = picking2.move_lines[0]
+ self.assertEqual(len(picking2.move_lines), 1)
+ self.assertEqual(move2.price_unit, 100)
+ self.assertEqual(move2.product_qty, 5)
+
+
+@tagged('post_install', '-at_install')
+class TestStockValuationWithCOA(AccountTestInvoicingCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.supplier_location = cls.env.ref('stock.stock_location_suppliers')
+ cls.stock_location = cls.env.ref('stock.stock_location_stock')
+ cls.partner_id = cls.env['res.partner'].create({'name': 'Wood Corner Partner'})
+ cls.product1 = cls.env['product.product'].create({'name': 'Large Desk'})
+
+ cls.cat = cls.env['product.category'].create({
+ 'name': 'cat',
+ })
+ cls.product1 = cls.env['product.product'].create({
+ 'name': 'product1',
+ 'type': 'product',
+ 'categ_id': cls.cat.id,
+ })
+ cls.product1_copy = cls.env['product.product'].create({
+ 'name': 'product1',
+ 'type': 'product',
+ 'categ_id': cls.cat.id,
+ })
+
+ Account = cls.env['account.account']
+ cls.usd_currency = cls.env.ref('base.USD')
+ cls.eur_currency = cls.env.ref('base.EUR')
+
+ cls.stock_input_account = Account.create({
+ 'name': 'Stock Input',
+ 'code': 'StockIn',
+ 'user_type_id': cls.env.ref('account.data_account_type_current_assets').id,
+ 'reconcile': True,
+ })
+ cls.stock_output_account = Account.create({
+ 'name': 'Stock Output',
+ 'code': 'StockOut',
+ 'user_type_id': cls.env.ref('account.data_account_type_current_assets').id,
+ 'reconcile': True,
+ })
+ cls.stock_valuation_account = Account.create({
+ 'name': 'Stock Valuation',
+ 'code': 'Stock Valuation',
+ 'user_type_id': cls.env.ref('account.data_account_type_current_assets').id,
+ })
+ cls.price_diff_account = Account.create({
+ 'name': 'price diff account',
+ 'code': 'price diff account',
+ 'user_type_id': cls.env.ref('account.data_account_type_current_assets').id,
+ })
+ cls.stock_journal = cls.env['account.journal'].create({
+ 'name': 'Stock Journal',
+ 'code': 'STJTEST',
+ 'type': 'general',
+ })
+ cls.product1.categ_id.write({
+ 'property_stock_account_input_categ_id': cls.stock_input_account.id,
+ 'property_stock_account_output_categ_id': cls.stock_output_account.id,
+ 'property_stock_valuation_account_id': cls.stock_valuation_account.id,
+ 'property_stock_journal': cls.stock_journal.id,
+ })
+
+ def test_fifo_anglosaxon_return(self):
+ self.env.company.anglo_saxon_accounting = True
+ 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
+
+ # Receive 10@10 ; create the vendor bill
+ po1 = self.env['purchase.order'].create({
+ 'partner_id': self.partner_id.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': self.product1.name,
+ 'product_id': self.product1.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.product1.uom_po_id.id,
+ 'price_unit': 10.0,
+ 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
+ }),
+ ],
+ })
+ po1.button_confirm()
+ receipt_po1 = po1.picking_ids[0]
+ receipt_po1.move_lines.quantity_done = 10
+ receipt_po1.button_validate()
+
+ move_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
+ move_form.invoice_date = move_form.date
+ move_form.partner_id = self.partner_id
+ move_form.purchase_id = po1
+ invoice_po1 = move_form.save()
+ invoice_po1.action_post()
+
+ # Receive 10@20 ; create the vendor bill
+ po2 = self.env['purchase.order'].create({
+ 'partner_id': self.partner_id.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': self.product1.name,
+ 'product_id': self.product1.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.product1.uom_po_id.id,
+ 'price_unit': 20.0,
+ 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
+ }),
+ ],
+ })
+ po2.button_confirm()
+ receipt_po2 = po2.picking_ids[0]
+ receipt_po2.move_lines.quantity_done = 10
+ receipt_po2.button_validate()
+
+ move_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
+ move_form.invoice_date = move_form.date
+ move_form.partner_id = self.partner_id
+ move_form.purchase_id = po2
+ invoice_po2 = move_form.save()
+ invoice_po2.action_post()
+
+ # valuation of product1 should be 300
+ self.assertEqual(self.product1.value_svl, 300)
+
+ # return the second po
+ stock_return_picking_form = Form(self.env['stock.return.picking']
+ .with_context(active_ids=receipt_po2.ids, active_id=receipt_po2.ids[0],
+ active_model='stock.picking'))
+ stock_return_picking = stock_return_picking_form.save()
+ stock_return_picking.product_return_moves.quantity = 10
+ stock_return_picking_action = stock_return_picking.create_returns()
+ return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
+ return_pick.move_lines[0].move_line_ids[0].qty_done = 10
+ return_pick.button_validate()
+
+ # valuation of product1 should be 200 as the first items will be sent out
+ self.assertEqual(self.product1.value_svl, 200)
+
+ # create a credit note for po2
+ move_form = Form(self.env['account.move'].with_context(default_move_type='in_refund'))
+ move_form.invoice_date = move_form.date
+ move_form.partner_id = self.partner_id
+ move_form.purchase_id = po2
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.quantity = 10
+ creditnote_po2 = move_form.save()
+ creditnote_po2.action_post()
+
+ # check the anglo saxon entries
+ price_diff_entry = self.env['account.move.line'].search([('account_id', '=', self.price_diff_account.id)])
+ self.assertEqual(price_diff_entry.credit, 100)
+
+ def test_anglosaxon_valuation(self):
+ self.env.company.anglo_saxon_accounting = True
+ 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
+
+ # Create PO
+ po_form = Form(self.env['purchase.order'])
+ po_form.partner_id = self.partner_id
+ with po_form.order_line.new() as po_line:
+ po_line.product_id = self.product1
+ po_line.product_qty = 1
+ po_line.price_unit = 10.0
+ order = po_form.save()
+ order.button_confirm()
+
+ # Receive the goods
+ receipt = order.picking_ids[0]
+ receipt.move_lines.quantity_done = 1
+ receipt.button_validate()
+
+ # Create an invoice with a different price
+ move_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
+ move_form.invoice_date = move_form.date
+ move_form.partner_id = order.partner_id
+ move_form.purchase_id = order
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.price_unit = 15.0
+ invoice = move_form.save()
+ invoice.action_post()
+
+ # Check what was posted in the price difference account
+ price_diff_aml = self.env['account.move.line'].search([('account_id','=', self.price_diff_account.id)])
+ self.assertEqual(len(price_diff_aml), 1, "Only one line should have been generated in the price difference account.")
+ self.assertAlmostEqual(price_diff_aml.debit, 5, "Price difference should be equal to 5 (15-10)")
+
+ # Check what was posted in stock input account
+ input_aml = self.env['account.move.line'].search([('account_id','=',self.stock_input_account.id)])
+ self.assertEqual(len(input_aml), 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_aml.filtered(lambda l: l.move_id == invoice)
+ picking_aml = input_aml - invoice_amls
+ self.assertAlmostEqual(sum(invoice_amls.mapped('debit')), 15, "Total debit value on stock input account should be equal to the original PO price of the product.")
+ self.assertAlmostEqual(sum(invoice_amls.mapped('credit')), 5, "Total debit value on stock input account should be equal to the original PO price of the product.")
+ self.assertAlmostEqual(sum(picking_aml.mapped('credit')), 10, "Total credit value on stock input account should be equal to the original PO price of the product.")
+
+ def test_valuation_from_increasing_tax(self):
+ """ Check that a tax without account will increment the stock value.
+ """
+
+ tax_with_no_account = self.env['account.tax'].create({
+ 'name': "Tax with no account",
+ 'amount_type': 'fixed',
+ 'amount': 5,
+ 'sequence': 8,
+ })
+
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
+
+ # Receive 10@10 ; create the vendor bill
+ po1 = self.env['purchase.order'].create({
+ 'partner_id': self.partner_id.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': self.product1.name,
+ 'product_id': self.product1.id,
+ 'taxes_id': [(4, tax_with_no_account.id)],
+ 'product_qty': 10.0,
+ 'product_uom': self.product1.uom_po_id.id,
+ 'price_unit': 10.0,
+ 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
+ }),
+ ],
+ })
+ po1.button_confirm()
+ receipt_po1 = po1.picking_ids[0]
+ receipt_po1.move_lines.quantity_done = 10
+ receipt_po1.button_validate()
+
+ # valuation of product1 should be 15 as the tax with no account set
+ # has gone to the stock account, and must be reflected in inventory valuation
+ self.assertEqual(self.product1.value_svl, 150)
+
+ def test_average_realtime_anglo_saxon_valuation_multicurrency_same_date(self):
+ """
+ The PO and invoice are in the same foreign currency.
+ The PO is invoiced on the same date as its creation.
+ This shouldn't create a price difference entry.
+ """
+ company = self.env.user.company_id
+ company.anglo_saxon_accounting = True
+ company.currency_id = self.usd_currency
+
+ date_po = '2019-01-01'
+
+ # SetUp product
+ self.product1.product_tmpl_id.cost_method = 'average'
+ self.product1.product_tmpl_id.valuation = 'real_time'
+ self.product1.product_tmpl_id.purchase_method = 'purchase'
+
+ self.product1.property_account_creditor_price_difference = self.price_diff_account
+
+ # SetUp currency and rates
+ 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': 1.5,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+
+ # Proceed
+ 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,
+ 'date_planned': date_po,
+ }),
+ ],
+ })
+ po.button_confirm()
+
+ 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()
+
+ move_lines = inv.line_ids
+ self.assertEqual(len(move_lines), 2)
+
+ payable_line = move_lines.filtered(lambda l: l.account_id.internal_type == 'payable')
+
+ self.assertEqual(payable_line.amount_currency, -100.0)
+ self.assertAlmostEqual(payable_line.balance, -66.67)
+
+ stock_line = move_lines.filtered(lambda l: l.account_id == self.stock_input_account)
+ self.assertEqual(stock_line.amount_currency, 100.0)
+ self.assertAlmostEqual(stock_line.balance, 66.67)
+
+ def test_realtime_anglo_saxon_valuation_multicurrency_different_dates(self):
+ """
+ The PO and invoice are in the same foreign currency.
+ The PO is invoiced at a later date than its creation.
+ This should create a price difference entry for standard cost method
+ Not for average cost method though, since the PO and invoice have the same currency
+ """
+ company = self.env.user.company_id
+ company.anglo_saxon_accounting = True
+ company.currency_id = self.usd_currency
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
+
+ date_po = '2019-01-01'
+ date_invoice = '2019-01-16'
+
+ # SetUp product Average
+ self.product1.product_tmpl_id.write({
+ 'purchase_method': 'purchase',
+ 'property_account_creditor_price_difference': self.price_diff_account.id,
+ })
+
+ # SetUp product Standard
+ # should have bought at 60 USD
+ # actually invoiced at 70 EUR > 35 USD
+ product_categ_standard = self.cat.copy({
+ 'property_cost_method': 'standard',
+ 'property_stock_account_input_categ_id': self.stock_input_account.id,
+ 'property_stock_account_output_categ_id': self.stock_output_account.id,
+ 'property_stock_valuation_account_id': self.stock_valuation_account.id,
+ 'property_stock_journal': self.stock_journal.id,
+ })
+ product_standard = self.product1_copy
+ product_standard.write({
+ 'categ_id': product_categ_standard.id,
+ 'name': 'Standard Val',
+ 'standard_price': 60,
+ 'property_account_creditor_price_difference': self.price_diff_account.id
+ })
+
+ # SetUp currency and rates
+ 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': 1.5,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+
+ self.env['res.currency.rate'].create({
+ 'name': date_invoice,
+ 'rate': 2,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+
+ # To allow testing validation of PO
+ def _today(*args, **kwargs):
+ return date_po
+ patchers = [
+ patch('odoo.fields.Date.context_today', _today),
+ ]
+
+ for p in patchers:
+ p.start()
+
+ # Proceed
+ 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,
+ 'date_planned': date_po,
+ }),
+ (0, 0, {
+ 'name': product_standard.name,
+ 'product_id': product_standard.id,
+ 'product_qty': 1.0,
+ 'product_uom': product_standard.uom_po_id.id,
+ 'price_unit': 40.0,
+ 'date_planned': date_po,
+ }),
+ ],
+ })
+ po.button_confirm()
+
+ line_product_average = po.order_line.filtered(lambda l: l.product_id == self.product1)
+ line_product_standard = po.order_line.filtered(lambda l: l.product_id == product_standard)
+
+ inv = self.env['account.move'].with_context(default_move_type='in_invoice').create({
+ 'move_type': 'in_invoice',
+ 'invoice_date': date_invoice,
+ 'date': date_invoice,
+ 'currency_id': self.eur_currency.id,
+ 'partner_id': self.partner_id.id,
+ 'invoice_line_ids': [
+ (0, 0, {
+ 'name': self.product1.name,
+ 'price_subtotal': 100.0,
+ 'price_unit': 100.0,
+ 'product_id': self.product1.id,
+ 'purchase_line_id': line_product_average.id,
+ 'quantity': 1.0,
+ 'account_id': self.stock_input_account.id,
+ }),
+ (0, 0, {
+ 'name': product_standard.name,
+ 'price_subtotal': 70.0,
+ 'price_unit': 70.0,
+ 'product_id': product_standard.id,
+ 'purchase_line_id': line_product_standard.id,
+ 'quantity': 1.0,
+ 'account_id': self.stock_input_account.id,
+ })
+ ]
+ })
+
+ inv.action_post()
+
+ for p in patchers:
+ p.stop()
+
+ move_lines = inv.line_ids
+ self.assertEqual(len(move_lines), 5)
+
+ # Ensure no exchange difference move has been created
+ self.assertTrue(all([not l.reconciled for l in move_lines]))
+
+ # PAYABLE CHECK
+ payable_line = move_lines.filtered(lambda l: l.account_id.internal_type == 'payable')
+ self.assertEqual(payable_line.amount_currency, -170.0)
+ self.assertAlmostEqual(payable_line.balance, -85.00)
+
+ # PRODUCTS CHECKS
+
+ # NO EXCHANGE DIFFERENCE (average)
+ # We ordered for a value of 100 EUR
+ # But by the time we are invoiced for it
+ # the foreign currency appreciated from 1.5 to 2.0
+ # We still have to pay 100 EUR, which now values at 50 USD
+ product_lines = move_lines.filtered(lambda l: l.product_id == self.product1)
+
+ # Stock-wise, we have been invoiced 100 EUR, and we ordered 100 EUR
+ # there is no price difference
+ # However, 100 EUR should be converted at the time of the invoice
+ stock_lines = product_lines.filtered(lambda l: l.account_id == self.stock_input_account)
+ self.assertAlmostEqual(sum(stock_lines.mapped('amount_currency')), 100.00)
+ self.assertAlmostEqual(sum(stock_lines.mapped('balance')), 50.00)
+
+ # PRICE DIFFERENCE (STANDARD)
+ # We ordered a product that should have cost 60 USD (120 EUR)
+ # However, we effectively got invoiced 70 EUR (35 USD)
+ product_lines = move_lines.filtered(lambda l: l.product_id == product_standard)
+
+ stock_lines = product_lines.filtered(lambda l: l.account_id == self.stock_input_account)
+ self.assertAlmostEqual(sum(stock_lines.mapped('amount_currency')), 120.00)
+ self.assertAlmostEqual(sum(stock_lines.mapped('balance')), 60.00)
+
+ price_diff_line = product_lines.filtered(lambda l: l.account_id == self.price_diff_account)
+ self.assertEqual(price_diff_line.amount_currency, -50.00)
+ self.assertAlmostEqual(price_diff_line.balance, -25.00)
+
+ def test_average_realtime_with_delivery_anglo_saxon_valuation_multicurrency_different_dates(self):
+ """
+ The PO and invoice are in the same foreign currency.
+ The delivery occurs in between PO validation and invoicing
+ The invoice is created at an even different date
+ This should create a price difference entry.
+ """
+ company = self.env.user.company_id
+ company.anglo_saxon_accounting = True
+ company.currency_id = self.usd_currency
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
+
+ date_po = '2019-01-01'
+ date_delivery = '2019-01-08'
+ date_invoice = '2019-01-16'
+
+ product_avg = self.product1_copy
+ product_avg.write({
+ 'purchase_method': 'purchase',
+ 'name': 'AVG',
+ 'standard_price': 60,
+ 'property_account_creditor_price_difference': self.price_diff_account.id
+ })
+
+ # SetUp currency and rates
+ 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': 1.5,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+
+ self.env['res.currency.rate'].create({
+ 'name': date_delivery,
+ 'rate': 0.7,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+
+ self.env['res.currency.rate'].create({
+ 'name': date_invoice,
+ 'rate': 2,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+
+ # To allow testing validation of PO and Delivery
+ today = date_po
+ def _today(*args, **kwargs):
+ return datetime.strptime(today, "%Y-%m-%d").date()
+ def _now(*args, **kwargs):
+ return datetime.strptime(today + ' 01:00:00', "%Y-%m-%d %H:%M:%S")
+
+ patchers = [
+ patch('odoo.fields.Date.context_today', _today),
+ patch('odoo.fields.Datetime.now', _now),
+ ]
+
+ for p in patchers:
+ p.start()
+
+ # Proceed
+ po = self.env['purchase.order'].create({
+ 'currency_id': self.eur_currency.id,
+ 'partner_id': self.partner_id.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': product_avg.name,
+ 'product_id': product_avg.id,
+ 'product_qty': 1.0,
+ 'product_uom': product_avg.uom_po_id.id,
+ 'price_unit': 30.0,
+ 'date_planned': date_po,
+ })
+ ],
+ })
+ po.button_confirm()
+
+ line_product_avg = po.order_line.filtered(lambda l: l.product_id == product_avg)
+
+ today = date_delivery
+ picking = po.picking_ids
+ (picking.move_lines
+ .filtered(lambda l: l.purchase_line_id == line_product_avg)
+ .write({'quantity_done': 1.0}))
+
+ picking.button_validate()
+ # 5 Units received at rate 0.7 = 42.86
+ self.assertAlmostEqual(product_avg.standard_price, 42.86)
+
+ today = date_invoice
+ inv = self.env['account.move'].with_context(default_move_type='in_invoice').create({
+ 'move_type': 'in_invoice',
+ 'invoice_date': date_invoice,
+ 'date': date_invoice,
+ 'currency_id': self.eur_currency.id,
+ 'partner_id': self.partner_id.id,
+ 'invoice_line_ids': [
+ (0, 0, {
+ 'name': product_avg.name,
+ 'price_unit': 30.0,
+ 'product_id': product_avg.id,
+ 'purchase_line_id': line_product_avg.id,
+ 'quantity': 1.0,
+ 'account_id': self.stock_input_account.id,
+ })
+ ]
+ })
+
+ inv.action_post()
+
+ for p in patchers:
+ p.stop()
+
+ move_lines = inv.line_ids
+ self.assertEqual(len(move_lines), 2)
+
+ # PAYABLE CHECK
+ payable_line = move_lines.filtered(lambda l: l.account_id.internal_type == 'payable')
+ self.assertEqual(payable_line.amount_currency, -30.0)
+ self.assertAlmostEqual(payable_line.balance, -15.00)
+
+ # PRODUCTS CHECKS
+
+ # DELIVERY DIFFERENCE (AVERAGE)
+ # We ordered a product at 30 EUR valued at 20 USD
+ # We received it when the exchange rate has appreciated
+ # So, the actualized 20 USD are now 20*1.5/0.7 = 42.86 USD
+ product_lines = move_lines.filtered(lambda l: l.product_id == product_avg)
+
+ # Although those 42.86 USD are just due to the exchange difference
+ stock_line = product_lines.filtered(lambda l: l.account_id == self.stock_input_account)
+ self.assertEqual(stock_line.journal_id, inv.journal_id)
+ self.assertEqual(stock_line.amount_currency, 30.00)
+ self.assertAlmostEqual(stock_line.balance, 15.00)
+ full_reconcile = stock_line.full_reconcile_id
+ self.assertTrue(full_reconcile.exists())
+
+ reconciled_lines = full_reconcile.reconciled_line_ids - stock_line
+ self.assertEqual(len(reconciled_lines), 2)
+
+ stock_journal_line = reconciled_lines.filtered(lambda l: l.journal_id == self.stock_journal)
+ self.assertEqual(stock_journal_line.amount_currency, -30.00)
+ self.assertAlmostEqual(stock_journal_line.balance, -42.86)
+
+ exhange_diff_journal = company.currency_exchange_journal_id.exists()
+ exchange_stock_line = reconciled_lines.filtered(lambda l: l.journal_id == exhange_diff_journal)
+ self.assertEqual(exchange_stock_line.amount_currency, 0.00)
+ self.assertAlmostEqual(exchange_stock_line.balance, 27.86)
+
+ def test_average_realtime_with_two_delivery_anglo_saxon_valuation_multicurrency_different_dates(self):
+ """
+ The PO and invoice are in the same foreign currency.
+ The deliveries occur at different times and rates
+ The invoice is created at an even different date
+ This should create a price difference entry.
+ """
+ company = self.env.user.company_id
+ company.anglo_saxon_accounting = True
+ company.currency_id = self.usd_currency
+ exchange_diff_journal = company.currency_exchange_journal_id.exists()
+
+ date_po = '2019-01-01'
+ date_delivery = '2019-01-08'
+ date_delivery1 = '2019-01-10'
+ date_invoice = '2019-01-16'
+ date_invoice1 = '2019-01-20'
+
+ self.product1.categ_id.property_valuation = 'real_time'
+ self.product1.categ_id.property_cost_method = 'average'
+ product_avg = self.product1_copy
+ product_avg.write({
+ 'purchase_method': 'purchase',
+ 'name': 'AVG',
+ 'standard_price': 0,
+ 'property_account_creditor_price_difference': self.price_diff_account.id
+ })
+
+ # SetUp currency and rates
+ 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': 1.5,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+
+ self.env['res.currency.rate'].create({
+ 'name': date_delivery,
+ 'rate': 0.7,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+ self.env['res.currency.rate'].create({
+ 'name': date_delivery1,
+ 'rate': 0.8,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+
+ self.env['res.currency.rate'].create({
+ 'name': date_invoice,
+ 'rate': 2,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+ self.env['res.currency.rate'].create({
+ 'name': date_invoice1,
+ 'rate': 2.2,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+
+ # To allow testing validation of PO and Delivery
+ today = date_po
+ def _today(*args, **kwargs):
+ return datetime.strptime(today, "%Y-%m-%d").date()
+ def _now(*args, **kwargs):
+ return datetime.strptime(today + ' 01:00:00', "%Y-%m-%d %H:%M:%S")
+
+ patchers = [
+ patch('odoo.fields.Date.context_today', _today),
+ patch('odoo.fields.Datetime.now', _now),
+ ]
+
+ for p in patchers:
+ p.start()
+
+ # Proceed
+ po = self.env['purchase.order'].create({
+ 'currency_id': self.eur_currency.id,
+ 'partner_id': self.partner_id.id,
+ 'date_order': date_po,
+ 'order_line': [
+ (0, 0, {
+ 'name': product_avg.name,
+ 'product_id': product_avg.id,
+ 'product_qty': 10.0,
+ 'product_uom': product_avg.uom_po_id.id,
+ 'price_unit': 30.0,
+ 'date_planned': date_po,
+ })
+ ],
+ })
+ po.button_confirm()
+
+ line_product_avg = po.order_line.filtered(lambda l: l.product_id == product_avg)
+
+ today = date_delivery
+ picking = po.picking_ids
+ (picking.move_lines
+ .filtered(lambda l: l.purchase_line_id == line_product_avg)
+ .write({'quantity_done': 5.0}))
+
+ picking.button_validate()
+ picking._action_done() # Create Backorder
+ # 5 Units received at rate 0.7 = 42.86
+ self.assertAlmostEqual(product_avg.standard_price, 42.86)
+
+ today = date_invoice
+ inv = self.env['account.move'].with_context(default_move_type='in_invoice').create({
+ 'move_type': 'in_invoice',
+ 'invoice_date': date_invoice,
+ 'date': date_invoice,
+ 'currency_id': self.eur_currency.id,
+ 'partner_id': self.partner_id.id,
+ 'invoice_line_ids': [
+ (0, 0, {
+ 'name': product_avg.name,
+ 'price_unit': 20.0,
+ 'product_id': product_avg.id,
+ 'purchase_line_id': line_product_avg.id,
+ 'quantity': 5.0,
+ 'account_id': self.stock_input_account.id,
+ })
+ ]
+ })
+
+ inv.action_post()
+
+ today = date_delivery1
+ backorder_picking = self.env['stock.picking'].search([('backorder_id', '=', picking.id)])
+ (backorder_picking.move_lines
+ .filtered(lambda l: l.purchase_line_id == line_product_avg)
+ .write({'quantity_done': 5.0}))
+ backorder_picking.button_validate()
+ # 5 Units received at rate 0.7 (42.86) + 5 Units received at rate 0.8 (37.50) = 40.18
+ self.assertAlmostEqual(product_avg.standard_price, 40.18)
+
+ today = date_invoice1
+ inv1 = self.env['account.move'].with_context(default_move_type='in_invoice').create({
+ 'move_type': 'in_invoice',
+ 'invoice_date': date_invoice1,
+ 'date': date_invoice1,
+ 'currency_id': self.eur_currency.id,
+ 'partner_id': self.partner_id.id,
+ 'invoice_line_ids': [
+ (0, 0, {
+ 'name': product_avg.name,
+ 'price_unit': 40.0,
+ 'product_id': product_avg.id,
+ 'purchase_line_id': line_product_avg.id,
+ 'quantity': 5.0,
+ 'account_id': self.stock_input_account.id,
+ })
+ ]
+ })
+
+ inv1.action_post()
+
+ for p in patchers:
+ p.stop()
+
+ ##########################
+ # Invoice 0 #
+ ##########################
+ move_lines = inv.line_ids
+ self.assertEqual(len(move_lines), 4)
+
+ # PAYABLE CHECK
+ payable_line = move_lines.filtered(lambda l: l.account_id.internal_type == 'payable')
+ self.assertEqual(payable_line.amount_currency, -100.0)
+ self.assertAlmostEqual(payable_line.balance, -50.00)
+
+ # # PRODUCTS CHECKS
+
+ # DELIVERY DIFFERENCE (AVERAGE)
+ stock_lines = move_lines.filtered(lambda l: l.account_id == self.stock_input_account)
+ self.assertEqual(len(stock_lines), 2)
+ self.assertAlmostEqual(sum(stock_lines.mapped('amount_currency')), 150.00)
+ self.assertAlmostEqual(sum(stock_lines.mapped('balance')), 75.00)
+
+ price_diff_line = move_lines.filtered(lambda l: l.account_id == self.price_diff_account)
+ self.assertAlmostEqual(price_diff_line.amount_currency, -50.00)
+ self.assertAlmostEqual(price_diff_line.balance, -25.00)
+
+ full_reconcile = stock_lines.mapped('full_reconcile_id')
+ self.assertTrue(full_reconcile.exists())
+
+ reconciled_lines = full_reconcile.reconciled_line_ids - stock_lines
+ self.assertEqual(len(reconciled_lines), 2)
+
+ stock_journal_line = reconciled_lines.filtered(lambda l: l.journal_id == self.stock_journal)
+ self.assertEqual(stock_journal_line.amount_currency, -150)
+ self.assertAlmostEqual(stock_journal_line.balance, -214.29)
+
+ exchange_stock_line = reconciled_lines.filtered(lambda l: l.journal_id == exchange_diff_journal)
+ self.assertEqual(exchange_stock_line.amount_currency, 0.00)
+ self.assertAlmostEqual(exchange_stock_line.balance, 139.29)
+
+ ##########################
+ # Invoice 1 #
+ ##########################
+ move_lines = inv1.line_ids
+ self.assertEqual(len(move_lines), 4)
+
+ # PAYABLE CHECK
+ payable_line = move_lines.filtered(lambda l: l.account_id.internal_type == 'payable')
+ self.assertEqual(payable_line.amount_currency, -200.0)
+ self.assertAlmostEqual(payable_line.balance, -90.91)
+
+ # # PRODUCTS CHECKS
+
+ # DELIVERY DIFFERENCE (AVERAGE)
+ stock_lines = move_lines.filtered(lambda l: l.account_id == self.stock_input_account)
+ self.assertEqual(stock_lines.mapped('journal_id'), inv.journal_id)
+ self.assertAlmostEqual(sum(stock_lines.mapped('amount_currency')), 150.00)
+ self.assertAlmostEqual(sum(stock_lines.mapped('balance')), 68.18)
+
+ price_diff_line = move_lines.filtered(lambda l: l.account_id == self.price_diff_account)
+ self.assertEqual(price_diff_line.amount_currency, 50.00)
+ self.assertAlmostEqual(price_diff_line.balance, 22.73)
+
+ full_reconcile = stock_lines.mapped('full_reconcile_id')
+ self.assertTrue(full_reconcile.exists())
+
+ reconciled_lines = full_reconcile.reconciled_line_ids - stock_lines
+ self.assertEqual(len(reconciled_lines), 3)
+
+ stock_journal_line = reconciled_lines.filtered(lambda l: l.journal_id == self.stock_journal)
+ self.assertEqual(stock_journal_line.amount_currency, -150)
+ self.assertAlmostEqual(stock_journal_line.balance, -187.5)
+
+ exchange_stock_lines = reconciled_lines.filtered(lambda l: l.journal_id == exchange_diff_journal)
+ self.assertAlmostEqual(sum(exchange_stock_lines.mapped('amount_currency')), 0.00)
+ self.assertAlmostEqual(sum(exchange_stock_lines.mapped('balance')), 119.32)
+
+ def test_anglosaxon_valuation_price_total_diff_discount(self):
+ """
+ PO: price unit: 110
+ Inv: price unit: 100
+ discount: 10
+ """
+ self.env.company.anglo_saxon_accounting = True
+ self.product1.categ_id.property_cost_method = 'fifo'
+ self.product1.categ_id.property_valuation = 'real_time'
+ self.product1.property_account_creditor_price_difference = self.price_diff_account
+
+ # Create PO
+ po_form = Form(self.env['purchase.order'])
+ po_form.partner_id = self.partner_id
+ with po_form.order_line.new() as po_line:
+ po_line.product_id = self.product1
+ po_line.product_qty = 1
+ po_line.price_unit = 110.0
+ order = po_form.save()
+ order.button_confirm()
+
+ # Receive the goods
+ receipt = order.picking_ids[0]
+ receipt.move_lines.quantity_done = 1
+ receipt.button_validate()
+
+ # Create an invoice with a different price and a discount
+ invoice_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
+ invoice_form.invoice_date = invoice_form.date
+ invoice_form.purchase_id = order
+ with invoice_form.invoice_line_ids.edit(0) as line_form:
+ line_form.price_unit = 100.0
+ line_form.discount = 10.0
+ invoice = invoice_form.save()
+ invoice.action_post()
+
+ # Check what was posted in the price difference account
+ price_diff_aml = self.env['account.move.line'].search([('account_id','=', self.price_diff_account.id)])
+ self.assertEqual(len(price_diff_aml), 1, "Only one line should have been generated in the price difference account.")
+ self.assertAlmostEqual(price_diff_aml.credit, 20, "Price difference should be equal to 20 (110-90)")
+
+ # Check what was posted in stock input account
+ input_aml = self.env['account.move.line'].search([('account_id','=', self.stock_input_account.id)])
+ self.assertEqual(len(input_aml), 3, "Only two lines should have been generated in stock input account: one when receiving the product, two when making the invoice.")
+ self.assertAlmostEqual(sum(input_aml.mapped('debit')), 110, "Total debit value on stock input account should be equal to the original PO price of the product.")
+ self.assertAlmostEqual(sum(input_aml.mapped('credit')), 110, "Total credit value on stock input account should be equal to the original PO price of the product.")
+
+ def test_anglosaxon_valuation_discount(self):
+ """
+ PO: price unit: 100
+ Inv: price unit: 100
+ discount: 10
+ """
+ self.env.company.anglo_saxon_accounting = True
+ self.product1.categ_id.property_cost_method = 'fifo'
+ self.product1.categ_id.property_valuation = 'real_time'
+ self.product1.property_account_creditor_price_difference = self.price_diff_account
+
+ # Create PO
+ po_form = Form(self.env['purchase.order'])
+ po_form.partner_id = self.partner_id
+ with po_form.order_line.new() as po_line:
+ po_line.product_id = self.product1
+ po_line.product_qty = 1
+ po_line.price_unit = 100.0
+ order = po_form.save()
+ order.button_confirm()
+
+ # Receive the goods
+ receipt = order.picking_ids[0]
+ receipt.move_lines.quantity_done = 1
+ receipt.button_validate()
+
+ # Create an invoice with a different price and a discount
+ invoice_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
+ invoice_form.invoice_date = invoice_form.date
+ invoice_form.purchase_id = order
+ with invoice_form.invoice_line_ids.edit(0) as line_form:
+ line_form.tax_ids.clear()
+ line_form.discount = 10.0
+ invoice = invoice_form.save()
+ invoice.action_post()
+
+ # Check what was posted in the price difference account
+ price_diff_aml = self.env['account.move.line'].search([('account_id', '=', self.price_diff_account.id)])
+ self.assertEqual(len(price_diff_aml), 1, "Only one line should have been generated in the price difference account.")
+ self.assertAlmostEqual(price_diff_aml.credit, 10, "Price difference should be equal to 10 (100-90)")
+
+ # Check what was posted in stock input account
+ input_aml = self.env['account.move.line'].search([('account_id', '=', self.stock_input_account.id)])
+ self.assertEqual(len(input_aml), 3, "Three lines generated in stock input account: one when receiving the product, two when making the invoice.")
+ self.assertAlmostEqual(sum(input_aml.mapped('debit')), 100, "Total debit value on stock input account should be equal to the original PO price of the product.")
+ self.assertAlmostEqual(sum(input_aml.mapped('credit')), 100, "Total credit value on stock input account should be equal to the original PO price of the product.")
+
+ def test_anglosaxon_valuation_price_unit_diff_discount(self):
+ """
+ PO: price unit: 90
+ Inv: price unit: 100
+ discount: 10
+ """
+ self.env.company.anglo_saxon_accounting = True
+ self.product1.categ_id.property_cost_method = 'fifo'
+ self.product1.categ_id.property_valuation = 'real_time'
+ self.product1.property_account_creditor_price_difference = self.price_diff_account
+
+ # Create PO
+ po_form = Form(self.env['purchase.order'])
+ po_form.partner_id = self.partner_id
+ with po_form.order_line.new() as po_line:
+ po_line.product_id = self.product1
+ po_line.product_qty = 1
+ po_line.price_unit = 90.0
+ order = po_form.save()
+ order.button_confirm()
+
+ # Receive the goods
+ receipt = order.picking_ids[0]
+ receipt.move_lines.quantity_done = 1
+ receipt.button_validate()
+
+ # Create an invoice with a different price and a discount
+ invoice_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
+ invoice_form.invoice_date = invoice_form.date
+ invoice_form.purchase_id = order
+ with invoice_form.invoice_line_ids.edit(0) as line_form:
+ line_form.price_unit = 100.0
+ line_form.discount = 10.0
+ invoice = invoice_form.save()
+ invoice.action_post()
+
+ # Check if something was posted in the price difference account
+ price_diff_aml = self.env['account.move.line'].search([('account_id','=', self.price_diff_account.id)])
+ self.assertEqual(len(price_diff_aml), 0, "No line should have been generated in the price difference account.")
+
+ # Check what was posted in stock input account
+ input_aml = self.env['account.move.line'].search([('account_id','=', self.stock_input_account.id)])
+ self.assertEqual(len(input_aml), 2, "Only two lines should have been generated in stock input account: one when receiving the product, one when making the invoice.")
+ self.assertAlmostEqual(sum(input_aml.mapped('debit')), 90, "Total debit value on stock input account should be equal to the original PO price of the product.")
+ self.assertAlmostEqual(sum(input_aml.mapped('credit')), 90, "Total credit value on stock input account should be equal to the original PO price of the product.")