mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-27 09:52:02 +02:00
Initial commit: Sale packages
This commit is contained in:
commit
14e3d26998
6469 changed files with 2479670 additions and 0 deletions
10
odoo-bringout-oca-ocb-sale_mrp/sale_mrp/tests/__init__.py
Normal file
10
odoo-bringout-oca-ocb-sale_mrp/sale_mrp/tests/__init__.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
2480
odoo-bringout-oca-ocb-sale_mrp/sale_mrp/tests/test_sale_mrp_flow.py
Normal file
2480
odoo-bringout-oca-ocb-sale_mrp/sale_mrp/tests/test_sale_mrp_flow.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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")
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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]
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue