123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533 |
- # -*- coding: utf-8 -*-
- # Part of Odoo. See LICENSE file for full copyright and licensing details.
- from odoo.tests import Form, tagged
- from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
- @tagged('post_install', '-at_install')
- class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
- @classmethod
- def setUpClass(cls, chart_template_ref=None):
- super().setUpClass(chart_template_ref=chart_template_ref)
- cls.env.user.company_id.anglo_saxon_accounting = True
- cls.uom_unit = cls.env.ref('uom.product_uom_unit')
- def _create_product(self, name, product_type, price):
- return self.env['product.product'].create({
- 'name': name,
- 'type': product_type,
- 'standard_price': price,
- 'categ_id': self.stock_account_product_categ.id if product_type == 'product' else self.env.ref('product.product_category_all').id,
- })
- def test_sale_mrp_kit_bom_cogs(self):
- """Check invoice COGS aml after selling and delivering a product
- with Kit BoM having another product with Kit BoM as component"""
- # ----------------------------------------------
- # BoM of Kit A:
- # - BoM Type: Kit
- # - Quantity: 3
- # - Components:
- # * 2 x Kit B
- # * 1 x Component A (Cost: $3, Storable)
- #
- # BoM of Kit B:
- # - BoM Type: Kit
- # - Quantity: 10
- # - Components:
- # * 2 x Component B (Cost: $4, Storable)
- # * 3 x Component BB (Cost: $5, Consumable)
- # ----------------------------------------------
- self.component_a = self._create_product('Component A', 'product', 3.00)
- self.component_b = self._create_product('Component B', 'product', 4.00)
- self.component_bb = self._create_product('Component BB', 'consu', 5.00)
- self.kit_a = self._create_product('Kit A', 'product', 0.00)
- self.kit_b = self._create_product('Kit B', 'consu', 0.00)
- self.kit_a.write({
- 'property_account_expense_id': self.company_data['default_account_expense'].id,
- 'property_account_income_id': self.company_data['default_account_revenue'].id,
- })
- # Create BoM for Kit A
- bom_product_form = Form(self.env['mrp.bom'])
- bom_product_form.product_id = self.kit_a
- bom_product_form.product_tmpl_id = self.kit_a.product_tmpl_id
- bom_product_form.product_qty = 3.0
- bom_product_form.type = 'phantom'
- with bom_product_form.bom_line_ids.new() as bom_line:
- bom_line.product_id = self.kit_b
- bom_line.product_qty = 2.0
- with bom_product_form.bom_line_ids.new() as bom_line:
- bom_line.product_id = self.component_a
- bom_line.product_qty = 1.0
- self.bom_a = bom_product_form.save()
- # Create BoM for Kit B
- bom_product_form = Form(self.env['mrp.bom'])
- bom_product_form.product_id = self.kit_b
- bom_product_form.product_tmpl_id = self.kit_b.product_tmpl_id
- bom_product_form.product_qty = 10.0
- bom_product_form.type = 'phantom'
- with bom_product_form.bom_line_ids.new() as bom_line:
- bom_line.product_id = self.component_b
- bom_line.product_qty = 2.0
- with bom_product_form.bom_line_ids.new() as bom_line:
- bom_line.product_id = self.component_bb
- bom_line.product_qty = 3.0
- self.bom_b = bom_product_form.save()
- so = self.env['sale.order'].create({
- 'partner_id': self.partner_a.id,
- 'order_line': [
- (0, 0, {
- 'name': self.kit_a.name,
- 'product_id': self.kit_a.id,
- 'product_uom_qty': 1.0,
- 'product_uom': self.kit_a.uom_id.id,
- 'price_unit': 1,
- 'tax_id': False,
- })],
- })
- so.action_confirm()
- so.picking_ids.move_ids.quantity_done = 1
- so.picking_ids.button_validate()
- invoice = so.with_context(default_journal_id=self.company_data['default_journal_sale'].id)._create_invoices()
- invoice.action_post()
- # Check the resulting accounting entries
- amls = invoice.line_ids
- self.assertEqual(len(amls), 4)
- stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
- self.assertEqual(stock_out_aml.debit, 0)
- self.assertAlmostEqual(stock_out_aml.credit, 1.53, "Should not include the value of consumable component")
- cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
- self.assertAlmostEqual(cogs_aml.debit, 1.53, "Should not include the value of consumable component")
- self.assertEqual(cogs_aml.credit, 0)
- def test_sale_mrp_anglo_saxon_variant(self):
- """Test the price unit of kit with variants"""
- # Check that the correct bom are selected when computing price_unit for COGS
- self.env.company.currency_id = self.env.ref('base.USD')
- # Create variant attributes
- self.prod_att_1 = self.env['product.attribute'].create({'name': 'Color'})
- self.prod_attr1_v1 = self.env['product.attribute.value'].create({'name': 'red', 'attribute_id': self.prod_att_1.id, 'sequence': 1})
- self.prod_attr1_v2 = self.env['product.attribute.value'].create({'name': 'blue', 'attribute_id': self.prod_att_1.id, 'sequence': 2})
- # Create Product template with variants
- self.product_template = self.env['product.template'].create({
- 'name': 'Product Template',
- 'type': 'product',
- 'uom_id': self.uom_unit.id,
- 'invoice_policy': 'delivery',
- 'categ_id': self.stock_account_product_categ.id,
- 'attribute_line_ids': [(0, 0, {
- 'attribute_id': self.prod_att_1.id,
- 'value_ids': [(6, 0, [self.prod_attr1_v1.id, self.prod_attr1_v2.id])]
- })]
- })
- # Get product variant
- self.pt_attr1_v1 = self.product_template.attribute_line_ids[0].product_template_value_ids[0]
- self.pt_attr1_v2 = self.product_template.attribute_line_ids[0].product_template_value_ids[1]
- self.variant_1 = self.product_template._get_variant_for_combination(self.pt_attr1_v1)
- self.variant_2 = self.product_template._get_variant_for_combination(self.pt_attr1_v2)
- def create_simple_bom_for_product(product, name, price):
- component = self.env['product.product'].create({
- 'name': 'Component ' + name,
- 'type': 'product',
- 'uom_id': self.uom_unit.id,
- 'categ_id': self.stock_account_product_categ.id,
- 'standard_price': price
- })
- self.env['stock.quant'].sudo().create({
- 'product_id': component.id,
- 'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
- 'quantity': 10.0,
- })
- bom = self.env['mrp.bom'].create({
- 'product_tmpl_id': self.product_template.id,
- 'product_id': product.id,
- 'product_qty': 1.0,
- 'type': 'phantom'
- })
- self.env['mrp.bom.line'].create({
- 'product_id': component.id,
- 'product_qty': 1.0,
- 'bom_id': bom.id
- })
- create_simple_bom_for_product(self.variant_1, "V1", 20)
- create_simple_bom_for_product(self.variant_2, "V2", 10)
- def create_post_sale_order(product):
- so_vals = {
- 'partner_id': self.partner_a.id,
- 'partner_invoice_id': self.partner_a.id,
- 'partner_shipping_id': self.partner_a.id,
- 'order_line': [(0, 0, {
- 'name': product.name,
- 'product_id': product.id,
- 'product_uom_qty': 2,
- 'product_uom': product.uom_id.id,
- 'price_unit': product.list_price
- })],
- 'pricelist_id': self.env.ref('product.list0').id,
- 'company_id': self.company_data['company'].id,
- }
- so = self.env['sale.order'].create(so_vals)
- # Validate the SO
- so.action_confirm()
- # Deliver the three finished products
- pick = so.picking_ids
- # To check the products on the picking
- wiz_act = pick.button_validate()
- wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save()
- wiz.process()
- # Create the invoice
- so._create_invoices()
- invoice = so.invoice_ids
- invoice.action_post()
- return invoice
- # Create a SO for variant 1
- self.invoice_1 = create_post_sale_order(self.variant_1)
- self.invoice_2 = create_post_sale_order(self.variant_2)
- def check_cogs_entry_values(invoice, expected_value):
- aml = invoice.line_ids
- aml_expense = aml.filtered(lambda l: l.display_type == 'cogs' and l.debit > 0)
- aml_output = aml.filtered(lambda l: l.display_type == 'cogs' and l.credit > 0)
- self.assertEqual(aml_expense.debit, expected_value, "Cost of Good Sold entry missing or mismatching for variant")
- self.assertEqual(aml_output.credit, expected_value, "Cost of Good Sold entry missing or mismatching for variant")
- # Check that the cost of Good Sold entries for variant 1 are equal to 2 * 20 = 40
- check_cogs_entry_values(self.invoice_1, 40)
- # Check that the cost of Good Sold entries for variant 2 are equal to 2 * 10 = 20
- check_cogs_entry_values(self.invoice_2, 20)
- def test_anglo_saxo_return_and_credit_note(self):
- """
- When posting a credit note for a returned kit, the value of the anglo-saxo lines
- should be based on the returned component's value
- """
- self.stock_account_product_categ.property_cost_method = 'fifo'
- kit = self._create_product('Simple Kit', 'product', 0)
- component = self._create_product('Compo A', 'product', 0)
- kit.property_account_expense_id = self.company_data['default_account_expense']
- self.env['mrp.bom'].create({
- 'product_tmpl_id': kit.product_tmpl_id.id,
- 'product_qty': 1.0,
- 'type': 'phantom',
- 'bom_line_ids': [(0, 0, {'product_id': component.id, 'product_qty': 1.0})]
- })
- # Receive 3 components: one @10, one @20 and one @60
- in_moves = self.env['stock.move'].create([{
- 'name': 'IN move @%s' % p,
- 'product_id': component.id,
- 'location_id': self.env.ref('stock.stock_location_suppliers').id,
- 'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
- 'product_uom': component.uom_id.id,
- 'product_uom_qty': 1,
- 'price_unit': p,
- } for p in [10, 20, 60]])
- in_moves._action_confirm()
- in_moves.quantity_done = 1
- in_moves._action_done()
- # Sell 3 kits
- so = self.env['sale.order'].create({
- 'partner_id': self.env.ref('base.res_partner_1').id,
- 'order_line': [
- (0, 0, {
- 'name': kit.name,
- 'product_id': kit.id,
- 'product_uom_qty': 3.0,
- 'product_uom': kit.uom_id.id,
- 'price_unit': 100,
- 'tax_id': False,
- })],
- })
- so.action_confirm()
- # Deliver the components: 1@10, then 1@20 and then 1@60
- pickings = []
- picking = so.picking_ids
- while picking:
- pickings.append(picking)
- picking.move_ids.quantity_done = 1
- action = picking.button_validate()
- if isinstance(action, dict):
- wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
- wizard.process()
- picking = picking.backorder_ids
- invoice = so._create_invoices()
- invoice.action_post()
- # Receive one @100
- in_moves = self.env['stock.move'].create({
- 'name': 'IN move @100',
- 'product_id': component.id,
- 'location_id': self.env.ref('stock.stock_location_suppliers').id,
- 'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
- 'product_uom': component.uom_id.id,
- 'product_uom_qty': 1,
- 'price_unit': 100,
- })
- in_moves._action_confirm()
- in_moves.quantity_done = 1
- in_moves._action_done()
- # Return the second picking (i.e. one component @20)
- ctx = {'active_id': pickings[1].id, 'active_model': 'stock.picking'}
- return_wizard = Form(self.env['stock.return.picking'].with_context(ctx)).save()
- return_picking_id, dummy = return_wizard._create_returns()
- return_picking = self.env['stock.picking'].browse(return_picking_id)
- return_picking.move_ids.quantity_done = 1
- return_picking.button_validate()
- # Add a credit note for the returned kit
- ctx = {'active_model': 'account.move', 'active_ids': invoice.ids}
- refund_wizard = self.env['account.move.reversal'].with_context(ctx).create({
- 'refund_method': 'refund',
- 'journal_id': invoice.journal_id.id,
- })
- action = refund_wizard.reverse_moves()
- reverse_invoice = self.env['account.move'].browse(action['res_id'])
- with Form(reverse_invoice) as reverse_invoice_form:
- with reverse_invoice_form.invoice_line_ids.edit(0) as line:
- line.quantity = 1
- reverse_invoice.action_post()
- amls = reverse_invoice.line_ids
- stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
- self.assertEqual(stock_out_aml.debit, 20, 'Should be to the value of the returned component')
- self.assertEqual(stock_out_aml.credit, 0)
- cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
- self.assertEqual(cogs_aml.debit, 0)
- self.assertEqual(cogs_aml.credit, 20, 'Should be to the value of the returned component')
- def test_anglo_saxo_return_and_create_invoice(self):
- """
- When creating an invoice for a returned kit, the value of the anglo-saxo lines
- should be based on the returned component's value
- """
- self.stock_account_product_categ.property_cost_method = 'fifo'
- kit = self._create_product('Simple Kit', 'product', 0)
- component = self._create_product('Compo A', 'product', 0)
- (kit + component).invoice_policy = 'delivery'
- kit.property_account_expense_id = self.company_data['default_account_expense']
- self.env['mrp.bom'].create({
- 'product_tmpl_id': kit.product_tmpl_id.id,
- 'product_qty': 1.0,
- 'type': 'phantom',
- 'bom_line_ids': [(0, 0, {'product_id': component.id, 'product_qty': 1.0})]
- })
- # Receive 3 components: one @10, one @20 and one @60
- in_moves = self.env['stock.move'].create([{
- 'name': 'IN move @%s' % p,
- 'product_id': component.id,
- 'location_id': self.env.ref('stock.stock_location_suppliers').id,
- 'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
- 'product_uom': component.uom_id.id,
- 'product_uom_qty': 1,
- 'price_unit': p,
- } for p in [10, 20, 60]])
- in_moves._action_confirm()
- in_moves.quantity_done = 1
- in_moves._action_done()
- # Sell 3 kits
- so = self.env['sale.order'].create({
- 'partner_id': self.env.ref('base.res_partner_1').id,
- 'order_line': [
- (0, 0, {
- 'name': kit.name,
- 'product_id': kit.id,
- 'product_uom_qty': 3.0,
- 'product_uom': kit.uom_id.id,
- 'price_unit': 100,
- 'tax_id': False,
- })],
- })
- so.action_confirm()
- # Deliver the components: 1@10, then 1@20 and then 1@60
- pickings = []
- picking = so.picking_ids
- while picking:
- pickings.append(picking)
- picking.move_ids.quantity_done = 1
- action = picking.button_validate()
- if isinstance(action, dict):
- wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
- wizard.process()
- picking = picking.backorder_ids
- invoice = so._create_invoices()
- invoice.action_post()
- # Receive one @100
- in_moves = self.env['stock.move'].create({
- 'name': 'IN move @100',
- 'product_id': component.id,
- 'location_id': self.env.ref('stock.stock_location_suppliers').id,
- 'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
- 'product_uom': component.uom_id.id,
- 'product_uom_qty': 1,
- 'price_unit': 100,
- })
- in_moves._action_confirm()
- in_moves.quantity_done = 1
- in_moves._action_done()
- # Return the second picking (i.e. one component @20)
- ctx = {'active_id': pickings[1].id, 'active_model': 'stock.picking'}
- return_wizard = Form(self.env['stock.return.picking'].with_context(ctx)).save()
- return_picking_id, dummy = return_wizard._create_returns()
- return_picking = self.env['stock.picking'].browse(return_picking_id)
- return_picking.move_ids.quantity_done = 1
- return_picking.button_validate()
- # Create a new invoice for the returned kit
- ctx = {'active_model': 'sale.order', 'active_ids': so.ids}
- create_invoice_wizard = self.env['sale.advance.payment.inv'].with_context(ctx).create(
- {'advance_payment_method': 'delivered'})
- create_invoice_wizard.create_invoices()
- reverse_invoice = so.invoice_ids[-1]
- with Form(reverse_invoice) as reverse_invoice_form:
- with reverse_invoice_form.invoice_line_ids.edit(0) as line:
- line.quantity = 1
- reverse_invoice.action_post()
- amls = reverse_invoice.line_ids
- stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
- self.assertEqual(stock_out_aml.debit, 20, 'Should be to the value of the returned component')
- self.assertEqual(stock_out_aml.credit, 0)
- cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
- self.assertEqual(cogs_aml.debit, 0)
- self.assertEqual(cogs_aml.credit, 20, 'Should be to the value of the returned component')
- def test_kit_avco_fully_owned_and_delivered_invoice_post_delivery(self):
- self.stock_account_product_categ.property_cost_method = 'average'
- compo01 = self._create_product('Compo 01', 'product', 10)
- compo02 = self._create_product('Compo 02', 'product', 20)
- kit = self._create_product('Kit', 'product', 0)
- (compo01 + compo02 + kit).invoice_policy = 'delivery'
- self.env['stock.quant']._update_available_quantity(compo01, self.company_data['default_warehouse'].lot_stock_id, 1, owner_id=self.partner_b)
- self.env['stock.quant']._update_available_quantity(compo02, self.company_data['default_warehouse'].lot_stock_id, 1, owner_id=self.partner_b)
- self.env['mrp.bom'].create({
- 'product_id': kit.id,
- 'product_tmpl_id': kit.product_tmpl_id.id,
- 'product_uom_id': kit.uom_id.id,
- 'product_qty': 1.0,
- 'type': 'phantom',
- 'bom_line_ids': [
- (0, 0, {'product_id': compo01.id, 'product_qty': 1.0}),
- (0, 0, {'product_id': compo02.id, 'product_qty': 1.0}),
- ],
- })
- so = self.env['sale.order'].create({
- 'partner_id': self.partner_a.id,
- 'order_line': [
- (0, 0, {
- 'name': kit.name,
- 'product_id': kit.id,
- 'product_uom_qty': 1.0,
- 'product_uom': kit.uom_id.id,
- 'price_unit': 5,
- 'tax_id': False,
- })],
- })
- so.action_confirm()
- so.picking_ids.move_ids.quantity_done = 1
- so.picking_ids.button_validate()
- invoice = so._create_invoices()
- invoice.action_post()
- # COGS should not exist because the products are owned by an external partner
- amls = invoice.line_ids
- self.assertRecordValues(amls, [
- # pylint: disable=bad-whitespace
- {'account_id': self.company_data['default_account_revenue'].id, 'debit': 0, 'credit': 5},
- {'account_id': self.company_data['default_account_receivable'].id, 'debit': 5, 'credit': 0},
- ])
- def test_kit_avco_partially_owned_and_delivered_invoice_post_delivery(self):
- self.stock_account_product_categ.property_cost_method = 'average'
- compo01 = self._create_product('Compo 01', 'product', 10)
- compo02 = self._create_product('Compo 02', 'product', 20)
- kit = self._create_product('Kit', 'product', 0)
- (compo01 + compo02 + kit).invoice_policy = 'delivery'
- self.env['stock.quant']._update_available_quantity(compo01, self.company_data['default_warehouse'].lot_stock_id, 1, owner_id=self.partner_b)
- self.env['stock.quant']._update_available_quantity(compo01, self.company_data['default_warehouse'].lot_stock_id, 1)
- self.env['stock.quant']._update_available_quantity(compo02, self.company_data['default_warehouse'].lot_stock_id, 1, owner_id=self.partner_b)
- self.env['stock.quant']._update_available_quantity(compo02, self.company_data['default_warehouse'].lot_stock_id, 1)
- self.env['mrp.bom'].create({
- 'product_id': kit.id,
- 'product_tmpl_id': kit.product_tmpl_id.id,
- 'product_uom_id': kit.uom_id.id,
- 'product_qty': 1.0,
- 'type': 'phantom',
- 'bom_line_ids': [
- (0, 0, {'product_id': compo01.id, 'product_qty': 1.0}),
- (0, 0, {'product_id': compo02.id, 'product_qty': 1.0}),
- ],
- })
- so = self.env['sale.order'].create({
- 'partner_id': self.partner_a.id,
- 'order_line': [
- (0, 0, {
- 'name': kit.name,
- 'product_id': kit.id,
- 'product_uom_qty': 2.0,
- 'product_uom': kit.uom_id.id,
- 'price_unit': 5,
- 'tax_id': False,
- })],
- })
- so.action_confirm()
- so.picking_ids.move_line_ids.qty_done = 1
- so.picking_ids.button_validate()
- invoice = so._create_invoices()
- invoice.action_post()
- # COGS should not exist because the products are owned by an external partner
- amls = invoice.line_ids
- self.assertRecordValues(amls, [
- # pylint: disable=bad-whitespace
- {'account_id': self.company_data['default_account_revenue'].id, 'debit': 0, 'credit': 10},
- {'account_id': self.company_data['default_account_receivable'].id, 'debit': 10, 'credit': 0},
- {'account_id': self.company_data['default_account_stock_out'].id, 'debit': 0, 'credit': 30},
- {'account_id': self.company_data['default_account_expense'].id, 'debit': 30, 'credit': 0},
- ])
|