Initial commit: Sale packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:49 +02:00
commit 14e3d26998
6469 changed files with 2479670 additions and 0 deletions

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_sale_mrp_flow
from . import test_sale_mrp_kit_bom
from . import test_sale_mrp_lead_time
from . import test_sale_mrp_procurement
from . import test_multistep_manufacturing
from . import test_sale_mrp_report
from . import test_sale_mrp_anglo_saxon_valuation

View file

@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import Form
from odoo.addons.mrp.tests.common import TestMrpCommon
class TestMultistepManufacturing(TestMrpCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Required for `uom_id ` to be visible in the view
cls.env.user.groups_id += cls.env.ref('uom.group_uom')
# Required for `manufacture_steps` to be visible in the view
cls.env.user.groups_id += cls.env.ref('stock.group_adv_location')
# Required for `product_id` to be visible in the view
cls.env.user.groups_id += cls.env.ref('product.group_product_variant')
cls.env.ref('stock.route_warehouse0_mto').active = True
cls.MrpProduction = cls.env['mrp.production']
# Create warehouse
warehouse_form = Form(cls.env['stock.warehouse'])
warehouse_form.name = 'Test'
warehouse_form.code = 'Test'
cls.warehouse = warehouse_form.save()
cls.uom_unit = cls.env.ref('uom.product_uom_unit')
# Create manufactured product
product_form = Form(cls.env['product.product'])
product_form.name = 'Stick'
product_form.uom_id = cls.uom_unit
product_form.uom_po_id = cls.uom_unit
product_form.route_ids.clear()
product_form.route_ids.add(cls.warehouse.manufacture_pull_id.route_id)
product_form.route_ids.add(cls.warehouse.mto_pull_id.route_id)
cls.product_manu = product_form.save()
# Create raw product for manufactured product
product_form = Form(cls.env['product.product'])
product_form.name = 'Raw Stick'
product_form.uom_id = cls.uom_unit
product_form.uom_po_id = cls.uom_unit
cls.product_raw = product_form.save()
# Create bom for manufactured product
bom_product_form = Form(cls.env['mrp.bom'])
bom_product_form.product_id = cls.product_manu
bom_product_form.product_tmpl_id = cls.product_manu.product_tmpl_id
bom_product_form.product_qty = 1.0
bom_product_form.type = 'normal'
with bom_product_form.bom_line_ids.new() as bom_line:
bom_line.product_id = cls.product_raw
bom_line.product_qty = 2.0
cls.bom_prod_manu = bom_product_form.save()
# Create sale order
sale_form = Form(cls.env['sale.order'])
sale_form.partner_id = cls.env['res.partner'].create({'name': 'My Test Partner'})
sale_form.picking_policy = 'direct'
sale_form.warehouse_id = cls.warehouse
with sale_form.order_line.new() as line:
line.name = cls.product_manu.name
line.product_id = cls.product_manu
line.product_uom_qty = 1.0
line.product_uom = cls.uom_unit
line.price_unit = 10.0
cls.sale_order = sale_form.save()
def test_00_manufacturing_step_one(self):
""" Testing for Step-1 """
# Change steps of manufacturing.
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'mrp_one_step'
# Confirm sale order.
self.sale_order.action_confirm()
# Check all procurements for created sale order
mo_procurement = self.MrpProduction.search([('origin', '=', self.sale_order.name)])
# Get manufactured procurement
self.assertEqual(mo_procurement.location_src_id.id, self.warehouse.lot_stock_id.id, "Source loction does not match.")
self.assertEqual(mo_procurement.location_dest_id.id, self.warehouse.lot_stock_id.id, "Destination location does not match.")
self.assertEqual(len(mo_procurement), 1, "No Procurement !")
def test_01_manufacturing_step_two(self):
""" Testing for Step-2 """
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'pbm'
self.sale_order.action_confirm()
# Get manufactured procurement
mo_procurement = self.MrpProduction.search([('origin', '=', self.sale_order.name)])
mo = self.env['mrp.production'].search([
('origin', '=', self.sale_order.name),
('product_id', '=', self.product_manu.id),
])
self.assertEqual(self.sale_order.action_view_mrp_production()['res_id'], mo.id)
self.assertEqual(mo_procurement.location_src_id.id, self.warehouse.pbm_loc_id.id, "Source loction does not match.")
self.assertEqual(mo_procurement.location_dest_id.id, self.warehouse.lot_stock_id.id, "Destination location does not match.")
self.assertEqual(len(mo_procurement), 1, "No Procurement !")
def test_cancel_multilevel_manufacturing(self):
""" Testing for multilevel Manufacturing orders.
When user creates multi-level manufacturing orders,
and then cancelles child manufacturing order,
an activity should be generated on parent MO, to notify user that
demands from child MO has been cancelled.
"""
product_form = Form(self.env['product.product'])
product_form.name = 'Screw'
self.product_screw = product_form.save()
# Add routes for manufacturing and make to order to the raw material product
with Form(self.product_raw) as p1:
p1.route_ids.clear()
p1.route_ids.add(self.warehouse_1.manufacture_pull_id.route_id)
p1.route_ids.add(self.warehouse_1.mto_pull_id.route_id)
# New BoM for raw material product, it will generate another Production order i.e. child Production order
bom_product_form = Form(self.env['mrp.bom'])
bom_product_form.product_id = self.product_raw
bom_product_form.product_tmpl_id = self.product_raw.product_tmpl_id
bom_product_form.product_qty = 1.0
with bom_product_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.product_screw
bom_line.product_qty = 5.0
self.bom_prod_manu = bom_product_form.save()
# create MO from sale order.
self.sale_order.action_confirm()
# Find child MO.
child_manufaturing = self.env['mrp.production'].search([('product_id', '=', self.product_raw.id)])
self.assertTrue((len(child_manufaturing.ids) == 1), 'Manufacturing order of raw material must be generated.')
# Cancel child MO.
child_manufaturing.action_cancel()
manufaturing_from_so = self.env['mrp.production'].search([('product_id', '=', self.product_manu.id)])
# Check if activity is generated or not on parent MO.
exception = self.env['mail.activity'].search([('res_model', '=', 'mrp.production'),
('res_id', '=', manufaturing_from_so.id)])
self.assertEqual(len(exception.ids), 1, 'When user cancelled child manufacturing, exception must be generated on parent manufacturing.')
def test_manufacturing_step_three(self):
""" Testing for Step-3 """
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'pbm_sam'
self.sale_order.action_confirm()
mo = self.env['mrp.production'].search([
('origin', '=', self.sale_order.name),
('product_id', '=', self.product_manu.id),
])
self.assertEqual(self.sale_order.mrp_production_count, 1)
self.assertEqual(mo.sale_order_count, 1)
self.assertEqual(self.sale_order.action_view_mrp_production()['res_id'], mo.id)
self.assertEqual(mo.action_view_sale_orders()['res_id'], self.sale_order.id)

View file

@ -0,0 +1,635 @@
# -*- 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, msg="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, msg="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['res.partner'].create({'name': 'Test Partner'}).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['res.partner'].create({'name': 'Test Partner'}).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},
])
def test_anglo_saxo_kit_subkits(self):
"""Check invoice COGS aml after selling and delivering a product
with Kit BoM producing 2 times the product and having
2 products with Kit BoM as components"""
# ----------------------------------------------
# BoM of Main kit:
# - BoM Type: Kit
# - Quantity: 4
# - Components:
# * 1 x Subkit A
# * 1 x Subkit B
#
# BoM of Subkit A:
# - BoM Type: Kit
# - Quantity: 1
# - Components:
# * 2 x Component A (Cost: $10, Storable)
#
# BoM of Subkit B:
# - BoM Type: Kit
# - Quantity: 1
# - Components:
# * 2 x Component B (Cost: $6, Storable)
# ----------------------------------------------
self.component_a = self._create_product('Component A', 'product', 10.00)
self.component_b = self._create_product('Component B', 'product', 6.00)
self.subkit_a = self._create_product('Subkit A', 'product', 0.00)
self.subkit_b = self._create_product('Subkit B', 'product', 0.00)
self.main_kit = self._create_product('Main kit', 'product', 0.00)
self.main_kit.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 Main kit
bom_product_form = Form(self.env['mrp.bom'])
bom_product_form.product_id = self.main_kit
bom_product_form.product_tmpl_id = self.main_kit.product_tmpl_id
bom_product_form.product_qty = 4.0
bom_product_form.type = 'phantom'
with bom_product_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.subkit_a
bom_line.product_qty = 1.0
with bom_product_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.subkit_b
bom_line.product_qty = 1.0
self.bom_main = bom_product_form.save()
# Create BoM for Subkit A
bom_product_form = Form(self.env['mrp.bom'])
bom_product_form.product_id = self.subkit_a
bom_product_form.product_tmpl_id = self.subkit_a.product_tmpl_id
bom_product_form.product_qty = 1.0
bom_product_form.type = 'phantom'
with bom_product_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.component_a
bom_line.product_qty = 2.0
self.bom_sub_b = bom_product_form.save()
# Create BoM for Subkit B
bom_product_form = Form(self.env['mrp.bom'])
bom_product_form.product_id = self.subkit_b
bom_product_form.product_tmpl_id = self.subkit_b.product_tmpl_id
bom_product_form.product_qty = 1.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
self.bom_sub_b = bom_product_form.save()
so = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [
(0, 0, {
'name': self.main_kit.name,
'product_id': self.main_kit.id,
'product_uom_qty': 1.0,
'product_uom': self.main_kit.uom_id.id,
'price_unit': 1,
'tax_id': False,
})],
})
so.action_confirm()
for move in so.picking_ids.move_ids:
move.quantity_done = move.product_qty
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, 8.00, msg="Should include include the components from all subkits, with the price adapted for 1 Main kit")
cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
self.assertAlmostEqual(cogs_aml.debit, 8.00, msg="Should include include the components from all subkits, with the price adapted for 1 Main kit")
self.assertEqual(cogs_aml.credit, 0)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,529 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase, Form, tagged
@tagged('post_install', '-at_install')
class TestSaleMrpKitBom(TransactionCase):
def _create_product(self, name, product_type, price):
return self.env['product.product'].create({
'name': name,
'type': product_type,
'standard_price': price,
})
def test_reset_avco_kit(self):
"""
Test a specific use case : One product with 2 variant, each variant has its own BoM with either component_1 or
component_2. Create a SO for one of the variant, confirm, cancel, reset to draft and then change the product of
the SO -> There should be no traceback
"""
component_1 = self.env['product.product'].create({'name': 'compo 1'})
component_2 = self.env['product.product'].create({'name': 'compo 2'})
product_category = self.env['product.category'].create({
'name': 'test avco kit',
'property_cost_method': 'average'
})
attributes = self.env['product.attribute'].create({'name': 'Legs'})
steel_legs = self.env['product.attribute.value'].create({'attribute_id': attributes.id, 'name': 'Steel'})
aluminium_legs = self.env['product.attribute.value'].create(
{'attribute_id': attributes.id, 'name': 'Aluminium'})
product_template = self.env['product.template'].create({
'name': 'test product',
'categ_id': product_category.id,
'attribute_line_ids': [(0, 0, {
'attribute_id': attributes.id,
'value_ids': [(6, 0, [steel_legs.id, aluminium_legs.id])]
})]
})
product_variant_ids = product_template.product_variant_ids
# BoM 1 with component_1
self.env['mrp.bom'].create({
'product_id': product_variant_ids[0].id,
'product_tmpl_id': product_variant_ids[0].product_tmpl_id.id,
'product_qty': 1.0,
'consumption': 'flexible',
'type': 'phantom',
'bom_line_ids': [(0, 0, {'product_id': component_1.id, 'product_qty': 1})]
})
# BoM 2 with component_2
self.env['mrp.bom'].create({
'product_id': product_variant_ids[1].id,
'product_tmpl_id': product_variant_ids[1].product_tmpl_id.id,
'product_qty': 1.0,
'consumption': 'flexible',
'type': 'phantom',
'bom_line_ids': [(0, 0, {'product_id': component_2.id, 'product_qty': 1})]
})
partner = self.env['res.partner'].create({'name': 'Testing Man'})
so = self.env['sale.order'].create({
'partner_id': partner.id,
})
# Create the order line
self.env['sale.order.line'].create({
'name': "Order line",
'product_id': product_variant_ids[0].id,
'order_id': so.id,
})
so.action_confirm()
so._action_cancel()
so.action_draft()
with Form(so) as so_form:
with so_form.order_line.edit(0) as order_line_change:
# The actual test, there should be no traceback here
order_line_change.product_id = product_variant_ids[1]
def test_sale_mrp_kit_cost(self):
"""
Check the total cost of a KIT:
# BoM of Kit A:
# - BoM Type: Kit
# - Quantity: 1
# - Components:
# * 1 x Component A (Cost: $ 6, QTY: 1, UOM: Dozens)
# * 1 x Component B (Cost: $ 10, QTY: 2, UOM: Unit)
# cost of Kit A = (6 * 1 * 12) + (10 * 2) = $ 92
"""
self.customer = self.env['res.partner'].create({
'name': 'customer'
})
self.kit_product = self._create_product('Kit Product', 'product', 1.00)
# Creating components
self.component_a = self._create_product('Component A', 'product', 1.00)
self.component_a.product_tmpl_id.standard_price = 6
self.component_b = self._create_product('Component B', 'product', 1.00)
self.component_b.product_tmpl_id.standard_price = 10
cat = self.env['product.category'].create({
'name': 'fifo',
'property_cost_method': 'fifo'
})
self.kit_product.product_tmpl_id.categ_id = cat
self.component_a.product_tmpl_id.categ_id = cat
self.component_b.product_tmpl_id.categ_id = cat
self.bom = self.env['mrp.bom'].create({
'product_tmpl_id': self.kit_product.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'phantom'
})
self.env['mrp.bom.line'].create({
'product_id': self.component_a.id,
'product_qty': 1.0,
'bom_id': self.bom.id,
'product_uom_id': self.env.ref('uom.product_uom_dozen').id,
})
self.env['mrp.bom.line'].create({
'product_id': self.component_b.id,
'product_qty': 2.0,
'bom_id': self.bom.id,
'product_uom_id': self.env.ref('uom.product_uom_unit').id,
})
# Create a SO with one unit of the kit product
so = self.env['sale.order'].create({
'partner_id': self.customer.id,
'order_line': [
(0, 0, {
'name': self.kit_product.name,
'product_id': self.kit_product.id,
'product_uom_qty': 1.0,
'product_uom': self.kit_product.uom_id.id,
})],
})
so.action_confirm()
line = so.order_line
purchase_price = line.product_id.with_company(line.company_id)._compute_average_price(0, line.product_uom_qty, line.move_ids)
self.assertEqual(purchase_price, 92, "The purchase price must be the total cost of the components multiplied by their unit of measure")
def test_qty_delivered_with_bom(self):
"""Check the quantity delivered, when a bom line has a non integer quantity"""
self.env.ref('product.decimal_product_uom').digits = 5
self.kit = self._create_product('Kit', 'product', 0.00)
self.comp = self._create_product('Component', 'product', 0.00)
# Create BoM for Kit
bom_product_form = Form(self.env['mrp.bom'])
bom_product_form.product_id = self.kit
bom_product_form.product_tmpl_id = self.kit.product_tmpl_id
bom_product_form.product_qty = 1.0
bom_product_form.type = 'phantom'
with bom_product_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.comp
bom_line.product_qty = 0.08600
self.bom = bom_product_form.save()
self.customer = self.env['res.partner'].create({
'name': 'customer',
})
so = self.env['sale.order'].create({
'partner_id': self.customer.id,
'order_line': [
(0, 0, {
'name': self.kit.name,
'product_id': self.kit.id,
'product_uom_qty': 10.0,
'product_uom': self.kit.uom_id.id,
'price_unit': 1,
'tax_id': False,
})],
})
so.action_confirm()
self.assertTrue(so.picking_ids)
self.assertEqual(so.order_line.qty_delivered, 0)
picking = so.picking_ids
picking.move_ids.quantity_done = 0.86000
picking.button_validate()
# Checks the delivery amount (must be 10).
self.assertEqual(so.order_line.qty_delivered, 10)
def test_qty_delivered_with_bom_using_kit(self):
"""Check the quantity delivered, when one product is a kit
and his bom uses another product that is also a kit"""
self.kitA = self._create_product('Kit A', 'consu', 0.00)
self.kitB = self._create_product('Kit B', 'consu', 0.00)
self.compA = self._create_product('ComponentA', 'consu', 0.00)
self.compB = self._create_product('ComponentB', 'consu', 0.00)
# Create BoM for KitB
bom_product_formA = Form(self.env['mrp.bom'])
bom_product_formA.product_id = self.kitB
bom_product_formA.product_tmpl_id = self.kitB.product_tmpl_id
bom_product_formA.product_qty = 1.0
bom_product_formA.type = 'phantom'
with bom_product_formA.bom_line_ids.new() as bom_line:
bom_line.product_id = self.compA
bom_line.product_qty = 1
with bom_product_formA.bom_line_ids.new() as bom_line:
bom_line.product_id = self.compB
bom_line.product_qty = 1
self.bomA = bom_product_formA.save()
# Create BoM for KitA
bom_product_formB = Form(self.env['mrp.bom'])
bom_product_formB.product_id = self.kitA
bom_product_formB.product_tmpl_id = self.kitA.product_tmpl_id
bom_product_formB.product_qty = 1.0
bom_product_formB.type = 'phantom'
with bom_product_formB.bom_line_ids.new() as bom_line:
bom_line.product_id = self.compA
bom_line.product_qty = 1
with bom_product_formB.bom_line_ids.new() as bom_line:
bom_line.product_id = self.kitB
bom_line.product_qty = 1
self.bomB = bom_product_formB.save()
self.customer = self.env['res.partner'].create({
'name': 'customer',
})
so = self.env['sale.order'].create({
'partner_id': self.customer.id,
'order_line': [
(0, 0, {
'name': self.kitA.name,
'product_id': self.kitA.id,
'product_uom_qty': 1.0,
'product_uom': self.kitA.uom_id.id,
'price_unit': 1,
'tax_id': False,
})],
})
so.action_confirm()
self.assertTrue(so.picking_ids)
self.assertEqual(so.order_line.qty_delivered, 0)
picking = so.picking_ids
action = picking.button_validate()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
# Checks the delivery amount (must be 1).
self.assertEqual(so.order_line.qty_delivered, 1)
def test_sale_kit_show_kit_in_delivery(self):
"""Create a kit with 2 product and activate 2 steps
delivery and check that every stock move contains
a bom_line_id
"""
wh = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
wh.write({'delivery_steps': 'pick_ship'})
kitA = self._create_product('Kit Product', 'product', 0.00)
compA = self._create_product('ComponentA', 'product', 0.00)
compB = self._create_product('ComponentB', 'product', 0.00)
# Create BoM for KitB
bom_product_formA = Form(self.env['mrp.bom'])
bom_product_formA.product_id = kitA
bom_product_formA.product_tmpl_id = kitA.product_tmpl_id
bom_product_formA.product_qty = 1.0
bom_product_formA.type = 'phantom'
with bom_product_formA.bom_line_ids.new() as bom_line:
bom_line.product_id = compA
bom_line.product_qty = 1
with bom_product_formA.bom_line_ids.new() as bom_line:
bom_line.product_id = compB
bom_line.product_qty = 1
bom_product_formA.save()
customer = self.env['res.partner'].create({
'name': 'customer',
})
so = self.env['sale.order'].create({
'partner_id': customer.id,
'order_line': [
(0, 0, {
'name': kitA.name,
'product_id': kitA.id,
'product_uom_qty': 1.0,
'product_uom': kitA.uom_id.id,
'price_unit': 1,
'tax_id': False,
})]
})
so.action_confirm()
pick = so.picking_ids[0]
ship = so.picking_ids[1]
self.assertTrue(pick.move_ids_without_package[0].bom_line_id, "All component from kits should have a bom line")
self.assertTrue(pick.move_ids_without_package[1].bom_line_id, "All component from kits should have a bom line")
self.assertTrue(ship.move_ids_without_package[0].bom_line_id, "All component from kits should have a bom line")
self.assertTrue(ship.move_ids_without_package[1].bom_line_id, "All component from kits should have a bom line")
def test_qty_delivered_with_bom_using_kit2(self):
"""Create 2 kits products that have common components and activate 2 steps delivery
Then create a sale order with these 2 products, and put everything in a pack in
the first step of the delivery. After the shipping is done, check the done quantity
is correct for each products.
"""
wh = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
wh.write({'delivery_steps': 'pick_ship'})
kitAB = self._create_product('Kit AB', 'product', 0.00)
kitABC = self._create_product('Kit ABC', 'product', 0.00)
compA = self._create_product('ComponentA', 'product', 0.00)
compB = self._create_product('ComponentB', 'product', 0.00)
compC = self._create_product('ComponentC', 'product', 0.00)
# Create BoM for KitB
bom_product_formA = Form(self.env['mrp.bom'])
bom_product_formA.product_id = kitAB
bom_product_formA.product_tmpl_id = kitAB.product_tmpl_id
bom_product_formA.product_qty = 1.0
bom_product_formA.type = 'phantom'
with bom_product_formA.bom_line_ids.new() as bom_line:
bom_line.product_id = compA
bom_line.product_qty = 1
with bom_product_formA.bom_line_ids.new() as bom_line:
bom_line.product_id = compB
bom_line.product_qty = 1
bom_product_formA.save()
# Create BoM for KitA
bom_product_formB = Form(self.env['mrp.bom'])
bom_product_formB.product_id = kitABC
bom_product_formB.product_tmpl_id = kitABC.product_tmpl_id
bom_product_formB.product_qty = 1.0
bom_product_formB.type = 'phantom'
with bom_product_formB.bom_line_ids.new() as bom_line:
bom_line.product_id = compA
bom_line.product_qty = 1
with bom_product_formB.bom_line_ids.new() as bom_line:
bom_line.product_id = compB
bom_line.product_qty = 1
with bom_product_formB.bom_line_ids.new() as bom_line:
bom_line.product_id = compC
bom_line.product_qty = 1
bom_product_formB.save()
customer = self.env['res.partner'].create({
'name': 'customer',
})
so = self.env['sale.order'].create({
'partner_id': customer.id,
'order_line': [
(0, 0, {
'name': kitAB.name,
'product_id': kitAB.id,
'product_uom_qty': 1.0,
'product_uom': kitAB.uom_id.id,
'price_unit': 1,
'tax_id': False,
}),
(0, 0, {
'name': kitABC.name,
'product_id': kitABC.id,
'product_uom_qty': 1.0,
'product_uom': kitABC.uom_id.id,
'price_unit': 1,
'tax_id': False,
})],
})
so.action_confirm()
pick = so.picking_ids[0]
ship = so.picking_ids[1]
for move in pick.move_ids:
move.quantity_done = 1
pick.action_put_in_pack()
pick.button_validate()
ship.package_level_ids.write({'is_done': True})
ship.package_level_ids._set_is_done()
for move_line in ship.move_line_ids:
self.assertEqual(move_line.move_id.product_uom_qty, move_line.qty_done, "Quantity done should be equal to the quantity reserved in the move line")
def test_kit_in_delivery_slip(self):
"""
Suppose this structure:
Sale order:
- Kit 1 with a sales description("test"):
|- Compo 1
- Product 1
- Kit 2
* Variant 1
- Compo 1
* Variant 2
- Compo 1
- Kit 4:
- Compo 1
- Kit 5
- Kit 4
- Compo 1
This test ensures that, when delivering a Kit product with a sales description,
the delivery report is correctly printed with all the products.
"""
kit_1, component_1, product_1, kit_3, kit_4 = self.env['product.product'].create([{
'name': n,
'type': 'product',
} for n in ['Kit 1', 'Compo 1', 'Product 1', 'Kit 3', 'Kit 4']])
kit_1.description_sale = "test"
self.env['mrp.bom'].create([{
'product_tmpl_id': kit_1.product_tmpl_id.id,
'product_qty': 1,
'type': 'phantom',
'bom_line_ids': [
(0, 0, {'product_id': component_1.id, 'product_qty': 1}),
],
}])
colors = ['red', 'blue']
prod_attr = self.env['product.attribute'].create({'name': 'Color', 'create_variant': 'always'})
prod_attr_values = self.env['product.attribute.value'].create([{'name': color, 'attribute_id': prod_attr.id, 'sequence': 1} for color in colors])
kit_2 = self.env['product.template'].create({
'name': 'Kit 2',
'attribute_line_ids': [(0, 0, {
'attribute_id': prod_attr.id,
'value_ids': [(6, 0, prod_attr_values.ids)]
})]
})
self.env['mrp.bom'].create([{
'product_tmpl_id': kit_2.id,
'product_id': kit_2.product_variant_ids[0].id,
'product_qty': 1,
'type': 'phantom',
'bom_line_ids': [
(0, 0, {'product_id': component_1.id, 'product_qty': 1}),
],
}])
self.env['mrp.bom'].create([{
'product_tmpl_id': kit_2.id,
'product_id': kit_2.product_variant_ids[1].id,
'product_qty': 1,
'type': 'phantom',
'bom_line_ids': [
(0, 0, {'product_id': component_1.id, 'product_qty': 1}),
],
}])
self.env['mrp.bom'].create([{
'product_tmpl_id': kit_3.product_tmpl_id.id,
'product_qty': 1,
'type': 'phantom',
'bom_line_ids': [
(0, 0, {'product_id': component_1.id, 'product_qty': 1}),
],
}])
self.env['mrp.bom'].create([{
'product_tmpl_id': kit_4.product_tmpl_id.id,
'product_qty': 1,
'type': 'phantom',
'bom_line_ids': [
(0, 0, {'product_id': component_1.id, 'product_qty': 1}),
(0, 0, {'product_id': kit_3.id, 'product_qty': 1}),
],
}])
customer = self.env['res.partner'].create({
'name': 'customer',
})
so = self.env['sale.order'].create({
'partner_id': customer.id,
'order_line': [
(0, 0, {
'product_id': kit_1.id,
'product_uom_qty': 1.0,
}),
(0, 0, {
'product_id': product_1.id,
'product_uom_qty': 1.0,
}),
(0, 0, {
'product_id': kit_2.product_variant_ids[0].id,
'product_uom_qty': 1.0,
}),
(0, 0, {
'product_id': kit_2.product_variant_ids[1].id,
'product_uom_qty': 1.0,
}),
(0, 0, {
'product_id': kit_3.id,
'product_uom_qty': 1.0,
}),
(0, 0, {
'product_id': kit_4.id,
'product_uom_qty': 1.0,
})],
})
so.action_confirm()
picking = so.picking_ids
self.assertEqual(len(so.picking_ids.move_ids_without_package), 7)
picking.move_ids.quantity_done = 1
picking.button_validate()
self.assertEqual(picking.state, 'done')
html_report = self.env['ir.actions.report']._render_qweb_html('stock.report_deliveryslip', picking.ids)[0].decode('utf-8').split('\n')
keys = [
"Kit 1", "Compo 1", "Kit 2 (red)", "Compo 1", "Kit 2 (blue)", "Compo 1",
"Kit 3", "Compo 1", "Kit 4", "Compo 1",
"Products not associated with a kit", "Product 1",
]
for line in html_report:
if not keys:
break
if keys[0] in line:
keys = keys[1:]
self.assertFalse(keys, "All keys should be in the report with the defined order")

