# Part of Odoo. See LICENSE file for full copyright and licensing details. from datetime import timedelta from freezegun import freeze_time from psycopg2.errors import IntegrityError from pytz import timezone from odoo import Command, fields from odoo.exceptions import UserError from odoo.tests import Form, tagged from odoo.tools import mute_logger from odoo.addons.account.tests.common import AccountTestInvoicingCommon @tagged('-at_install', 'post_install') class TestPurchase(AccountTestInvoicingCommon): @classmethod def setUpClass(cls): super().setUpClass() cls.company_data_2 = cls.setup_other_company() def test_date_planned(self): """Set a date planned on 2 PO lines. Check that the PO date_planned is the earliest PO line date planned. Change one of the dates so it is even earlier and check that the date_planned is set to this earlier date. """ po = Form(self.env['purchase.order']) po.partner_id = self.partner_a with po.order_line.new() as po_line: po_line.product_id = self.product_a po_line.product_qty = 1 po_line.price_unit = 100 with po.order_line.new() as po_line: po_line.product_id = self.product_b po_line.product_qty = 10 po_line.price_unit = 200 po = po.save() # Check that the same date is planned on both PO lines. self.assertNotEqual(po.order_line[0].date_planned, False) self.assertAlmostEqual(po.order_line[0].date_planned, po.order_line[1].date_planned, delta=timedelta(seconds=10)) self.assertAlmostEqual(po.order_line[0].date_planned, po.date_planned, delta=timedelta(seconds=10)) orig_date_planned = po.order_line[0].date_planned # Set an earlier date planned on a PO line and check that the PO expected date matches it. new_date_planned = orig_date_planned - timedelta(hours=1) po.order_line[0].date_planned = new_date_planned self.assertAlmostEqual(po.order_line[0].date_planned, po.date_planned, delta=timedelta(seconds=10)) # Set an even earlier date planned on the other PO line and check that the PO expected date matches it. # Also check that the other PO line's date planned is not modified. new_date_planned_2 = orig_date_planned - timedelta(hours=72) po_form = Form(po) with po_form.order_line.edit(1) as po_line: po_line.date_planned = new_date_planned_2 po = po_form.save() self.assertAlmostEqual(po.order_line[1].date_planned, po.date_planned, delta=timedelta(seconds=10)) self.assertAlmostEqual(po.order_line[0].date_planned, new_date_planned, delta=timedelta(seconds=10)) def test_date_planned_2(self): """ Check that the date_planned of the onchange is correctly applied: Create a PO, change its date_planned to tommorow and check that the date_planned of the lines are updated. Create a new line (this will update the date_planned of the PO but should not alter the other lines). """ po = self.env['purchase.order'].create({ 'partner_id': self.partner_a.id, 'order_line': [Command.create({ 'name': self.product_a.name, 'product_id': self.product_a.id, 'product_uom_qty': 10, 'product_uom_id': self.product_a.uom_id.id, 'price_unit': 1, })], }) with Form(po) as po_form: po_form.date_planned = fields.Datetime.now() + timedelta(days=1) self.assertEqual(po.order_line.date_planned, po.date_planned) with Form(po) as po_form: with po_form.order_line.new() as new_line: new_line.product_id = self.product_b new_line.product_qty = 10 new_line.price_unit = 200 self.assertEqual(po.order_line[1].date_planned, po.date_planned) self.assertNotEqual(po.order_line[0].date_planned, po.date_planned) def test_purchase_order_sequence(self): PurchaseOrder = self.env['purchase.order'].with_context(tracking_disable=True) company = self.env.user.company_id self.env['ir.sequence'].search([ ('code', '=', 'purchase.order'), ]).write({ 'use_date_range': True, 'prefix': 'PO/%(range_year)s/', }) vals = { 'partner_id': self.partner_a.id, 'company_id': company.id, 'currency_id': company.currency_id.id, 'date_order': '2019-01-01', } purchase_order = PurchaseOrder.create(vals.copy()) self.assertTrue(purchase_order.name.startswith('PO/2019/')) vals['date_order'] = '2020-01-01' purchase_order = PurchaseOrder.create(vals.copy()) self.assertTrue(purchase_order.name.startswith('PO/2020/')) # In EU/BXL tz, this is actually already 01/01/2020 vals['date_order'] = '2019-12-31 23:30:00' purchase_order = PurchaseOrder.with_context(tz='Europe/Brussels').create(vals.copy()) self.assertTrue(purchase_order.name.startswith('PO/2020/')) def test_reminder_1(self): """Set to send reminder tomorrow, check if a reminder can be send to the partner. """ # set partner to send reminder in Company 2 self.partner_a.with_company(self.company_data_2['company']).receipt_reminder_email = True self.partner_a.with_company(self.company_data_2['company']).reminder_date_before_receipt = 1 # Create the PO in Company 1 self.env.user.tz = 'Europe/Brussels' po = Form(self.env['purchase.order']) po.partner_id = self.partner_a with po.order_line.new() as po_line: po_line.product_id = self.product_a po_line.product_qty = 1 po_line.price_unit = 100 with po.order_line.new() as po_line: po_line.product_id = self.product_b po_line.product_qty = 10 po_line.price_unit = 200 # set to send reminder today date_planned = fields.Datetime.now().replace(hour=23, minute=0) + timedelta(days=2) po.date_planned = date_planned po = po.save() po.button_confirm() # Check that reminder is not set in Company 1 and the mail will not be sent self.assertEqual(po.company_id, self.company) self.assertFalse(po.receipt_reminder_email) self.assertEqual(po.reminder_date_before_receipt, 1, "The default value should be taken from the company") old_messages = po.message_ids po._send_reminder_mail() messages_send = po.message_ids - old_messages self.assertFalse(messages_send) # Set to send reminder in Company 1 self.partner_a.receipt_reminder_email = True self.partner_a.reminder_date_before_receipt = 2 # Invalidate the cache to ensure that the computed fields are recomputed self.env.invalidate_all() self.assertTrue(po.receipt_reminder_email) self.assertEqual(po.reminder_date_before_receipt, 2) # check date_planned is correctly set self.assertEqual(po.date_planned, date_planned) po_tz = timezone(po.user_id.tz) localized_date_planned = po.date_planned.astimezone(po_tz) self.assertEqual(localized_date_planned, po.get_localized_date_planned()) # Ensure that the function get_localized_date_planned can accept a date in string format self.assertEqual(localized_date_planned, po.get_localized_date_planned(po.date_planned.strftime('%Y-%m-%d %H:%M:%S'))) # check vendor is a message recipient self.assertFalse(po.partner_id in po.message_partner_ids, 'Customer should not automatically be added in followers') # check reminder send old_messages = po.message_ids po._send_reminder_mail() messages_send = po.message_ids - old_messages self.assertTrue(messages_send) self.assertFalse(po.partner_id in po.message_partner_ids, 'Customer should not automatically be added in followers') po.action_acknowledge() self.assertTrue(po.acknowledged) def test_reminder_2(self): """Set to send reminder tomorrow, check if no reminder can be send. """ po = Form(self.env['purchase.order']) po.partner_id = self.partner_a with po.order_line.new() as po_line: po_line.product_id = self.product_a po_line.product_qty = 1 po_line.price_unit = 100 with po.order_line.new() as po_line: po_line.product_id = self.product_b po_line.product_qty = 10 po_line.price_unit = 200 # set to send reminder tomorrow po.date_planned = fields.Datetime.now() + timedelta(days=2) po = po.save() self.partner_a.receipt_reminder_email = True self.partner_a.reminder_date_before_receipt = 1 po.button_confirm() # check vendor is a message recipient self.assertFalse(po.partner_id in po.message_partner_ids, 'Customer should not automatically be added in followers') old_messages = po.message_ids po._send_reminder_mail() messages_send = po.message_ids - old_messages # check no reminder send self.assertFalse(messages_send) def test_update_date_planned(self): po = Form(self.env['purchase.order']) po.partner_id = self.partner_a with po.order_line.new() as po_line: po_line.product_id = self.product_a po_line.product_qty = 1 po_line.price_unit = 100 po_line.date_planned = '2020-06-06 00:00:00' with po.order_line.new() as po_line: po_line.product_id = self.product_b po_line.product_qty = 10 po_line.price_unit = 200 po_line.date_planned = '2020-06-06 00:00:00' po = po.save() po.button_confirm() # update first line po._update_date_planned_for_lines([(po.order_line[0], fields.Datetime.today())]) self.assertEqual(po.order_line[0].date_planned, fields.Datetime.today()) activity = self.env['mail.activity'].search([ ('summary', '=', 'Date Updated'), ('res_model_id', '=', 'purchase.order'), ('res_id', '=', po.id), ]) self.assertTrue(activity) self.assertIn( '
partner_a modified receipt dates for the following products:
\n' '- product_a from 2020-06-06 to %s
' % fields.Date.today(), activity.note, ) # update second line po._update_date_planned_for_lines([(po.order_line[1], fields.Datetime.today())]) self.assertEqual(po.order_line[1].date_planned, fields.Datetime.today()) self.assertIn( 'partner_a modified receipt dates for the following products:
\n' '- product_a from 2020-06-06 to %(today)s
\n' '- product_b from 2020-06-06 to %(today)s
' % {'today': fields.Date.today()}, activity.note, ) def test_with_different_uom(self): """ This test ensures that the unit price is correctly computed""" # Required for `product_uom_id` to be visibile in the view self.env.user.group_ids += self.env.ref('uom.group_uom') uom_units = self.env.ref('uom.product_uom_unit') uom_dozens = self.env.ref('uom.product_uom_dozen') uom_pairs = self.env['uom.uom'].create({ 'name': 'Pairs', 'relative_factor': 2, 'relative_uom_id': uom_units.id, }) product_data = { 'name': 'SuperProduct', 'type': 'consu', 'uom_id': uom_units.id, 'seller_ids': [Command.create({ 'partner_id': self.partner_a.id, 'product_uom_id': uom_pairs.id, 'price': 200, })] } product_01 = self.env['product.product'].create(product_data) product_02 = self.env['product.product'].create(product_data) po_form = Form(self.env['purchase.order']) po_form.partner_id = self.partner_a with po_form.order_line.new() as po_line: po_line.product_id = product_01 with po_form.order_line.new() as po_line: po_line.product_id = product_02 po_line.product_uom_id = uom_dozens po = po_form.save() self.assertEqual(po.order_line[0].price_unit, 200) self.assertEqual(po.order_line[1].price_unit, 0, "No vendor with matching UoM is found, so price should be 0") def test_on_change_quantity_description(self): """ When a user changes the quantity of a product in a purchase order it should not change the description if the descritpion was changed by the user before """ self.env.user.write({'company_id': self.company_data['company'].id}) po = Form(self.env['purchase.order']) po.partner_id = self.partner_a with po.order_line.new() as pol: pol.product_id = self.product_a pol.product_qty = 1 pol.name = "New custom description" pol.product_qty += 1 self.assertEqual(pol.name, "New custom description") def test_purchase_multicurrency(self): """ Purchase order lines should keep unit price precision of products Also the products having prices in different currencies should be correctly handled when creating a purchase order i-e product having a price of 100 usd and when purchasing in EUR company the correct conversion should be applied """ self.env['decimal.precision'].search([ ('name', '=', 'Product Price'), ]).digits = 5 product = self.env['product.product'].create({ 'name': 'product_test', 'uom_id': self.env.ref('uom.product_uom_unit').id, 'lst_price': 10.0, 'standard_price': 0.12345, }) currency = self.env['res.currency'].create({ 'name': 'Dark Chocolate Coin', 'symbol': '🍫', 'rounding': 0.001, 'position': 'after', 'currency_unit_label': 'Dark Choco', 'currency_subunit_label': 'Dark Cacao Powder', }) currency_rate = self.env['res.currency.rate'].create({ 'name': '2016-01-01', 'rate': 2, 'currency_id': currency.id, 'company_id': self.env.company.id, }) po_form = Form(self.env['purchase.order']) po_form.partner_id = self.partner_a with po_form.order_line.new() as po_line: po_line.product_id = product purchase_order_usd = po_form.save() self.assertEqual(purchase_order_usd.order_line.price_unit, product.standard_price, "Value shouldn't be rounded $") self.assertEqual(purchase_order_usd.amount_total_cc, purchase_order_usd.amount_total, "Company Total should be 0.14$") po_form = Form(self.env['purchase.order']) po_form.partner_id = self.partner_a po_form.currency_id = currency with po_form.order_line.new() as po_line: po_line.product_id = product purchase_order_coco = po_form.save() self.assertEqual(purchase_order_coco.order_line.price_unit, currency_rate.rate * product.standard_price, "Value shouldn't be rounded 🍫") self.assertEqual(purchase_order_coco.amount_total_cc, round(purchase_order_coco.amount_total / currency_rate.rate, 2), "Company Total should be 0.14$, since 1$ = 0.5🍫") self.assertEqual(purchase_order_coco.tax_totals['amount_total_cc'], "($\xa00.14)") #check if the correct currency is set on the purchase order by comparing the expected price and actual price company_a = self.company_data['company'] company_b = self.company_data_2['company'] company_b.currency_id = currency self.env['res.currency.rate'].create({ 'name': '2023-01-01', 'rate': 2, 'currency_id': currency.id, 'company_id': company_b.id, }) product_b = self.env['product.product'].with_company(company_a).create({ 'name': 'product_2', 'uom_id': self.env.ref('uom.product_uom_unit').id, 'standard_price': 0.0, }) self.assertEqual(product_b.cost_currency_id, company_a.currency_id, 'The cost currency should be the one set on' ' the company') product_b = product_b.with_company(company_b) self.assertEqual(product_b.cost_currency_id, currency, 'The cost currency should be the one set on the company,' ' as the product is now opened in another company') product_b.supplier_taxes_id = False product_b.update({'standard_price': 10.0}) #create a purchase order with the product from company B order_b = self.env['purchase.order'].with_company(company_b).create({ 'partner_id': self.partner_a.id, 'order_line': [(0, 0, { 'product_id': product_b.id, 'product_qty': 1, 'product_uom_id': self.env.ref('uom.product_uom_unit').id, })], }) self.assertEqual(order_b.order_line.price_unit, 10.0, 'The price unit should be 10.0') self.assertEqual(order_b.amount_total_cc, order_b.amount_total, 'Company Total should be 10.0$') # since the order currency matches the company currency we don't want to redisplay the total amount with self.assertRaises(KeyError): order_b.tax_totals['amount_total_cc'] def test_discount_and_price_update_on_quantity_change(self): """ Purchase order line price and discount should update accordingly based on quantity """ product = self.env['product.product'].create({ 'name': 'Product', 'standard_price': 12, 'seller_ids': [ Command.create({ 'partner_id': self.partner_a.id, 'min_qty': 10, 'price': 10, 'discount': 10, }), Command.create({ 'partner_id': self.partner_a.id, 'min_qty': 20, 'price': 10, 'discount': 15, }) ] }) purchase_order = self.env['purchase.order'].with_company(self.company_data['company']).create({ 'partner_id': self.partner_a.id, 'order_line': [Command.create({ 'product_id': product.id, 'product_uom_id': product.uom_id.id, })], }) po_line = purchase_order.order_line po_line.product_qty = 10 self.assertEqual(po_line.discount, 10, "first seller should be selected so discount should be 10") self.assertEqual(po_line.price_subtotal, 90, "0.1 discount applied price should be 90") po_line.product_qty = 22 self.assertEqual(po_line.discount, 15, "second seller should be selected so discount should be 15") self.assertEqual(po_line.price_subtotal, 187, "0.15 discount applied price should be 187") po_line.product_qty = 2 self.assertEqual(po_line.discount, 0, "no seller should be selected so discount should be 0") self.assertEqual(po_line.price_subtotal, 24, "No seller") def test_purchase_not_creating_useless_product_vendor(self): """ This test ensures that the product vendor is not created when the product is not set on the purchase order line. """ #create a contact of type contact contact = self.env['res.partner'].create({ 'name': 'Contact', 'type': 'contact', }) #create a contact of type Delivery Address lnked to the contact delivery_address = self.env['res.partner'].create({ 'name': 'Delivery Address', 'type': 'delivery', 'parent_id': contact.id, }) #create a product that use the delivery address as vendor product = self.env['product.product'].create({ 'name': 'Product A', 'seller_ids': [(0, 0, { 'partner_id': delivery_address.id, 'min_qty': 1.0, 'price': 1.0, })] }) #create a purchase order with the delivery address as partner po_form = Form(self.env['purchase.order']) po_form.partner_id = delivery_address with po_form.order_line.new() as po_line: po_line.product_id = product po_line.product_qty = 1.0 po = po_form.save() po.button_confirm() self.assertEqual(po.order_line.product_id.seller_ids.mapped('partner_id'), delivery_address) def test_supplier_list_in_product_with_multicompany(self): """ Check that a different supplier list can be added to a product for each company. """ company_a = self.company_data['company'] company_b = self.company_data_2['company'] product = self.env['product.product'].create({ 'name': 'product_test', }) # create a purchase order in the company A self.env['purchase.order'].with_company(company_a).create({ 'partner_id': self.partner_a.id, 'order_line': [(0, 0, { 'product_id': product.id, 'product_qty': 1, 'product_uom_id': self.env.ref('uom.product_uom_unit').id, 'price_unit': 1, })], }).button_confirm() self.assertEqual(product.seller_ids[0].partner_id, self.partner_a) self.assertEqual(product.seller_ids[0].company_id, company_a) # switch to the company B self.env['purchase.order'].with_company(company_b).create({ 'partner_id': self.partner_b.id, 'order_line': [(0, 0, { 'product_id': product.id, 'product_qty': 1, 'product_uom_id': self.env.ref('uom.product_uom_unit').id, 'price_unit': 2, })], }).button_confirm() product = product.with_company(company_b) self.assertEqual(product.seller_ids[0].partner_id, self.partner_b) self.assertEqual(product.seller_ids[0].company_id, company_b) # Switch to the company A and check that the vendor list is still the same product = product.with_company(company_a) self.assertEqual(product.seller_ids[0].partner_id, self.partner_a) self.assertEqual(product.seller_ids[0].company_id, company_a) product._invalidate_cache() self.assertEqual(product.seller_ids[0].partner_id, self.partner_a) self.assertEqual(product.seller_ids[0].company_id, company_a) def test_discount_po_line_vendorpricelist(self): """ Set a discount in VendorPriceList and check if that discount comes in po line and if vendor select a product which is not present in vendorPriceList then it should be created. """ po = Form(self.env['purchase.order']) po.partner_id = self.partner_a with po.order_line.new() as po_line: po_line.product_id = self.product_a po_line.product_qty = 1 po_line.price_unit = 100 po_line.discount = 20 po = po.save() po.button_confirm() supplierinfo_id = self.env['product.supplierinfo'].search([ ('partner_id', '=', self.partner_a.id), ('product_tmpl_id', '=', self.product_a.product_tmpl_id.id), ], limit=1) self.assertTrue(supplierinfo_id) self.assertEqual(supplierinfo_id.discount, 20) # checking the same discount self.env['product.supplierinfo'].create({ 'partner_id': self.partner_b.id, 'product_tmpl_id': self.product_a.product_tmpl_id.id, 'min_qty': 1, 'price': 100, 'discount': 30, }) po1 = Form(self.env['purchase.order']) po1.partner_id = self.partner_b with po1.order_line.new() as po_line: po_line.product_id = self.product_a po_line.product_qty = 1 po1 = po1.save() self.assertEqual(po1.order_line[0].price_unit, 100) self.assertEqual(po1.order_line[0].discount, 30) def test_orderline_supplierinfo_description(self): supplierinfo_vals = { 'partner_id': self.partner_a.id, 'min_qty': 1, 'product_id': self.product_a.id, 'product_tmpl_id': self.product_a.product_tmpl_id.id, } self.env["product.supplierinfo"].create([ { **supplierinfo_vals, 'price': 10, 'product_name': 'Name 1', 'product_code': 'Code 1', }, { **supplierinfo_vals, 'price': 20, 'product_name': 'Name 2', 'product_code': 'Code 2', }, { 'partner_id': self.partner_a.id, 'min_qty': 1, 'product_id': self.product_b.id, 'product_tmpl_id': self.product_b.product_tmpl_id.id, 'price': 5, 'product_name': 'Name 3', 'product_code': 'Code 3', } ]) po_form = Form(self.env['purchase.order']) po_form.partner_id = self.partner_a with po_form.order_line.new() as line: line.product_id = self.product_a line.product_qty = 1 po = po_form.save() self.assertEqual(po.order_line.name, '[Code 1] Name 1') with po_form.order_line.edit(0) as line: line.product_id = self.product_b po = po_form.save() self.assertEqual(po.order_line.name, '[Code 3] Name 3') def test_purchase_order_line_product_taxes_on_branch(self): """ Check taxes populated on PO lines from product on branch company. Taxes from the branch company should be taken with a fallback on parent company. """ # create the following branch hierarchy: # Parent company # |----> Branch X # |----> Branch XX company = self.env.company branch_x = self.env['res.company'].create({ 'name': 'Branch X', 'country_id': company.country_id.id, 'parent_id': company.id, }) branch_xx = self.env['res.company'].create({ 'name': 'Branch XX', 'country_id': company.country_id.id, 'parent_id': branch_x.id, }) # create taxes for the parent company and its branches tax_groups = self.env['account.tax.group'].create([{ 'name': 'Tax Group', 'company_id': company.id, }, { 'name': 'Tax Group X', 'company_id': branch_x.id, }, { 'name': 'Tax Group XX', 'company_id': branch_xx.id, }]) tax_a = self.env['account.tax'].create({ 'name': 'Tax A', 'type_tax_use': 'purchase', 'amount_type': 'percent', 'amount': 10, 'tax_group_id': tax_groups[0].id, 'company_id': company.id, }) tax_b = self.env['account.tax'].create({ 'name': 'Tax B', 'type_tax_use': 'purchase', 'amount_type': 'percent', 'amount': 15, 'tax_group_id': tax_groups[0].id, 'company_id': company.id, }) tax_x = self.env['account.tax'].create({ 'name': 'Tax X', 'type_tax_use': 'purchase', 'amount_type': 'percent', 'amount': 20, 'tax_group_id': tax_groups[1].id, 'company_id': branch_x.id, }) tax_xx = self.env['account.tax'].create({ 'name': 'Tax XX', 'type_tax_use': 'purchase', 'amount_type': 'percent', 'amount': 25, 'tax_group_id': tax_groups[2].id, 'company_id': branch_xx.id, }) # create several products with different taxes combination product_all_taxes = self.env['product.product'].create({ 'name': 'Product all taxes', 'supplier_taxes_id': [Command.set((tax_a + tax_b + tax_x + tax_xx).ids)], }) product_no_xx_tax = self.env['product.product'].create({ 'name': 'Product no tax from XX', 'supplier_taxes_id': [Command.set((tax_a + tax_b + tax_x).ids)], }) product_no_branch_tax = self.env['product.product'].create({ 'name': 'Product no tax from branch', 'supplier_taxes_id': [Command.set((tax_a + tax_b).ids)], }) product_no_tax = self.env['product.product'].create({ 'name': 'Product no tax', 'supplier_taxes_id': [], }) # create a PO from Branch XX po_form = Form(self.env['purchase.order'].with_company(branch_xx)) po_form.partner_id = self.partner_a # add 4 PO lines with the different products: # - Product all taxes => tax from Branch XX should be set # - Product no tax from XX => tax from Branch X should be set # - Product no tax from branch => 2 taxes from parent company should be set # - Product no tax => no tax should be set with po_form.order_line.new() as line: line.product_id = product_all_taxes with po_form.order_line.new() as line: line.product_id = product_no_xx_tax with po_form.order_line.new() as line: line.product_id = product_no_branch_tax with po_form.order_line.new() as line: line.product_id = product_no_tax po = po_form.save() self.assertRecordValues(po.order_line, [ {'product_id': product_all_taxes.id, 'tax_ids': tax_xx.ids}, {'product_id': product_no_xx_tax.id, 'tax_ids': tax_x.ids}, {'product_id': product_no_branch_tax.id, 'tax_ids': (tax_a + tax_b).ids}, {'product_id': product_no_tax.id, 'tax_ids': []}, ]) @freeze_time('2024-07-08') def test_description_price__date_depending_on_vendor(self): """ Test that the description and the price are updated accordingly when the vendor is changed. """ self.product_a.seller_ids = [ Command.create({ 'partner_id': self.partner_a.id, 'min_qty': 1, 'price': 5, 'product_code': 'Vendor A', 'delay': 5, }), Command.create({ 'partner_id': self.partner_b.id, 'min_qty': 1, 'price': 10, 'product_code': 'Vendor B', 'delay': 6, }), ] self.assertFalse(False) # Create PO and set vendor A po_form = Form(self.env['purchase.order']) po_form.partner_id = self.partner_a with po_form.order_line.new() as po_line: po_line.product_id = self.product_a po_line.product_qty = 10 po = po_form.save() self.assertEqual(po.order_line.price_unit, 5) self.assertEqual(po.order_line.name, '[Vendor A] product_a') self.assertEqual(po.order_line.product_qty, 10) self.assertEqual(po.order_line.date_planned, fields.Datetime.now() + timedelta(days=5)) po.partner_id = self.partner_b self.assertEqual(po.order_line.price_unit, 10) self.assertEqual(po.order_line.name, '[Vendor B] product_a') self.assertEqual(po.order_line.product_qty, 10) self.assertEqual(po.order_line.date_planned, fields.Datetime.now() + timedelta(days=6)) def test_merge_purchase_order(self): PurchaseOrder = self.env['purchase.order'] user_1 = self.env['res.users'].search([])[0] user_2 = self.env['res.users'].search([])[1] payment_term_id_1 = self.env['account.payment.term'].search([])[0] payment_term_id_2 = self.env['account.payment.term'].search([])[1] incoterm_id_1 = self.env['account.incoterms'].search([])[0] incoterm_id_2 = self.env['account.incoterms'].search([])[1] po_1 = Form(PurchaseOrder) po_1.partner_id = self.partner_a po_1.partner_ref = "azure" po_1.origin = "s0003" po_1.user_id = user_1 po_1.payment_term_id = payment_term_id_1 with po_1.order_line.new() as po_line: po_line.product_id = self.product_a po_line.product_qty = 1 po_line.price_unit = 100 with po_1.order_line.new() as po_line: po_line.display_type = 'line_note' po_line.name = 'Test Note' po_1 = po_1.save() po_1.incoterm_id = incoterm_id_1 po_2 = Form(PurchaseOrder) po_2.partner_id = self.partner_a po_2.partner_ref = "wood corner" po_2.origin = "s0004" po_2.user_id = user_2 po_2.payment_term_id = payment_term_id_2 with po_2.order_line.new() as po_line_1: po_line_1.product_id = self.product_a po_line_1.product_qty = 5 po_line_1.price_unit = 100 with po_2.order_line.new() as po_line_2: po_line_2.product_id = self.product_b po_line_2.product_qty = 5 po_line_2.price_unit = 500 with po_2.order_line.new() as po_line: po_line.display_type = 'line_section' po_line.name = 'Test section' po_2 = po_2.save() po_2.incoterm_id = incoterm_id_2 with self.assertRaises(UserError) as context: PurchaseOrder.browse([po_1.id]).action_merge() self.assertEqual(context.exception.args[0], "Please select at least two purchase orders with state RFQ and RFQ sent to merge.") selected_purchase_orders = po_1 | po_2 selected_purchase_orders.action_merge() self.assertTrue(po_2.state == 'cancel') self.assertEqual(po_1.order_line[0].product_qty, 6) self.assertEqual(po_1.partner_ref, "azure, wood corner") self.assertEqual(po_1.origin, "s0003, s0004") self.assertEqual(po_1.user_id, user_1) self.assertEqual(po_1.payment_term_id, payment_term_id_1) self.assertEqual(po_1.incoterm_id, incoterm_id_1) def test_vendor_price_by_purchase_order_company(self): """ Test that in case a vendor has multiple price for two company A and B, and the purchase_order.company_id != env.company_id the price of chosen is the one of the company specified in the purchase order """ company_a = self.env.company company_b = self.env['res.company'].create({'name': 'Saucisson Inc.'}) self.env = company_a.with_company(company_a).env self.product_a.write({ 'seller_ids': [ Command.create({ 'partner_id': self.partner_a, 'product_code': 'A', 'company_id': company_a.id, 'price': 10.0, }), Command.create({ 'partner_id': self.partner_a, 'product_code': 'B', 'company_id': company_b.id, 'price': 15.0, }), ] }) po = self.env['purchase.order'].with_context(allowed_company_ids=[company_a.id, company_b.id]).with_company(company_b).create({ 'partner_id': self.partner_a.id, 'company_id': company_b.id, 'order_line': [Command.create({ 'name': self.product_a.name, 'product_id': self.product_a.id, })], }) self.assertEqual(po.amount_untaxed, 15.0) po.company_id = company_a.id self.assertEqual(po.amount_untaxed, 10.0) def test_print_purchase_order_without_state_change(self): """ Check that printing a confirmed purchase order does not reset its state. """ po_form = Form(self.env['purchase.order']) po_form.partner_id = self.partner_a with po_form.order_line.new() as po_line: po_line.product_id = self.product po_line.product_qty = 1.0 po = po_form.save() po.button_confirm() self.assertEqual(po.state, 'purchase') po.print_quotation() self.assertEqual(po.state, 'purchase') po.button_cancel() self.assertEqual(po.state, 'cancel') po.print_quotation() self.assertEqual(po.state, 'cancel') def test_purchase_warnings(self): """Test warnings when partner/products with purchase warnings are used.""" partner_with_warning = self.env['res.partner'].create({ 'name': 'Test Partner', 'purchase_warn_msg': 'Highly infectious disease'}) child_partner = self.env['res.partner'].create({ 'type': 'invoice', 'parent_id': partner_with_warning.id, 'purchase_warn_msg': 'Slightly infectious disease'}) purchase_order = self.env['purchase.order'].create({'partner_id': partner_with_warning.id}) purchase_order2 = self.env['purchase.order'].create({'partner_id': child_partner.id}) product_with_warning1 = self.env['product.product'].create({ 'name': 'Test Product 1', 'purchase_line_warn_msg': 'Highly corrosive'}) product_with_warning2 = self.env['product.product'].create({ 'name': 'Test Product 2', 'purchase_line_warn_msg': 'Toxic pollutant'}) self.env['purchase.order.line'].create([ { 'order_id': purchase_order.id, 'product_id': product_with_warning1.id, }, { 'order_id': purchase_order.id, 'product_id': product_with_warning2.id, }, # Warnings for duplicate products should not appear. { 'order_id': purchase_order.id, 'product_id': product_with_warning1.id, }, { 'order_id': purchase_order2.id, 'product_id': product_with_warning2.id, }, ]) group_warning_purchase = self.env.ref('purchase.group_warning_purchase') self.env.user.group_ids.implied_ids = [Command.link(group_warning_purchase.id)] purchase_order2.button_confirm() purchase_order2.action_create_invoice() invoice = Form(purchase_order2.invoice_ids[0]) expected_warnings = ('Test Partner - Highly infectious disease', 'Test Product 1 - Highly corrosive', 'Test Product 2 - Toxic pollutant') expected_warnings_for_purchase_order2 = ('Test Partner, Invoice - Slightly infectious disease', 'Test Partner - Highly infectious disease', 'Test Product 2 - Toxic pollutant') self.assertEqual(purchase_order.purchase_warning_text, '\n'.join(expected_warnings)) self.assertEqual(purchase_order2.purchase_warning_text, '\n'.join(expected_warnings_for_purchase_order2)) self.assertEqual(invoice.purchase_warning_text, '\n'.join(expected_warnings_for_purchase_order2)) # without warning group, there should be no warning self.env.user.group_ids.implied_ids = [Command.unlink(group_warning_purchase.id)] self.assertEqual(purchase_order.purchase_warning_text, '') self.assertEqual(purchase_order2.purchase_warning_text, '') invoice = Form(purchase_order2.invoice_ids[0]) self.assertEqual(invoice.purchase_warning_text, '') def test_bill_in_purchase_matching_individual(self): """ Tests that if the vendor is an individual with a company, the bill will still appear when using the purchase matching button on a vendor bill """ company_partner = self.env['res.partner'].create({ 'name': 'Small Company', 'company_type': 'company', }) self.partner_a.parent_id = company_partner purchase_order = self.env['purchase.order'].create({ 'partner_id': self.partner_a.id, 'order_line': [(0, 0, { 'product_id': self.product_a.id, 'product_qty': 1, 'price_unit': 100, })] }) purchase_order.button_confirm() vendor_bill = self.env['account.move'].create({ 'move_type': 'in_invoice', 'partner_id': self.partner_a.id, 'invoice_line_ids': [(0, 0, { 'product_id': self.product_a.id, 'quantity': 1, 'price_unit': 100, })] }) self.env['purchase.order'].flush_model() self.env['purchase.order.line'].flush_model() result = vendor_bill.action_purchase_matching() matching_records = self.env['purchase.bill.line.match'].search(result['domain']) result_bill_matching = purchase_order.action_bill_matching() matching_records_from_po = self.env['purchase.bill.line.match'].search(result_bill_matching['domain']) # Ensure that calling `action_add_to_po()` on multiple records # does not raise a singleton ValueError when the vendor is an individual # linked to a company. matching_records.action_add_to_po() self.assertEqual(len(matching_records), 2) self.assertEqual(matching_records.account_move_id, vendor_bill) self.assertEqual(matching_records.purchase_order_id, purchase_order) self.assertEqual(len(matching_records_from_po), 2) self.assertEqual(matching_records_from_po.account_move_id, vendor_bill) self.assertEqual(matching_records_from_po.purchase_order_id, purchase_order) def test_purchase_suggest_qty(self): """ Checks the suggested qty of POL is correctly set based on valid supplier-info leading to correctly compute the price unit, product_qty and product_desc """ self.env['product.supplierinfo'].create([ { 'partner_id': self.partner_a.id, 'product_id': self.product_a.id, 'min_qty': 1, 'price': 50, 'date_start': fields.Date.today() - timedelta(days=5), 'date_end': fields.Date.today() - timedelta(days=3), 'product_code': 'product_code_1', }, { 'partner_id': self.partner_a.id, 'product_id': self.product_a.id, 'min_qty': 10, 'price': 100, 'date_start': fields.Date.today() - timedelta(days=5), 'date_end': fields.Date.today() + timedelta(days=3), 'product_code': 'HHH', }, { 'partner_id': self.partner_a.id, 'product_id': self.product_a.id, 'min_qty': 20, 'price': 80, 'date_start': fields.Date.today() - timedelta(days=5), 'date_end': fields.Date.today() + timedelta(days=3), 'product_code': 'HHH-min_qty_20', }, ]) po_form = Form(self.env['purchase.order']) po_form.partner_id = self.partner_a with po_form.order_line.new() as po_line: po_line.product_id = self.product_a po = po_form.save() self.assertEqual(po.order_line.product_qty, 10.0) self.assertEqual(po.order_line.name, '[HHH] product_a') def test_purchase_order_uom(self): fuzzy_drink = self.env['product.product'].create({ 'name': 'Fuzzy Drink', 'uom_id': self.env.ref('uom.product_uom_unit').id, 'seller_ids': [Command.create({ 'partner_id': self.partner_a.id, 'product_uom_id': self.env.ref('uom.product_uom_unit').id, 'price': 1, }), Command.create({ 'partner_id': self.partner_a.id, 'product_uom_id': self.env.ref('uom.product_uom_pack_6').id, 'min_qty': 2, 'price': 5, })], }) po = self.env['purchase.order'].create({ 'partner_id': self.partner_a.id, 'order_line': [Command.create({ 'product_id': fuzzy_drink.id, 'product_qty': 15, 'product_uom_id': self.env.ref('uom.product_uom_unit').id, })], }) self.assertEqual(po.order_line.price_unit, 1) po.order_line.product_qty = 1 po.order_line.product_uom_id = self.env.ref('uom.product_uom_pack_6') self.assertEqual(po.order_line.price_unit, 6) po.order_line.product_qty = 2 self.assertEqual(po.order_line.price_unit, 5) def test_purchase_order_lock(self): """ Test that the purchase order can be locked and unlocked without the lock_confirmed_po setting. """ po = self.env['purchase.order'].create({ 'partner_id': self.partner_a.id, 'order_line': [Command.create({ 'product_id': self.product_a.id, })], }) po.button_confirm() self.assertFalse(po.locked) # Lock the purchase order po.button_lock() self.assertTrue(po.locked) # Unlocking should not raise an error regardless of the 'Lock Confirmed Orders' setting. self.assertNotEqual(po.lock_confirmed_po, 'lock') po.button_unlock() self.assertFalse(po.locked) po.button_lock() self.assertTrue(po.locked) po.lock_confirmed_po = 'lock' po.button_unlock() self.assertFalse(po.locked) def test_purchase_order_mail_links_to_correct_website(self): """Check that purchase order emails link to the order's company website.""" if 'website_id' not in self.env.company: self.skipTest("The `website` module is required to support multiple company websites.") company1 = self.company_data['company'] company2 = self.company_data_2['company'] companies = company1 + company2 companies.website_id = None self.env['website'].create([{ 'name': f"{company.name}'s Website", 'domain': f"http://website{company.id}.example.com", 'company_id': company.id, } for company in companies]) self.assertEqual(len(companies.website_id), 2) rfq = self.env['purchase.order'].create({ 'partner_id': self.partner_a.id, 'company_id': company2.id, 'order_line': [Command.create({'product_id': self.product_a.id})], }) email_ctx = rfq.action_rfq_send().get('context', {}) mail_template = self.env['mail.template'].browse(email_ctx.get('default_template_id')) mail_template.auto_delete = False message = rfq.with_context(**email_ctx).message_post_with_source( mail_template, subtype_xmlid='mail.mt_comment', ) self.assertIn( company2.website_id.domain, message.mail_ids.body_html, "Mail should link to the website of the order's company", ) self.assertNotIn( company1.website_id.domain, message.mail_ids.body_html, "Mail shouldn't link to the website of the first company", ) self.assertNotIn( self.env['base'].get_base_url(), message.mail_ids.body_html, "Mail shouldn't link to the base URL", ) def test_action_view_po_when_product_template_archived(self): """ Test to ensure that the purchased_product_qty value remains the same after archiving the product template. Also check that the purchased smart button returns the correct purchase order lines. """ po = self.env['purchase.order'].create({ 'partner_id': self.partner_a.id, 'order_line': [ Command.create({ 'product_id': self.product_a.id, 'product_qty': 10, 'price_unit': 1, }), ], }) po.button_confirm() product_tmpl = self.product_a.product_tmpl_id self.assertEqual(product_tmpl.purchased_product_qty, 10) product_tmpl.active = False # Need to flush the recordsets to recalculate the purchased_product_qty after archiving product_tmpl.invalidate_recordset() self.assertEqual(product_tmpl.purchased_product_qty, 10) action = product_tmpl.action_view_po() action_record = self.env[action['res_model']].search(action['domain']) self.assertEqual(action_record, po.order_line) def test_currency_computed_from_partner(self): """Test that the currency of the purchase order is computed from the partner when the partner is set, and that default_currency_id in context overrides compute. """ eur = self.env.ref('base.EUR') gbp = self.env.ref('base.GBP') self.partner_a.property_purchase_currency_id = eur po = self.env['purchase.order'].create({ 'partner_id': self.partner_a.id, }) self.assertEqual(po.currency_id, eur, "The currency should be computed from the partner's purchase currency") po_2 = self.env['purchase.order'].with_context(default_currency_id=gbp).create({ 'partner_id': self.partner_a.id, }) self.assertEqual(po_2.currency_id, gbp, "The currency should be set from context default_currency_id, bypassing the compute") def test_prevent_recompute_price_on_manual_set(self): """Ensure manually set unit price on a purchase order line remains unchanged when quantity is updated.""" self.product_a.seller_ids = [Command.create({ 'partner_id': self.partner_a.id, 'min_qty': 1, 'price': 5, 'product_code': 'Vendor A', })] po_form = Form(self.env['purchase.order']) po_form.partner_id = self.partner_a with po_form.order_line.new() as po_line: po_line.product_id = self.product_a po_line.product_qty = 1 po = po_form.save() self.assertEqual(po.order_line.price_unit, 5) # Update the price manually and then change the quantity with Form(po.order_line) as line: line.price_unit = 100.0 po.order_line.product_qty = 10 self.assertEqual(po.order_line.price_unit, 100.0, "Price should remain 100.0 after changing the quantity") def test_purchase_order_line_without_uom(self): uom_test = self.env['uom.uom'].create({ 'name': 'Test Uom', 'rounding': 1.0, }) po = self.env['purchase.order'].create({ 'partner_id': self.partner_a.id, 'order_line': [ (0, 0, { 'product_id': self.product_a.id, 'product_qty': 1.0, 'product_uom_id': uom_test.id, })], }) with (self.assertRaises(IntegrityError), self.cr.savepoint(), mute_logger("odoo.sql_db")): uom_test.unlink() self.assertEqual(po.order_line[0].product_uom_id, uom_test) def test_locked_purchase_order_cannot_cancel(self): """Test that a locked purchase order cannot be cancelled. A purchase order must be unlocked before it can be cancelled. """ po = self.env['purchase.order'].create({ 'partner_id': self.partner_a.id, }) po.button_confirm() self.assertFalse(po.locked) # Lock the purchase order. po.button_lock() self.assertTrue(po.locked, "The purchase order should be locked.") # Try to cancel the locked PO, should raise a UserError. with self.assertRaises(UserError): po.button_cancel() # Unlock the PO and then cancel it, should succeed. po.button_unlock() po.button_cancel() self.assertEqual(po.state, 'cancel', "The purchase order should be cancelled.")