diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/purchase_stock/tests | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/purchase_stock/tests')
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.") |