View file

@ -0,0 +1,167 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import timedelta
from odoo import fields
from odoo.addons.stock.tests.common2 import TestStockCommon
from odoo.tests import Form
class TestSaleMrpLeadTime(TestStockCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env.ref('stock.route_warehouse0_mto').active = True
# Update the product_1 with type, route, Manufacturing Lead Time and Customer Lead Time
with Form(cls.product_1) as p1:
# `type` is invisible in the view,
# and it's a compute field based on `detailed_type` which is the field visible in the view
p1.detailed_type = 'product'
p1.produce_delay = 5.0
p1.sale_delay = 5.0
p1.route_ids.clear()
p1.route_ids.add(cls.warehouse_1.manufacture_pull_id.route_id)
p1.route_ids.add(cls.warehouse_1.mto_pull_id.route_id)
# Update the product_2 with type
with Form(cls.product_2) as p2:
# `type` is invisible in the view,
# and it's a compute field based on `detailed_type` which is the field visible in the view
p2.detailed_type = 'consu'
# Create Bill of materials for product_1
with Form(cls.env['mrp.bom']) as bom:
bom.product_tmpl_id = cls.product_1.product_tmpl_id
bom.product_qty = 2
with bom.bom_line_ids.new() as line:
line.product_id = cls.product_2
line.product_qty = 4
def test_00_product_company_level_delays(self):
""" In order to check schedule date, set product's Manufacturing Lead Time
and Customer Lead Time and also set company's Manufacturing Lead Time
and Sales Safety Days."""
company = self.env.ref('base.main_company')
# Update company with Manufacturing Lead Time and Sales Safety Days
company.write({'manufacturing_lead': 3.0,
'security_lead': 3.0})
# Create sale order of product_1
order_form = Form(self.env['sale.order'])
order_form.partner_id = self.partner_1
with order_form.order_line.new() as line:
line.product_id = self.product_1
line.product_uom_qty = 10
order = order_form.save()
# Confirm sale order
order.action_confirm()
# Check manufacturing order created or not
manufacturing_order = self.env['mrp.production'].search([('product_id', '=', self.product_1.id), ('move_dest_ids', 'in', order.picking_ids[0].move_ids.ids)])
self.assertTrue(manufacturing_order, 'Manufacturing order should be created.')
# Check schedule date of picking
deadline_picking = fields.Datetime.from_string(order.date_order) + timedelta(days=self.product_1.sale_delay)
out_date = deadline_picking - timedelta(days=company.security_lead)
self.assertAlmostEqual(
order.picking_ids[0].scheduled_date, out_date,
delta=timedelta(seconds=1),
msg='Schedule date of picking should be equal to: Order date + Customer Lead Time - Sales Safety Days.'
)
self.assertAlmostEqual(
order.picking_ids[0].date_deadline, deadline_picking,
delta=timedelta(seconds=1),
msg='Deadline date of picking should be equal to: Order date + Customer Lead Time.'
)
# Check schedule date and deadline of manufacturing order
mo_scheduled = out_date - timedelta(days=self.product_1.produce_delay) - timedelta(days=company.manufacturing_lead)
self.assertAlmostEqual(
fields.Datetime.from_string(manufacturing_order.date_planned_start), mo_scheduled,
delta=timedelta(seconds=1),
msg="Schedule date of manufacturing order should be equal to: Schedule date of picking - product's Manufacturing Lead Time - company's Manufacturing Lead Time."
)
self.assertAlmostEqual(
fields.Datetime.from_string(manufacturing_order.date_deadline), deadline_picking,
delta=timedelta(seconds=1),
msg="Deadline date of manufacturing order should be equal to the deadline of sale picking"
)
def test_01_product_route_level_delays(self):
""" In order to check schedule dates, set product's Manufacturing Lead Time
and Customer Lead Time and also set warehouse route's delay."""
# Update warehouse_1 with Outgoing Shippings pick + pack + ship
self.warehouse_1.write({'delivery_steps': 'pick_pack_ship'})
# Set delay on pull rule
for pull_rule in self.warehouse_1.delivery_route_id.rule_ids:
pull_rule.write({'delay': 2})
# Create sale order of product_1
order_form = Form(self.env['sale.order'])
order_form.partner_id = self.partner_1
order_form.warehouse_id = self.warehouse_1
with order_form.order_line.new() as line:
line.product_id = self.product_1
line.product_uom_qty = 6
order = order_form.save()
# Confirm sale order
order.action_confirm()
# Run scheduler
self.env['procurement.group'].run_scheduler()
# Check manufacturing order created or not
manufacturing_order = self.env['mrp.production'].search([('product_id', '=', self.product_1.id)])
self.assertTrue(manufacturing_order, 'Manufacturing order should be created.')
# Check the picking crated or not
self.assertTrue(order.picking_ids, "Pickings should be created.")
# Check schedule date of ship type picking
out = order.picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse_1.out_type_id)
out_min_date = fields.Datetime.from_string(out.scheduled_date)
out_date = fields.Datetime.from_string(order.date_order) + timedelta(days=self.product_1.sale_delay) - timedelta(days=out.move_ids[0].rule_id.delay)
self.assertAlmostEqual(
out_min_date, out_date,
delta=timedelta(seconds=10),
msg='Schedule date of ship type picking should be equal to: order date + Customer Lead Time - pull rule delay.'
)
# Check schedule date of pack type picking
pack = order.picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse_1.pack_type_id)
pack_min_date = fields.Datetime.from_string(pack.scheduled_date)
pack_date = out_date - timedelta(days=pack.move_ids[0].rule_id.delay)
self.assertAlmostEqual(
pack_min_date, pack_date,
delta=timedelta(seconds=10),
msg='Schedule date of pack type picking should be equal to: Schedule date of ship type picking - pull rule delay.'
)
# Check schedule date of pick type picking
pick = order.picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse_1.pick_type_id)
pick_min_date = fields.Datetime.from_string(pick.scheduled_date)
self.assertAlmostEqual(
pick_min_date, pack_date,
delta=timedelta(seconds=10),
msg='Schedule date of pick type picking should be equal to: Schedule date of pack type picking.'
)
# Check schedule date and deadline date of manufacturing order
mo_scheduled = out_date - timedelta(days=self.product_1.produce_delay) - timedelta(days=self.warehouse_1.delivery_route_id.rule_ids[0].delay) - timedelta(days=self.env.ref('base.main_company').manufacturing_lead)
self.assertAlmostEqual(
fields.Datetime.from_string(manufacturing_order.date_planned_start), mo_scheduled,
delta=timedelta(seconds=1),
msg="Schedule date of manufacturing order should be equal to: Schedule date of picking - product's Manufacturing Lead Time- delay pull_rule."
)
self.assertAlmostEqual(
manufacturing_order.date_deadline, order.picking_ids[0].date_deadline,
delta=timedelta(seconds=1),
msg="Deadline date of manufacturing order should be equal to the deadline of sale picking"
)

