Files
odoo_source/addons/purchase_stock/tests/test_stockvaluation.py
T
william 82dc0cb7b9 [IMP] account: soft post entries in the future
Add an easy way to not post the entries in the future when calling
post() on it, but rather set it to be auto-posted at accounting date.
This is useful when we are creating a lot of entries in batch and some
might be in the future, some in the past, and we don't want to separate
that in two batch every time. (asset, accrual, transfer,... )
2020-08-05 11:57:10 +00:00

1322 lines
55 KiB
Python

# -*- 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.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.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.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.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.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.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.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.")