summaryrefslogtreecommitdiff
path: root/addons/purchase_stock/tests
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
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/purchase_stock/tests')
-rw-r--r--addons/purchase_stock/tests/__init__.py20
-rw-r--r--addons/purchase_stock/tests/common.py57
-rw-r--r--addons/purchase_stock/tests/test_anglo_saxon_valuation_reconciliation.py212
-rw-r--r--addons/purchase_stock/tests/test_average_price.py132
-rw-r--r--addons/purchase_stock/tests/test_create_picking.py516
-rw-r--r--addons/purchase_stock/tests/test_fifo_price.py366
-rw-r--r--addons/purchase_stock/tests/test_fifo_returns.py89
-rw-r--r--addons/purchase_stock/tests/test_move_cancel_propagation.py293
-rw-r--r--addons/purchase_stock/tests/test_onchange_product.py121
-rw-r--r--addons/purchase_stock/tests/test_product_template.py26
-rw-r--r--addons/purchase_stock/tests/test_purchase_delete_order.py42
-rw-r--r--addons/purchase_stock/tests/test_purchase_lead_time.py341
-rw-r--r--addons/purchase_stock/tests/test_purchase_order.py332
-rw-r--r--addons/purchase_stock/tests/test_purchase_order_process.py29
-rw-r--r--addons/purchase_stock/tests/test_purchase_stock_report.py147
-rw-r--r--addons/purchase_stock/tests/test_reordering_rule.py520
-rw-r--r--addons/purchase_stock/tests/test_replenish_wizard.py249
-rw-r--r--addons/purchase_stock/tests/test_routes.py53
-rw-r--r--addons/purchase_stock/tests/test_stockvaluation.py1328
19 files changed, 4873 insertions, 0 deletions
diff --git a/addons/purchase_stock/tests/__init__.py b/addons/purchase_stock/tests/__init__.py
new file mode 100644
index 00000000..31ce9c71
--- /dev/null
+++ b/addons/purchase_stock/tests/__init__.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import test_anglo_saxon_valuation_reconciliation
+from . import test_average_price
+from . import test_create_picking
+from . import test_fifo_price
+from . import test_fifo_returns
+from . import test_onchange_product
+from . import test_purchase_delete_order
+from . import test_purchase_lead_time
+from . import test_purchase_order
+from . import test_purchase_order_process
+from . import test_purchase_stock_report
+from . import test_stockvaluation
+from . import test_replenish_wizard
+from . import test_reordering_rule
+from . import test_move_cancel_propagation
+from . import test_product_template
+from . import test_routes
diff --git a/addons/purchase_stock/tests/common.py b/addons/purchase_stock/tests/common.py
new file mode 100644
index 00000000..f3a98e04
--- /dev/null
+++ b/addons/purchase_stock/tests/common.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+from datetime import timedelta
+
+from odoo import fields
+from odoo.addons.stock.tests.common2 import TestStockCommon
+from odoo import tools
+from odoo.modules.module import get_module_resource
+
+
+class PurchaseTestCommon(TestStockCommon):
+
+ def _create_make_procurement(self, product, product_qty, date_planned=False):
+ ProcurementGroup = self.env['procurement.group']
+ order_values = {
+ 'warehouse_id': self.warehouse_1,
+ 'action': 'pull_push',
+ 'date_planned': date_planned or fields.Datetime.to_string(fields.datetime.now() + timedelta(days=10)), # 10 days added to current date of procurement to get future schedule date and order date of purchase order.
+ 'group_id': self.env['procurement.group'],
+ }
+ return ProcurementGroup.run([self.env['procurement.group'].Procurement(
+ product, product_qty, self.uom_unit, self.warehouse_1.lot_stock_id,
+ product.name, '/', self.env.company, order_values)
+ ])
+
+ @classmethod
+ def setUpClass(cls):
+ super(PurchaseTestCommon, cls).setUpClass()
+ cls.env.ref('stock.route_warehouse0_mto').active = True
+
+ cls.route_buy = cls.warehouse_1.buy_pull_id.route_id.id
+ cls.route_mto = cls.warehouse_1.mto_pull_id.route_id.id
+
+ # Update product_1 with type, route and Delivery Lead Time
+ cls.product_1.write({
+ 'type': 'product',
+ 'route_ids': [(6, 0, [cls.route_buy, cls.route_mto])],
+ 'seller_ids': [(0, 0, {'name': cls.partner_1.id, 'delay': 5})]})
+
+ cls.t_shirt = cls.env['product.product'].create({
+ 'name': 'T-shirt',
+ 'route_ids': [(6, 0, [cls.route_buy, cls.route_mto])],
+ 'seller_ids': [(0, 0, {'name': cls.partner_1.id, 'delay': 5})]
+ })
+
+ # Update product_2 with type, route and Delivery Lead Time
+ cls.product_2.write({
+ 'type': 'product',
+ 'route_ids': [(6, 0, [cls.route_buy, cls.route_mto])],
+ 'seller_ids': [(0, 0, {'name': cls.partner_1.id, 'delay': 2})]})
+
+ cls.res_users_purchase_user = cls.env['res.users'].create({
+ 'company_id': cls.env.ref('base.main_company').id,
+ 'name': "Purchase User",
+ 'login': "pu",
+ 'email': "purchaseuser@yourcompany.com",
+ 'groups_id': [(6, 0, [cls.env.ref('purchase.group_purchase_user').id])],
+ })
diff --git a/addons/purchase_stock/tests/test_anglo_saxon_valuation_reconciliation.py b/addons/purchase_stock/tests/test_anglo_saxon_valuation_reconciliation.py
new file mode 100644
index 00000000..b54aedd2
--- /dev/null
+++ b/addons/purchase_stock/tests/test_anglo_saxon_valuation_reconciliation.py
@@ -0,0 +1,212 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
+from odoo.tests.common import Form, tagged
+
+
+@tagged('post_install', '-at_install')
+class TestValuationReconciliation(ValuationReconciliationTestCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.stock_account_product_categ.property_account_creditor_price_difference_categ = cls.company_data['default_account_stock_price_diff']
+
+ @classmethod
+ def setup_company_data(cls, company_name, chart_template=None, **kwargs):
+ company_data = super().setup_company_data(company_name, chart_template=chart_template, **kwargs)
+
+ # Create stock config.
+ company_data.update({
+ 'default_account_stock_price_diff': cls.env['account.account'].create({
+ 'name': 'default_account_stock_price_diff',
+ 'code': 'STOCKDIFF',
+ 'reconcile': True,
+ 'user_type_id': cls.env.ref('account.data_account_type_current_assets').id,
+ 'company_id': company_data['company'].id,
+ }),
+ })
+ return company_data
+
+ def _create_purchase(self, product, date, quantity=1.0, set_tax=False, price_unit=66.0):
+ rslt = self.env['purchase.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'order_line': [
+ (0, 0, {
+ 'name': product.name,
+ 'product_id': product.id,
+ 'product_qty': quantity,
+ 'product_uom': product.uom_po_id.id,
+ 'price_unit': price_unit,
+ 'date_planned': date,
+ 'taxes_id': [(6, 0, product.supplier_taxes_id.ids)] if set_tax else False,
+ })],
+ 'date_order': date,
+ })
+ rslt.button_confirm()
+ return rslt
+
+ def _create_invoice_for_po(self, purchase_order, date):
+ move_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice', default_date=date))
+ move_form.invoice_date = date
+ move_form.partner_id = self.partner_a
+ move_form.currency_id = self.currency_data['currency']
+ move_form.purchase_id = purchase_order
+ return move_form.save()
+
+ def test_shipment_invoice(self):
+ """ Tests the case into which we receive the goods first, and then make the invoice.
+ """
+ test_product = self.test_product_delivery
+ date_po_and_delivery = '2018-01-01'
+
+ purchase_order = self._create_purchase(test_product, date_po_and_delivery)
+ self._process_pickings(purchase_order.picking_ids, date=date_po_and_delivery)
+
+ invoice = self._create_invoice_for_po(purchase_order, '2018-02-02')
+ invoice.action_post()
+ picking = self.env['stock.picking'].search([('purchase_id','=',purchase_order.id)])
+ self.check_reconciliation(invoice, picking)
+ # cancel the invoice
+ invoice.button_cancel()
+
+ def test_invoice_shipment(self):
+ """ Tests the case into which we make the invoice first, and then receive the goods.
+ """
+ # Create a PO and an invoice for it
+ test_product = self.test_product_order
+ purchase_order = self._create_purchase(test_product, '2017-12-01')
+
+ invoice = self._create_invoice_for_po(purchase_order, '2017-12-23')
+ move_form = Form(invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.quantity = 1
+ invoice = move_form.save()
+
+ # Validate the invoice and refund the goods
+ invoice.action_post()
+ self._process_pickings(purchase_order.picking_ids, date='2017-12-24')
+ picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)])
+ self.check_reconciliation(invoice, picking)
+
+ # Return the goods and refund the invoice
+ stock_return_picking_form = Form(self.env['stock.return.picking']
+ .with_context(active_ids=picking.ids, active_id=picking.ids[0],
+ active_model='stock.picking'))
+ stock_return_picking = stock_return_picking_form.save()
+ stock_return_picking.product_return_moves.quantity = 1.0
+ stock_return_picking_action = stock_return_picking.create_returns()
+ return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
+ return_pick.action_assign()
+ return_pick.move_lines.quantity_done = 1
+ return_pick._action_done()
+ self._change_pickings_date(return_pick, '2018-01-13')
+
+ # Refund the invoice
+ refund_invoice_wiz = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=[invoice.id]).create({
+ 'reason': 'test_invoice_shipment_refund',
+ 'refund_method': 'cancel',
+ 'date': '2018-03-15',
+ })
+ refund_invoice = self.env['account.move'].browse(refund_invoice_wiz.reverse_moves()['res_id'])
+
+ # Check the result
+ self.assertEqual(invoice.payment_state, 'reversed', "Invoice should be in 'reversed' state")
+ self.assertEqual(refund_invoice.payment_state, 'paid', "Refund should be in 'paid' state")
+ self.check_reconciliation(refund_invoice, return_pick)
+
+ def test_multiple_shipments_invoices(self):
+ """ Tests the case into which we receive part of the goods first, then 2 invoices at different rates, and finally the remaining quantities
+ """
+ test_product = self.test_product_delivery
+ date_po_and_delivery0 = '2017-01-01'
+ purchase_order = self._create_purchase(test_product, date_po_and_delivery0, quantity=5.0)
+ self._process_pickings(purchase_order.picking_ids, quantity=2.0, date=date_po_and_delivery0)
+ picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)], order="id asc", limit=1)
+
+ invoice = self._create_invoice_for_po(purchase_order, '2017-01-15')
+ move_form = Form(invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.quantity = 3.0
+ invoice = move_form.save()
+ invoice.action_post()
+ self.check_reconciliation(invoice, picking, full_reconcile=False)
+
+ invoice2 = self._create_invoice_for_po(purchase_order, '2017-02-15')
+ move_form = Form(invoice2)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.quantity = 2.0
+ invoice2 = move_form.save()
+ invoice2.action_post()
+ self.check_reconciliation(invoice2, picking, full_reconcile=False)
+
+ # We don't need to make the date of processing explicit since the very last rate
+ # will be taken
+ self._process_pickings(purchase_order.picking_ids.filtered(lambda x: x.state != 'done'), quantity=3.0)
+ picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)], order='id desc', limit=1)
+ self.check_reconciliation(invoice2, picking)
+
+ def test_rounding_discount(self):
+ self.env.ref("product.decimal_discount").digits = 5
+ tax_exclude_id = self.env["account.tax"].create(
+ {
+ "name": "Exclude tax",
+ "amount": "0.00",
+ "type_tax_use": "purchase",
+ }
+ )
+
+ test_product = self.test_product_delivery
+ test_product.supplier_taxes_id = [(6, 0, tax_exclude_id.ids)]
+ date_po_and_delivery = '2018-01-01'
+
+ purchase_order = self._create_purchase(test_product, date_po_and_delivery, quantity=10000, set_tax=True)
+ self._process_pickings(purchase_order.picking_ids, date=date_po_and_delivery)
+
+ invoice = self._create_invoice_for_po(purchase_order, '2018-01-01')
+
+ # Set a discount
+ move_form = Form(invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.discount = 0.92431
+ move_form.save()
+
+ invoice.action_post()
+
+ # Check the price difference amount.
+ price_diff_line = invoice.line_ids.filtered(lambda l: l.account_id == self.stock_account_product_categ.property_account_creditor_price_difference_categ)
+ self.assertTrue(len(price_diff_line) == 1, "A price difference line should be created")
+ self.assertAlmostEqual(price_diff_line.price_total, -6100.446)
+
+ picking = self.env['stock.picking'].search([('purchase_id','=',purchase_order.id)])
+ self.check_reconciliation(invoice, picking)
+
+ def test_rounding_price_unit(self):
+ self.env.ref("product.decimal_price").digits = 6
+
+ test_product = self.test_product_delivery
+ date_po_and_delivery = '2018-01-01'
+
+ purchase_order = self._create_purchase(test_product, date_po_and_delivery, quantity=1000000, price_unit=0.0005)
+ self._process_pickings(purchase_order.picking_ids, date=date_po_and_delivery)
+
+ invoice = self._create_invoice_for_po(purchase_order, '2018-01-01')
+
+ # Set a discount
+ move_form = Form(invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.price_unit = 0.0006
+ move_form.save()
+
+ invoice.action_post()
+
+ # Check the price difference amount. It's expected that price_unit * qty != price_total.
+ price_diff_line = invoice.line_ids.filtered(lambda l: l.account_id == self.stock_account_product_categ.property_account_creditor_price_difference_categ)
+ self.assertTrue(len(price_diff_line) == 1, "A price difference line should be created")
+ self.assertAlmostEqual(price_diff_line.price_unit, 0.0001)
+ self.assertAlmostEqual(price_diff_line.price_total, 100.0)
+
+ picking = self.env['stock.picking'].search([('purchase_id','=',purchase_order.id)])
+ self.check_reconciliation(invoice, picking)
diff --git a/addons/purchase_stock/tests/test_average_price.py b/addons/purchase_stock/tests/test_average_price.py
new file mode 100644
index 00000000..031fe0d2
--- /dev/null
+++ b/addons/purchase_stock/tests/test_average_price.py
@@ -0,0 +1,132 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
+from odoo.tests import tagged, Form
+
+import time
+
+
+@tagged('-at_install', 'post_install')
+class TestAveragePrice(ValuationReconciliationTestCommon):
+
+ def test_00_average_price(self):
+ """ Testcase for average price computation"""
+
+ res_partner_3 = self.env['res.partner'].create({
+ 'name': 'Gemini Partner',
+ })
+
+ # Set a product as using average price.
+ product_cable_management_box = self.env['product.product'].create({
+ 'default_code': 'AVG',
+ 'name': 'Average Ice Cream',
+ 'type': 'product',
+ 'categ_id': self.stock_account_product_categ.id,
+ 'list_price': 100.0,
+ 'standard_price': 60.0,
+ 'uom_id': self.env.ref('uom.product_uom_kgm').id,
+ 'uom_po_id': self.env.ref('uom.product_uom_kgm').id,
+ 'supplier_taxes_id': [],
+ 'description': 'FIFO Ice Cream',
+ })
+ product_cable_management_box.categ_id.property_cost_method = 'average'
+
+ # I create a draft Purchase Order for first incoming shipment for 10 pieces at 60€
+ purchase_order_1 = self.env['purchase.order'].create({
+ 'partner_id': res_partner_3.id,
+ 'order_line': [(0, 0, {
+ 'name': 'Average Ice Cream',
+ 'product_id': product_cable_management_box.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.env.ref('uom.product_uom_kgm').id,
+ 'price_unit': 60.0,
+ 'date_planned': time.strftime('%Y-%m-%d'),
+ })]
+ })
+
+ # Confirm the first purchase order
+ purchase_order_1.button_confirm()
+
+ # Check the "Approved" status of purchase order 1
+ self.assertEqual(purchase_order_1.state, 'purchase', "Wrong state of purchase order!")
+
+ # Process the reception of purchase order 1
+ picking = purchase_order_1.picking_ids[0]
+ res = picking.button_validate()
+ Form(self.env[res['res_model']].with_context(res['context'])).save().process()
+
+ # Check the average_price of the product (average icecream).
+ self.assertEqual(product_cable_management_box.qty_available, 10.0, 'Wrong quantity in stock after first reception')
+ self.assertEqual(product_cable_management_box.standard_price, 60.0, 'Standard price should be the price of the first reception!')
+
+ # I create a draft Purchase Order for second incoming shipment for 30 pieces at 80€
+ purchase_order_2 = self.env['purchase.order'].create({
+ 'partner_id': res_partner_3.id,
+ 'order_line': [(0, 0, {
+ 'name': product_cable_management_box.name,
+ 'product_id': product_cable_management_box.id,
+ 'product_qty': 30.0,
+ 'product_uom': self.env.ref('uom.product_uom_kgm').id,
+ 'price_unit': 80.0,
+ 'date_planned': time.strftime('%Y-%m-%d'),
+ })]
+ })
+
+ # Confirm the second purchase order
+ purchase_order_2.button_confirm()
+ # Process the reception of purchase order 2
+ picking = purchase_order_2.picking_ids[0]
+ res = picking.button_validate()
+ Form(self.env['stock.immediate.transfer'].with_context(res['context'])).save().process()
+
+ # Check the standard price
+ self.assertEqual(product_cable_management_box.standard_price, 75.0, 'After second reception, we should have an average price of 75.0 on the product')
+
+ # Create picking to send some goods
+ outgoing_shipment = self.env['stock.picking'].create({
+ 'picking_type_id': self.company_data['default_warehouse'].out_type_id.id,
+ 'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id,
+ 'move_lines': [(0, 0, {
+ 'name': 'outgoing_shipment_avg_move',
+ 'product_id': product_cable_management_box.id,
+ 'product_uom_qty': 20.0,
+ 'product_uom': self.env.ref('uom.product_uom_kgm').id,
+ 'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id})]
+ })
+
+ # Assign this outgoing shipment and process the delivery
+ outgoing_shipment.action_assign()
+ res = outgoing_shipment.button_validate()
+ Form(self.env['stock.immediate.transfer'].with_context(res['context'])).save().process()
+
+ # Check the average price (60 * 10 + 30 * 80) / 40 = 75.0€ did not change
+ self.assertEqual(product_cable_management_box.standard_price, 75.0, 'Average price should not have changed with outgoing picking!')
+ self.assertEqual(product_cable_management_box.qty_available, 20.0, 'Pieces were not picked correctly as the quantity on hand is wrong')
+
+ # Make a new purchase order with 500 g Average Ice Cream at a price of 0.2€/g
+ purchase_order_3 = self.env['purchase.order'].create({
+ 'partner_id': res_partner_3.id,
+ 'order_line': [(0, 0, {
+ 'name': product_cable_management_box.name,
+ 'product_id': product_cable_management_box.id,
+ 'product_qty': 500.0,
+ 'product_uom': self.ref('uom.product_uom_gram'),
+ 'price_unit': 0.2,
+ 'date_planned': time.strftime('%Y-%m-%d'),
+ })]
+ })
+
+ # Confirm the first purchase order
+ purchase_order_3.button_confirm()
+ # Process the reception of purchase order 3 in grams
+
+ picking = purchase_order_3.picking_ids[0]
+ res = picking.button_validate()
+ Form(self.env[res['res_model']].with_context(res['context'])).save().process()
+
+ # Check price is (75.0 * 20 + 200*0.5) / 20.5 = 78.04878€
+ self.assertEqual(product_cable_management_box.qty_available, 20.5, 'Reception of purchase order in grams leads to wrong quantity in stock')
+ self.assertEqual(round(product_cable_management_box.standard_price, 2), 78.05,
+ 'Standard price as average price of third reception with other UoM incorrect! Got %s instead of 78.05' % (round(product_cable_management_box.standard_price, 2)))
diff --git a/addons/purchase_stock/tests/test_create_picking.py b/addons/purchase_stock/tests/test_create_picking.py
new file mode 100644
index 00000000..9ab50671
--- /dev/null
+++ b/addons/purchase_stock/tests/test_create_picking.py
@@ -0,0 +1,516 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import date, datetime, timedelta
+
+from odoo.addons.mail.tests.common import mail_new_test_user
+from odoo.addons.product.tests import common
+from odoo.tests import Form
+from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
+
+
+class TestCreatePicking(common.TestProductCommon):
+
+ def setUp(self):
+ super(TestCreatePicking, self).setUp()
+ self.partner_id = self.env['res.partner'].create({'name': 'Wood Corner Partner'})
+ self.product_id_1 = self.env['product.product'].create({'name': 'Large Desk'})
+ self.product_id_2 = self.env['product.product'].create({'name': 'Conference Chair'})
+
+ self.user_purchase_user = mail_new_test_user(
+ self.env,
+ name='Pauline Poivraisselle',
+ login='pauline',
+ email='pur@example.com',
+ notification_type='inbox',
+ groups='purchase.group_purchase_user',
+ )
+
+ self.po_vals = {
+ 'partner_id': self.partner_id.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': self.product_id_1.name,
+ 'product_id': self.product_id_1.id,
+ 'product_qty': 5.0,
+ 'product_uom': self.product_id_1.uom_po_id.id,
+ 'price_unit': 500.0,
+ 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
+ })],
+ }
+
+ def test_00_create_picking(self):
+
+ # Draft purchase order created
+ self.po = self.env['purchase.order'].create(self.po_vals)
+ self.assertTrue(self.po, 'Purchase: no purchase order created')
+
+ # Purchase order confirm
+ self.po.button_confirm()
+ self.assertEqual(self.po.state, 'purchase', 'Purchase: PO state should be "Purchase')
+ self.assertEqual(self.po.picking_count, 1, 'Purchase: one picking should be created')
+ self.assertEqual(len(self.po.order_line.move_ids), 1, 'One move should be created')
+ # Change purchase order line product quantity
+ self.po.order_line.write({'product_qty': 7.0})
+ self.assertEqual(len(self.po.order_line.move_ids), 1, 'The two moves should be merged in one')
+
+ # Validate first shipment
+ self.picking = self.po.picking_ids[0]
+ for ml in self.picking.move_line_ids:
+ ml.qty_done = ml.product_uom_qty
+ self.picking._action_done()
+ self.assertEqual(self.po.order_line.mapped('qty_received'), [7.0], 'Purchase: all products should be received')
+
+
+ # create new order line
+ self.po.write({'order_line': [
+ (0, 0, {
+ 'name': self.product_id_2.name,
+ 'product_id': self.product_id_2.id,
+ 'product_qty': 5.0,
+ 'product_uom': self.product_id_2.uom_po_id.id,
+ 'price_unit': 250.0,
+ 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
+ })]})
+ self.assertEqual(self.po.picking_count, 2, 'New picking should be created')
+ moves = self.po.order_line.mapped('move_ids').filtered(lambda x: x.state not in ('done', 'cancel'))
+ self.assertEqual(len(moves), 1, 'One moves should have been created')
+
+ def test_01_check_double_validation(self):
+
+ # make double validation two step
+ self.env.company.write({'po_double_validation': 'two_step','po_double_validation_amount':2000.00})
+
+ # Draft purchase order created
+ self.po = self.env['purchase.order'].with_user(self.user_purchase_user).create(self.po_vals)
+ self.assertTrue(self.po, 'Purchase: no purchase order created')
+
+ # Purchase order confirm
+ self.po.button_confirm()
+ self.assertEqual(self.po.state, 'to approve', 'Purchase: PO state should be "to approve".')
+
+ # PO approved by manager
+ self.po.env.user.groups_id += self.env.ref("purchase.group_purchase_manager")
+ self.po.button_approve()
+ self.assertEqual(self.po.state, 'purchase', 'PO state should be "Purchase".')
+
+ def test_02_check_mto_chain(self):
+ """ Simulate a mto chain with a purchase order. Cancel the
+ purchase order should also change the procure_method of the
+ following move to MTS in order to be able to link it to a
+ manually created purchase order.
+ """
+ stock_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_stock')
+ customer_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_customers')
+ # route buy should be there by default
+ partner = self.env['res.partner'].create({
+ 'name': 'Jhon'
+ })
+
+ vendor = self.env['res.partner'].create({
+ 'name': 'Roger'
+ })
+
+ seller = self.env['product.supplierinfo'].create({
+ 'name': partner.id,
+ 'price': 12.0,
+ })
+
+ product = self.env['product.product'].create({
+ 'name': 'product',
+ 'type': 'product',
+ 'route_ids': [(4, self.ref('stock.route_warehouse0_mto')), (4, self.ref('purchase_stock.route_warehouse0_buy'))],
+ 'seller_ids': [(6, 0, [seller.id])],
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ 'supplier_taxes_id': [(6, 0, [])],
+ })
+
+ customer_move = self.env['stock.move'].create({
+ 'name': 'move out',
+ 'location_id': stock_location.id,
+ 'location_dest_id': customer_location.id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 100.0,
+ 'procure_method': 'make_to_order',
+ })
+
+ customer_move._action_confirm()
+
+ purchase_order = self.env['purchase.order'].search([('partner_id', '=', partner.id)])
+ self.assertTrue(purchase_order, 'No purchase order created.')
+
+ # Check purchase order line data.
+ purchase_order_line = purchase_order.order_line
+ self.assertEqual(purchase_order_line.product_id, product, 'The product on the purchase order line is not correct.')
+ self.assertEqual(purchase_order_line.price_unit, seller.price, 'The purchase order line price should be the same as the seller.')
+ self.assertEqual(purchase_order_line.product_qty, customer_move.product_uom_qty, 'The purchase order line qty should be the same as the move.')
+ self.assertEqual(purchase_order_line.price_subtotal, 1200.0, 'The purchase order line subtotal should be equal to the move qty * seller price.')
+
+ purchase_order.button_cancel()
+ self.assertEqual(purchase_order.state, 'cancel', 'Purchase order should be cancelled.')
+ self.assertEqual(customer_move.procure_method, 'make_to_stock', 'Customer move should be passed to mts.')
+
+ purchase = purchase_order.create({
+ 'partner_id': vendor.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': product.name,
+ 'product_id': product.id,
+ 'product_qty': 100.0,
+ 'product_uom': product.uom_po_id.id,
+ 'price_unit': 11.0,
+ 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
+ })],
+ })
+ self.assertTrue(purchase, 'RFQ should be created')
+ purchase.button_confirm()
+
+ picking = purchase.picking_ids
+ self.assertTrue(picking, 'Picking should be created')
+
+ # Process pickings
+ picking.action_confirm()
+ picking.move_lines.quantity_done = 100.0
+ picking.button_validate()
+
+ # mts move will be automatically assigned
+ self.assertEqual(customer_move.state, 'assigned', 'Automatically assigned due to the incoming move makes it available.')
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(product, stock_location), 0.0, 'Wrong quantity in stock.')
+
+ def test_03_uom(self):
+ """ Buy a dozen of products stocked in units. Check that the quantities on the purchase order
+ lines as well as the received quantities are handled in dozen while the moves themselves
+ are handled in units. Edit the ordered quantities, check that the quantites are correctly
+ updated on the moves. Edit the ir.config_parameter to propagate the uom of the purchase order
+ lines to the moves and edit a last time the ordered quantities. Receive, check the quantities.
+ """
+ uom_unit = self.env.ref('uom.product_uom_unit')
+ uom_dozen = self.env.ref('uom.product_uom_dozen')
+
+ self.assertEqual(self.product_id_1.uom_po_id.id, uom_unit.id)
+
+ # buy a dozen
+ po = self.env['purchase.order'].create(self.po_vals)
+
+ po.order_line.product_qty = 1
+ po.order_line.product_uom = uom_dozen.id
+ po.button_confirm()
+
+ # the move should be 12 units
+ # note: move.product_qty = computed field, always in the uom of the quant
+ # move.product_uom_qty = stored field representing the initial demand in move.product_uom
+ move1 = po.picking_ids.move_lines.sorted()[0]
+ self.assertEqual(move1.product_uom_qty, 12)
+ self.assertEqual(move1.product_uom.id, uom_unit.id)
+ self.assertEqual(move1.product_qty, 12)
+
+ # edit the so line, sell 2 dozen, the move should now be 24 units
+ po.order_line.product_qty = 2
+ move1 = po.picking_ids.move_lines.sorted()[0]
+ self.assertEqual(move1.product_uom_qty, 24)
+ self.assertEqual(move1.product_uom.id, uom_unit.id)
+ self.assertEqual(move1.product_qty, 24)
+
+ # force the propagation of the uom, sell 3 dozen
+ self.env['ir.config_parameter'].sudo().set_param('stock.propagate_uom', '1')
+ po.order_line.product_qty = 3
+ move2 = po.picking_ids.move_lines.filtered(lambda m: m.product_uom.id == uom_dozen.id)
+ self.assertEqual(move2.product_uom_qty, 1)
+ self.assertEqual(move2.product_uom.id, uom_dozen.id)
+ self.assertEqual(move2.product_qty, 12)
+
+ # deliver everything
+ move1.quantity_done = 24
+ move2.quantity_done = 1
+ po.picking_ids.button_validate()
+
+ # check the delivered quantity
+ self.assertEqual(po.order_line.qty_received, 3.0)
+
+ def test_04_mto_multiple_po(self):
+ """ Simulate a mto chain with 2 purchase order.
+ Create a move with qty 1, confirm the RFQ then create a new
+ move that will not be merged in the first one(simulate an increase
+ order quantity on a SO). It should generate a new RFQ, validate
+ and receipt the picking then try to reserve the delivery
+ picking.
+ """
+ stock_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_stock')
+ customer_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_customers')
+ picking_type_out = self.env['ir.model.data'].xmlid_to_object('stock.picking_type_out')
+ # route buy should be there by default
+ partner = self.env['res.partner'].create({
+ 'name': 'Jhon'
+ })
+
+ seller = self.env['product.supplierinfo'].create({
+ 'name': partner.id,
+ 'price': 12.0,
+ })
+
+ product = self.env['product.product'].create({
+ 'name': 'product',
+ 'type': 'product',
+ 'route_ids': [(4, self.ref('stock.route_warehouse0_mto')), (4, self.ref('purchase_stock.route_warehouse0_buy'))],
+ 'seller_ids': [(6, 0, [seller.id])],
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+
+ # A picking is require since only moves inside the same picking are merged.
+ customer_picking = self.env['stock.picking'].create({
+ 'location_id': stock_location.id,
+ 'location_dest_id': customer_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': picking_type_out.id,
+ })
+
+ customer_move = self.env['stock.move'].create({
+ 'name': 'move out',
+ 'location_id': stock_location.id,
+ 'location_dest_id': customer_location.id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 80.0,
+ 'procure_method': 'make_to_order',
+ 'picking_id': customer_picking.id,
+ })
+
+ customer_move._action_confirm()
+
+ purchase_order = self.env['purchase.order'].search([('partner_id', '=', partner.id)])
+ self.assertTrue(purchase_order, 'No purchase order created.')
+
+ # Check purchase order line data.
+ purchase_order_line = purchase_order.order_line
+ self.assertEqual(purchase_order_line.product_id, product, 'The product on the purchase order line is not correct.')
+ self.assertEqual(purchase_order_line.price_unit, seller.price, 'The purchase order line price should be the same as the seller.')
+ self.assertEqual(purchase_order_line.product_qty, customer_move.product_uom_qty, 'The purchase order line qty should be the same as the move.')
+
+ purchase_order.button_confirm()
+
+ customer_move_2 = self.env['stock.move'].create({
+ 'name': 'move out',
+ 'location_id': stock_location.id,
+ 'location_dest_id': customer_location.id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 20.0,
+ 'procure_method': 'make_to_order',
+ 'picking_id': customer_picking.id,
+ })
+
+ customer_move_2._action_confirm()
+
+ self.assertTrue(customer_move_2.exists(), 'The second customer move should not be merged in the first.')
+ self.assertEqual(sum(customer_picking.move_lines.mapped('product_uom_qty')), 100.0)
+
+ purchase_order_2 = self.env['purchase.order'].search([('partner_id', '=', partner.id), ('state', '=', 'draft')])
+ self.assertTrue(purchase_order_2, 'No purchase order created.')
+
+ purchase_order_2.button_confirm()
+
+ purchase_order.picking_ids.move_lines.quantity_done = 80.0
+ purchase_order.picking_ids.button_validate()
+
+ purchase_order_2.picking_ids.move_lines.quantity_done = 20.0
+ purchase_order_2.picking_ids.button_validate()
+
+ self.assertEqual(sum(customer_picking.move_lines.mapped('reserved_availability')), 100.0, 'The total quantity for the customer move should be available and reserved.')
+
+ def test_04_rounding(self):
+ """ We set the Unit(s) rounding to 1.0 and ensure buying 1.2 units in a PO is rounded to 1.0
+ at reception.
+ """
+ uom_unit = self.env.ref('uom.product_uom_unit')
+ uom_unit.rounding = 1.0
+
+ # buy a dozen
+ po = self.env['purchase.order'].create(self.po_vals)
+
+ po.order_line.product_qty = 1.2
+ po.button_confirm()
+
+ # the move should be 1.0 units
+ move1 = po.picking_ids.move_lines[0]
+ self.assertEqual(move1.product_uom_qty, 1.0)
+ self.assertEqual(move1.product_uom.id, uom_unit.id)
+ self.assertEqual(move1.product_qty, 1.0)
+
+ # edit the so line, buy 2.4 units, the move should now be 2.0 units
+ po.order_line.product_qty = 2.0
+ self.assertEqual(move1.product_uom_qty, 2.0)
+ self.assertEqual(move1.product_uom.id, uom_unit.id)
+ self.assertEqual(move1.product_qty, 2.0)
+
+ # deliver everything
+ move1.quantity_done = 2.0
+ po.picking_ids.button_validate()
+
+ # check the delivered quantity
+ self.assertEqual(po.order_line.qty_received, 2.0)
+
+ def test_05_uom_rounding(self):
+ """ We set the Unit(s) and Dozen(s) rounding to 1.0 and ensure buying 1.3 dozens in a PO is
+ rounded to 1.0 at reception.
+ """
+ uom_unit = self.env.ref('uom.product_uom_unit')
+ uom_dozen = self.env.ref('uom.product_uom_dozen')
+ uom_unit.rounding = 1.0
+ uom_dozen.rounding = 1.0
+
+ # buy 1.3 dozen
+ po = self.env['purchase.order'].create(self.po_vals)
+
+ po.order_line.product_qty = 1.3
+ po.order_line.product_uom = uom_dozen.id
+ po.button_confirm()
+
+ # the move should be 16.0 units
+ move1 = po.picking_ids.move_lines[0]
+ self.assertEqual(move1.product_uom_qty, 16.0)
+ self.assertEqual(move1.product_uom.id, uom_unit.id)
+ self.assertEqual(move1.product_qty, 16.0)
+
+ # force the propagation of the uom, buy 2.6 dozens, the move 2 should have 2 dozens
+ self.env['ir.config_parameter'].sudo().set_param('stock.propagate_uom', '1')
+ po.order_line.product_qty = 2.6
+ move2 = po.picking_ids.move_lines.filtered(lambda m: m.product_uom.id == uom_dozen.id)
+ self.assertEqual(move2.product_uom_qty, 2)
+ self.assertEqual(move2.product_uom.id, uom_dozen.id)
+ self.assertEqual(move2.product_qty, 24)
+
+ def create_delivery_order(self):
+ stock_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_stock')
+ customer_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_customers')
+ unit = self.ref("uom.product_uom_unit")
+ picking_type_out = self.env['ir.model.data'].xmlid_to_object('stock.picking_type_out')
+ partner = self.env['res.partner'].create({'name': 'AAA', 'email': 'from.test@example.com'})
+ supplier_info1 = self.env['product.supplierinfo'].create({
+ 'name': partner.id,
+ 'price': 50,
+ })
+
+ warehouse1 = self.env.ref('stock.warehouse0')
+ route_buy = warehouse1.buy_pull_id.route_id
+ route_mto = warehouse1.mto_pull_id.route_id
+
+ product = self.env['product.product'].create({
+ 'name': 'Usb Keyboard',
+ 'type': 'product',
+ 'uom_id': unit,
+ 'uom_po_id': unit,
+ 'seller_ids': [(6, 0, [supplier_info1.id])],
+ 'route_ids': [(6, 0, [route_buy.id, route_mto.id])]
+ })
+
+ delivery_order = self.env['stock.picking'].create({
+ 'location_id': stock_location.id,
+ 'location_dest_id': customer_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': picking_type_out.id,
+ })
+
+ customer_move = self.env['stock.move'].create({
+ 'name': 'move out',
+ 'location_id': stock_location.id,
+ 'location_dest_id': customer_location.id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 10.0,
+ 'procure_method': 'make_to_order',
+ 'picking_id': delivery_order.id,
+ })
+
+ customer_move._action_confirm()
+ # find created po the product
+ purchase_order = self.env['purchase.order'].search([('partner_id', '=', partner.id)])
+
+ return delivery_order, purchase_order
+
+ def test_05_propagate_deadline(self):
+ """ In order to check deadline date of the delivery order is changed and the planned date not."""
+
+ # Create Delivery Order and with propagate date and minimum delta
+ delivery_order, purchase_order = self.create_delivery_order()
+
+ # check po is created or not
+ self.assertTrue(purchase_order, 'No purchase order created.')
+
+ purchase_order_line = purchase_order.order_line
+
+ # change scheduled date of po line.
+ purchase_order_line.write({'date_planned': purchase_order_line.date_planned + timedelta(days=5)})
+
+ # Now check scheduled date and deadline of delivery order.
+ self.assertNotEqual(
+ purchase_order_line.date_planned, delivery_order.scheduled_date,
+ 'Scheduled delivery order date should not changed.')
+ self.assertEqual(
+ purchase_order_line.date_planned, delivery_order.date_deadline,
+ 'Delivery deadline date should be changed.')
+
+ def test_07_differed_schedule_date(self):
+ warehouse = self.env['stock.warehouse'].search([], limit=1)
+
+ with Form(warehouse) as w:
+ w.reception_steps = 'three_steps'
+ po_form = Form(self.env['purchase.order'])
+ po_form.partner_id = self.partner_id
+ with po_form.order_line.new() as line:
+ line.product_id = self.product_id_1
+ line.date_planned = datetime.today()
+ line.product_qty = 1.0
+ with po_form.order_line.new() as line:
+ line.product_id = self.product_id_1
+ line.date_planned = datetime.today() + timedelta(days=7)
+ line.product_qty = 1.0
+ po = po_form.save()
+
+ po.button_approve()
+
+ po.picking_ids.move_line_ids.write({
+ 'qty_done': 1.0
+ })
+ po.picking_ids.button_validate()
+
+ pickings = self.env['stock.picking'].search([('group_id', '=', po.group_id.id)])
+ for picking in pickings:
+ self.assertEqual(picking.scheduled_date.date(), date.today())
+
+ def test_update_quantity_and_return(self):
+ po = self.env['purchase.order'].create(self.po_vals)
+
+ po.order_line.product_qty = 10
+ po.button_confirm()
+
+ first_picking = po.picking_ids
+ first_picking.move_lines.quantity_done = 5
+ # create the backorder
+ backorder_wizard_dict = first_picking.button_validate()
+ backorder_wizard = Form(self.env[backorder_wizard_dict['res_model']].with_context(backorder_wizard_dict['context'])).save()
+ backorder_wizard.process()
+
+ self.assertEqual(len(po.picking_ids), 2)
+
+ # Create a partial return
+ stock_return_picking_form = Form(
+ self.env['stock.return.picking'].with_context(
+ active_ids=first_picking.ids,
+ active_id=first_picking.ids[0],
+ active_model='stock.picking'
+ )
+ )
+ stock_return_picking = stock_return_picking_form.save()
+ stock_return_picking.product_return_moves.quantity = 2.0
+ stock_return_picking_action = stock_return_picking.create_returns()
+ return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
+ return_pick.action_assign()
+ return_pick.move_lines.quantity_done = 2
+ return_pick._action_done()
+
+ self.assertEqual(po.order_line.qty_received, 3)
+
+ po.order_line.product_qty += 2
+ backorder = po.picking_ids.filtered(lambda picking: picking.state == 'assigned')
+ self.assertEqual(backorder.move_lines.product_uom_qty, 9)
diff --git a/addons/purchase_stock/tests/test_fifo_price.py b/addons/purchase_stock/tests/test_fifo_price.py
new file mode 100644
index 00000000..848d967c
--- /dev/null
+++ b/addons/purchase_stock/tests/test_fifo_price.py
@@ -0,0 +1,366 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
+from odoo.tests import tagged, Form
+
+import time
+
+
+@tagged('-at_install', 'post_install')
+class TestFifoPrice(ValuationReconciliationTestCommon):
+
+ def test_00_test_fifo(self):
+ """ Test product cost price with fifo removal strategy."""
+
+ res_partner_3 = self.env['res.partner'].create({
+ 'name': 'Gemini Partner',
+ })
+
+ # Set a product as using fifo price
+ product_cable_management_box = self.env['product.product'].create({
+ 'default_code': 'FIFO',
+ 'name': 'FIFO Ice Cream',
+ 'type': 'product',
+ 'categ_id': self.stock_account_product_categ.id,
+ 'list_price': 100.0,
+ 'standard_price': 70.0,
+ 'uom_id': self.env.ref('uom.product_uom_kgm').id,
+ 'uom_po_id': self.env.ref('uom.product_uom_kgm').id,
+ 'supplier_taxes_id': [],
+ 'description': 'FIFO Ice Cream',
+ })
+
+ # I create a draft Purchase Order for first in move for 10 kg at 50 euro
+ purchase_order_1 = self.env['purchase.order'].create({
+ 'partner_id': res_partner_3.id,
+ 'order_line': [(0, 0, {
+ 'name': 'FIFO Ice Cream',
+ 'product_id': product_cable_management_box.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.env.ref('uom.product_uom_kgm').id,
+ 'price_unit': 50.0,
+ 'date_planned': time.strftime('%Y-%m-%d')})],
+ })
+
+ # Confirm the first purchase order
+ purchase_order_1.button_confirm()
+
+ # Check the "Purchase" status of purchase order 1
+ self.assertEqual(purchase_order_1.state, 'purchase')
+
+ # Process the reception of purchase order 1 and set date
+ picking = purchase_order_1.picking_ids[0]
+ res = picking.button_validate()
+ Form(self.env[res['res_model']].with_context(res['context'])).save().process()
+
+ # Check the standard price of the product (fifo icecream), that should have changed
+ # because the unit cost of the purchase order is 50
+ self.assertAlmostEqual(product_cable_management_box.standard_price, 50.0)
+ self.assertEqual(product_cable_management_box.value_svl, 500.0, 'Wrong stock value')
+
+ # I create a draft Purchase Order for second shipment for 30 kg at 80 euro
+ purchase_order_2 = self.env['purchase.order'].create({
+ 'partner_id': res_partner_3.id,
+ 'order_line': [(0, 0, {
+ 'name': 'FIFO Ice Cream',
+ 'product_id': product_cable_management_box.id,
+ 'product_qty': 30.0,
+ 'product_uom': self.env.ref('uom.product_uom_kgm').id,
+ 'price_unit': 80.0,
+ 'date_planned': time.strftime('%Y-%m-%d')})],
+ })
+
+ # Confirm the second purchase order
+ purchase_order_2.button_confirm()
+
+ # Process the reception of purchase order 2
+ picking = purchase_order_2.picking_ids[0]
+ res = picking.button_validate()
+ Form(self.env[res['res_model']].with_context(res['context'])).save().process()
+
+ # Check the standard price of the product, that should have not changed because we
+ # still have icecream in stock
+ self.assertEqual(product_cable_management_box.standard_price, 50.0, 'Standard price as fifo price of second reception incorrect!')
+ self.assertEqual(product_cable_management_box.value_svl, 2900.0, 'Stock valuation should be 2900')
+
+ # Let us send some goods
+ outgoing_shipment = self.env['stock.picking'].create({
+ 'picking_type_id': self.company_data['default_warehouse'].out_type_id.id,
+ 'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id,
+ 'move_lines': [(0, 0, {
+ 'name': product_cable_management_box.name,
+ 'product_id': product_cable_management_box.id,
+ 'product_uom_qty': 20.0,
+ 'product_uom': self.env.ref('uom.product_uom_kgm').id,
+ 'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id,
+ 'picking_type_id': self.company_data['default_warehouse'].out_type_id.id})]
+ })
+
+ # I assign this outgoing shipment
+ outgoing_shipment.action_assign()
+
+ # Process the delivery of the outgoing shipment
+ res = outgoing_shipment.button_validate()
+ Form(self.env[res['res_model']].with_context(res['context'])).save().process()
+
+ # Check stock value became 1600 .
+ self.assertEqual(product_cable_management_box.value_svl, 1600.0, 'Stock valuation should be 1600')
+
+ # Do a delivery of an extra 500 g (delivery order)
+ outgoing_shipment_uom = self.env['stock.picking'].create({
+ 'picking_type_id': self.company_data['default_warehouse'].out_type_id.id,
+ 'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id,
+ 'move_lines': [(0, 0, {
+ 'name': product_cable_management_box.name,
+ 'product_id': product_cable_management_box.id,
+ 'product_uom_qty': 500.0,
+ 'product_uom': self.env.ref('uom.product_uom_gram').id,
+ 'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id,
+ 'picking_type_id': self.company_data['default_warehouse'].out_type_id.id})]
+ })
+
+ # I assign this outgoing shipment
+ outgoing_shipment_uom.action_assign()
+
+ # Process the delivery of the outgoing shipment
+ res = outgoing_shipment_uom.button_validate()
+ Form(self.env[res['res_model']].with_context(res['context'])).save().process()
+
+ # Check stock valuation and qty in stock
+ self.assertEqual(product_cable_management_box.value_svl, 1560.0, 'Stock valuation should be 1560')
+ self.assertEqual(product_cable_management_box.qty_available, 19.5, 'Should still have 19.5 in stock')
+
+ # We will temporarily change the currency rate on the sixth of June to have the same results all year
+ NewUSD = self.env['res.currency'].create({
+ 'name': 'new_usd',
+ 'symbol': '$²',
+ 'rate_ids': [(0, 0, {'rate': 1.2834, 'name': time.strftime('%Y-%m-%d')})],
+ })
+
+ # Create PO for 30000 g at 0.150$/g and 10 kg at 150$/kg
+ purchase_order_usd = self.env['purchase.order'].create({
+ 'partner_id': res_partner_3.id,
+ 'currency_id': NewUSD.id,
+ 'order_line': [(0, 0, {
+ 'name': 'FIFO Ice Cream',
+ 'product_id': product_cable_management_box.id,
+ 'product_qty': 30,
+ 'product_uom': self.env.ref('uom.product_uom_kgm').id,
+ 'price_unit': 0.150,
+ 'date_planned': time.strftime('%Y-%m-%d')}),
+ (0, 0, {
+ 'name': product_cable_management_box.name,
+ 'product_id': product_cable_management_box.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.env.ref('uom.product_uom_kgm').id,
+ 'price_unit': 150.0,
+ 'date_planned': time.strftime('%Y-%m-%d')})]
+ })
+
+ # Confirm the purchase order in USD
+ purchase_order_usd.button_confirm()
+ # Process the reception of purchase order with USD
+ picking = purchase_order_usd.picking_ids[0]
+ res = picking.button_validate()
+ Form(self.env[res['res_model']].with_context(res['context'])).save().process()
+
+ # Create delivery order of 49.5 kg
+ outgoing_shipment_cur = self.env['stock.picking'].create({
+ 'picking_type_id': self.company_data['default_warehouse'].out_type_id.id,
+ 'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id,
+ 'move_lines': [(0, 0, {
+ 'name': product_cable_management_box.name,
+ 'product_id': product_cable_management_box.id,
+ 'product_uom_qty': 49.5,
+ 'product_uom': self.env.ref('uom.product_uom_kgm').id,
+ 'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id,
+ 'picking_type_id': self.company_data['default_warehouse'].out_type_id.id})]
+ })
+
+ # I assign this outgoing shipment
+ outgoing_shipment_cur.action_assign()
+
+ # Process the delivery of the outgoing shipment
+ res = outgoing_shipment_cur.button_validate()
+ Form(self.env[res['res_model']].with_context(res['context'])).save().process()
+
+ # Do a delivery of an extra 10 kg
+ outgoing_shipment_ret = self.env['stock.picking'].create({
+ 'picking_type_id': self.company_data['default_warehouse'].out_type_id.id,
+ 'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id,
+ 'move_lines': [(0, 0, {
+ 'name': product_cable_management_box.name,
+ 'product_id': product_cable_management_box.id,
+ 'product_uom_qty': 10,
+ 'product_uom': self.env.ref('uom.product_uom_kgm').id,
+ 'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id,
+ 'picking_type_id': self.company_data['default_warehouse'].out_type_id.id})]
+ })
+
+ # I assign this outgoing shipment
+ outgoing_shipment_ret.action_assign()
+ res = outgoing_shipment_ret.button_validate()
+ Form(self.env[res['res_model']].with_context(res['context'])).save().process()
+
+ # Check rounded price is 150.0 / 1.2834
+ self.assertEqual(round(product_cable_management_box.qty_available), 0.0, 'Wrong quantity in stock after first reception.')
+
+ # Let us create some outs to get negative stock for a new product using the same config
+ product_fifo_negative = self.env['product.product'].create({
+ 'default_code': 'NEG',
+ 'name': 'FIFO Negative',
+ 'type': 'product',
+ 'categ_id': self.stock_account_product_categ.id,
+ 'list_price': 100.0,
+ 'standard_price': 70.0,
+ 'uom_id': self.env.ref('uom.product_uom_kgm').id,
+ 'uom_po_id': self.env.ref('uom.product_uom_kgm').id,
+ 'supplier_taxes_id': [],
+ 'description': 'FIFO Ice Cream',
+ })
+
+ # Create outpicking.create delivery order of 100 kg.
+ outgoing_shipment_neg = self.env['stock.picking'].create({
+ 'picking_type_id': self.company_data['default_warehouse'].out_type_id.id,
+ 'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id,
+ 'move_lines': [(0, 0, {
+ 'name': product_fifo_negative.name,
+ 'product_id': product_fifo_negative.id,
+ 'product_uom_qty': 100,
+ 'product_uom': self.env.ref('uom.product_uom_kgm').id,
+ 'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id,
+ 'picking_type_id': self.company_data['default_warehouse'].out_type_id.id})]
+ })
+
+ # Process the delivery of the first outgoing shipment
+ outgoing_shipment_neg.action_confirm()
+ outgoing_shipment_neg.move_lines[0].quantity_done = 100.0
+ outgoing_shipment_neg._action_done()
+
+ # Check qty available = -100
+ self.assertEqual(product_fifo_negative.qty_available, -100, 'Stock qty should be -100')
+
+ # The behavior of fifo/lifo is not garantee if the quants are created at the same second, so just wait one second
+ time.sleep(1)
+
+ # Let create another out shipment of 400 kg
+ outgoing_shipment_neg2 = self.env['stock.picking'].create({
+ 'picking_type_id': self.company_data['default_warehouse'].out_type_id.id,
+ 'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id,
+ 'move_lines': [(0, 0, {
+ 'name': product_fifo_negative.name,
+ 'product_id': product_fifo_negative.id,
+ 'product_uom_qty': 400,
+ 'product_uom': self.env.ref('uom.product_uom_kgm').id,
+ 'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'location_dest_id': self.env.ref('stock.stock_location_customers').id,
+ 'picking_type_id': self.company_data['default_warehouse'].out_type_id.id})]
+ })
+
+ # Process the delivery of the outgoing shipments
+ outgoing_shipment_neg2.action_confirm()
+ outgoing_shipment_neg2.move_lines[0].quantity_done = 400.0
+ outgoing_shipment_neg2._action_done()
+
+ # Check qty available = -500
+ self.assertEqual(product_fifo_negative.qty_available, -500, 'Stock qty should be -500')
+
+ # Receive purchase order with 50 kg Ice Cream at 50€/kg
+ purchase_order_neg = self.env['purchase.order'].create({
+ 'partner_id': res_partner_3.id,
+ 'order_line': [(0, 0, {
+ 'name': 'FIFO Ice Cream',
+ 'product_id': product_fifo_negative.id,
+ 'product_qty': 50.0,
+ 'product_uom': self.env.ref('uom.product_uom_kgm').id,
+ 'price_unit': 50.0,
+ 'date_planned': time.strftime('%Y-%m-%d')})],
+ })
+
+ # I confirm the first purchase order
+ purchase_order_neg.button_confirm()
+
+ # Process the reception of purchase order neg
+ picking = purchase_order_neg.picking_ids[0]
+ res = picking.button_validate()
+ Form(self.env[res['res_model']].with_context(res['context'])).save().process()
+
+ # Receive purchase order with 600 kg FIFO Ice Cream at 80 euro/kg
+ purchase_order_neg2 = self.env['purchase.order'].create({
+ 'partner_id': res_partner_3.id,
+ 'order_line': [(0, 0, {
+ 'name': product_cable_management_box.name,
+ 'product_id': product_fifo_negative.id,
+ 'product_qty': 600.0,
+ 'product_uom': self.env.ref('uom.product_uom_kgm').id,
+ 'price_unit': 80.0,
+ 'date_planned': time.strftime('%Y-%m-%d')})],
+ })
+
+ # I confirm the second negative purchase order
+ purchase_order_neg2.button_confirm()
+
+ # Process the reception of purchase order neg2
+ picking = purchase_order_neg2.picking_ids[0]
+ res = picking.button_validate()
+ Form(self.env[res['res_model']].with_context(res['context'])).save().process()
+
+ original_out_move = outgoing_shipment_neg.move_lines[0]
+ self.assertEqual(original_out_move.product_id.value_svl, 12000.0, 'Value of the move should be 12000')
+ self.assertEqual(original_out_move.product_id.qty_available, 150.0, 'Qty available should be 150')
+
+ def test_01_test_fifo(self):
+ """" This test ensures that unit price keeps its decimal precision """
+
+ unit_price_precision = self.env['ir.model.data'].xmlid_to_object('product.decimal_price')
+ unit_price_precision.digits = 3
+
+ tax = self.env["account.tax"].create({
+ "name": "Dummy Tax",
+ "amount": "0.00",
+ "type_tax_use": "purchase",
+ })
+
+ super_product = self.env['product.product'].create({
+ 'name': 'Super Product',
+ 'type': 'product',
+ 'categ_id': self.stock_account_product_categ.id,
+ 'standard_price': 0.035,
+ })
+ self.assertEqual(super_product.cost_method, 'fifo')
+ self.assertEqual(super_product.valuation, 'real_time')
+
+ purchase_order = self.env['purchase.order'].create({
+ 'partner_id': self.env.ref('base.res_partner_3').id,
+ 'order_line': [(0, 0, {
+ 'name': super_product.name,
+ 'product_id': super_product.id,
+ 'product_qty': 1000,
+ 'product_uom': super_product.uom_id.id,
+ 'price_unit': super_product.standard_price,
+ 'date_planned': time.strftime('%Y-%m-%d'),
+ 'taxes_id': [(4, tax.id)],
+ })],
+ })
+
+ purchase_order.button_confirm()
+ self.assertEqual(purchase_order.state, 'purchase')
+
+ picking = purchase_order.picking_ids[0]
+ res = picking.button_validate()
+ Form(self.env[res['res_model']].with_context(res['context'])).save().process()
+
+ self.assertEqual(super_product.standard_price, 0.035)
+ self.assertEqual(super_product.value_svl, 35.0)
+ self.assertEqual(picking.move_lines.price_unit, 0.035)
diff --git a/addons/purchase_stock/tests/test_fifo_returns.py b/addons/purchase_stock/tests/test_fifo_returns.py
new file mode 100644
index 00000000..b53fcf76
--- /dev/null
+++ b/addons/purchase_stock/tests/test_fifo_returns.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+
+import time
+
+from odoo.tests import tagged, Form
+from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
+
+
+@tagged('-at_install', 'post_install')
+class TestFifoReturns(ValuationReconciliationTestCommon):
+
+ def test_fifo_returns(self):
+ """Test to create product and purchase order to test the FIFO returns of the product"""
+ res_partner_3 = self.env['res.partner'].create({
+ 'name': 'Gemini Partner',
+ })
+
+ # Set a product as using fifo price
+ product_fiforet_icecream = self.env['product.product'].create({
+ 'default_code': 'FIFORET',
+ 'name': 'FIFO Ice Cream',
+ 'type': 'product',
+ 'categ_id': self.stock_account_product_categ.id,
+ 'standard_price': 0.0,
+ 'uom_id': self.env.ref('uom.product_uom_kgm').id,
+ 'uom_po_id': self.env.ref('uom.product_uom_kgm').id,
+ 'description': 'FIFO Ice Cream',
+ })
+
+ # I create a draft Purchase Order for first in move for 10 kg at 50 euro
+ purchase_order_1 = self.env['purchase.order'].create({
+ 'partner_id': res_partner_3.id,
+ 'order_line': [(0, 0, {
+ 'name': 'FIFO Ice Cream',
+ 'product_id': product_fiforet_icecream.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.env.ref('uom.product_uom_kgm').id,
+ 'price_unit': 50.0,
+ 'date_planned': time.strftime('%Y-%m-%d'),
+ })],
+ })
+
+ # Create a draft Purchase Order for second shipment for 30kg at 80€/kg
+ purchase_order_2 = self.env['purchase.order'].create({
+ 'partner_id': res_partner_3.id,
+ 'order_line': [(0, 0, {
+ 'name': 'FIFO Ice Cream',
+ 'product_id': product_fiforet_icecream.id,
+ 'product_qty': 30.0,
+ 'product_uom': self.env.ref('uom.product_uom_kgm').id,
+ 'price_unit': 80.0,
+ 'date_planned': time.strftime('%Y-%m-%d'),
+ })],
+ })
+
+ # Confirm the first purchase order
+ purchase_order_1.button_confirm()
+
+ # Process the reception of purchase order 1
+ picking = purchase_order_1.picking_ids[0]
+ res = picking.button_validate()
+ Form(self.env[res['res_model']].with_context(res['context'])).save().process()
+
+ # Check the standard price of the product (fifo icecream)
+ self.assertAlmostEqual(product_fiforet_icecream.standard_price, 50)
+
+ # Confirm the second purchase order
+ purchase_order_2.button_confirm()
+ picking = purchase_order_2.picking_ids[0]
+ res = picking.button_validate()
+ Form(self.env[res['res_model']].with_context(res['context'])).save().process()
+
+ # Return the goods of purchase order 2
+ picking = purchase_order_2.picking_ids[0]
+ stock_return_picking_form = Form(self.env['stock.return.picking']
+ .with_context(active_ids=picking.ids, active_id=picking.ids[0],
+ active_model='stock.picking'))
+ return_pick_wiz = stock_return_picking_form.save()
+ return_picking_id, dummy = return_pick_wiz.with_context(active_id=picking.id)._create_returns()
+
+ # Important to pass through confirmation and assignation
+ return_picking = self.env['stock.picking'].browse(return_picking_id)
+ return_picking.action_confirm()
+ return_picking.move_lines[0].quantity_done = return_picking.move_lines[0].product_uom_qty
+ return_picking._action_done()
+
+ # After the return only 10 of the second purchase order should still be in stock as it applies fifo on the return too
+ self.assertEqual(product_fiforet_icecream.qty_available, 10.0, 'Qty available should be 10.0')
+ self.assertEqual(product_fiforet_icecream.value_svl, 800.0, 'Stock value should be 800')
diff --git a/addons/purchase_stock/tests/test_move_cancel_propagation.py b/addons/purchase_stock/tests/test_move_cancel_propagation.py
new file mode 100644
index 00000000..111ba1e0
--- /dev/null
+++ b/addons/purchase_stock/tests/test_move_cancel_propagation.py
@@ -0,0 +1,293 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo.tests import tagged
+from .common import PurchaseTestCommon
+
+
+class TestMoveCancelPropagation(PurchaseTestCommon):
+
+ def setUp(self):
+ super(TestMoveCancelPropagation, self).setUp()
+ self.customer = self.env['res.partner'].create({'name': 'abc'})
+ self.group = self.env['procurement.group'].create({'partner_id': self.customer.id, 'name': 'New Group'})
+ self.warehouse = self.env.ref('stock.warehouse0')
+ cust_location = self.env.ref('stock.stock_location_customers')
+ seller = self.env['product.supplierinfo'].create({
+ 'name': self.customer.id,
+ 'price': 100.0,
+ })
+ product = self.env['product.product'].create({
+ 'name': 'Geyser',
+ 'type': 'product',
+ 'route_ids': [(4, self.route_mto), (4, self.route_buy)],
+ 'seller_ids': [(6, 0, [seller.id])],
+ })
+ self.picking_out = self.env['stock.picking'].create({
+ 'location_id': self.warehouse.out_type_id.default_location_src_id.id,
+ 'location_dest_id': cust_location.id,
+ 'partner_id': self.customer.id,
+ 'group_id': self.group.id,
+ 'picking_type_id': self.ref('stock.picking_type_out'),
+ })
+ self.move = self.env['stock.move'].create({
+ 'name': product.name,
+ 'product_id': product.id,
+ 'product_uom_qty': 10,
+ 'product_uom': product.uom_id.id,
+ 'picking_id': self.picking_out.id,
+ 'group_id': self.group.id,
+ 'location_id': self.warehouse.out_type_id.default_location_src_id.id,
+ 'location_dest_id': cust_location.id,
+ 'procure_method': 'make_to_order',
+ })
+
+ def test_01_cancel_draft_purchase_order_one_steps(self):
+ """ Check the picking and moves status related PO, When canceling purchase order
+ Ex.
+ 1) Set one steps of receiption and delivery on the warehouse.
+ 2) Create Delivery order with mto move and confirm the order, related RFQ should be generated.
+ 3) Cancel 'draft' purchase order should not cancel < Delivery >
+ """
+ self.warehouse.write({'delivery_steps': 'ship_only', 'reception_steps': 'one_step'})
+ self.picking_out.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
+ self.move.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
+ self.picking_out.action_confirm()
+
+ # Find PO related to picking.
+ purchase_order = self.env['purchase.order'].search([('partner_id', '=', self.customer.id)])
+
+ # Po should be create related picking.
+ self.assertTrue(purchase_order, 'No purchase order created.')
+
+ # Check status of Purchase Order
+ self.assertEqual(purchase_order.state, 'draft', "Purchase order should be in 'draft' state.")
+
+ # Cancel Purchase order.
+ purchase_order.button_cancel()
+
+ # Check the status of picking after canceling po.
+ self.assertNotEqual(self.picking_out.state, 'cancel')
+
+ def test_02_cancel_confirm_purchase_order_one_steps(self):
+ """ Check the picking and moves status related purchase order, When canceling purchase order
+ after confirming.
+ Ex.
+ 1) Set one steps of receiption and delivery on the warehouse.
+ 2) Create Delivery order with mto move and confirm the order, related RFQ should be generated.
+ 3) Cancel 'confirmed' purchase order, should cancel releted < Receiption >
+ but it should not cancel < Delivery > order.
+ """
+ self.warehouse.write({'delivery_steps': 'ship_only', 'reception_steps': 'one_step'})
+ self.picking_out.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
+ self.move.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
+ self.picking_out.action_confirm()
+
+ # Find PO related to picking.
+ purchase_order = self.env['purchase.order'].search([('partner_id', '=', self.customer.id)])
+ # Po should be create related picking.
+ self.assertTrue(purchase_order, 'No purchase order created.')
+
+ # Check status of Purchase Order
+ self.assertEqual(purchase_order.state, 'draft', "Purchase order should be in 'draft' state.")
+ purchase_order .button_confirm()
+ picking_in = purchase_order.picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.in_type_id)
+ # Cancel Purchase order.
+ purchase_order .button_cancel()
+
+ # Check the status of picking after canceling po.
+ self.assertEqual(picking_in.state, 'cancel')
+ self.assertNotEqual(self.picking_out.state, 'cancel')
+
+ def test_03_cancel_draft_purchase_order_two_steps(self):
+ """ Check the picking and moves status related PO, When canceling purchase order
+ in 'draft' state.
+ Ex.
+ 1) Set two steps of receiption and delivery on the warehouse.
+ 2) Create Delivery order with mto move and confirm the order, related RFQ should be generated.
+ 3) Cancel 'draft' purchase order should cancel < Input to Stock>
+ but it should not cancel < PICK, Delivery >
+ """
+ self.warehouse.write({'delivery_steps': 'pick_ship', 'reception_steps': 'two_steps'})
+ self.picking_out.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
+ self.move.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
+ self.picking_out.action_confirm()
+
+ # Find purchase order related to picking.
+ purchase_order = self.env['purchase.order'].search([('partner_id', '=', self.customer.id)])
+ # Purchase order should be created for picking.
+ self.assertTrue(purchase_order, 'No purchase order created.')
+
+ picking_ids = self.env['stock.picking'].search([('group_id', '=', self.group.id)])
+
+ internal = picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.int_type_id and r.group_id.id == self.group.id)
+ pick = picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.pick_type_id and r.group_id.id == self.group.id)
+
+ # Check status of Purchase Order
+ self.assertEqual(purchase_order.state, 'draft', "Purchase order should be in 'draft' state.")
+ # Cancel Purchase order.
+ purchase_order.button_cancel()
+
+ # Check the status of picking after canceling po.
+ for res in internal:
+ self.assertEqual(res.state, 'cancel')
+ self.assertNotEqual(pick.state, 'cancel')
+ self.assertNotEqual(self.picking_out.state, 'cancel')
+
+ def test_04_cancel_confirm_purchase_order_two_steps(self):
+ """ Check the picking and moves status related PO, When canceling purchase order
+ Ex.
+ 1) Set 2 steps of receiption and delivery on the warehouse.
+ 2) Create Delivery order with mto move and confirm the order, related RFQ should be generated.
+ 3) Cancel 'comfirm' purchase order should cancel releted < Receiption Picking IN, INT>
+ not < PICK, SHIP >
+ """
+ self.warehouse.write({'delivery_steps': 'pick_ship', 'reception_steps': 'two_steps'})
+ self.picking_out.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
+ self.move.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
+ self.picking_out.action_confirm()
+
+ # Find PO related to picking.
+ purchase_order = self.env['purchase.order'].search([('partner_id', '=', self.customer.id)])
+ # Po should be create related picking.
+ self.assertTrue(purchase_order, 'purchase order is created.')
+
+ picking_ids = self.env['stock.picking'].search([('group_id', '=', self.group.id)])
+
+ internal = picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.int_type_id)
+ pick = picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.pick_type_id)
+
+ # Check status of Purchase Order
+ self.assertEqual(purchase_order.state, 'draft', "Purchase order should be in 'draft' state.")
+
+ purchase_order.button_confirm()
+ picking_in = purchase_order.picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.in_type_id)
+ # Cancel Purchase order.
+ purchase_order.button_cancel()
+
+ # Check the status of picking after canceling po.
+ self.assertEqual(picking_in.state, 'cancel')
+ for res in internal:
+ self.assertEqual(res.state, 'cancel')
+ self.assertNotEqual(pick.state, 'cancel')
+ self.assertNotEqual(self.picking_out.state, 'cancel')
+
+ def test_05_cancel_draft_purchase_order_three_steps(self):
+ """ Check the picking and moves status related PO, When canceling purchase order
+ Ex.
+ 1) Set 3 steps of receiption and delivery on the warehouse.
+ 2) Create Delivery order with mto move and confirm the order, related RFQ should be generated.
+ 3) Cancel 'draft' purchase order should cancel releted < Receiption Picking IN>
+ not < PICK, PACK, SHIP >
+ """
+ self.warehouse.write({'delivery_steps': 'pick_pack_ship', 'reception_steps': 'three_steps'})
+ self.picking_out.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
+ self.move.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
+ self.picking_out.action_confirm()
+
+ # Find PO related to picking.
+ purchase_order = self.env['purchase.order'].search([('partner_id', '=', self.customer.id)])
+ # Po should be create related picking.
+ self.assertTrue(purchase_order, 'No purchase order created.')
+
+ picking_ids = self.env['stock.picking'].search([('group_id', '=', self.group.id)])
+
+ internal = picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.int_type_id)
+ pick = picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.pick_type_id)
+ pack = picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.pack_type_id)
+
+ # Check status of Purchase Order
+ self.assertEqual(purchase_order.state, 'draft', "Purchase order should be in 'draft' state.")
+ # Cancel Purchase order.
+ purchase_order.button_cancel()
+
+ # Check the status of picking after canceling po.
+ for res in internal:
+ self.assertEqual(res.state, 'cancel')
+ self.assertNotEqual(pick.state, 'cancel')
+ self.assertNotEqual(pack.state, 'cancel')
+ self.assertNotEqual(self.picking_out.state, 'cancel')
+
+ def test_06_cancel_confirm_purchase_order_three_steps(self):
+ """ Check the picking and moves status related PO, When canceling purchase order
+ Ex.
+ 1) Set 3 steps of receiption and delivery on the warehouse.
+ 2) Create Delivery order with mto move and confirm the order, related RFQ should be generated.
+ 3) Cancel 'comfirm' purchase order should cancel releted < Receiption Picking IN, INT>
+ not < PICK, PACK, SHIP >
+ """
+ self.warehouse.write({'delivery_steps': 'pick_pack_ship', 'reception_steps': 'three_steps'})
+ self.picking_out.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
+ self.move.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
+ self.picking_out.action_confirm()
+
+ # Find PO related to picking.
+ purchase_order = self.env['purchase.order'].search([('partner_id', '=', self.customer.id)])
+ # Po should be create related picking.
+ self.assertTrue(purchase_order, 'No purchase order created.')
+
+ picking_ids = self.env['stock.picking'].search([('group_id', '=', self.group.id)])
+
+ internal = picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.int_type_id)
+ pick = picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.pick_type_id)
+ pack = picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.pack_type_id)
+
+ # Check status of Purchase Order
+ self.assertEqual(purchase_order.state, 'draft', "Purchase order should be in 'draft' state.")
+
+ purchase_order.button_confirm()
+ picking_in = purchase_order.picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.in_type_id)
+ # Cancel Purchase order.
+ purchase_order.button_cancel()
+
+ # Check the status of picking after canceling po.
+ self.assertEqual(picking_in.state, 'cancel')
+ for res in internal:
+ self.assertEqual(res.state, 'cancel')
+ self.assertNotEqual(pick.state, 'cancel')
+ self.assertNotEqual(pack.state, 'cancel')
+ self.assertNotEqual(self.picking_out.state, 'cancel')
+
+ def test_cancel_move_lines_operation(self):
+ """Check for done and cancelled moves. Ensure that the RFQ cancellation
+ will not impact the delivery state if it's already cancelled.
+ """
+ stock_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_stock')
+ customer_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_customers')
+ picking_type_out = self.env['ir.model.data'].xmlid_to_object('stock.picking_type_out')
+
+ partner = self.env['res.partner'].create({
+ 'name': 'Steve'
+ })
+ seller = self.env['product.supplierinfo'].create({
+ 'name': partner.id,
+ 'price': 10.0,
+ })
+ product_car = self.env['product.product'].create({
+ 'name': 'Car',
+ 'type': 'product',
+ 'route_ids': [(4, self.ref('stock.route_warehouse0_mto')), (4, self.ref('purchase_stock.route_warehouse0_buy'))],
+ 'seller_ids': [(6, 0, [seller.id])],
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ customer_picking = self.env['stock.picking'].create({
+ 'location_id': stock_location.id,
+ 'location_dest_id': customer_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': picking_type_out.id,
+ })
+ customer_move = self.env['stock.move'].create({
+ 'name': 'move out',
+ 'location_id': stock_location.id,
+ 'location_dest_id': customer_location.id,
+ 'product_id': product_car.id,
+ 'product_uom': product_car.uom_id.id,
+ 'product_uom_qty': 10.0,
+ 'procure_method': 'make_to_order',
+ 'picking_id': customer_picking.id,
+ })
+ customer_move._action_confirm()
+ purchase_order = self.env['purchase.order'].search([('partner_id', '=', partner.id)])
+ customer_move._action_cancel()
+ self.assertEqual(customer_move.state, 'cancel', 'Move should be cancelled')
+ purchase_order.button_cancel()
+ self.assertEqual(customer_move.state, 'cancel', 'State of cancelled and done moves should not change.')
diff --git a/addons/purchase_stock/tests/test_onchange_product.py b/addons/purchase_stock/tests/test_onchange_product.py
new file mode 100644
index 00000000..b029839e
--- /dev/null
+++ b/addons/purchase_stock/tests/test_onchange_product.py
@@ -0,0 +1,121 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import datetime
+
+from odoo import fields
+from odoo.tests.common import TransactionCase, Form
+from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
+
+class TestOnchangeProductId(TransactionCase):
+ """Test that when an included tax is mapped by a fiscal position, the included tax must be
+ subtracted to the price of the product.
+ """
+
+ def setUp(self):
+ super(TestOnchangeProductId, self).setUp()
+ self.fiscal_position_model = self.env['account.fiscal.position']
+ self.fiscal_position_tax_model = self.env['account.fiscal.position.tax']
+ self.tax_model = self.env['account.tax']
+ self.po_model = self.env['purchase.order']
+ self.po_line_model = self.env['purchase.order.line']
+ self.res_partner_model = self.env['res.partner']
+ self.product_tmpl_model = self.env['product.template']
+ self.product_model = self.env['product.product']
+ self.product_uom_model = self.env['uom.uom']
+ self.supplierinfo_model = self.env["product.supplierinfo"]
+
+ def test_onchange_product_id(self):
+
+ uom_id = self.product_uom_model.search([('name', '=', 'Units')])[0]
+
+ partner_id = self.res_partner_model.create(dict(name="George"))
+ tax_include_id = self.tax_model.create(dict(name="Include tax",
+ amount='21.00',
+ price_include=True,
+ type_tax_use='purchase'))
+ tax_exclude_id = self.tax_model.create(dict(name="Exclude tax",
+ amount='0.00',
+ type_tax_use='purchase'))
+ supplierinfo_vals = {
+ 'name': partner_id.id,
+ 'price': 121.0,
+ }
+
+ supplierinfo = self.supplierinfo_model.create(supplierinfo_vals)
+
+ product_tmpl_id = self.product_tmpl_model.create(dict(name="Voiture",
+ list_price=121,
+ seller_ids=[(6, 0, [supplierinfo.id])],
+ supplier_taxes_id=[(6, 0, [tax_include_id.id])]))
+ product_id = product_tmpl_id.product_variant_id
+
+ fp_id = self.fiscal_position_model.create(dict(name="fiscal position", sequence=1))
+
+ fp_tax_id = self.fiscal_position_tax_model.create(dict(position_id=fp_id.id,
+ tax_src_id=tax_include_id.id,
+ tax_dest_id=tax_exclude_id.id))
+ po_vals = {
+ 'partner_id': partner_id.id,
+ 'fiscal_position_id': fp_id.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': product_id.name,
+ 'product_id': product_id.id,
+ 'product_qty': 1.0,
+ 'product_uom': uom_id.id,
+ 'price_unit': 121.0,
+ 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
+ })],
+ }
+ po = self.po_model.create(po_vals)
+
+ po_line = po.order_line[0]
+ po_line.onchange_product_id()
+ self.assertEqual(100, po_line.price_unit, "The included tax must be subtracted to the price")
+
+ supplierinfo.write({'min_qty': 24})
+ po_line.write({'product_qty': 20})
+ po_line._onchange_quantity()
+ self.assertEqual(0, po_line.price_unit, "Unit price should be reset to 0 since the supplier supplies minimum of 24 quantities")
+
+ po_line.write({'product_qty': 3, 'product_uom': self.ref("uom.product_uom_dozen")})
+ po_line._onchange_quantity()
+ self.assertEqual(1200, po_line.price_unit, "Unit price should be 1200 for one Dozen")
+ ipad_uom = self.env['uom.category'].create({'name': 'Ipad Unit'})
+ ipad_lot = self.env['uom.uom'].create({
+ 'name': 'Ipad',
+ 'category_id': ipad_uom.id,
+ 'uom_type': 'reference',
+ 'rounding': 0.001
+ })
+ ipad_lot_10 = self.env['uom.uom'].create({
+ 'name': '10 Ipad',
+ 'category_id': ipad_uom.id,
+ 'uom_type': 'bigger',
+ 'rounding': 0.001,
+ "factor_inv": 10
+ })
+ product_ipad = self.env['product.product'].create({
+ 'name': 'Conference Chair',
+ 'standard_price': 100,
+ 'uom_id': ipad_lot.id,
+ 'uom_po_id': ipad_lot.id,
+ })
+ po_line2 = self.po_line_model.create({
+ 'name': product_ipad.name,
+ 'product_id': product_ipad.id,
+ 'order_id': po.id,
+ 'product_qty': 5,
+ 'product_uom': ipad_uom.id,
+ 'date_planned': fields.Date().today()
+ })
+
+ po_line2.onchange_product_id()
+ self.assertEqual(100, po_line2.price_unit, "No vendor supplies this product, hence unit price should be set to 100")
+
+ po_form = Form(po)
+ with po_form.order_line.edit(1) as order_line:
+ order_line.product_uom = ipad_lot_10
+ po_form.save()
+ self.assertEqual(1000, po_line2.price_unit, "The product_uom is multiplied by 10, hence unit price should be set to 1000")
diff --git a/addons/purchase_stock/tests/test_product_template.py b/addons/purchase_stock/tests/test_product_template.py
new file mode 100644
index 00000000..0692ac02
--- /dev/null
+++ b/addons/purchase_stock/tests/test_product_template.py
@@ -0,0 +1,26 @@
+from odoo.tests.common import TransactionCase
+
+
+class TestProductTemplate(TransactionCase):
+ def test_name_search(self):
+ partner = self.env['res.partner'].create({
+ 'name': 'Azure Interior',
+ })
+
+ seller = self.env['product.supplierinfo'].create({
+ 'name': partner.id,
+ 'price': 12.0,
+ 'delay': 1,
+ 'product_code': 'VOB2a',
+ })
+
+ product_tmpl = self.env['product.template'].create({
+ 'name': 'Rubber Duck',
+ 'type': 'product',
+ 'default_code': 'VOB2A',
+ 'seller_ids': [seller.id],
+ 'purchase_ok': True,
+ })
+ ns = self.env['product.template'].with_context(partner_id=partner.id).name_search('VOB2', [['purchase_ok', '=', True]])
+ self.assertEqual(len(ns), 1, "name_search should have 1 item")
+ self.assertEqual(ns[0][1], '[VOB2A] Rubber Duck', "name_search should return the expected result")
diff --git a/addons/purchase_stock/tests/test_purchase_delete_order.py b/addons/purchase_stock/tests/test_purchase_delete_order.py
new file mode 100644
index 00000000..0687e76d
--- /dev/null
+++ b/addons/purchase_stock/tests/test_purchase_delete_order.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.exceptions import UserError
+from .common import PurchaseTestCommon
+
+
+class TestDeleteOrder(PurchaseTestCommon):
+
+ def test_00_delete_order(self):
+ ''' Testcase for deleting purchase order with purchase user group'''
+
+ # In order to test delete process on purchase order,tried to delete a confirmed order and check Error Message.
+ partner = self.env['res.partner'].create({'name': 'My Partner'})
+
+ purchase_order = self.env['purchase.order'].create({
+ 'partner_id': partner.id,
+ 'state': 'purchase',
+ })
+ purchase_order_1 = purchase_order.with_user(self.res_users_purchase_user)
+ with self.assertRaises(UserError):
+ purchase_order_1.unlink()
+
+ # Delete 'cancelled' purchase order with user group
+ purchase_order = self.env['purchase.order'].create({
+ 'partner_id': partner.id,
+ 'state': 'purchase',
+ })
+ purchase_order_2 = purchase_order.with_user(self.res_users_purchase_user)
+ purchase_order_2.button_cancel()
+ self.assertEqual(purchase_order_2.state, 'cancel', 'PO is cancelled!')
+ purchase_order_2.unlink()
+
+ # Delete 'draft' purchase order with user group
+ purchase_order = self.env['purchase.order'].create({
+ 'partner_id': partner.id,
+ 'state': 'draft',
+ })
+ purchase_order_3 = purchase_order.with_user(self.res_users_purchase_user)
+ purchase_order_3.button_cancel()
+ self.assertEqual(purchase_order_3.state, 'cancel', 'PO is cancelled!')
+ purchase_order_3.unlink()
diff --git a/addons/purchase_stock/tests/test_purchase_lead_time.py b/addons/purchase_stock/tests/test_purchase_lead_time.py
new file mode 100644
index 00000000..6fbae5c5
--- /dev/null
+++ b/addons/purchase_stock/tests/test_purchase_lead_time.py
@@ -0,0 +1,341 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import datetime, timedelta, time
+from unittest.mock import patch
+
+from odoo import fields
+from .common import PurchaseTestCommon
+from odoo.tests.common import Form
+
+
+class TestPurchaseLeadTime(PurchaseTestCommon):
+
+ def test_00_product_company_level_delays(self):
+ """ To check dates, set product's Delivery Lead Time
+ and company's Purchase Lead Time."""
+
+ company = self.env.ref('base.main_company')
+
+ # Update company with Purchase Lead Time
+ company.write({'po_lead': 3.00})
+
+ # Make procurement request from product_1's form view, create procurement and check it's state
+ date_planned = fields.Datetime.to_string(fields.datetime.now() + timedelta(days=10))
+ self._create_make_procurement(self.product_1, 15.00, date_planned=date_planned)
+ purchase = self.env['purchase.order.line'].search([('product_id', '=', self.product_1.id)], limit=1).order_id
+
+ # Confirm purchase order
+ purchase.button_confirm()
+
+ # Check order date of purchase order
+ order_date = fields.Datetime.from_string(date_planned) - timedelta(days=company.po_lead) - timedelta(days=self.product_1.seller_ids.delay)
+ self.assertEqual(purchase.date_order, order_date, 'Order date should be equal to: Date of the procurement order - Purchase Lead Time - Delivery Lead Time.')
+
+ # Check scheduled date of purchase order
+ schedule_date = datetime.combine(order_date + timedelta(days=self.product_1.seller_ids.delay), time.min).replace(tzinfo=None, hour=12)
+ self.assertEqual(purchase.order_line.date_planned, schedule_date, 'Schedule date should be equal to: Order date of Purchase order + Delivery Lead Time.')
+
+ # check the picking created or not
+ self.assertTrue(purchase.picking_ids, "Picking should be created.")
+
+ # Check scheduled and deadline date of In Type shipment
+ self.assertEqual(purchase.picking_ids.scheduled_date, schedule_date, 'Schedule date of In type shipment should be equal to: schedule date of purchase order.')
+ self.assertEqual(purchase.picking_ids.date_deadline, schedule_date + timedelta(days=company.po_lead), 'Deadline date of should be equal to: schedule date of purchase order + lead_po.')
+
+ def test_01_product_level_delay(self):
+ """ To check schedule dates of multiple purchase order line of the same purchase order,
+ we create two procurements for the two different product with same vendor
+ and different Delivery Lead Time."""
+
+ company = self.env.ref('base.main_company')
+ company.write({'po_lead': 0.00})
+
+ # Make procurement request from product_1's form view, create procurement and check it's state
+ date_planned1 = fields.Datetime.to_string(fields.datetime.now() + timedelta(days=10))
+ self._create_make_procurement(self.product_1, 10.00, date_planned=date_planned1)
+ purchase1 = self.env['purchase.order.line'].search([('product_id', '=', self.product_1.id)], limit=1).order_id
+
+ # Make procurement request from product_2's form view, create procurement and check it's state
+ date_planned2 = fields.Datetime.to_string(fields.datetime.now() + timedelta(days=10))
+ self._create_make_procurement(self.product_2, 5.00, date_planned=date_planned2)
+ purchase2 = self.env['purchase.order.line'].search([('product_id', '=', self.product_2.id)], limit=1).order_id
+
+ # Check purchase order is same or not
+ self.assertEqual(purchase1, purchase2, 'Purchase orders should be same for the two different product with same vendor.')
+
+ # Confirm purchase order
+ purchase1.button_confirm()
+
+ # Check order date of purchase order
+ order_line_pro_1 = purchase2.order_line.filtered(lambda r: r.product_id == self.product_1)
+ order_line_pro_2 = purchase2.order_line.filtered(lambda r: r.product_id == self.product_2)
+ order_date = fields.Datetime.from_string(date_planned1) - timedelta(days=self.product_1.seller_ids.delay)
+ self.assertEqual(purchase2.date_order, order_date, 'Order date should be equal to: Date of the procurement order - Delivery Lead Time.')
+
+ # Check scheduled date of purchase order line for product_1
+ schedule_date_1 = datetime.combine(order_date + timedelta(days=self.product_1.seller_ids.delay), time.min).replace(tzinfo=None, hour=12)
+ self.assertEqual(order_line_pro_1.date_planned, schedule_date_1, 'Schedule date of purchase order line for product_1 should be equal to: Order date of purchase order + Delivery Lead Time of product_1.')
+
+ # Check scheduled date of purchase order line for product_2
+ schedule_date_2 = datetime.combine(order_date + timedelta(days=self.product_2.seller_ids.delay), time.min).replace(tzinfo=None, hour=12)
+ self.assertEqual(order_line_pro_2.date_planned, schedule_date_2, 'Schedule date of purchase order line for product_2 should be equal to: Order date of purchase order + Delivery Lead Time of product_2.')
+
+ # Check scheduled date of purchase order
+ po_schedule_date = min(schedule_date_1, schedule_date_2)
+ self.assertEqual(purchase2.order_line[1].date_planned, po_schedule_date, 'Schedule date of purchase order should be minimum of schedule dates of purchase order lines.')
+
+ # Check the picking created or not
+ self.assertTrue(purchase2.picking_ids, "Picking should be created.")
+
+ # Check scheduled date of In Type shipment
+ self.assertEqual(purchase2.picking_ids.scheduled_date, po_schedule_date, 'Schedule date of In type shipment should be same as schedule date of purchase order.')
+
+ # Check deadline of pickings
+ self.assertEqual(purchase2.picking_ids.date_deadline, purchase2.date_planned, "Deadline of pickings should be equals to the receipt date of purchase")
+ purchase_form = Form(purchase2)
+ purchase_form.date_planned = purchase2.date_planned + timedelta(days=2)
+ purchase_form.save()
+ self.assertEqual(purchase2.picking_ids.date_deadline, purchase2.date_planned, "Deadline of pickings should be propagate")
+
+ def test_02_product_route_level_delays(self):
+ """ In order to check dates, set product's Delivery Lead Time
+ and warehouse route's delay."""
+
+ company = self.env.ref('base.main_company')
+ company.write({'po_lead': 1.00})
+
+ # Update warehouse_1 with Incoming Shipments 3 steps
+ self.warehouse_1.write({'reception_steps': 'three_steps'})
+
+ # Set delay on push rule
+ for push_rule in self.warehouse_1.reception_route_id.rule_ids:
+ push_rule.write({'delay': 2})
+
+ rule_delay = sum(self.warehouse_1.reception_route_id.rule_ids.mapped('delay'))
+
+ date_planned = fields.Datetime.to_string(fields.datetime.now() + timedelta(days=10))
+ # Create procurement order of product_1
+ self.env['procurement.group'].run([self.env['procurement.group'].Procurement(
+ self.product_1, 5.000, self.uom_unit, self.warehouse_1.lot_stock_id, 'Test scheduler for RFQ', '/', self.env.company,
+ {
+ 'warehouse_id': self.warehouse_1,
+ 'date_planned': date_planned, # 10 days added to current date of procurement to get future schedule date and order date of purchase order.
+ 'date_deadline': date_planned, # 10 days added to current date of procurement to get future schedule date and order date of purchase order.
+ 'rule_id': self.warehouse_1.buy_pull_id,
+ 'group_id': False,
+ 'route_ids': [],
+ }
+ )])
+
+ # Confirm purchase order
+ purchase = self.env['purchase.order.line'].search([('product_id', '=', self.product_1.id)], limit=1).order_id
+ purchase.button_confirm()
+
+ # Check order date of purchase order
+ order_date = fields.Datetime.from_string(date_planned) - timedelta(days=self.product_1.seller_ids.delay + rule_delay + company.po_lead)
+ self.assertEqual(purchase.date_order, order_date, 'Order date should be equal to: Date of the procurement order - Delivery Lead Time(supplier and pull rules).')
+
+ # Check scheduled date of purchase order
+ schedule_date = order_date + timedelta(days=self.product_1.seller_ids.delay + rule_delay + company.po_lead)
+ self.assertEqual(date_planned, str(schedule_date), 'Schedule date should be equal to: Order date of Purchase order + Delivery Lead Time(supplier and pull rules).')
+
+ # Check the picking crated or not
+ self.assertTrue(purchase.picking_ids, "Picking should be created.")
+
+ # Check scheduled date of Internal Type shipment
+ incoming_shipment1 = self.env['stock.picking'].search([('move_lines.product_id', 'in', (self.product_1.id, self.product_2.id)), ('picking_type_id', '=', self.warehouse_1.int_type_id.id), ('location_id', '=', self.warehouse_1.wh_input_stock_loc_id.id), ('location_dest_id', '=', self.warehouse_1.wh_qc_stock_loc_id.id)])
+ incoming_shipment1_date = order_date + timedelta(days=self.product_1.seller_ids.delay + company.po_lead)
+ self.assertEqual(incoming_shipment1.scheduled_date, incoming_shipment1_date, 'Schedule date of Internal Type shipment for input stock location should be equal to: schedule date of purchase order + push rule delay.')
+ self.assertEqual(incoming_shipment1.date_deadline, incoming_shipment1_date)
+ old_deadline1 = incoming_shipment1.date_deadline
+
+ incoming_shipment2 = self.env['stock.picking'].search([('picking_type_id', '=', self.warehouse_1.int_type_id.id), ('location_id', '=', self.warehouse_1.wh_qc_stock_loc_id.id), ('location_dest_id', '=', self.warehouse_1.lot_stock_id.id)])
+ incoming_shipment2_date = schedule_date - timedelta(days=incoming_shipment2.move_lines[0].rule_id.delay)
+ self.assertEqual(incoming_shipment2.scheduled_date, incoming_shipment2_date, 'Schedule date of Internal Type shipment for quality control stock location should be equal to: schedule date of Internal type shipment for input stock location + push rule delay..')
+ self.assertEqual(incoming_shipment2.date_deadline, incoming_shipment2_date)
+ old_deadline2 = incoming_shipment2.date_deadline
+
+ # Modify the date_planned of the purchase -> propagate the deadline
+ purchase_form = Form(purchase)
+ purchase_form.date_planned = purchase.date_planned + timedelta(days=1)
+ purchase_form.save()
+ self.assertEqual(incoming_shipment2.date_deadline, old_deadline2 + timedelta(days=1), 'Deadline should be propagate')
+ self.assertEqual(incoming_shipment1.date_deadline, old_deadline1 + timedelta(days=1), 'Deadline should be propagate')
+
+ def test_merge_po_line(self):
+ """Change that merging po line for same procurement is done."""
+
+ # create a product with manufacture route
+ product_1 = self.env['product.product'].create({
+ 'name': 'AAA',
+ 'route_ids': [(4, self.route_buy)],
+ 'seller_ids': [(0, 0, {'name': self.partner_1.id, 'delay': 5})]
+ })
+
+ # create a move for product_1 from stock to output and reserve to trigger the
+ # rule
+ move_1 = self.env['stock.move'].create({
+ 'name': 'move_1',
+ 'product_id': product_1.id,
+ 'product_uom': self.ref('uom.product_uom_unit'),
+ 'location_id': self.ref('stock.stock_location_stock'),
+ 'location_dest_id': self.ref('stock.stock_location_output'),
+ 'product_uom_qty': 10,
+ 'procure_method': 'make_to_order'
+ })
+
+ move_1._action_confirm()
+ po_line = self.env['purchase.order.line'].search([
+ ('product_id', '=', product_1.id),
+ ])
+ self.assertEqual(len(po_line), 1, 'the purchase order line is not created')
+ self.assertEqual(po_line.product_qty, 10, 'the purchase order line has a wrong quantity')
+
+ move_2 = self.env['stock.move'].create({
+ 'name': 'move_2',
+ 'product_id': product_1.id,
+ 'product_uom': self.ref('uom.product_uom_unit'),
+ 'location_id': self.ref('stock.stock_location_stock'),
+ 'location_dest_id': self.ref('stock.stock_location_output'),
+ 'product_uom_qty': 5,
+ 'procure_method': 'make_to_order'
+ })
+
+ move_2._action_confirm()
+ po_line = self.env['purchase.order.line'].search([
+ ('product_id', '=', product_1.id),
+ ])
+ self.assertEqual(len(po_line), 1, 'the purchase order lines should be merged')
+ self.assertEqual(po_line.product_qty, 15, 'the purchase order line has a wrong quantity')
+
+ def test_merge_po_line_3(self):
+ """Change merging po line if same procurement is done depending on custom values."""
+ company = self.env.ref('base.main_company')
+ company.write({'po_lead': 0.00})
+
+ # The seller has a specific product name and code which must be kept in the PO line
+ self.t_shirt.seller_ids.write({
+ 'product_name': 'Vendor Name',
+ 'product_code': 'Vendor Code',
+ })
+ partner = self.t_shirt.seller_ids[:1].name
+ t_shirt = self.t_shirt.with_context(
+ lang=partner.lang,
+ partner_id=partner.id,
+ )
+
+ # Create procurement order of product_1
+ ProcurementGroup = self.env['procurement.group']
+ procurement_values = {
+ 'warehouse_id': self.warehouse_1,
+ 'rule_id': self.warehouse_1.buy_pull_id,
+ 'date_planned': fields.Datetime.to_string(fields.datetime.now() + timedelta(days=10)),
+ 'group_id': False,
+ 'route_ids': [],
+ }
+
+ procurement_values['product_description_variants'] = 'Color (Red)'
+ order_1_values = procurement_values
+ ProcurementGroup.run([self.env['procurement.group'].Procurement(
+ self.t_shirt, 5, self.uom_unit, self.warehouse_1.lot_stock_id,
+ self.t_shirt.name, '/', self.env.company, order_1_values)
+ ])
+ purchase_order = self.env['purchase.order.line'].search([('product_id', '=', self.t_shirt.id)], limit=1).order_id
+ order_line_description = purchase_order.order_line.product_id._get_description(purchase_order.picking_type_id)
+ self.assertEqual(len(purchase_order.order_line), 1, 'wrong number of order line is created')
+ self.assertEqual(purchase_order.order_line.name, t_shirt.display_name + "\n" + "Color (Red)", 'wrong description in po lines')
+
+ procurement_values['product_description_variants'] = 'Color (Red)'
+ order_2_values = procurement_values
+ ProcurementGroup.run([self.env['procurement.group'].Procurement(
+ self.t_shirt, 10, self.uom_unit, self.warehouse_1.lot_stock_id,
+ self.t_shirt.name, '/', self.env.company, order_2_values)
+ ])
+ self.env['procurement.group'].run_scheduler()
+ self.assertEqual(len(purchase_order.order_line), 1, 'line with same custom value should be merged')
+ self.assertEqual(purchase_order.order_line[0].product_qty, 15, 'line with same custom value should be merged and qty should be update')
+
+ procurement_values['product_description_variants'] = 'Color (Green)'
+
+ order_3_values = procurement_values
+ ProcurementGroup.run([self.env['procurement.group'].Procurement(
+ self.t_shirt, 10, self.uom_unit, self.warehouse_1.lot_stock_id,
+ self.t_shirt.name, '/', self.env.company, order_3_values)
+ ])
+ self.assertEqual(len(purchase_order.order_line), 2, 'line with different custom value should not be merged')
+ self.assertEqual(purchase_order.order_line.filtered(lambda x: x.product_qty == 15).name, t_shirt.display_name + "\n" + "Color (Red)", 'wrong description in po lines')
+ self.assertEqual(purchase_order.order_line.filtered(lambda x: x.product_qty == 10).name, t_shirt.display_name + "\n" + "Color (Green)", 'wrong description in po lines')
+
+ purchase_order.button_confirm()
+ self.assertEqual(purchase_order.picking_ids[0].move_ids_without_package.filtered(lambda x: x.product_uom_qty == 15).description_picking, order_line_description + "\nColor (Red)", 'wrong description in picking')
+ self.assertEqual(purchase_order.picking_ids[0].move_ids_without_package.filtered(lambda x: x.product_uom_qty == 10).description_picking, order_line_description + "\nColor (Green)", 'wrong description in picking')
+
+ def test_reordering_days_to_purchase(self):
+ company = self.env.ref('base.main_company')
+ company2 = self.env['res.company'].create({
+ 'name': 'Second Company',
+ })
+ company.write({'po_lead': 0.00})
+ self.patcher = patch('odoo.addons.stock.models.stock_orderpoint.fields.Date', wraps=fields.Date)
+ self.mock_date = self.patcher.start()
+
+ vendor = self.env['res.partner'].create({
+ 'name': 'Colruyt'
+ })
+ vendor2 = self.env['res.partner'].create({
+ 'name': 'Delhaize'
+ })
+
+ self.env.company.days_to_purchase = 2.0
+
+ product = self.env['product.product'].create({
+ 'name': 'Chicory',
+ 'type': 'product',
+ 'seller_ids': [
+ (0, 0, {'name': vendor2.id, 'delay': 15.0, 'company_id': company2.id}),
+ (0, 0, {'name': vendor.id, 'delay': 1.0, 'company_id': company.id})
+ ]
+ })
+ orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
+ orderpoint_form.product_id = product
+ orderpoint_form.product_min_qty = 0.0
+ orderpoint = orderpoint_form.save()
+
+ orderpoint_form = Form(self.env['stock.warehouse.orderpoint'].with_company(company2))
+ orderpoint_form.product_id = product
+ orderpoint_form.product_min_qty = 0.0
+ orderpoint = orderpoint_form.save()
+
+ warehouse = self.env['stock.warehouse'].search([], limit=1)
+ delivery_moves = self.env['stock.move']
+ for i in range(0, 6):
+ delivery_moves |= self.env['stock.move'].create({
+ 'name': 'Delivery',
+ 'date': datetime.today() + timedelta(days=i),
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 5.0,
+ 'location_id': warehouse.lot_stock_id.id,
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
+ })
+ delivery_moves._action_confirm()
+ self.env['procurement.group'].run_scheduler()
+ po_line = self.env['purchase.order.line'].search([('product_id', '=', product.id)])
+ self.assertEqual(fields.Date.to_date(po_line.order_id.date_order), fields.Date.today() + timedelta(days=2))
+ self.assertEqual(len(po_line), 1)
+ self.assertEqual(po_line.product_uom_qty, 20.0)
+ self.assertEqual(len(po_line.order_id), 1)
+ orderpoint_form = Form(orderpoint)
+ orderpoint_form.save()
+
+ self.mock_date.today.return_value = fields.Date.today() + timedelta(days=1)
+ orderpoint._compute_qty()
+ self.env['procurement.group'].run_scheduler()
+ po_line = self.env['purchase.order.line'].search([('product_id', '=', product.id)])
+ self.assertEqual(len(po_line), 2)
+ self.assertEqual(len(po_line.order_id), 2)
+ new_order = po_line.order_id.sorted('date_order')[-1]
+ self.assertEqual(fields.Date.to_date(new_order.date_order), fields.Date.today() + timedelta(days=2))
+ self.assertEqual(new_order.order_line.product_uom_qty, 5.0)
+ self.patcher.stop()
diff --git a/addons/purchase_stock/tests/test_purchase_order.py b/addons/purchase_stock/tests/test_purchase_order.py
new file mode 100644
index 00000000..7d98d247
--- /dev/null
+++ b/addons/purchase_stock/tests/test_purchase_order.py
@@ -0,0 +1,332 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import datetime, timedelta
+
+from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
+from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
+from odoo.tests import Form, tagged
+
+
+@tagged('post_install', '-at_install')
+class TestPurchaseOrder(ValuationReconciliationTestCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.product_id_1 = cls.env['product.product'].create({'name': 'Large Desk', 'purchase_method': 'purchase'})
+ cls.product_id_2 = cls.env['product.product'].create({'name': 'Conference Chair', 'purchase_method': 'purchase'})
+
+ cls.po_vals = {
+ 'partner_id': cls.partner_a.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': cls.product_id_1.name,
+ 'product_id': cls.product_id_1.id,
+ 'product_qty': 5.0,
+ 'product_uom': cls.product_id_1.uom_po_id.id,
+ 'price_unit': 500.0,
+ 'date_planned': datetime.today().replace(hour=9).strftime(DEFAULT_SERVER_DATETIME_FORMAT),
+ }),
+ (0, 0, {
+ 'name': cls.product_id_2.name,
+ 'product_id': cls.product_id_2.id,
+ 'product_qty': 5.0,
+ 'product_uom': cls.product_id_2.uom_po_id.id,
+ 'price_unit': 250.0,
+ 'date_planned': datetime.today().replace(hour=9).strftime(DEFAULT_SERVER_DATETIME_FORMAT),
+ })],
+ }
+
+ def test_00_purchase_order_flow(self):
+ # Ensure product_id_2 doesn't have res_partner_1 as supplier
+ if self.partner_a in self.product_id_2.seller_ids.mapped('name'):
+ id_to_remove = self.product_id_2.seller_ids.filtered(lambda r: r.name == self.partner_a).ids[0] if self.product_id_2.seller_ids.filtered(lambda r: r.name == self.partner_a) else False
+ if id_to_remove:
+ self.product_id_2.write({
+ 'seller_ids': [(2, id_to_remove, False)],
+ })
+ self.assertFalse(self.product_id_2.seller_ids.filtered(lambda r: r.name == self.partner_a), 'Purchase: the partner should not be in the list of the product suppliers')
+
+ self.po = self.env['purchase.order'].create(self.po_vals)
+ self.assertTrue(self.po, 'Purchase: no purchase order created')
+ self.assertEqual(self.po.invoice_status, 'no', 'Purchase: PO invoice_status should be "Not purchased"')
+ self.assertEqual(self.po.order_line.mapped('qty_received'), [0.0, 0.0], 'Purchase: no product should be received"')
+ self.assertEqual(self.po.order_line.mapped('qty_invoiced'), [0.0, 0.0], 'Purchase: no product should be invoiced"')
+
+ self.po.button_confirm()
+ self.assertEqual(self.po.state, 'purchase', 'Purchase: PO state should be "Purchase"')
+ self.assertEqual(self.po.invoice_status, 'to invoice', 'Purchase: PO invoice_status should be "Waiting Invoices"')
+
+ self.assertTrue(self.product_id_2.seller_ids.filtered(lambda r: r.name == self.partner_a), 'Purchase: the partner should be in the list of the product suppliers')
+
+ seller = self.product_id_2._select_seller(partner_id=self.partner_a, quantity=2.0, date=self.po.date_planned, uom_id=self.product_id_2.uom_po_id)
+ price_unit = seller.price if seller else 0.0
+ if price_unit and seller and self.po.currency_id and seller.currency_id != self.po.currency_id:
+ price_unit = seller.currency_id._convert(price_unit, self.po.currency_id, self.po.company_id, self.po.date_order)
+ self.assertEqual(price_unit, 250.0, 'Purchase: the price of the product for the supplier should be 250.0.')
+
+ self.assertEqual(self.po.picking_count, 1, 'Purchase: one picking should be created"')
+ self.picking = self.po.picking_ids[0]
+ self.picking.move_line_ids.write({'qty_done': 5.0})
+ self.picking.button_validate()
+ self.assertEqual(self.po.order_line.mapped('qty_received'), [5.0, 5.0], 'Purchase: all products should be received"')
+
+ move_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
+ move_form.partner_id = self.partner_a
+ move_form.purchase_id = self.po
+ self.invoice = move_form.save()
+
+ self.assertEqual(self.po.order_line.mapped('qty_invoiced'), [5.0, 5.0], 'Purchase: all products should be invoiced"')
+
+ def test_02_po_return(self):
+ """
+ Test a PO with a product on Incoming shipment. Validate the PO, then do a return
+ of the picking with Refund.
+ """
+ # Draft purchase order created
+ self.po = self.env['purchase.order'].create(self.po_vals)
+ self.assertTrue(self.po, 'Purchase: no purchase order created')
+ self.assertEqual(self.po.order_line.mapped('qty_received'), [0.0, 0.0], 'Purchase: no product should be received"')
+ self.assertEqual(self.po.order_line.mapped('qty_invoiced'), [0.0, 0.0], 'Purchase: no product should be invoiced"')
+
+ self.po.button_confirm()
+ self.assertEqual(self.po.state, 'purchase', 'Purchase: PO state should be "Purchase"')
+ self.assertEqual(self.po.invoice_status, 'to invoice', 'Purchase: PO invoice_status should be "Waiting Invoices"')
+
+ # Confirm the purchase order
+ self.po.button_confirm()
+ self.assertEqual(self.po.state, 'purchase', 'Purchase: PO state should be "Purchase')
+ self.assertEqual(self.po.picking_count, 1, 'Purchase: one picking should be created"')
+ self.picking = self.po.picking_ids[0]
+ self.picking.move_line_ids.write({'qty_done': 5.0})
+ self.picking.button_validate()
+ self.assertEqual(self.po.order_line.mapped('qty_received'), [5.0, 5.0], 'Purchase: all products should be received"')
+
+ #After Receiving all products create vendor bill.
+ 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_a
+ move_form.purchase_id = self.po
+ self.invoice = move_form.save()
+ self.invoice.action_post()
+
+ self.assertEqual(self.po.order_line.mapped('qty_invoiced'), [5.0, 5.0], 'Purchase: all products should be invoiced"')
+
+ # Check quantity received
+ received_qty = sum(pol.qty_received for pol in self.po.order_line)
+ self.assertEqual(received_qty, 10.0, 'Purchase: Received quantity should be 10.0 instead of %s after validating incoming shipment' % received_qty)
+
+ # Create return picking
+ pick = self.po.picking_ids
+ stock_return_picking_form = Form(self.env['stock.return.picking']
+ .with_context(active_ids=pick.ids, active_id=pick.ids[0],
+ active_model='stock.picking'))
+ return_wiz = stock_return_picking_form.save()
+ return_wiz.product_return_moves.write({'quantity': 2.0, 'to_refund': True}) # Return only 2
+ res = return_wiz.create_returns()
+ return_pick = self.env['stock.picking'].browse(res['res_id'])
+
+ # Validate picking
+ return_pick.move_line_ids.write({'qty_done': 2})
+
+ return_pick.button_validate()
+
+ # Check Received quantity
+ self.assertEqual(self.po.order_line[0].qty_received, 3.0, 'Purchase: delivered quantity should be 3.0 instead of "%s" after picking return' % self.po.order_line[0].qty_received)
+ #Create vendor bill for refund qty
+ 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_a
+ move_form.purchase_id = self.po
+ self.invoice = move_form.save()
+ move_form = Form(self.invoice)
+ with move_form.invoice_line_ids.edit(0) as line_form:
+ line_form.quantity = 2.0
+ with move_form.invoice_line_ids.edit(1) as line_form:
+ line_form.quantity = 2.0
+ self.invoice = move_form.save()
+ self.invoice.action_post()
+
+ self.assertEqual(self.po.order_line.mapped('qty_invoiced'), [3.0, 3.0], 'Purchase: Billed quantity should be 3.0')
+
+ def test_03_po_return_and_modify(self):
+ """Change the picking code of the delivery to internal. Make a PO for 10 units, go to the
+ picking and return 5, edit the PO line to 15 units.
+ The purpose of the test is to check the consistencies across the received quantities and the
+ procurement quantities.
+ """
+ # Change the code of the picking type delivery
+ self.env['stock.picking.type'].search([('code', '=', 'outgoing')]).write({'code': 'internal'})
+
+ # Sell and deliver 10 units
+ item1 = self.product_id_1
+ uom_unit = self.env.ref('uom.product_uom_unit')
+ po1 = self.env['purchase.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': item1.name,
+ 'product_id': item1.id,
+ 'product_qty': 10,
+ 'product_uom': uom_unit.id,
+ 'price_unit': 123.0,
+ 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
+ }),
+ ],
+ })
+ po1.button_confirm()
+
+ picking = po1.picking_ids
+ wiz_act = picking.button_validate()
+ wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save()
+ wiz.process()
+
+ # Return 5 units
+ stock_return_picking_form = Form(self.env['stock.return.picking'].with_context(
+ active_ids=picking.ids,
+ active_id=picking.ids[0],
+ active_model='stock.picking'
+ ))
+ return_wiz = stock_return_picking_form.save()
+ for return_move in return_wiz.product_return_moves:
+ return_move.write({
+ 'quantity': 5,
+ 'to_refund': True
+ })
+ res = return_wiz.create_returns()
+ return_pick = self.env['stock.picking'].browse(res['res_id'])
+ wiz_act = return_pick.button_validate()
+ wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save()
+ wiz.process()
+
+ self.assertEqual(po1.order_line.qty_received, 5)
+
+ # Deliver 15 instead of 10.
+ po1.write({
+ 'order_line': [
+ (1, po1.order_line[0].id, {'product_qty': 15}),
+ ]
+ })
+
+ # A new move of 10 unit (15 - 5 units)
+ self.assertEqual(po1.order_line.qty_received, 5)
+ self.assertEqual(po1.picking_ids[-1].move_lines.product_qty, 10)
+
+ def test_04_update_date_planned(self):
+ today = datetime.today().replace(hour=9, microsecond=0)
+ tomorrow = datetime.today().replace(hour=9, microsecond=0) + timedelta(days=1)
+ po = self.env['purchase.order'].create(self.po_vals)
+ po.button_confirm()
+
+ # update first line
+ po._update_date_planned_for_lines([(po.order_line[0], tomorrow)])
+ self.assertEqual(po.order_line[0].date_planned, tomorrow)
+ activity = self.env['mail.activity'].search([
+ ('summary', '=', 'Date Updated'),
+ ('res_model_id', '=', 'purchase.order'),
+ ('res_id', '=', po.id),
+ ])
+ self.assertTrue(activity)
+ self.assertIn(
+ '<p> partner_a modified receipt dates for the following products:</p><p> \xa0 - Large Desk from %s to %s </p><p>Those dates have been updated accordingly on the receipt %s.</p>' % (today.date(), tomorrow.date(), po.picking_ids.name),
+ activity.note,
+ )
+
+ # receive products
+ wiz_act = po.picking_ids.button_validate()
+ wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save()
+ wiz.process()
+
+ # update second line
+ old_date = po.order_line[1].date_planned
+ po._update_date_planned_for_lines([(po.order_line[1], tomorrow)])
+ self.assertEqual(po.order_line[1].date_planned, old_date)
+ self.assertIn(
+ '<p> partner_a modified receipt dates for the following products:</p><p> \xa0 - Large Desk from %s to %s </p><p> \xa0 - Conference Chair from %s to %s </p><p>Those dates couldn’t be modified accordingly on the receipt %s which had already been validated.</p>' % (today.date(), tomorrow.date(), today.date(), tomorrow.date(), po.picking_ids.name),
+ activity.note,
+ )
+
+ def test_05_multi_company(self):
+ company_a = self.env.user.company_id
+ company_b = self.env['res.company'].create({
+ "name": "Test Company",
+ "currency_id": self.env['res.currency'].with_context(active_test=False).search([
+ ('id', '!=', company_a.currency_id.id),
+ ], limit=1).id
+ })
+ self.env.user.write({
+ 'company_id': company_b.id,
+ 'company_ids': [(4, company_b.id), (4, company_a.id)],
+ })
+ po = self.env['purchase.order'].create(dict(company_id=company_a.id, partner_id=self.partner_a.id))
+
+ self.assertEqual(po.company_id, company_a)
+ self.assertEqual(po.picking_type_id.warehouse_id.company_id, company_a)
+ self.assertEqual(po.currency_id, po.company_id.currency_id)
+
+ def test_06_on_time_rate(self):
+ company_a = self.env.user.company_id
+ company_b = self.env['res.company'].create({
+ "name": "Test Company",
+ "currency_id": self.env['res.currency'].with_context(active_test=False).search([
+ ('id', '!=', company_a.currency_id.id),
+ ], limit=1).id
+ })
+
+ # Create a purchase order with 90% qty received for company A
+ self.env.user.write({
+ 'company_id': company_a.id,
+ 'company_ids': [(6, 0, [company_a.id])],
+ })
+ po = self.env['purchase.order'].create(self.po_vals)
+ po.order_line.write({'product_qty': 10})
+ po.button_confirm()
+ picking = po.picking_ids[0]
+ # Process 9.0 out of the 10.0 ordered qty
+ picking.move_line_ids.write({'qty_done': 9.0})
+ res_dict = picking.button_validate()
+ # No backorder
+ self.env['stock.backorder.confirmation'].with_context(res_dict['context']).process_cancel_backorder()
+ # `on_time_rate` should be equals to the ratio of quantity received against quantity ordered
+ expected_rate = sum(picking.move_line_ids.mapped("qty_done")) / sum(po.order_line.mapped("product_qty")) * 100
+ self.assertEqual(expected_rate, po.on_time_rate)
+
+ # Create a purchase order with 80% qty received for company B
+ # The On-Time Delivery Rate shouldn't be shared accross multiple companies
+ self.env.user.write({
+ 'company_id': company_b.id,
+ 'company_ids': [(6, 0, [company_b.id])],
+ })
+ po = self.env['purchase.order'].create(self.po_vals)
+ po.order_line.write({'product_qty': 10})
+ po.button_confirm()
+ picking = po.picking_ids[0]
+ # Process 8.0 out of the 10.0 ordered qty
+ picking.move_line_ids.write({'qty_done': 8.0})
+ res_dict = picking.button_validate()
+ # No backorder
+ self.env['stock.backorder.confirmation'].with_context(res_dict['context']).process_cancel_backorder()
+ # `on_time_rate` should be equal to the ratio of quantity received against quantity ordered
+ expected_rate = sum(picking.move_line_ids.mapped("qty_done")) / sum(po.order_line.mapped("product_qty")) * 100
+ self.assertEqual(expected_rate, po.on_time_rate)
+
+ # Tricky corner case
+ # As `purchase.order.on_time_rate` is a related to `partner_id.on_time_rate`
+ # `on_time_rate` on the PO should equals `on_time_rate` on the partner.
+ # Related fields are by default computed as sudo
+ # while non-stored computed fields are not computed as sudo by default
+ # If the computation of the related field (`purchase.order.on_time_rate`) was asked
+ # and `res.partner.on_time_rate` was not yet in the cache
+ # the `sudo` requested for the computation of the related `purchase.order.on_time_rate`
+ # was propagated to the computation of `res.partner.on_time_rate`
+ # and therefore the multi-company record rules were ignored.
+ # 1. Compute `res.partner.on_time_rate` regular non-stored comptued field
+ partner_on_time_rate = po.partner_id.on_time_rate
+ # 2. Invalidate the cache for that record and field, so it's not reused in the next step.
+ po.partner_id.invalidate_cache(fnames=["on_time_rate"], ids=po.partner_id.ids)
+ # 3. Compute the related field `purchase.order.on_time_rate`
+ po_on_time_rate = po.on_time_rate
+ # 4. Check both are equals.
+ self.assertEqual(partner_on_time_rate, po_on_time_rate)
diff --git a/addons/purchase_stock/tests/test_purchase_order_process.py b/addons/purchase_stock/tests/test_purchase_order_process.py
new file mode 100644
index 00000000..56f09106
--- /dev/null
+++ b/addons/purchase_stock/tests/test_purchase_order_process.py
@@ -0,0 +1,29 @@
+from .common import PurchaseTestCommon
+
+
+class TestPurchaseOrderProcess(PurchaseTestCommon):
+
+ def test_00_cancel_purchase_order_flow(self):
+ """ Test cancel purchase order with group user."""
+
+ # In order to test the cancel flow,start it from canceling confirmed purchase order.
+ purchase_order = self.env['purchase.order'].create({
+ 'partner_id': self.env['res.partner'].create({'name': 'My Partner'}).id,
+ 'state': 'draft',
+ })
+ po_edit_with_user = purchase_order.with_user(self.res_users_purchase_user)
+
+ # Confirm the purchase order.
+ po_edit_with_user.button_confirm()
+
+ # Check the "Approved" status after confirmed RFQ.
+ self.assertEqual(po_edit_with_user.state, 'purchase', 'Purchase: PO state should be "Purchase')
+
+ # First cancel receptions related to this order if order shipped.
+ po_edit_with_user.picking_ids.action_cancel()
+
+ # Able to cancel purchase order.
+ po_edit_with_user.button_cancel()
+
+ # Check that order is cancelled.
+ self.assertEqual(po_edit_with_user.state, 'cancel', 'Purchase: PO state should be "Cancel')
diff --git a/addons/purchase_stock/tests/test_purchase_stock_report.py b/addons/purchase_stock/tests/test_purchase_stock_report.py
new file mode 100644
index 00000000..824a11e7
--- /dev/null
+++ b/addons/purchase_stock/tests/test_purchase_stock_report.py
@@ -0,0 +1,147 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests.common import Form
+from odoo.addons.stock.tests.test_report import TestReportsCommon
+
+
+class TestPurchaseStockReports(TestReportsCommon):
+ def test_report_forecast_1_purchase_order_multi_receipt(self):
+ """ Create a PO for 5 product, receive them then increase the quantity to 10.
+ """
+ po_form = Form(self.env['purchase.order'])
+ po_form.partner_id = self.partner
+ with po_form.order_line.new() as line:
+ line.product_id = self.product
+ line.product_qty = 5
+ po = po_form.save()
+
+ # Checks the report.
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ draft_picking_qty_in = docs['draft_picking_qty']['in']
+ draft_purchase_qty = docs['draft_purchase_qty']
+ pending_qty_in = docs['qty']['in']
+ self.assertEqual(len(lines), 0, "Must have 0 line for now.")
+ self.assertEqual(draft_picking_qty_in, 0)
+ self.assertEqual(draft_purchase_qty, 5)
+ self.assertEqual(pending_qty_in, 5)
+
+ # Confirms the PO and checks the report again.
+ po.button_confirm()
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ draft_picking_qty_in = docs['draft_picking_qty']['in']
+ draft_purchase_qty = docs['draft_purchase_qty']
+ pending_qty_in = docs['qty']['in']
+ self.assertEqual(len(lines), 1)
+ self.assertEqual(lines[0]['document_in'].id, po.id)
+ self.assertEqual(lines[0]['quantity'], 5)
+ self.assertEqual(lines[0]['document_out'], False)
+ self.assertEqual(draft_picking_qty_in, 0)
+ self.assertEqual(draft_purchase_qty, 0)
+ self.assertEqual(pending_qty_in, 0)
+
+ # Receives 5 products.
+ receipt = po.picking_ids
+ res_dict = receipt.button_validate()
+ wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
+ wizard.process()
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ draft_picking_qty_in = docs['draft_picking_qty']['in']
+ draft_purchase_qty = docs['draft_purchase_qty']
+ pending_qty_in = docs['qty']['in']
+ self.assertEqual(len(lines), 0)
+ self.assertEqual(draft_picking_qty_in, 0)
+ self.assertEqual(draft_purchase_qty, 0)
+ self.assertEqual(pending_qty_in, 0)
+
+ # Increase the PO quantity to 10, so must create a second receipt.
+ po_form = Form(po)
+ with po_form.order_line.edit(0) as line:
+ line.product_qty = 10
+ po = po_form.save()
+ # Checks the report.
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ draft_picking_qty_in = docs['draft_picking_qty']['in']
+ draft_purchase_qty = docs['draft_purchase_qty']
+ pending_qty_in = docs['qty']['in']
+ self.assertEqual(len(lines), 1, "Must have 1 line for now.")
+ self.assertEqual(lines[0]['document_in'].id, po.id)
+ self.assertEqual(lines[0]['quantity'], 5)
+ self.assertEqual(draft_picking_qty_in, 0)
+ self.assertEqual(draft_purchase_qty, 0)
+ self.assertEqual(pending_qty_in, 0)
+
+ def test_report_forecast_2_purchase_order_three_step_receipt(self):
+ """ Create a PO for 4 product, receive them then increase the quantity
+ to 10, but use three steps receipt.
+ """
+ grp_multi_loc = self.env.ref('stock.group_stock_multi_locations')
+ grp_multi_routes = self.env.ref('stock.group_adv_location')
+ self.env.user.write({'groups_id': [(4, grp_multi_loc.id)]})
+ self.env.user.write({'groups_id': [(4, grp_multi_routes.id)]})
+ # Configure warehouse.
+ warehouse = self.env.ref('stock.warehouse0')
+ warehouse.reception_steps = 'three_steps'
+
+ po_form = Form(self.env['purchase.order'])
+ po_form.partner_id = self.partner
+ with po_form.order_line.new() as line:
+ line.product_id = self.product
+ line.product_qty = 4
+ po = po_form.save()
+
+ # Checks the report -> Must be empty for now, just display some pending qty.
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ draft_picking_qty_in = docs['draft_picking_qty']['in']
+ draft_purchase_qty = docs['draft_purchase_qty']
+ pending_qty_in = docs['qty']['in']
+ self.assertEqual(len(lines), 0, "Must have 0 line for now.")
+ self.assertEqual(draft_picking_qty_in, 0)
+ self.assertEqual(draft_purchase_qty, 4)
+ self.assertEqual(pending_qty_in, 4)
+
+ # Confirms the PO and checks the report again.
+ po.button_confirm()
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ draft_picking_qty_in = docs['draft_picking_qty']['in']
+ draft_purchase_qty = docs['draft_purchase_qty']
+ pending_qty_in = docs['qty']['in']
+ self.assertEqual(len(lines), 1)
+ self.assertEqual(lines[0]['document_in'].id, po.id)
+ self.assertEqual(lines[0]['quantity'], 4)
+ self.assertEqual(lines[0]['document_out'], False)
+ self.assertEqual(draft_picking_qty_in, 0)
+ self.assertEqual(draft_purchase_qty, 0)
+ self.assertEqual(pending_qty_in, 0)
+ # Get back the different transfers.
+ receipt = po.picking_ids
+
+ # Receives 4 products.
+ res_dict = receipt.button_validate()
+ wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
+ wizard.process()
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ draft_picking_qty_in = docs['draft_picking_qty']['in']
+ draft_purchase_qty = docs['draft_purchase_qty']
+ pending_qty_in = docs['qty']['in']
+ self.assertEqual(len(lines), 0)
+ self.assertEqual(draft_picking_qty_in, 0)
+ self.assertEqual(draft_purchase_qty, 0)
+ self.assertEqual(pending_qty_in, 0)
+
+ # Increase the PO quantity to 10, so must create a second receipt.
+ po_form = Form(po)
+ with po_form.order_line.edit(0) as line:
+ line.product_qty = 10
+ po = po_form.save()
+ # Checks the report.
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ draft_picking_qty_in = docs['draft_picking_qty']['in']
+ draft_purchase_qty = docs['draft_purchase_qty']
+ pending_qty_in = docs['qty']['in']
+ self.assertEqual(len(lines), 1)
+ self.assertEqual(lines[0]['document_in'].id, po.id)
+ self.assertEqual(lines[0]['quantity'], 6)
+ self.assertEqual(draft_picking_qty_in, 0)
+ self.assertEqual(draft_purchase_qty, 0)
+ self.assertEqual(pending_qty_in, 0)
diff --git a/addons/purchase_stock/tests/test_reordering_rule.py b/addons/purchase_stock/tests/test_reordering_rule.py
new file mode 100644
index 00000000..bcb2f48a
--- /dev/null
+++ b/addons/purchase_stock/tests/test_reordering_rule.py
@@ -0,0 +1,520 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import datetime as dt
+from datetime import timedelta as td
+
+from odoo import SUPERUSER_ID
+from odoo.tests import Form
+from odoo.tests.common import SavepointCase
+
+
+class TestReorderingRule(SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestReorderingRule, cls).setUpClass()
+ cls.partner = cls.env['res.partner'].create({
+ 'name': 'Smith'
+ })
+
+ # create product and set the vendor
+ product_form = Form(cls.env['product.product'])
+ product_form.name = 'Product A'
+ product_form.type = 'product'
+ product_form.description = 'Internal Notes'
+ with product_form.seller_ids.new() as seller:
+ seller.name = cls.partner
+ product_form.route_ids.add(cls.env.ref('purchase_stock.route_warehouse0_buy'))
+ cls.product_01 = product_form.save()
+
+ def test_reordering_rule_1(self):
+ """
+ - Receive products in 2 steps
+ - The product has a reordering rule
+ - On the po generated, the source document should be the name of the reordering rule
+ - Increase the quantity on the RFQ, the extra quantity should follow the push rules
+ - Increase the quantity on the PO, the extra quantity should follow the push rules
+ - There should be one move supplier -> input and two moves input -> stock
+ """
+ warehouse_1 = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
+ warehouse_1.write({'reception_steps': 'two_steps'})
+
+ # create reordering rule
+ orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
+ orderpoint_form.warehouse_id = warehouse_1
+ orderpoint_form.location_id = warehouse_1.lot_stock_id
+ orderpoint_form.product_id = self.product_01
+ orderpoint_form.product_min_qty = 0.000
+ orderpoint_form.product_max_qty = 0.000
+ order_point = orderpoint_form.save()
+ # Create Delivery Order of 10 product
+ picking_form = Form(self.env['stock.picking'])
+ picking_form.partner_id = self.partner
+ picking_form.picking_type_id = self.env.ref('stock.picking_type_out')
+ with picking_form.move_ids_without_package.new() as move:
+ move.product_id = self.product_01
+ move.product_uom_qty = 10.0
+ customer_picking = picking_form.save()
+ # picking confirm
+ customer_picking.action_confirm()
+ # Run scheduler
+ self.env['procurement.group'].run_scheduler()
+
+ # Check purchase order created or not
+ purchase_order = self.env['purchase.order'].search([('partner_id', '=', self.partner.id)])
+ self.assertTrue(purchase_order, 'No purchase order created.')
+
+ # On the po generated, the source document should be the name of the reordering rule
+ self.assertEqual(order_point.name, purchase_order.origin, 'Source document on purchase order should be the name of the reordering rule.')
+ self.assertEqual(purchase_order.order_line.product_qty, 10)
+ self.assertEqual(purchase_order.order_line.name, 'Product A')
+
+ # Increase the quantity on the RFQ before confirming it
+ purchase_order.order_line.product_qty = 12
+ purchase_order.button_confirm()
+
+ self.assertEqual(purchase_order.picking_ids.move_lines.filtered(lambda m: m.product_id == self.product_01).product_qty, 12)
+ next_picking = purchase_order.picking_ids.move_lines.move_dest_ids.picking_id
+ self.assertEqual(len(next_picking), 2)
+ self.assertEqual(next_picking[0].move_lines.filtered(lambda m: m.product_id == self.product_01).product_qty, 10)
+ self.assertEqual(next_picking[1].move_lines.filtered(lambda m: m.product_id == self.product_01).product_qty, 2)
+
+ # Increase the quantity on the PO
+ purchase_order.order_line.product_qty = 15
+ self.assertEqual(purchase_order.picking_ids.move_lines.product_qty, 15)
+ self.assertEqual(next_picking[0].move_lines.filtered(lambda m: m.product_id == self.product_01).product_qty, 10)
+ self.assertEqual(next_picking[1].move_lines.filtered(lambda m: m.product_id == self.product_01).product_qty, 5)
+
+ def test_reordering_rule_2(self):
+ """
+ - Receive products in 1 steps
+ - The product has two reordering rules, each one applying in a sublocation
+ - Processing the purchase order should fulfill the two sublocations
+ - Increase the quantity on the RFQ for one of the POL, the extra quantity will go to
+ the original subloc since we don't know where to push it (no move dest)
+ - Increase the quantity on the PO, the extra quantity should follow the push rules and
+ thus go to stock
+ """
+ warehouse_1 = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
+ subloc_1 = self.env['stock.location'].create({'name': 'subloc_1', 'location_id': warehouse_1.lot_stock_id.id})
+ subloc_2 = self.env['stock.location'].create({'name': 'subloc_2', 'location_id': warehouse_1.lot_stock_id.id})
+
+ # create reordering rules
+ orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
+ orderpoint_form.warehouse_id = warehouse_1
+ orderpoint_form.location_id = subloc_1
+ orderpoint_form.product_id = self.product_01
+ orderpoint_form.product_min_qty = 0.000
+ orderpoint_form.product_max_qty = 0.000
+ order_point_1 = orderpoint_form.save()
+ orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
+ orderpoint_form.warehouse_id = warehouse_1
+ orderpoint_form.location_id = subloc_2
+ orderpoint_form.product_id = self.product_01
+ orderpoint_form.product_min_qty = 0.000
+ orderpoint_form.product_max_qty = 0.000
+ order_point_2 = orderpoint_form.save()
+
+ # Create Delivery Order of 10 product
+ picking_form = Form(self.env['stock.picking'])
+ picking_form.partner_id = self.partner
+ picking_form.picking_type_id = self.env.ref('stock.picking_type_out')
+ with picking_form.move_ids_without_package.new() as move:
+ move.product_id = self.product_01
+ move.product_uom_qty = 10.0
+ with picking_form.move_ids_without_package.new() as move:
+ move.product_id = self.product_01
+ move.product_uom_qty = 10.0
+ customer_picking = picking_form.save()
+ customer_picking.move_lines[0].location_id = subloc_1.id
+ customer_picking.move_lines[1].location_id = subloc_2.id
+
+ # picking confirm
+ customer_picking.action_confirm()
+ self.assertEqual(self.product_01.with_context(location=subloc_1.id).virtual_available, -10)
+ self.assertEqual(self.product_01.with_context(location=subloc_2.id).virtual_available, -10)
+
+ # Run scheduler
+ self.env['procurement.group'].run_scheduler()
+
+ # Check purchase order created or not
+ purchase_order = self.env['purchase.order'].search([('partner_id', '=', self.partner.id)])
+ self.assertTrue(purchase_order, 'No purchase order created.')
+ self.assertEqual(len(purchase_order.order_line), 2, 'Not enough purchase order lines created.')
+
+ # increment the qty of the first po line
+ purchase_order.order_line.filtered(lambda pol: pol.orderpoint_id == order_point_1).product_qty = 15
+ purchase_order.button_confirm()
+ self.assertEqual(self.product_01.with_context(location=subloc_1.id).virtual_available, 5)
+ self.assertEqual(self.product_01.with_context(location=subloc_2.id).virtual_available, 0)
+
+ # increment the qty of the second po line
+ purchase_order.order_line.filtered(lambda pol: pol.orderpoint_id == order_point_2).with_context(debug=True).product_qty = 15
+ self.assertEqual(self.product_01.with_context(location=subloc_1.id).virtual_available, 5)
+ self.assertEqual(self.product_01.with_context(location=subloc_2.id).virtual_available, 0)
+ self.assertEqual(self.product_01.with_context(location=warehouse_1.lot_stock_id.id).virtual_available, 10) # 5 on the main loc, 5 on subloc_1
+
+ self.assertEqual(purchase_order.picking_ids.move_lines[-1].product_qty, 5)
+ self.assertEqual(purchase_order.picking_ids.move_lines[-1].location_dest_id, warehouse_1.lot_stock_id)
+
+ def test_replenish_report_1(self):
+ """Tests the auto generation of manual orderpoints.
+
+ Opening multiple times the report should not duplicate the generated orderpoints.
+ MTO products should not trigger the creation of generated orderpoints
+ """
+ partner = self.env['res.partner'].create({
+ 'name': 'Tintin'
+ })
+ route_buy = self.env.ref('purchase_stock.route_warehouse0_buy')
+ route_mto = self.env.ref('stock.route_warehouse0_mto')
+
+ product_form = Form(self.env['product.product'])
+ product_form.name = 'Simple Product'
+ product_form.type = 'product'
+ with product_form.seller_ids.new() as s:
+ s.name = partner
+ product = product_form.save()
+
+ product_form = Form(self.env['product.product'])
+ product_form.name = 'Product BUY + MTO'
+ product_form.type = 'product'
+ product_form.route_ids.add(route_buy)
+ product_form.route_ids.add(route_mto)
+ with product_form.seller_ids.new() as s:
+ s.name = partner
+ product_buy_mto = product_form.save()
+
+ # Create Delivery Order of 20 product and 10 buy + MTO
+ picking_form = Form(self.env['stock.picking'])
+ picking_form.partner_id = partner
+ picking_form.picking_type_id = self.env.ref('stock.picking_type_out')
+ with picking_form.move_ids_without_package.new() as move:
+ move.product_id = product
+ move.product_uom_qty = 10.0
+ with picking_form.move_ids_without_package.new() as move:
+ move.product_id = product
+ move.product_uom_qty = 10.0
+ with picking_form.move_ids_without_package.new() as move:
+ move.product_id = product_buy_mto
+ move.product_uom_qty = 10.0
+ customer_picking = picking_form.save()
+ customer_picking.move_lines.filtered(lambda m: m.product_id == product_buy_mto).procure_method = 'make_to_order'
+ customer_picking.action_confirm()
+ self.env['stock.warehouse.orderpoint']._get_orderpoint_action()
+ self.env['stock.warehouse.orderpoint']._get_orderpoint_action()
+
+ orderpoint_product = self.env['stock.warehouse.orderpoint'].search(
+ [('product_id', '=', product.id)])
+ orderpoint_product_mto_buy = self.env['stock.warehouse.orderpoint'].search(
+ [('product_id', '=', product_buy_mto.id)])
+ self.assertFalse(orderpoint_product_mto_buy)
+ self.assertEqual(len(orderpoint_product), 1.0)
+ self.assertEqual(orderpoint_product.qty_to_order, 20.0)
+ self.assertEqual(orderpoint_product.trigger, 'manual')
+ self.assertEqual(orderpoint_product.create_uid.id, SUPERUSER_ID)
+
+ orderpoint_product.action_replenish()
+ po = self.env['purchase.order'].search([('partner_id', '=', partner.id)])
+ self.assertTrue(po)
+ self.assertEqual(len(po.order_line), 2.0)
+ po_line_product_mto = po.order_line.filtered(lambda l: l.product_id == product_buy_mto)
+ po_line_product = po.order_line.filtered(lambda l: l.product_id == product)
+ self.assertEqual(po_line_product_mto.product_uom_qty, 10.0)
+ self.assertEqual(po_line_product.product_uom_qty, 20.0)
+
+ self.env['stock.warehouse.orderpoint']._get_orderpoint_action()
+ orderpoint_product = self.env['stock.warehouse.orderpoint'].search(
+ [('product_id', '=', product.id)])
+ orderpoint_product_mto_buy = self.env['stock.warehouse.orderpoint'].search(
+ [('product_id', '=', product_buy_mto.id)])
+ self.assertFalse(orderpoint_product)
+ self.assertFalse(orderpoint_product_mto_buy)
+
+ # Create Delivery Order of 10 product and 10 buy + MTO
+ picking_form = Form(self.env['stock.picking'])
+ picking_form.partner_id = partner
+ picking_form.picking_type_id = self.env.ref('stock.picking_type_out')
+ with picking_form.move_ids_without_package.new() as move:
+ move.product_id = product
+ move.product_uom_qty = 10.0
+ with picking_form.move_ids_without_package.new() as move:
+ move.product_id = product_buy_mto
+ move.product_uom_qty = 10.0
+ customer_picking = picking_form.save()
+ customer_picking.move_lines.filtered(lambda m: m.product_id == product_buy_mto).procure_method = 'make_to_order'
+ customer_picking.action_confirm()
+ self.env['stock.warehouse.orderpoint'].flush()
+
+ self.env['stock.warehouse.orderpoint']._get_orderpoint_action()
+ orderpoint_product = self.env['stock.warehouse.orderpoint'].search(
+ [('product_id', '=', product.id)])
+ orderpoint_product_mto_buy = self.env['stock.warehouse.orderpoint'].search(
+ [('product_id', '=', product_buy_mto.id)])
+ self.assertFalse(orderpoint_product_mto_buy)
+ self.assertEqual(len(orderpoint_product), 1.0)
+ self.assertEqual(orderpoint_product.qty_to_order, 10.0)
+ self.assertEqual(orderpoint_product.trigger, 'manual')
+ self.assertEqual(orderpoint_product.create_uid.id, SUPERUSER_ID)
+
+ def test_replenish_report_2(self):
+ """Same then `test_replenish_report_1` but with two steps receipt enabled"""
+ partner = self.env['res.partner'].create({
+ 'name': 'Tintin'
+ })
+ for wh in self.env['stock.warehouse'].search([]):
+ wh.write({'reception_steps': 'two_steps'})
+ route_buy = self.env.ref('purchase_stock.route_warehouse0_buy')
+ route_mto = self.env.ref('stock.route_warehouse0_mto')
+
+ product_form = Form(self.env['product.product'])
+ product_form.name = 'Simple Product'
+ product_form.type = 'product'
+ with product_form.seller_ids.new() as s:
+ s.name = partner
+ product = product_form.save()
+
+ product_form = Form(self.env['product.product'])
+ product_form.name = 'Product BUY + MTO'
+ product_form.type = 'product'
+ product_form.route_ids.add(route_buy)
+ product_form.route_ids.add(route_mto)
+ with product_form.seller_ids.new() as s:
+ s.name = partner
+ product_buy_mto = product_form.save()
+
+ # Create Delivery Order of 20 product and 10 buy + MTO
+ picking_form = Form(self.env['stock.picking'])
+ picking_form.partner_id = partner
+ picking_form.picking_type_id = self.env.ref('stock.picking_type_out')
+ with picking_form.move_ids_without_package.new() as move:
+ move.product_id = product
+ move.product_uom_qty = 10.0
+ with picking_form.move_ids_without_package.new() as move:
+ move.product_id = product
+ move.product_uom_qty = 10.0
+ with picking_form.move_ids_without_package.new() as move:
+ move.product_id = product_buy_mto
+ move.product_uom_qty = 10.0
+ customer_picking = picking_form.save()
+ customer_picking.move_lines.filtered(lambda m: m.product_id == product_buy_mto).procure_method = 'make_to_order'
+ customer_picking.action_confirm()
+ self.env['stock.warehouse.orderpoint']._get_orderpoint_action()
+ orderpoint_product = self.env['stock.warehouse.orderpoint'].search(
+ [('product_id', '=', product.id)])
+ orderpoint_product_mto_buy = self.env['stock.warehouse.orderpoint'].search(
+ [('product_id', '=', product_buy_mto.id)])
+ self.assertFalse(orderpoint_product_mto_buy)
+ self.assertEqual(len(orderpoint_product), 1.0)
+ self.assertEqual(orderpoint_product.qty_to_order, 20.0)
+ self.assertEqual(orderpoint_product.trigger, 'manual')
+ self.assertEqual(orderpoint_product.create_uid.id, SUPERUSER_ID)
+
+ orderpoint_product.action_replenish()
+ po = self.env['purchase.order'].search([('partner_id', '=', partner.id)])
+ self.assertTrue(po)
+ self.assertEqual(len(po.order_line), 2.0)
+ po_line_product_mto = po.order_line.filtered(lambda l: l.product_id == product_buy_mto)
+ po_line_product = po.order_line.filtered(lambda l: l.product_id == product)
+ self.assertEqual(po_line_product_mto.product_uom_qty, 10.0)
+ self.assertEqual(po_line_product.product_uom_qty, 20.0)
+
+ self.env['stock.warehouse.orderpoint'].flush()
+ self.env['stock.warehouse.orderpoint']._get_orderpoint_action()
+ orderpoint_product = self.env['stock.warehouse.orderpoint'].search(
+ [('product_id', '=', product.id)])
+ orderpoint_product_mto_buy = self.env['stock.warehouse.orderpoint'].search(
+ [('product_id', '=', product_buy_mto.id)])
+ self.assertFalse(orderpoint_product)
+ self.assertFalse(orderpoint_product_mto_buy)
+
+ # Create Delivery Order of 10 product and 10 buy + MTO
+ picking_form = Form(self.env['stock.picking'])
+ picking_form.partner_id = partner
+ picking_form.picking_type_id = self.env.ref('stock.picking_type_out')
+ with picking_form.move_ids_without_package.new() as move:
+ move.product_id = product
+ move.product_uom_qty = 10.0
+ with picking_form.move_ids_without_package.new() as move:
+ move.product_id = product_buy_mto
+ move.product_uom_qty = 10.0
+ customer_picking = picking_form.save()
+ customer_picking.move_lines.filtered(lambda m: m.product_id == product_buy_mto).procure_method = 'make_to_order'
+ customer_picking.action_confirm()
+ self.env['stock.warehouse.orderpoint'].flush()
+
+ self.env['stock.warehouse.orderpoint']._get_orderpoint_action()
+ orderpoint_product = self.env['stock.warehouse.orderpoint'].search(
+ [('product_id', '=', product.id)])
+ orderpoint_product_mto_buy = self.env['stock.warehouse.orderpoint'].search(
+ [('product_id', '=', product_buy_mto.id)])
+ self.assertFalse(orderpoint_product_mto_buy)
+ self.assertEqual(len(orderpoint_product), 1.0)
+ self.assertEqual(orderpoint_product.qty_to_order, 10.0)
+ self.assertEqual(orderpoint_product.trigger, 'manual')
+ self.assertEqual(orderpoint_product.create_uid.id, SUPERUSER_ID)
+
+ def test_procure_not_default_partner(self):
+ """Define a product with 2 vendors. First run a "standard" procurement,
+ default vendor should be used. Then, call a procurement with
+ `partner_id` specified in values, the specified vendor should be
+ used."""
+ purchase_route = self.env.ref("purchase_stock.route_warehouse0_buy")
+ uom_unit = self.env.ref("uom.product_uom_unit")
+ warehouse = self.env['stock.warehouse'].search(
+ [('company_id', '=', self.env.company.id)], limit=1)
+ product = self.env["product.product"].create({
+ "name": "product TEST",
+ "standard_price": 100.0,
+ "type": "product",
+ "uom_id": uom_unit.id,
+ "default_code": "A",
+ "route_ids": [(6, 0, purchase_route.ids)],
+ })
+ default_vendor = self.env["res.partner"].create({
+ "name": "Supplier A",
+ })
+ secondary_vendor = self.env["res.partner"].create({
+ "name": "Supplier B",
+ })
+ self.env["product.supplierinfo"].create({
+ "name": default_vendor.id,
+ "product_tmpl_id": product.product_tmpl_id.id,
+ "delay": 7,
+ })
+ self.env["product.supplierinfo"].create({
+ "name": secondary_vendor.id,
+ "product_tmpl_id": product.product_tmpl_id.id,
+ "delay": 10,
+ })
+
+ # Test standard procurement.
+ po_line = self.env["purchase.order.line"].search(
+ [("product_id", "=", product.id)])
+ self.assertFalse(po_line)
+ self.env["procurement.group"].run(
+ [self.env["procurement.group"].Procurement(
+ product, 100, uom_unit,
+ warehouse.lot_stock_id, "Test default vendor", "/",
+ self.env.company,
+ {
+ "warehouse_id": warehouse,
+ "date_planned": dt.today() + td(days=15),
+ "rule_id": warehouse.buy_pull_id,
+ "group_id": False,
+ "route_ids": [],
+ }
+ )])
+ po_line = self.env["purchase.order.line"].search(
+ [("product_id", "=", product.id)])
+ self.assertTrue(po_line)
+ self.assertEqual(po_line.partner_id, default_vendor)
+ po_line.order_id.button_cancel()
+ po_line.order_id.unlink()
+
+ # now force the vendor:
+ po_line = self.env["purchase.order.line"].search(
+ [("product_id", "=", product.id)])
+ self.assertFalse(po_line)
+ self.env["procurement.group"].run(
+ [self.env["procurement.group"].Procurement(
+ product, 100, uom_unit,
+ warehouse.lot_stock_id, "Test default vendor", "/",
+ self.env.company,
+ {
+ "warehouse_id": warehouse,
+ "date_planned": dt.today() + td(days=15),
+ "rule_id": warehouse.buy_pull_id,
+ "group_id": False,
+ "route_ids": [],
+ "supplierinfo_name": secondary_vendor,
+ }
+ )])
+ po_line = self.env["purchase.order.line"].search(
+ [("product_id", "=", product.id)])
+ self.assertTrue(po_line)
+ self.assertEqual(po_line.partner_id, secondary_vendor)
+
+ def test_procure_multi_lingual(self):
+ """
+ Define a product with description in English and French.
+ Run a procurement specifying a group_id with a partner (customer)
+ set up with French as language. Verify that the PO is generated
+ using the default (English) language.
+ """
+ purchase_route = self.env.ref("purchase_stock.route_warehouse0_buy")
+ # create a new warehouse to make sure it gets the mts/mto rule
+ warehouse = self.env['stock.warehouse'].create({
+ "name": "test warehouse",
+ "active": True,
+ 'reception_steps': 'one_step',
+ 'delivery_steps': 'ship_only',
+ 'code': 'TEST'
+ })
+ customer_loc, _ = warehouse._get_partner_locations()
+ mto_rule = self.env['stock.rule'].search(
+ [('warehouse_id', '=', warehouse.id),
+ ('procure_method', '=', 'mts_else_mto'),
+ ('location_id', '=', customer_loc.id)
+ ]
+ )
+ route_mto = self.env["stock.location.route"].create({
+ "name": "MTO",
+ "active": True,
+ "sequence": 3,
+ "product_selectable": True,
+ "rule_ids": [(6, 0, [
+ mto_rule.id
+ ])]
+ })
+ uom_unit = self.env.ref("uom.product_uom_unit")
+ product = self.env["product.product"].create({
+ "name": "product TEST",
+ "standard_price": 100.0,
+ "type": "product",
+ "uom_id": uom_unit.id,
+ "default_code": "A",
+ "route_ids": [(6, 0, [
+ route_mto.id,
+ purchase_route.id,
+ ])],
+ })
+ self.env['res.lang']._activate_lang('fr_FR')
+ self.env['ir.translation']._set_ids('product.template,name', 'model', 'fr_FR', product.product_tmpl_id.ids, 'produit en français')
+ self.env['ir.translation']._set_ids('product.product,name', 'model', 'fr_FR', product.ids, 'produit en français')
+ default_vendor = self.env["res.partner"].create({
+ "name": "Supplier A",
+ })
+ self.env["product.supplierinfo"].create({
+ "name": default_vendor.id,
+ "product_tmpl_id": product.product_tmpl_id.id,
+ "delay": 7,
+ })
+ customer = self.env["res.partner"].create({
+ "name": "Customer",
+ "lang": "fr_FR"
+ })
+ proc_group = self.env["procurement.group"].create({
+ "partner_id": customer.id
+ })
+ procurement = self.env["procurement.group"].Procurement(
+ product, 100, uom_unit,
+ customer.property_stock_customer,
+ "Test default vendor",
+ "/",
+ self.env.company,
+ {
+ "warehouse_id": warehouse,
+ "date_planned": dt.today() + td(days=15),
+ "group_id": proc_group,
+ "route_ids": [],
+ }
+ )
+ self.env.cache.invalidate()
+
+ self.env["procurement.group"].run([procurement])
+
+ po_line = self.env["purchase.order.line"].search(
+ [("product_id", "=", product.id)])
+ self.assertTrue(po_line)
+ self.assertEqual("[A] product TEST", po_line.name)
diff --git a/addons/purchase_stock/tests/test_replenish_wizard.py b/addons/purchase_stock/tests/test_replenish_wizard.py
new file mode 100644
index 00000000..bdb126dd
--- /dev/null
+++ b/addons/purchase_stock/tests/test_replenish_wizard.py
@@ -0,0 +1,249 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo.addons.stock.tests.common import TestStockCommon
+
+
+class TestReplenishWizard(TestStockCommon):
+ def setUp(self):
+ super(TestReplenishWizard, self).setUp()
+ self.vendor = self.env['res.partner'].create(dict(name='The Replenisher'))
+ self.product1_price = 500
+
+ # Create a supplier info witch the previous vendor
+ self.supplierinfo = self.env['product.supplierinfo'].create({
+ 'name': self.vendor.id,
+ 'price': self.product1_price,
+ })
+
+ # Create a product with the 'buy' route and
+ # the 'supplierinfo' prevously created
+ self.product1 = self.env['product.product'].create({
+ 'name': 'product a',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ 'seller_ids': [(4, self.supplierinfo.id, 0)],
+ 'route_ids': [(4, self.env.ref('purchase_stock.route_warehouse0_buy').id, 0)],
+ })
+
+ # Additional Values required by the replenish wizard
+ self.uom_unit = self.env.ref('uom.product_uom_unit')
+ self.wh = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
+
+ def test_replenish_buy_1(self):
+ """ Set a quantity to replenish via the "Buy" route and check if
+ a purchase order is created with the correct values
+ """
+ self.product_uom_qty = 42
+
+ replenish_wizard = self.env['product.replenish'].create({
+ 'product_id': self.product1.id,
+ 'product_tmpl_id': self.product1.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'quantity': self.product_uom_qty,
+ 'warehouse_id': self.wh.id,
+ })
+ replenish_wizard.launch_replenishment()
+ last_po_id = self.env['purchase.order'].search([
+ ('origin', 'ilike', '%Manual Replenishment%'),
+ ('partner_id', '=', self.vendor.id)
+ ])[-1]
+ self.assertTrue(last_po_id, 'Purchase Order not found')
+ order_line = last_po_id.order_line.search([('product_id', '=', self.product1.id)])
+ self.assertTrue(order_line, 'The product is not in the Purchase Order')
+ self.assertEqual(order_line.product_qty, self.product_uom_qty, 'Quantities does not match')
+ self.assertEqual(order_line.price_unit, self.product1_price, 'Prices does not match')
+
+ def test_chose_supplier_1(self):
+ """ Choose supplier based on the ordered quantity and minimum price
+
+ replenish 10
+
+ 1)seq1 vendor1 140 min qty 1
+ 2)seq2 vendor1 100 min qty 10
+ -> 2) should be chosen
+ """
+ product_to_buy = self.env['product.product'].create({
+ 'name': "Furniture Service",
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ 'route_ids': [(4, self.env.ref('purchase_stock.route_warehouse0_buy').id, 0)],
+ })
+ vendor1 = self.env['res.partner'].create({'name': 'vendor1', 'email': 'from.test@example.com'})
+
+ supplierinfo1 = self.env['product.supplierinfo'].create({
+ 'product_tmpl_id': product_to_buy.product_tmpl_id.id,
+ 'name': vendor1.id,
+ 'min_qty': 1,
+ 'price': 140,
+ 'sequence': 1,
+ })
+ supplierinfo2 = self.env['product.supplierinfo'].create({
+ 'product_tmpl_id': product_to_buy.product_tmpl_id.id,
+ 'name': vendor1.id,
+ 'min_qty': 10,
+ 'price': 100,
+ 'sequence': 2,
+ })
+
+ replenish_wizard = self.env['product.replenish'].create({
+ 'product_id': product_to_buy.id,
+ 'product_tmpl_id': product_to_buy.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'quantity': 10,
+ 'warehouse_id': self.wh.id,
+ })
+ replenish_wizard.launch_replenishment()
+ last_po_id = self.env['purchase.order'].search([
+ ('origin', 'ilike', '%Manual Replenishment%'),
+ ])[-1]
+ self.assertEqual(last_po_id.partner_id, vendor1)
+ self.assertEqual(last_po_id.order_line.price_unit, 100)
+
+ def test_chose_supplier_2(self):
+ """ Choose supplier based on the ordered quantity and minimum price
+
+ replenish 10
+
+ 1)seq1 vendor1 140 min qty 1
+ 2)seq2 vendor2 90 min qty 10
+ 3)seq3 vendor1 100 min qty 10
+ -> 3) should be chosen
+ """
+ product_to_buy = self.env['product.product'].create({
+ 'name': "Furniture Service",
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ 'route_ids': [(4, self.env.ref('purchase_stock.route_warehouse0_buy').id, 0)],
+ })
+ vendor1 = self.env['res.partner'].create({'name': 'vendor1', 'email': 'from.test@example.com'})
+ vendor2 = self.env['res.partner'].create({'name': 'vendor2', 'email': 'from.test2@example.com'})
+
+ supplierinfo1 = self.env['product.supplierinfo'].create({
+ 'product_tmpl_id': product_to_buy.product_tmpl_id.id,
+ 'name': vendor1.id,
+ 'min_qty': 1,
+ 'price': 140,
+ 'sequence': 1,
+ })
+ supplierinfo2 = self.env['product.supplierinfo'].create({
+ 'product_tmpl_id': product_to_buy.product_tmpl_id.id,
+ 'name': vendor2.id,
+ 'min_qty': 10,
+ 'price': 90,
+ 'sequence': 2,
+ })
+ supplierinfo3 = self.env['product.supplierinfo'].create({
+ 'product_tmpl_id': product_to_buy.product_tmpl_id.id,
+ 'name': vendor1.id,
+ 'min_qty': 10,
+ 'price': 100,
+ 'sequence': 3,
+ })
+
+ replenish_wizard = self.env['product.replenish'].create({
+ 'product_id': product_to_buy.id,
+ 'product_tmpl_id': product_to_buy.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'quantity': 10,
+ 'warehouse_id': self.wh.id,
+ })
+ replenish_wizard.launch_replenishment()
+ last_po_id = self.env['purchase.order'].search([
+ ('origin', 'ilike', '%Manual Replenishment%'),
+ ])[-1]
+ self.assertEqual(last_po_id.partner_id, vendor1)
+ self.assertEqual(last_po_id.order_line.price_unit, 100)
+
+ def test_chose_supplier_3(self):
+ """ Choose supplier based on the ordered quantity and minimum price
+
+ replenish 10
+
+ 1)seq2 vendor1 50
+ 2)seq1 vendor2 50
+ -> 2) should be chosen
+ """
+ product_to_buy = self.env['product.product'].create({
+ 'name': "Furniture Service",
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ 'route_ids': [(4, self.env.ref('purchase_stock.route_warehouse0_buy').id, 0)],
+ })
+ vendor1 = self.env['res.partner'].create({'name': 'vendor1', 'email': 'from.test@example.com'})
+ vendor2 = self.env['res.partner'].create({'name': 'vendor2', 'email': 'from.test2@example.com'})
+
+ supplierinfo1 = self.env['product.supplierinfo'].create({
+ 'product_tmpl_id': product_to_buy.product_tmpl_id.id,
+ 'name': vendor1.id,
+ 'price': 50,
+ 'sequence': 2,
+ })
+ supplierinfo2 = self.env['product.supplierinfo'].create({
+ 'product_tmpl_id': product_to_buy.product_tmpl_id.id,
+ 'name': vendor2.id,
+ 'price': 50,
+ 'sequence': 1,
+ })
+
+ replenish_wizard = self.env['product.replenish'].create({
+ 'product_id': product_to_buy.id,
+ 'product_tmpl_id': product_to_buy.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'quantity': 10,
+ 'warehouse_id': self.wh.id,
+ })
+ replenish_wizard.launch_replenishment()
+ last_po_id = self.env['purchase.order'].search([
+ ('origin', 'ilike', '%Manual Replenishment%'),
+ ])[-1]
+ self.assertEqual(last_po_id.partner_id, vendor2)
+
+ def test_chose_supplier_4(self):
+ """ Choose supplier based on the ordered quantity and minimum price
+
+ replenish 10
+
+ 1)seq1 vendor1 100 min qty 2
+ 2)seq2 vendor1 60 min qty 10
+ 2)seq3 vendor1 80 min qty 5
+ -> 2) should be chosen
+ """
+ product_to_buy = self.env['product.product'].create({
+ 'name': "Furniture Service",
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ 'route_ids': [(4, self.env.ref('purchase_stock.route_warehouse0_buy').id, 0)],
+ })
+ vendor1 = self.env['res.partner'].create({'name': 'vendor1', 'email': 'from.test@example.com'})
+ supplierinfo1 = self.env['product.supplierinfo'].create({
+ 'name': vendor1.id,
+ 'price': 100,
+ 'product_tmpl_id': product_to_buy.product_tmpl_id.id,
+ 'min_qty': 2
+ })
+ supplierinfo2 = self.env['product.supplierinfo'].create({
+ 'name': vendor1.id,
+ 'price': 60,
+ 'product_tmpl_id': product_to_buy.product_tmpl_id.id,
+ 'min_qty': 10
+ })
+ supplierinfo3 = self.env['product.supplierinfo'].create({
+ 'name': vendor1.id,
+ 'price': 80,
+ 'product_tmpl_id': product_to_buy.product_tmpl_id.id,
+ 'min_qty': 5
+ })
+ replenish_wizard = self.env['product.replenish'].create({
+ 'product_id': product_to_buy.id,
+ 'product_tmpl_id': product_to_buy.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'quantity': 10,
+ 'warehouse_id': self.wh.id,
+ })
+ replenish_wizard.launch_replenishment()
+ last_po_id = self.env['purchase.order'].search([
+ ('origin', 'ilike', '%Manual Replenishment%'),
+ ])[-1]
+
+ self.assertEqual(last_po_id.partner_id, vendor1)
+ self.assertEqual(last_po_id.order_line.price_unit, 60)
diff --git a/addons/purchase_stock/tests/test_routes.py b/addons/purchase_stock/tests/test_routes.py
new file mode 100644
index 00000000..7510011b
--- /dev/null
+++ b/addons/purchase_stock/tests/test_routes.py
@@ -0,0 +1,53 @@
+from odoo.tests.common import TransactionCase, Form
+
+
+class TestRoutes(TransactionCase):
+
+ def test_allow_rule_creation_for_route_without_company(self):
+ self.env['res.config.settings'].write({
+ 'group_stock_adv_location': True,
+ 'group_stock_multi_locations': True,
+ })
+
+ warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
+
+ location_1 = self.env['stock.location'].create({
+ 'name': 'loc1',
+ 'location_id': warehouse.id
+ })
+
+ location_2 = self.env['stock.location'].create({
+ 'name': 'loc2',
+ 'location_id': warehouse.id
+ })
+
+ receipt_1 = self.env['stock.picking.type'].create({
+ 'name': 'Receipts from loc1',
+ 'sequence_code': 'IN1',
+ 'code': 'incoming',
+ 'warehouse_id': warehouse.id,
+ 'default_location_dest_id': location_1.id,
+ })
+
+ receipt_2 = self.env['stock.picking.type'].create({
+ 'name': 'Receipts from loc2',
+ 'sequence_code': 'IN2',
+ 'code': 'incoming',
+ 'warehouse_id': warehouse.id,
+ 'default_location_dest_id': location_2.id,
+ })
+
+ route = self.env['stock.location.route'].create({
+ 'name': 'Buy',
+ 'company_id': False
+ })
+
+ with Form(route) as r:
+ with r.rule_ids.new() as line:
+ line.name = 'first rule'
+ line.action = 'buy'
+ line.picking_type_id = receipt_1
+ with r.rule_ids.new() as line:
+ line.name = 'second rule'
+ line.action = 'buy'
+ line.picking_type_id = receipt_2
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.")