View file

@ -0,0 +1,310 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import time
from odoo.tests.common import TransactionCase, Form
from odoo.tools import mute_logger
from odoo import Command
class TestSaleMrpProcurement(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env.ref('base.group_user').write({'implied_ids': [(4, cls.env.ref('product.group_product_variant').id)]})
def test_sale_mrp(self):
# Required for `uom_id` to be visible in the view
self.env.user.groups_id += self.env.ref('uom.group_uom')
self.env.ref('stock.route_warehouse0_mto').active = True
warehouse0 = self.env.ref('stock.warehouse0')
# In order to test the sale_mrp module in OpenERP, I start by creating a new product 'Slider Mobile'
# I define product category Mobile Products Sellable.
with mute_logger('odoo.tests.common.onchange'):
# Suppress warning on "Changing your cost method" when creating a
# product category
pc = Form(self.env['product.category'])
pc.name = 'Mobile Products Sellable'
product_category_allproductssellable0 = pc.save()
uom_unit = self.env.ref('uom.product_uom_unit')
self.assertIn("seller_ids", self.env['product.template'].fields_get())
# I define product for Slider Mobile.
product = Form(self.env['product.template'])
product.categ_id = product_category_allproductssellable0
product.list_price = 200.0
product.name = 'Slider Mobile'
product.detailed_type = 'product'
product.uom_id = uom_unit
product.uom_po_id = uom_unit
product.route_ids.clear()
product.route_ids.add(warehouse0.manufacture_pull_id.route_id)
product.route_ids.add(warehouse0.mto_pull_id.route_id)
product_template_slidermobile0 = product.save()
product_template_slidermobile0.standard_price = 189
product_component = Form(self.env['product.product'])
product_component.name = 'Battery'
product_product_bettery = product_component.save()
with Form(self.env['mrp.bom']) as bom:
bom.product_tmpl_id = product_template_slidermobile0
with bom.bom_line_ids.new() as line:
line.product_id = product_product_bettery
line.product_qty = 4
# I create a sale order for product Slider mobile
so_form = Form(self.env['sale.order'])
so_form.partner_id = self.env['res.partner'].create({'name': 'Another Test Partner'})
with so_form.order_line.new() as line:
line.product_id = product_template_slidermobile0.product_variant_ids
line.price_unit = 200
line.product_uom_qty = 500.0
line.customer_lead = 7.0
sale_order_so0 = so_form.save()
# I confirm the sale order
sale_order_so0.action_confirm()
# I verify that a manufacturing order has been generated, and that its name and reference are correct
mo = self.env['mrp.production'].search([('origin', 'like', sale_order_so0.name)], limit=1)
self.assertTrue(mo, 'Manufacturing order has not been generated')
# Check the mo is displayed on the so
self.assertEqual(mo.id, sale_order_so0.action_view_mrp_production()['res_id'])
def test_sale_mrp_pickings(self):
""" Test sale of multiple mrp products in MTO
to avoid generating multiple deliveries
to the customer location
"""
# Required for `uom_id` to be visible in the view
self.env.user.groups_id += self.env.ref('uom.group_uom')
# Required for `manufacture_step` to be visible in the view
self.env.user.groups_id += self.env.ref('stock.group_adv_location')
self.env.ref('stock.route_warehouse0_mto').active = True
# Create warehouse
self.customer_location = self.env['ir.model.data']._xmlid_to_res_id('stock.stock_location_customers')
self.warehouse = self.env['stock.warehouse'].create({
'name': 'Test Warehouse',
'code': 'TWH'
})
self.uom_unit = self.env.ref('uom.product_uom_unit')
# Create raw product for manufactured product
product_form = Form(self.env['product.product'])
product_form.name = 'Raw Stick'
product_form.detailed_type = 'product'
product_form.uom_id = self.uom_unit
product_form.uom_po_id = self.uom_unit
self.raw_product = product_form.save()
# Create manufactured product
product_form = Form(self.env['product.product'])
product_form.name = 'Stick'
product_form.uom_id = self.uom_unit
product_form.uom_po_id = self.uom_unit
product_form.detailed_type = 'product'
product_form.route_ids.clear()
product_form.route_ids.add(self.warehouse.manufacture_pull_id.route_id)
product_form.route_ids.add(self.warehouse.mto_pull_id.route_id)
self.finished_product = product_form.save()
# Create manifactured product which uses another manifactured
product_form = Form(self.env['product.product'])
product_form.name = 'Arrow'
product_form.detailed_type = 'product'
product_form.route_ids.clear()
product_form.route_ids.add(self.warehouse.manufacture_pull_id.route_id)
product_form.route_ids.add(self.warehouse.mto_pull_id.route_id)
self.complex_product = product_form.save()
## Create raw product for manufactured product
product_form = Form(self.env['product.product'])
product_form.name = 'Raw Iron'
product_form.detailed_type = 'product'
product_form.uom_id = self.uom_unit
product_form.uom_po_id = self.uom_unit
self.raw_product_2 = product_form.save()
# Create bom for manufactured product
bom_product_form = Form(self.env['mrp.bom'])
bom_product_form.product_id = self.finished_product
bom_product_form.product_tmpl_id = self.finished_product.product_tmpl_id
bom_product_form.product_qty = 1.0
bom_product_form.type = 'normal'
with bom_product_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.raw_product
bom_line.product_qty = 2.0
self.bom = bom_product_form.save()
## Create bom for manufactured product
bom_product_form = Form(self.env['mrp.bom'])
bom_product_form.product_id = self.complex_product
bom_product_form.product_tmpl_id = self.complex_product.product_tmpl_id
with bom_product_form.bom_line_ids.new() as line:
line.product_id = self.finished_product
line.product_qty = 1.0
with bom_product_form.bom_line_ids.new() as line:
line.product_id = self.raw_product_2
line.product_qty = 1.0
self.complex_bom = bom_product_form.save()
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'pbm_sam'
so_form = Form(self.env['sale.order'])
so_form.partner_id = self.env['res.partner'].create({'name': 'Another Test Partner'})
with so_form.order_line.new() as line:
line.product_id = self.complex_product
line.price_unit = 1
line.product_uom_qty = 1
with so_form.order_line.new() as line:
line.product_id = self.finished_product
line.price_unit = 1
line.product_uom_qty = 1
sale_order_so0 = so_form.save()
sale_order_so0.action_confirm()
# Verify buttons are working as expected
self.assertEqual(sale_order_so0.mrp_production_count, 2, "User should see the correct number of manufacture orders in smart button")
pickings = sale_order_so0.picking_ids
# One delivery...
self.assertEqual(len(pickings), 1)
# ...with two products
self.assertEqual(len(pickings[0].move_ids), 2)
def test_post_prod_location_child_of_stock_location(self):
"""
3-steps manufacturing, the post-prod location is a child of the stock
location. Have a manufactured product with the manufacture route and a
RR min=max=0. Confirm a SO with that product -> It should generate a MO
"""
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
manufacture_route = warehouse.manufacture_pull_id.route_id
warehouse.manufacture_steps = 'pbm_sam'
warehouse.sam_loc_id.location_id = warehouse.lot_stock_id
product, component = self.env['product.product'].create([{
'name': 'Finished',
'type': 'product',
'route_ids': [(6, 0, manufacture_route.ids)],
}, {
'name': 'Component',
'type': 'consu',
}])
self.env['mrp.bom'].create({
'product_id': product.id,
'product_tmpl_id': product.product_tmpl_id.id,
'product_uom_id': product.uom_id.id,
'product_qty': 1.0,
'type': 'normal',
'bom_line_ids': [
(0, 0, {'product_id': component.id, 'product_qty': 1}),
],
})
self.env['stock.warehouse.orderpoint'].create({
'name': product.name,
'location_id': warehouse.lot_stock_id.id,
'product_id': product.id,
'product_min_qty': 0,
'product_max_qty': 0,
'trigger': 'auto',
})
so = self.env['sale.order'].create({
'partner_id': self.env['res.partner'].create({'name': 'Super Partner'}).id,
'order_line': [
(0, 0, {
'name': product.name,
'product_id': product.id,
'product_uom_qty': 1.0,
'product_uom': product.uom_id.id,
'price_unit': 1,
})],
})
so.action_confirm()
self.assertEqual(so.state, 'sale')
mo = self.env['mrp.production'].search([('product_id', '=', product.id)], order='id desc', limit=1)
self.assertIn(so.name, mo.origin)
def test_so_reordering_rule(self):
kit_1, component_1 = self.env['product.product'].create([{
'name': n,
'type': 'product',
} for n in ['Kit 1', 'Compo 1']])
self.env['mrp.bom'].create([{
'product_tmpl_id': kit_1.product_tmpl_id.id,
'product_qty': 1,
'type': 'phantom',
'bom_line_ids': [
(0, 0, {'product_id': component_1.id, 'product_qty': 1}),
],
}])
customer = self.env['res.partner'].create({
'name': 'customer',
})
so = self.env['sale.order'].create({
'partner_id': customer.id,
'order_line': [
(0, 0, {
'product_id': kit_1.id,
'product_uom_qty': 1.0,
})],
})
so.action_confirm()
self.env['stock.warehouse.orderpoint']._get_orderpoint_action()
orderpoint_product = self.env['stock.warehouse.orderpoint'].search(
[('product_id', '=', kit_1.id)])
self.assertFalse(orderpoint_product)
def test_sale_mrp_avoid_multiple_pickings(self):
"""
Test sale of multiple products. Avoid multiple pickings being
generated when we are not in 3 steps manufacturing.
"""
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
warehouse.sam_loc_id = warehouse.lot_stock_id
so = self.env['sale.order'].create({
'partner_id': self.env['res.partner'].create({'name': 'My Partner'}).id,
'order_line': [
Command.create({
'name': 'sol_p1',
'product_id': self.env['product.product'].create({'name': 'p1'}).id,
'product_uom_qty': 1,
'product_uom': self.env.ref('uom.product_uom_unit').id,
}),
Command.create({
'name': 'sol_p2',
'product_id': self.env['product.product'].create({'name': 'p2'}).id,
'product_uom_qty': 1,
'product_uom': self.env.ref('uom.product_uom_unit').id,
}),
],
})
so.action_confirm()
self.assertEqual(len(so.picking_ids), 1)
self.assertEqual(so.picking_ids.picking_type_id, warehouse.out_type_id)

View file

@ -0,0 +1,130 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import common, Form
from odoo.tools import html2plaintext
@common.tagged('post_install', '-at_install')
class TestSaleMrpInvoices(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.product_by_lot = cls.env['product.product'].create({
'name': 'Product By Lot',
'type': 'product',
'tracking': 'lot',
})
cls.warehouse = cls.env['stock.warehouse'].search([('company_id', '=', cls.env.company.id)], limit=1)
cls.stock_location = cls.warehouse.lot_stock_id
cls.lot = cls.env['stock.lot'].create({
'name': 'LOT0001',
'product_id': cls.product_by_lot.id,
'company_id': cls.env.company.id,
})
cls.env['stock.quant']._update_available_quantity(cls.product_by_lot, cls.stock_location, 10, lot_id=cls.lot)
cls.tracked_kit = cls.env['product.product'].create({
'name': 'Simple Kit',
'type': 'consu',
})
cls.env['mrp.bom'].create({
'product_tmpl_id': cls.tracked_kit.product_tmpl_id.id,
'type': 'phantom',
'bom_line_ids': [(0, 0, {
'product_id': cls.product_by_lot.id,
'product_qty': 1,
})]
})
cls.partner = cls.env['res.partner'].create({'name': 'Test Partner'})
def test_deliver_and_invoice_tracked_components(self):
"""
Suppose the lots are printed on the invoices.
The user sells a kit that has one tracked component.
The lot of the delivered component should be on the invoice.
"""
display_lots = self.env.ref('stock_account.group_lot_on_invoice')
display_uom = self.env.ref('uom.group_uom')
self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]})
so = self.env['sale.order'].create({
'partner_id': self.partner.id,
'order_line': [
(0, 0, {'name': self.tracked_kit.name, 'product_id': self.tracked_kit.id, 'product_uom_qty': 1}),
],
})
so.action_confirm()
action = so.picking_ids.button_validate()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
invoice = so._create_invoices()
invoice.action_post()
html = self.env['ir.actions.report']._render_qweb_html(
'account.report_invoice_with_payments', invoice.ids)[0]
text = html2plaintext(html)
self.assertRegex(text, r'Product By Lot\n1.00Units\nLOT0001', "There should be a line that specifies 1 x LOT0001")
def test_report_forecast_for_mto_procure_method(self):
"""
Check that mto moves are not reported as taking from stock in the forecast report
"""
mto_route = self.env.ref('stock.route_warehouse0_mto')
mto_route.active = True
manufacturing_route = self.env.ref('mrp.route_warehouse0_manufacture')
product = self.env['product.product'].create({
'name': 'SuperProduct',
'type': 'product',
'route_ids': [Command.set((mto_route + manufacturing_route).ids)]
})
warehouse = self.warehouse
# make 2 so: so_1 can be fulfilled and so_2 requires a replenishment
self.env['stock.quant']._update_available_quantity(product, warehouse.lot_stock_id, 10.0)
so_1, so_2 = self.env['sale.order'].create([
{
'partner_id': self.partner_a.id,
'order_line': [Command.create({
'name': product.name,
'product_id': product.id,
'product_uom_qty': 8.0,
'product_uom': product.uom_id.id,
'price_unit': product.list_price,
})]
},
{
'partner_id': self.partner_a.id,
'order_line': [Command.create({
'name': product.name,
'product_id': product.id,
'product_uom_qty': 7.0,
'product_uom': product.uom_id.id,
'price_unit': product.list_price,
})]
},
])
(so_1 | so_2).action_confirm()
report_lines = self.env['report.stock.report_product_product_replenishment'].with_context(warehouse=warehouse.id).get_report_values(docids=product.ids)['docs']['lines']
self.assertEqual(len(report_lines), 3)
so_1_line = next(filter(lambda line: line.get('document_out') == so_1, report_lines))
self.assertEqual(
[so_1_line['quantity'], so_1_line['move_out'], so_1_line['replenishment_filled']],
[8.0, so_1.picking_ids.move_ids, True]
)
so_2_line = next(filter(lambda line: line.get('document_out') == so_2, report_lines))
self.assertEqual(
[so_2_line['quantity'], so_2_line['move_out'], so_2_line['replenishment_filled']],
[7.0, so_2.picking_ids.move_ids, False]
)
quant_line = next(filter(lambda line: not line.get('document_out'), report_lines))
self.assertEqual(
[quant_line['document_out'], quant_line['quantity'], quant_line['replenishment_filled']],
[False, 2.0, True]
)