mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-27 13:32:03 +02:00
19.0 vanilla
This commit is contained in:
parent
79f83631d5
commit
73afc09215
6267 changed files with 1534193 additions and 1130106 deletions
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_sale_mrp_flow
|
||||
|
|
|
|||
|
|
@ -12,19 +12,20 @@ class TestMultistepManufacturing(TestMrpCommon):
|
|||
super().setUpClass()
|
||||
|
||||
# Required for `uom_id ` to be visible in the view
|
||||
cls.env.user.groups_id += cls.env.ref('uom.group_uom')
|
||||
cls.env.user.group_ids += 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')
|
||||
cls.env.user.group_ids += 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._enable_variants()
|
||||
|
||||
cls.env.ref('stock.route_warehouse0_mto').active = True
|
||||
cls.route_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.warehouse.mto_pull_id.route_id.rule_ids.procure_method = "make_to_order"
|
||||
|
||||
cls.uom_unit = cls.env.ref('uom.product_uom_unit')
|
||||
|
||||
|
|
@ -32,7 +33,6 @@ class TestMultistepManufacturing(TestMrpCommon):
|
|||
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)
|
||||
|
|
@ -42,12 +42,10 @@ class TestMultistepManufacturing(TestMrpCommon):
|
|||
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'
|
||||
|
|
@ -65,7 +63,6 @@ class TestMultistepManufacturing(TestMrpCommon):
|
|||
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()
|
||||
|
||||
|
|
@ -120,7 +117,6 @@ class TestMultistepManufacturing(TestMrpCommon):
|
|||
|
||||
# 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:
|
||||
|
|
@ -157,3 +153,56 @@ class TestMultistepManufacturing(TestMrpCommon):
|
|||
|
||||
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)
|
||||
|
||||
def test_sales_order_with_mto_manufacturing(self):
|
||||
self.route_mto.active = True
|
||||
warehouse = self.warehouse_1
|
||||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
prod1 = self.env['product.product'].create({
|
||||
'name': 'elct1',
|
||||
'type': 'consu',
|
||||
'route_ids': [(6, 0, [
|
||||
warehouse.manufacture_pull_id.route_id.id,
|
||||
warehouse.mto_pull_id.route_id.id
|
||||
])],
|
||||
})
|
||||
prod2 = self.env['product.product'].create({
|
||||
'name': 'elct2',
|
||||
'type': 'consu',
|
||||
'route_ids': [(6, 0, [
|
||||
warehouse.manufacture_pull_id.route_id.id,
|
||||
warehouse.mto_pull_id.route_id.id
|
||||
])],
|
||||
})
|
||||
partner = self.env['res.partner'].create({'name': 'Steve Buscemi'})
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': partner.id,
|
||||
'order_line': [(0, 0, {'product_id': prod1.id, 'product_uom_qty': 1}),
|
||||
(0, 0, {'product_id': prod2.id, 'product_uom_qty': 1})],
|
||||
'client_order_ref': 'Test Reference'
|
||||
})
|
||||
so.action_confirm()
|
||||
|
||||
def test_mto_cancel_3_steps_mo(self):
|
||||
'''
|
||||
In 3 step manufacturing, test that when the MO gets cancelled, the
|
||||
delivery (to the client) can be made from stock.
|
||||
'''
|
||||
self.warehouse.manufacture_steps = 'pbm_sam'
|
||||
self.sale_order.order_line.product_id.is_storable = True
|
||||
self.env['stock.quant']._update_available_quantity(
|
||||
self.sale_order.order_line.product_id,
|
||||
self.sale_order.warehouse_id.lot_stock_id,
|
||||
10
|
||||
)
|
||||
self.sale_order.action_confirm()
|
||||
self.assertEqual(self.sale_order.picking_ids.state, 'waiting')
|
||||
self.assertEqual(self.sale_order.picking_ids.move_ids.procure_method, 'make_to_order')
|
||||
mo = self.sale_order.mrp_production_ids
|
||||
self.assertTrue(mo)
|
||||
self.assertEqual(self.sale_order.picking_ids.move_ids.move_orig_ids, mo.move_finished_ids)
|
||||
mo.action_cancel()
|
||||
self.assertEqual(self.sale_order.picking_ids.state, 'confirmed')
|
||||
self.assertFalse(self.sale_order.picking_ids.move_ids.move_orig_ids)
|
||||
self.sale_order.picking_ids.action_assign()
|
||||
self.assertEqual(self.sale_order.picking_ids.move_ids.quantity, 1.0)
|
||||
|
|
|
|||
|
|
@ -1,33 +1,33 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from unittest import skip
|
||||
|
||||
from odoo.fields import Command
|
||||
from odoo.tests import Form, tagged
|
||||
|
||||
from odoo.addons.sale.tests.common import TestSaleCommon
|
||||
from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
||||
@skip('Temporary to fast merge new valuation')
|
||||
class TestSaleMRPAngloSaxonValuation(TestSaleCommon, ValuationReconciliationTestCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
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,
|
||||
})
|
||||
@classmethod
|
||||
def _create_product(cls, **create_vals):
|
||||
if create_vals.get('is_storable'):
|
||||
create_vals['categ_id'] = cls.stock_account_product_categ.id
|
||||
return super()._create_product(**create_vals)
|
||||
|
||||
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
|
||||
|
|
@ -44,11 +44,12 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
# * 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.component_a = self._create_product(name='Component A', is_storable=True, standard_price=3.00)
|
||||
self.component_b = self._create_product(name='Component B', is_storable=True, standard_price=4.00)
|
||||
self.component_bb = self._create_product(name='Component BB', is_storable=False, standard_price=5.00)
|
||||
self.kit_a = self._create_product(name='Kit A', is_storable=True, standard_price=0.00)
|
||||
self.kit_b = self._create_product(name='Kit B', is_storable=False, standard_price=0.00)
|
||||
pack_2 = self.env['uom.uom'].create({'name': 'Pack of 2', 'relative_factor': 2, 'relative_uom_id': self.kit_a.uom_id.id})
|
||||
|
||||
self.kit_a.write({
|
||||
'property_account_expense_id': self.company_data['default_account_expense'].id,
|
||||
|
|
@ -57,7 +58,6 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
|
||||
# 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'
|
||||
|
|
@ -71,7 +71,6 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
|
||||
# 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'
|
||||
|
|
@ -89,14 +88,14 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
(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,
|
||||
'product_uom_qty': 0.5,
|
||||
'product_uom_id': pack_2.id,
|
||||
'price_unit': 1,
|
||||
'tax_id': False,
|
||||
'tax_ids': False,
|
||||
})],
|
||||
})
|
||||
so.action_confirm()
|
||||
so.picking_ids.move_ids.quantity_done = 1
|
||||
so.picking_ids.move_ids.write({'quantity': 1, 'picked': True})
|
||||
so.picking_ids.button_validate()
|
||||
|
||||
invoice = so.with_context(default_journal_id=self.company_data['default_journal_sale'].id)._create_invoices()
|
||||
|
|
@ -126,7 +125,7 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
# Create Product template with variants
|
||||
self.product_template = self.env['product.template'].create({
|
||||
'name': 'Product Template',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'uom_id': self.uom_unit.id,
|
||||
'invoice_policy': 'delivery',
|
||||
'categ_id': self.stock_account_product_categ.id,
|
||||
|
|
@ -145,7 +144,7 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
def create_simple_bom_for_product(product, name, price):
|
||||
component = self.env['product.product'].create({
|
||||
'name': 'Component ' + name,
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'uom_id': self.uom_unit.id,
|
||||
'categ_id': self.stock_account_product_categ.id,
|
||||
'standard_price': price
|
||||
|
|
@ -179,10 +178,8 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
'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)
|
||||
|
|
@ -191,9 +188,7 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
# 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()
|
||||
pick.button_validate()
|
||||
# Create the invoice
|
||||
so._create_invoices()
|
||||
invoice = so.invoice_ids
|
||||
|
|
@ -223,8 +218,8 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
"""
|
||||
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 = self._create_product(name='Simple Kit', is_storable=True, standard_price=0)
|
||||
component = self._create_product(name='Compo A', is_storable=True, standard_price=0)
|
||||
kit.property_account_expense_id = self.company_data['default_account_expense']
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
|
|
@ -236,7 +231,6 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
|
||||
# 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,
|
||||
|
|
@ -245,7 +239,7 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
'price_unit': p,
|
||||
} for p in [10, 20, 60]])
|
||||
in_moves._action_confirm()
|
||||
in_moves.quantity_done = 1
|
||||
in_moves.write({'quantity': 1, 'picked': True})
|
||||
in_moves._action_done()
|
||||
|
||||
# Sell 3 kits
|
||||
|
|
@ -256,9 +250,8 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
'name': kit.name,
|
||||
'product_id': kit.id,
|
||||
'product_uom_qty': 3.0,
|
||||
'product_uom': kit.uom_id.id,
|
||||
'price_unit': 100,
|
||||
'tax_id': False,
|
||||
'tax_ids': False,
|
||||
})],
|
||||
})
|
||||
so.action_confirm()
|
||||
|
|
@ -268,11 +261,10 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
picking = so.picking_ids
|
||||
while picking:
|
||||
pickings.append(picking)
|
||||
picking.move_ids.quantity_done = 1
|
||||
picking.move_ids.write({'quantity': 1, 'picked': True})
|
||||
action = picking.button_validate()
|
||||
if isinstance(action, dict):
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
wizard.process()
|
||||
Form.from_action(self.env, action).save().process()
|
||||
picking = picking.backorder_ids
|
||||
|
||||
invoice = so._create_invoices()
|
||||
|
|
@ -280,7 +272,6 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
|
||||
# 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,
|
||||
|
|
@ -289,24 +280,23 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
'price_unit': 100,
|
||||
})
|
||||
in_moves._action_confirm()
|
||||
in_moves.quantity_done = 1
|
||||
in_moves.write({'quantity': 1, 'picked': True})
|
||||
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_wizard.product_return_moves.quantity = 1
|
||||
return_picking = return_wizard._create_return()
|
||||
return_picking.move_ids.write({'quantity': 1, 'picked': True})
|
||||
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()
|
||||
action = refund_wizard.refund_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:
|
||||
|
|
@ -328,8 +318,8 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
"""
|
||||
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 = self._create_product(name='Simple Kit', is_storable=True, standard_price=0)
|
||||
component = self._create_product(name='Compo A', is_storable=True, standard_price=0)
|
||||
(kit + component).invoice_policy = 'delivery'
|
||||
kit.property_account_expense_id = self.company_data['default_account_expense']
|
||||
|
||||
|
|
@ -342,7 +332,6 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
|
||||
# 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,
|
||||
|
|
@ -351,7 +340,7 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
'price_unit': p,
|
||||
} for p in [10, 20, 60]])
|
||||
in_moves._action_confirm()
|
||||
in_moves.quantity_done = 1
|
||||
in_moves.write({'quantity': 1, 'picked': True})
|
||||
in_moves._action_done()
|
||||
|
||||
# Sell 3 kits
|
||||
|
|
@ -362,9 +351,8 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
'name': kit.name,
|
||||
'product_id': kit.id,
|
||||
'product_uom_qty': 3.0,
|
||||
'product_uom': kit.uom_id.id,
|
||||
'price_unit': 100,
|
||||
'tax_id': False,
|
||||
'tax_ids': False,
|
||||
})],
|
||||
})
|
||||
so.action_confirm()
|
||||
|
|
@ -374,11 +362,10 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
picking = so.picking_ids
|
||||
while picking:
|
||||
pickings.append(picking)
|
||||
picking.move_ids.quantity_done = 1
|
||||
picking.move_ids.write({'quantity': 1, 'picked': True})
|
||||
action = picking.button_validate()
|
||||
if isinstance(action, dict):
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
wizard.process()
|
||||
Form.from_action(self.env, action).save().process()
|
||||
picking = picking.backorder_ids
|
||||
|
||||
invoice = so._create_invoices()
|
||||
|
|
@ -386,7 +373,6 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
|
||||
# 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,
|
||||
|
|
@ -395,15 +381,15 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
'price_unit': 100,
|
||||
})
|
||||
in_moves._action_confirm()
|
||||
in_moves.quantity_done = 1
|
||||
in_moves.write({'quantity': 1, 'picked': True})
|
||||
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_wizard.product_return_moves.quantity = 1
|
||||
return_picking = return_wizard._create_return()
|
||||
return_picking.move_ids.write({'quantity': 1, 'picked': True})
|
||||
return_picking.button_validate()
|
||||
|
||||
# Create a new invoice for the returned kit
|
||||
|
|
@ -428,9 +414,9 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
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 = self._create_product(name='Compo 01', is_storable=True, standard_price=10)
|
||||
compo02 = self._create_product(name='Compo 02', is_storable=True, standard_price=20)
|
||||
kit = self._create_product(name='Kit', is_storable=True, standard_price=0)
|
||||
|
||||
(compo01 + compo02 + kit).invoice_policy = 'delivery'
|
||||
|
||||
|
|
@ -456,13 +442,12 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
'name': kit.name,
|
||||
'product_id': kit.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'product_uom': kit.uom_id.id,
|
||||
'price_unit': 5,
|
||||
'tax_id': False,
|
||||
'tax_ids': False,
|
||||
})],
|
||||
})
|
||||
so.action_confirm()
|
||||
so.picking_ids.move_ids.quantity_done = 1
|
||||
so.picking_ids.move_ids.write({'quantity': 1, 'picked': True})
|
||||
so.picking_ids.button_validate()
|
||||
|
||||
invoice = so._create_invoices()
|
||||
|
|
@ -479,9 +464,9 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
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 = self._create_product(name='Compo 01', is_storable=True, standard_price=10)
|
||||
compo02 = self._create_product(name='Compo 02', is_storable=True, standard_price=20)
|
||||
kit = self._create_product(name='Kit', is_storable=True, standard_price=0)
|
||||
|
||||
(compo01 + compo02 + kit).invoice_policy = 'delivery'
|
||||
|
||||
|
|
@ -509,13 +494,13 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
'name': kit.name,
|
||||
'product_id': kit.id,
|
||||
'product_uom_qty': 2.0,
|
||||
'product_uom': kit.uom_id.id,
|
||||
'price_unit': 5,
|
||||
'tax_id': False,
|
||||
'tax_ids': False,
|
||||
})],
|
||||
})
|
||||
so.action_confirm()
|
||||
so.picking_ids.move_line_ids.qty_done = 1
|
||||
so.picking_ids.move_line_ids.quantity = 1
|
||||
so.picking_ids.move_ids.picked = True
|
||||
so.picking_ids.button_validate()
|
||||
|
||||
invoice = so._create_invoices()
|
||||
|
|
@ -557,68 +542,63 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
# * 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)
|
||||
component_a = self._create_product(name='Component A', is_storable=True, standard_price=10.00)
|
||||
component_b = self._create_product(name='Component B', is_storable=True, standard_price=6.00)
|
||||
subkit_a = self._create_product(name='Subkit A', is_storable=True, standard_price=0.00)
|
||||
subkit_b = self._create_product(name='Subkit B', is_storable=True, standard_price=0.00)
|
||||
main_kit = self._create_product(name='Main kit', is_storable=True, standard_price=0.00)
|
||||
|
||||
self.main_kit.write({
|
||||
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()
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': main_kit.id,
|
||||
'product_tmpl_id': main_kit.product_tmpl_id.id,
|
||||
'product_qty': 4.0,
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': subkit_a.id, 'product_qty': 1.0}),
|
||||
(0, 0, {'product_id': subkit_b.id, 'product_qty': 1.0}),
|
||||
],
|
||||
})
|
||||
# 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()
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': subkit_a.id,
|
||||
'product_tmpl_id': subkit_a.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': component_a.id, 'product_qty': 2.0}),
|
||||
],
|
||||
})
|
||||
# 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()
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': subkit_b.id,
|
||||
'product_tmpl_id': subkit_b.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': component_b.id, 'product_qty': 2.0}),
|
||||
],
|
||||
})
|
||||
|
||||
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,
|
||||
'name': main_kit.name,
|
||||
'product_id': main_kit.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'product_uom': self.main_kit.uom_id.id,
|
||||
'price_unit': 1,
|
||||
'tax_id': False,
|
||||
'tax_ids': False,
|
||||
})],
|
||||
})
|
||||
so.action_confirm()
|
||||
for move in so.picking_ids.move_ids:
|
||||
move.quantity_done = move.product_qty
|
||||
move.quantity = move.product_uom_qty
|
||||
so.picking_ids.button_validate()
|
||||
|
||||
invoice = so.with_context(default_journal_id=self.company_data['default_journal_sale'].id)._create_invoices()
|
||||
|
|
@ -633,3 +613,59 @@ class TestSaleMRPAngloSaxonValuation(ValuationReconciliationTestCommon):
|
|||
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)
|
||||
|
||||
def test_sell_kit_invoice_before_delivery(self):
|
||||
""" When a kit product is invoiced prior to delivery, we want to make sure to reconcile all
|
||||
the AMLs from its explosion together, else we risk re-reconciliation attempts (which will
|
||||
block certain actions from being performed altogether).
|
||||
"""
|
||||
self.stock_account_product_categ.property_cost_method = 'average'
|
||||
|
||||
compo01 = self._create_product(name="Compo 01", is_storable=True, standard_price=10)
|
||||
compo02 = self._create_product(name="Compo 02", is_storable=True, standard_price=20)
|
||||
kit = self._create_product(name="Kit", is_storable=True, standard_price=30)
|
||||
(compo01 + compo02 + kit).write({'invoice_policy': 'order'})
|
||||
warehouse = self.company_data['default_warehouse']
|
||||
self.env['stock.quant']._update_available_quantity(compo01, warehouse.lot_stock_id, 1.0)
|
||||
self.env['stock.quant']._update_available_quantity(compo02, warehouse.lot_stock_id, 2.0)
|
||||
self.env['mrp.bom'].create({
|
||||
'type': 'phantom',
|
||||
'product_id': kit.id,
|
||||
'product_tmpl_id': kit.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'bom_line_ids': [
|
||||
Command.create({'product_id': compo01.id, 'product_qty': 1}),
|
||||
Command.create({'product_id': compo02.id, 'product_qty': 1}),
|
||||
],
|
||||
})
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'product_id': kit.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': 10,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': compo02.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': 5,
|
||||
}),
|
||||
],
|
||||
})
|
||||
sale_order.action_confirm()
|
||||
invoice = sale_order.with_context(default_journal_id=self.company_data['default_journal_sale'].id)._create_invoices()
|
||||
invoice.action_post()
|
||||
delivery = sale_order.picking_ids
|
||||
# would fail due to attempted re-reconciliation prior to this commit
|
||||
delivery.button_validate()
|
||||
stock_output_amls = self.env['account.move.line'].search([('account_id', '=', self.company_data['default_account_stock_out'].id)], order='id asc')
|
||||
self.assertRecordValues(stock_output_amls,
|
||||
[
|
||||
{'product_id': kit.id, 'reconciled': True, 'debit': 0.0, 'credit': 30.0},
|
||||
{'product_id': compo02.id, 'reconciled': True, 'debit': 0.0, 'credit': 20.0},
|
||||
{'product_id': compo01.id, 'reconciled': True, 'debit': 10.0, 'credit': 0.0},
|
||||
{'product_id': compo02.id, 'reconciled': True, 'debit': 20.0, 'credit': 0.0},
|
||||
{'product_id': compo02.id, 'reconciled': True, 'debit': 20.0, 'credit': 0.0},
|
||||
]
|
||||
)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,16 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests.common import TransactionCase, Form, tagged
|
||||
from unittest import skip
|
||||
|
||||
from odoo.tests import Form, tagged
|
||||
|
||||
from odoo import Command
|
||||
from odoo.addons.base.tests.common import BaseCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSaleMrpKitBom(TransactionCase):
|
||||
class TestSaleMrpKitBom(BaseCommon):
|
||||
|
||||
def _create_product(self, name, product_type, price):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env.ref('base.user_admin').write({
|
||||
'email': 'mitchell.admin@example.com',
|
||||
})
|
||||
cls.env.user.group_ids += cls.quick_ref('product.group_product_variant')
|
||||
|
||||
def _create_product(self, name, storable, price):
|
||||
return self.env['product.product'].create({
|
||||
'name': name,
|
||||
'type': product_type,
|
||||
'is_storable': storable,
|
||||
'standard_price': price,
|
||||
})
|
||||
|
||||
|
|
@ -77,6 +89,7 @@ class TestSaleMrpKitBom(TransactionCase):
|
|||
# The actual test, there should be no traceback here
|
||||
order_line_change.product_id = product_variant_ids[1]
|
||||
|
||||
@skip('Temporary to fast merge new valuation')
|
||||
def test_sale_mrp_kit_cost(self):
|
||||
"""
|
||||
Check the total cost of a KIT:
|
||||
|
|
@ -92,11 +105,11 @@ class TestSaleMrpKitBom(TransactionCase):
|
|||
'name': 'customer'
|
||||
})
|
||||
|
||||
self.kit_product = self._create_product('Kit Product', 'product', 1.00)
|
||||
self.kit_product = self._create_product('Kit Product', True, 1.00)
|
||||
# Creating components
|
||||
self.component_a = self._create_product('Component A', 'product', 1.00)
|
||||
self.component_a = self._create_product('Component A', True, 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 = self._create_product('Component B', True, 1.00)
|
||||
self.component_b.product_tmpl_id.standard_price = 10
|
||||
|
||||
cat = self.env['product.category'].create({
|
||||
|
|
@ -134,25 +147,94 @@ class TestSaleMrpKitBom(TransactionCase):
|
|||
'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(line.move_ids.mapped('description_picking'), ['Kit Product - 1/2', 'Kit Product - 2/2'])
|
||||
self.assertEqual(purchase_price, 92, "The purchase price must be the total cost of the components multiplied by their unit of measure")
|
||||
|
||||
def test_sale_mrp_kit_sale_price(self):
|
||||
"""Check the total sale price of a KIT:
|
||||
# BoM of Kit A:
|
||||
# - BoM Type: Kit
|
||||
# - Quantity: 1
|
||||
# - Components:
|
||||
# * 1 x Component A (Price: $ 8, QTY: 10, UOM: Meter)
|
||||
# * 1 x Component B (Price: $ 5, QTY: 2, UOM: Dozen)
|
||||
# sale price of Kit A = (8 * 10) + (5 * 2 * 12) = $ 200
|
||||
"""
|
||||
if "sale_price" not in self.env["stock.move.line"]._fields:
|
||||
self.skipTest("This test only runs with both sale_mrp and stock_delivery installed")
|
||||
|
||||
self.customer = self.env['res.partner'].create({
|
||||
'name': 'customer',
|
||||
})
|
||||
self.warehouse = self.env["stock.warehouse"].create({
|
||||
'name': 'Warehouse #2',
|
||||
'code': 'WH02',
|
||||
})
|
||||
|
||||
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.uom_id = self.env.ref('uom.product_uom_meter').id
|
||||
self.component_a.product_tmpl_id.list_price = 8
|
||||
self.component_b = self._create_product('Component B', 'product', 1.00)
|
||||
self.component_b.product_tmpl_id.list_price = 5
|
||||
|
||||
location_id = self.warehouse.lot_stock_id.id
|
||||
self.env["stock.quant"].with_context(inventory_mode=True).create([
|
||||
{"product_id": self.component_a.id, "inventory_quantity": 10, "location_id": location_id},
|
||||
{"product_id": self.component_b.id, "inventory_quantity": 24, "location_id": location_id},
|
||||
]).action_apply_inventory()
|
||||
|
||||
self.bom = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': self.kit_product.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [
|
||||
Command.create({
|
||||
'product_id': self.component_a.id,
|
||||
'product_qty': 10.0,
|
||||
'product_uom_id': self.env.ref('uom.product_uom_meter').id,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.component_b.id,
|
||||
'product_qty': 2.0,
|
||||
'product_uom_id': self.env.ref('uom.product_uom_dozen').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_id': self.kit_product.uom_id.id,
|
||||
})],
|
||||
'warehouse_id': self.warehouse.id,
|
||||
})
|
||||
so.action_confirm()
|
||||
so.picking_ids._action_done()
|
||||
move_lines = so.picking_ids.move_ids.move_line_ids
|
||||
self.assertEqual(move_lines.mapped("sale_price"), [80, 120], 'wrong shipping value')
|
||||
|
||||
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.env.ref('uom.decimal_product_uom').digits = 5
|
||||
|
||||
self.kit = self._create_product('Kit', 'product', 0.00)
|
||||
self.comp = self._create_product('Component', 'product', 0.00)
|
||||
self.kit = self._create_product('Kit', True, 0.00)
|
||||
self.comp = self._create_product('Component', True, 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'
|
||||
|
|
@ -173,9 +255,8 @@ class TestSaleMrpKitBom(TransactionCase):
|
|||
'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,
|
||||
'tax_ids': False,
|
||||
})],
|
||||
})
|
||||
so.action_confirm()
|
||||
|
|
@ -184,7 +265,7 @@ class TestSaleMrpKitBom(TransactionCase):
|
|||
self.assertEqual(so.order_line.qty_delivered, 0)
|
||||
|
||||
picking = so.picking_ids
|
||||
picking.move_ids.quantity_done = 0.86000
|
||||
picking.move_ids.write({'quantity': 0.86000, 'picked': True})
|
||||
picking.button_validate()
|
||||
|
||||
# Checks the delivery amount (must be 10).
|
||||
|
|
@ -194,14 +275,13 @@ class TestSaleMrpKitBom(TransactionCase):
|
|||
"""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)
|
||||
self.kitA = self._create_product('Kit A', False, 0.00)
|
||||
self.kitB = self._create_product('Kit B', False, 0.00)
|
||||
self.compA = self._create_product('ComponentA', False, 0.00)
|
||||
self.compB = self._create_product('ComponentB', False, 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'
|
||||
|
|
@ -215,7 +295,6 @@ class TestSaleMrpKitBom(TransactionCase):
|
|||
|
||||
# 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'
|
||||
|
|
@ -238,9 +317,8 @@ class TestSaleMrpKitBom(TransactionCase):
|
|||
'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,
|
||||
'tax_ids': False,
|
||||
})],
|
||||
})
|
||||
so.action_confirm()
|
||||
|
|
@ -249,9 +327,7 @@ class TestSaleMrpKitBom(TransactionCase):
|
|||
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()
|
||||
picking.button_validate()
|
||||
|
||||
# Checks the delivery amount (must be 1).
|
||||
self.assertEqual(so.order_line.qty_delivered, 1)
|
||||
|
|
@ -265,13 +341,12 @@ class TestSaleMrpKitBom(TransactionCase):
|
|||
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)
|
||||
kitA = self._create_product('Kit Product', True, 0.00)
|
||||
compA = self._create_product('ComponentA', True, 0.00)
|
||||
compB = self._create_product('ComponentB', True, 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'
|
||||
|
|
@ -294,20 +369,21 @@ class TestSaleMrpKitBom(TransactionCase):
|
|||
'name': kitA.name,
|
||||
'product_id': kitA.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'product_uom': kitA.uom_id.id,
|
||||
'price_unit': 1,
|
||||
'tax_id': False,
|
||||
'tax_ids': False,
|
||||
})]
|
||||
})
|
||||
so.action_confirm()
|
||||
|
||||
pick = so.picking_ids[0]
|
||||
ship = so.picking_ids[1]
|
||||
self.assertTrue(pick.move_ids[0].bom_line_id, "All component from kits should have a bom line")
|
||||
self.assertTrue(pick.move_ids[1].bom_line_id, "All component from kits should have a bom line")
|
||||
pick.move_ids.write({'quantity': 1, 'picked': True})
|
||||
pick.button_validate()
|
||||
|
||||
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")
|
||||
ship = so.picking_ids[1]
|
||||
self.assertTrue(ship.move_ids[0].bom_line_id, "All component from kits should have a bom line")
|
||||
self.assertTrue(ship.move_ids[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
|
||||
|
|
@ -319,15 +395,14 @@ class TestSaleMrpKitBom(TransactionCase):
|
|||
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)
|
||||
kitAB = self._create_product('Kit AB', True, 0.00)
|
||||
kitABC = self._create_product('Kit ABC', True, 0.00)
|
||||
compA = self._create_product('ComponentA', True, 0.00)
|
||||
compB = self._create_product('ComponentB', True, 0.00)
|
||||
compC = self._create_product('ComponentC', True, 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'
|
||||
|
|
@ -341,7 +416,6 @@ class TestSaleMrpKitBom(TransactionCase):
|
|||
|
||||
# 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'
|
||||
|
|
@ -367,35 +441,30 @@ class TestSaleMrpKitBom(TransactionCase):
|
|||
'name': kitAB.name,
|
||||
'product_id': kitAB.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'product_uom': kitAB.uom_id.id,
|
||||
'price_unit': 1,
|
||||
'tax_id': False,
|
||||
'tax_ids': 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,
|
||||
'tax_ids': False,
|
||||
})],
|
||||
})
|
||||
so.action_confirm()
|
||||
|
||||
pick = so.picking_ids[0]
|
||||
ship = so.picking_ids[1]
|
||||
|
||||
for move in pick.move_ids:
|
||||
move.quantity_done = 1
|
||||
move.write({'quantity': 1, 'picked': True})
|
||||
|
||||
pick.action_put_in_pack()
|
||||
pick.button_validate()
|
||||
|
||||
ship.package_level_ids.write({'is_done': True})
|
||||
ship.package_level_ids._set_is_done()
|
||||
ship = so.picking_ids[1]
|
||||
|
||||
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")
|
||||
self.assertEqual(move_line.move_id.product_uom_qty, move_line.quantity, "Quantity done should be equal to the quantity reserved in the move line")
|
||||
|
||||
def test_kit_in_delivery_slip(self):
|
||||
"""
|
||||
|
|
@ -420,7 +489,7 @@ class TestSaleMrpKitBom(TransactionCase):
|
|||
"""
|
||||
kit_1, component_1, product_1, kit_3, kit_4 = self.env['product.product'].create([{
|
||||
'name': n,
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
} for n in ['Kit 1', 'Compo 1', 'Product 1', 'Kit 3', 'Kit 4']])
|
||||
kit_1.description_sale = "test"
|
||||
|
||||
|
|
@ -510,8 +579,8 @@ class TestSaleMrpKitBom(TransactionCase):
|
|||
})
|
||||
so.action_confirm()
|
||||
picking = so.picking_ids
|
||||
self.assertEqual(len(so.picking_ids.move_ids_without_package), 7)
|
||||
picking.move_ids.quantity_done = 1
|
||||
self.assertEqual(len(so.picking_ids.move_ids), 7)
|
||||
picking.move_ids.write({'quantity': 1, 'picked': True})
|
||||
picking.button_validate()
|
||||
self.assertEqual(picking.state, 'done')
|
||||
|
||||
|
|
@ -527,3 +596,257 @@ class TestSaleMrpKitBom(TransactionCase):
|
|||
if keys[0] in line:
|
||||
keys = keys[1:]
|
||||
self.assertFalse(keys, "All keys should be in the report with the defined order")
|
||||
|
||||
def test_sale_multistep_kit_qty_change(self):
|
||||
warehouse = self.env['stock.warehouse'].search([], limit=1)
|
||||
warehouse.write({'delivery_steps': 'pick_ship'})
|
||||
self.partner = self.env['res.partner'].create({'name': 'Test Partner'})
|
||||
|
||||
kit_prod = self._create_product('kit_prod', 'product', 0.00)
|
||||
sub_kit = self._create_product('sub_kit', 'product', 0.00)
|
||||
component = self._create_product('component', 'product', 0.00)
|
||||
component.uom_id = self.env.ref('uom.product_uom_dozen')
|
||||
self.env['stock.quant']._update_available_quantity(component, warehouse.lot_stock_id, 30)
|
||||
# 6 kit_prod == 5 component
|
||||
self.env['mrp.bom'].create([{ # 2 kit_prod == 5 sub_kit
|
||||
'product_tmpl_id': kit_prod.product_tmpl_id.id,
|
||||
'product_qty': 2.0,
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [(0, 0, {
|
||||
'product_id': sub_kit.id,
|
||||
'product_qty': 5,
|
||||
})],
|
||||
}, { # 3 sub_kit == 1 component
|
||||
'product_tmpl_id': sub_kit.product_tmpl_id.id,
|
||||
'product_qty': 3.0,
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [(0, 0, {
|
||||
'product_id': component.id,
|
||||
'product_qty': 1,
|
||||
})],
|
||||
}])
|
||||
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'order_line': [(0, 0, {
|
||||
'name': kit_prod.name,
|
||||
'product_id': kit_prod.id,
|
||||
'product_uom_qty': 30,
|
||||
})],
|
||||
})
|
||||
# Validate the SO
|
||||
so.action_confirm()
|
||||
picking_pick = so.picking_ids[0]
|
||||
picking_pick.picking_type_id.create_backorder = 'never'
|
||||
|
||||
# Check the component qty in the created picking should be 25
|
||||
self.assertEqual(picking_pick.move_ids.product_qty, 30 * 5 / 6)
|
||||
|
||||
# Update the kit quantity in the SO
|
||||
so.order_line[0].product_uom_qty = 60
|
||||
# Check the component qty after the update should be 50
|
||||
self.assertEqual(picking_pick.move_ids.product_qty, 60 * 5 / 6)
|
||||
|
||||
# Deliver half the quantity 25 component == 30 kit_prod
|
||||
picking_pick.move_ids.quantity = 25
|
||||
picking_pick.button_validate()
|
||||
|
||||
picking_ship = so.picking_ids[1]
|
||||
picking_ship.picking_type_id.create_backorder = 'never'
|
||||
picking_ship.move_ids.quantity = 25
|
||||
picking_ship.button_validate()
|
||||
self.assertEqual(so.order_line.qty_delivered, 25 / 5 * 6)
|
||||
|
||||
# Return 10 components
|
||||
stock_return_picking_form = Form(self.env['stock.return.picking']
|
||||
.with_context(active_ids=picking_ship.ids, active_id=picking_ship.id,
|
||||
active_model='stock.picking'))
|
||||
return_wiz = stock_return_picking_form.save()
|
||||
for return_move in return_wiz.product_return_moves:
|
||||
return_move.write({
|
||||
'quantity': 10,
|
||||
'to_refund': True
|
||||
})
|
||||
res = return_wiz.action_create_returns()
|
||||
return_pick = self.env['stock.picking'].browse(res['res_id'])
|
||||
|
||||
# Process all components and validate the return
|
||||
return_pick.button_validate()
|
||||
self.assertEqual(so.order_line.qty_delivered, 15 / 5 * 6)
|
||||
|
||||
# Resend 5 components
|
||||
stock_return_picking_form = Form(self.env['stock.return.picking']
|
||||
.with_context(active_ids=return_pick.ids, active_id=return_pick.id,
|
||||
active_model='stock.picking'))
|
||||
return_wiz = stock_return_picking_form.save()
|
||||
for return_move in return_wiz.product_return_moves:
|
||||
return_move.write({
|
||||
'quantity': 5,
|
||||
'to_refund': True
|
||||
})
|
||||
res = return_wiz.action_create_returns()
|
||||
|
||||
# Validate the return
|
||||
self.env['stock.picking'].browse(res['res_id']).button_validate()
|
||||
self.assertEqual(so.order_line.qty_delivered, 20 / 5 * 6)
|
||||
|
||||
def test_sale_kit_qty_change(self):
|
||||
|
||||
# Create record rule
|
||||
mrp_bom_model = self.env['ir.model']._get('mrp.bom')
|
||||
self.env['ir.rule'].create({
|
||||
'name': "No one allowed to access BoMs",
|
||||
'model_id': mrp_bom_model.id,
|
||||
'domain_force': [(0, '=', 1)],
|
||||
})
|
||||
|
||||
# Create BoM
|
||||
kit_product = self._create_product('Kit Product', 'product', 1)
|
||||
component_a = self._create_product('Component A', 'product', 1)
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': kit_product.id,
|
||||
'product_tmpl_id': kit_product.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'consumption': 'flexible',
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [(0, 0, {'product_id': component_a.id, 'product_qty': 1})]
|
||||
})
|
||||
|
||||
# Create sale order
|
||||
partner = self.env['res.partner'].create({'name': 'Testing Man'})
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': partner.id,
|
||||
})
|
||||
sol = self.env['sale.order.line'].create({
|
||||
'name': "Order line",
|
||||
'product_id': kit_product.id,
|
||||
'order_id': so.id,
|
||||
})
|
||||
so.action_confirm()
|
||||
|
||||
user_admin = self.env['res.users'].search([('login', '=', 'admin')])
|
||||
sol.with_user(user_admin).write({'product_uom_qty': 5})
|
||||
|
||||
self.assertEqual(sum(sol.move_ids.mapped('product_uom_qty')), 5)
|
||||
|
||||
def test_sale_kit_with_mto_components_qty_change(self):
|
||||
"""
|
||||
Check that updating the demand on a sale order line for a kit product
|
||||
updates the associated deliveries accordingly
|
||||
"""
|
||||
partner = self.env['res.partner'].create({'name': 'Test Partner'})
|
||||
warehouse = self.env.ref('stock.warehouse0')
|
||||
mto_route = self.env.ref('stock.route_warehouse0_mto')
|
||||
mto_route.action_unarchive()
|
||||
manufacturing_route_id = self.ref('mrp.route_warehouse0_manufacture')
|
||||
kit_product, comp, mto_comp, subcomp = self.env['product.product'].create([
|
||||
{
|
||||
'name': 'kit_product',
|
||||
'is_storable': True,
|
||||
'route_ids': [],
|
||||
},
|
||||
{
|
||||
'name': 'component',
|
||||
'is_storable': True,
|
||||
'route_ids': [],
|
||||
},
|
||||
{
|
||||
'name': 'mto_component',
|
||||
'is_storable': True,
|
||||
'route_ids': [Command.set([mto_route.id, manufacturing_route_id])],
|
||||
},
|
||||
{
|
||||
'name': 'subcomponent',
|
||||
'is_storable': True,
|
||||
'route_ids': [],
|
||||
},
|
||||
])
|
||||
self.env['stock.quant']._update_available_quantity(comp, warehouse.lot_stock_id, 30.0)
|
||||
self.env['mrp.bom'].create([
|
||||
{ # 2 kit_prod -> 5 comp and 3 mto_comp
|
||||
'product_tmpl_id': kit_product.product_tmpl_id.id,
|
||||
'product_qty': 2.0,
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [
|
||||
Command.create({'product_id': comp.id, 'product_qty': 5}),
|
||||
Command.create({'product_id': mto_comp.id, 'product_qty': 3}),
|
||||
],
|
||||
},
|
||||
{ # bom to manufacture mto_comp
|
||||
'product_tmpl_id': mto_comp.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'bom_line_ids': [
|
||||
Command.create({'product_id': subcomp.id, 'product_qty': 1}),
|
||||
],
|
||||
}
|
||||
])
|
||||
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': partner.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'name': kit_product.name,
|
||||
'product_id': kit_product.id,
|
||||
'product_uom_qty': 4,
|
||||
})],
|
||||
})
|
||||
# confirm the SO and check the delivery
|
||||
so.action_confirm()
|
||||
self.assertRecordValues(so.picking_ids.move_ids.sorted('product_uom_qty'), [
|
||||
{'product_id': mto_comp.id, 'product_uom_qty': 6.0},
|
||||
{'product_id': comp.id, 'product_uom_qty': 10.0},
|
||||
])
|
||||
with Form(so) as so_form:
|
||||
with so_form.order_line.edit(0) as line_form:
|
||||
line_form.product_uom_qty = 10
|
||||
# the moves assocaited to the mto component are expected to be separated as
|
||||
# the are linked to a different MO
|
||||
self.assertRecordValues(so.picking_ids.move_ids.sorted('product_uom_qty'), [
|
||||
{'product_id': mto_comp.id, 'product_uom_qty': 6.0},
|
||||
{'product_id': mto_comp.id, 'product_uom_qty': 9.0},
|
||||
{'product_id': comp.id, 'product_uom_qty': 25.0},
|
||||
])
|
||||
|
||||
def test_inter_company_qty_delivered_with_kit(self):
|
||||
"""
|
||||
Test that the delivered quantity is updated on a sale order line when selling a kit
|
||||
through an inter-company transaction.
|
||||
"""
|
||||
self.env.user.write({'group_ids': [(4, self.env.ref('base.group_multi_company').id)]})
|
||||
# Create the kit product and BoM
|
||||
kit_product = self._create_product('Kit', 'product', 1)
|
||||
component_product = self._create_product('Component', 'product', 1)
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': kit_product.id,
|
||||
'product_tmpl_id': kit_product.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'consumption': 'flexible',
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [(0, 0, {'product_id': component_product.id, 'product_qty': 1})]
|
||||
})
|
||||
|
||||
# Create the sale order with a partner that uses the inter company location
|
||||
inter_comp_location = self.env.ref('stock.stock_location_inter_company')
|
||||
partner = self.env['res.partner'].create({'name': 'Testing Partner'})
|
||||
partner.property_stock_customer = inter_comp_location
|
||||
partner.property_stock_supplier = inter_comp_location
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': partner.id,
|
||||
'order_line': [
|
||||
(0, 0, {
|
||||
'name': kit_product.name,
|
||||
'product_id': kit_product.id,
|
||||
'product_uom_qty': 1.0,
|
||||
})
|
||||
]
|
||||
})
|
||||
so.action_confirm()
|
||||
|
||||
self.assertTrue(so.picking_ids)
|
||||
self.assertEqual(so.order_line.qty_delivered, 0)
|
||||
|
||||
picking = so.picking_ids
|
||||
picking.move_ids.write({'quantity': 1, 'picked': True})
|
||||
picking.button_validate()
|
||||
|
||||
self.assertEqual(so.order_line.qty_delivered, 1)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from odoo import fields
|
||||
from odoo.addons.stock.tests.common2 import TestStockCommon
|
||||
from odoo.addons.stock.tests.common import TestStockCommon
|
||||
|
||||
from odoo.tests import Form
|
||||
|
||||
|
|
@ -14,13 +14,12 @@ class TestSaleMrpLeadTime(TestStockCommon):
|
|||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env.ref('stock.route_warehouse0_mto').active = True
|
||||
cls.route_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
|
||||
# and it's a compute field based on `type` which is the field visible in the view
|
||||
p1.is_storable = True
|
||||
p1.sale_delay = 5.0
|
||||
p1.route_ids.clear()
|
||||
p1.route_ids.add(cls.warehouse_1.manufacture_pull_id.route_id)
|
||||
|
|
@ -29,17 +28,33 @@ class TestSaleMrpLeadTime(TestStockCommon):
|
|||
# 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'
|
||||
# and it's a compute field based on `type` which is the field visible in the view
|
||||
p2.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
|
||||
bom.produce_delay = 5.0
|
||||
with bom.bom_line_ids.new() as line:
|
||||
line.product_id = cls.product_2
|
||||
line.product_qty = 4
|
||||
|
||||
cls.warehouse_3_steps_pull = cls.env['stock.warehouse'].create({
|
||||
'name': 'Warehouse 3 steps',
|
||||
'code': '3S',
|
||||
'delivery_steps': 'pick_pack_ship',
|
||||
})
|
||||
delivery_route_3 = cls.warehouse_3_steps_pull.delivery_route_id
|
||||
delivery_route_3.rule_ids[0].write({
|
||||
'location_dest_id': delivery_route_3.rule_ids[1].location_src_id.id,
|
||||
})
|
||||
delivery_route_3.rule_ids[1].write({'action': 'pull'})
|
||||
delivery_route_3.rule_ids[2].write({'action': 'pull'})
|
||||
cls.warehouse_3_steps_pull.mto_pull_id.write({
|
||||
'location_dest_id': delivery_route_3.rule_ids[1].location_src_id.id,
|
||||
})
|
||||
|
||||
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
|
||||
|
|
@ -48,8 +63,7 @@ class TestSaleMrpLeadTime(TestStockCommon):
|
|||
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})
|
||||
company.security_lead = 3
|
||||
|
||||
# Create sale order of product_1
|
||||
order_form = Form(self.env['sale.order'])
|
||||
|
|
@ -80,9 +94,9 @@ class TestSaleMrpLeadTime(TestStockCommon):
|
|||
)
|
||||
|
||||
# Check schedule date and deadline of manufacturing order
|
||||
mo_scheduled = out_date - timedelta(days=self.product_1.produce_delay) - timedelta(days=company.manufacturing_lead)
|
||||
mo_date_start = out_date - timedelta(days=manufacturing_order.bom_id.produce_delay)
|
||||
self.assertAlmostEqual(
|
||||
fields.Datetime.from_string(manufacturing_order.date_planned_start), mo_scheduled,
|
||||
fields.Datetime.from_string(manufacturing_order.date_start), mo_date_start,
|
||||
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."
|
||||
)
|
||||
|
|
@ -92,21 +106,20 @@ class TestSaleMrpLeadTime(TestStockCommon):
|
|||
msg="Deadline date of manufacturing order should be equal to the deadline of sale picking"
|
||||
)
|
||||
|
||||
def test_01_product_route_level_delays(self):
|
||||
def test_01_product_route_mrp_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'})
|
||||
warehouse = self.warehouse_3_steps_pull
|
||||
|
||||
# Set delay on pull rule
|
||||
for pull_rule in self.warehouse_1.delivery_route_id.rule_ids:
|
||||
for pull_rule in warehouse.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
|
||||
order_form.warehouse_id = warehouse
|
||||
with order_form.order_line.new() as line:
|
||||
line.product_id = self.product_1
|
||||
line.product_uom_qty = 6
|
||||
|
|
@ -115,17 +128,17 @@ class TestSaleMrpLeadTime(TestStockCommon):
|
|||
order.action_confirm()
|
||||
|
||||
# Run scheduler
|
||||
self.env['procurement.group'].run_scheduler()
|
||||
self.env['stock.rule'].run_scheduler()
|
||||
|
||||
# Check manufacturing order created or not
|
||||
manufacturing_order = self.env['mrp.production'].search([('product_id', '=', self.product_1.id)])
|
||||
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 = order.picking_ids.filtered(lambda r: r.picking_type_id == warehouse.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(
|
||||
|
|
@ -135,7 +148,7 @@ class TestSaleMrpLeadTime(TestStockCommon):
|
|||
)
|
||||
|
||||
# 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 = order.picking_ids.filtered(lambda r: r.picking_type_id == warehouse.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(
|
||||
|
|
@ -145,7 +158,7 @@ class TestSaleMrpLeadTime(TestStockCommon):
|
|||
)
|
||||
|
||||
# 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 = order.picking_ids.filtered(lambda r: r.picking_type_id == warehouse.pick_type_id)
|
||||
pick_min_date = fields.Datetime.from_string(pick.scheduled_date)
|
||||
self.assertAlmostEqual(
|
||||
pick_min_date, pack_date,
|
||||
|
|
@ -154,9 +167,9 @@ class TestSaleMrpLeadTime(TestStockCommon):
|
|||
)
|
||||
|
||||
# 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)
|
||||
mo_date_start = out_date - timedelta(days=manufacturing_order.bom_id.produce_delay) - timedelta(days=warehouse.delivery_route_id.rule_ids[0].delay)
|
||||
self.assertAlmostEqual(
|
||||
fields.Datetime.from_string(manufacturing_order.date_planned_start), mo_scheduled,
|
||||
fields.Datetime.from_string(manufacturing_order.date_start), mo_date_start,
|
||||
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."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import time
|
||||
|
||||
from odoo.tests.common import TransactionCase, Form
|
||||
from odoo.tests import Form, TransactionCase
|
||||
from odoo.tools import mute_logger
|
||||
from odoo import Command
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ class TestSaleMrpProcurement(TransactionCase):
|
|||
|
||||
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.user.group_ids += 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'
|
||||
|
|
@ -40,9 +40,8 @@ class TestSaleMrpProcurement(TransactionCase):
|
|||
product.categ_id = product_category_allproductssellable0
|
||||
product.list_price = 200.0
|
||||
product.name = 'Slider Mobile'
|
||||
product.detailed_type = 'product'
|
||||
product.is_storable = True
|
||||
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)
|
||||
|
|
@ -86,9 +85,9 @@ class TestSaleMrpProcurement(TransactionCase):
|
|||
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')
|
||||
self.env.user.group_ids += 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.user.group_ids += 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')
|
||||
|
|
@ -102,17 +101,15 @@ class TestSaleMrpProcurement(TransactionCase):
|
|||
# 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.is_storable = True
|
||||
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.is_storable = True
|
||||
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)
|
||||
|
|
@ -121,7 +118,7 @@ class TestSaleMrpProcurement(TransactionCase):
|
|||
# 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.is_storable = True
|
||||
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)
|
||||
|
|
@ -130,14 +127,12 @@ class TestSaleMrpProcurement(TransactionCase):
|
|||
## 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.is_storable = True
|
||||
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'
|
||||
|
|
@ -149,7 +144,6 @@ class TestSaleMrpProcurement(TransactionCase):
|
|||
|
||||
## 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
|
||||
|
|
@ -178,7 +172,9 @@ class TestSaleMrpProcurement(TransactionCase):
|
|||
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")
|
||||
self.assertEqual(sale_order_so0.mrp_production_count, 2, "2 Mos for the 2 sale order line")
|
||||
self.assertEqual(sale_order_so0.mrp_production_ids[0].product_qty, 1)
|
||||
self.assertEqual(sale_order_so0.mrp_production_ids[1].product_qty, 2)
|
||||
|
||||
pickings = sale_order_so0.picking_ids
|
||||
|
||||
|
|
@ -202,7 +198,7 @@ class TestSaleMrpProcurement(TransactionCase):
|
|||
|
||||
product, component = self.env['product.product'].create([{
|
||||
'name': 'Finished',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'route_ids': [(6, 0, manufacture_route.ids)],
|
||||
}, {
|
||||
'name': 'Component',
|
||||
|
|
@ -236,7 +232,6 @@ class TestSaleMrpProcurement(TransactionCase):
|
|||
'name': product.name,
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'product_uom': product.uom_id.id,
|
||||
'price_unit': 1,
|
||||
})],
|
||||
})
|
||||
|
|
@ -249,7 +244,7 @@ class TestSaleMrpProcurement(TransactionCase):
|
|||
def test_so_reordering_rule(self):
|
||||
kit_1, component_1 = self.env['product.product'].create([{
|
||||
'name': n,
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
} for n in ['Kit 1', 'Compo 1']])
|
||||
|
||||
self.env['mrp.bom'].create([{
|
||||
|
|
@ -278,6 +273,68 @@ class TestSaleMrpProcurement(TransactionCase):
|
|||
[('product_id', '=', kit_1.id)])
|
||||
self.assertFalse(orderpoint_product)
|
||||
|
||||
def test_so_reordering_rule_02(self):
|
||||
"""
|
||||
Have a manufactured product in kg unit of measure with the manufacturing route,
|
||||
the mto route and a BoM in grams.
|
||||
Confirm a SO with that product in 510 grams -> It should generate a MO with 510g.
|
||||
Create a second SO with 510g -> It should update the MO to 1020g.
|
||||
"""
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
|
||||
manufacture_route = warehouse.manufacture_pull_id.route_id
|
||||
mto_route = warehouse.mto_pull_id.route_id
|
||||
mto_route.active = True
|
||||
|
||||
uom_kg = self.env.ref('uom.product_uom_kgm')
|
||||
uom_gram = self.env.ref('uom.product_uom_gram')
|
||||
|
||||
product, component = self.env['product.product'].create([{
|
||||
'name': 'Finished',
|
||||
'is_storable': True,
|
||||
'uom_id': uom_kg.id,
|
||||
'route_ids': [
|
||||
Command.link(manufacture_route.id),
|
||||
Command.link(mto_route.id),
|
||||
],
|
||||
}, {
|
||||
'name': 'Component',
|
||||
'type': 'consu',
|
||||
}])
|
||||
|
||||
self.env['mrp.bom'].create({
|
||||
'product_id': product.id,
|
||||
'product_tmpl_id': product.product_tmpl_id.id,
|
||||
'product_uom_id': uom_gram.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': component.id, 'product_qty': 1}),
|
||||
],
|
||||
})
|
||||
|
||||
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': 510,
|
||||
'product_uom_id': uom_gram.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)
|
||||
self.assertEqual(mo.product_uom_id, uom_gram)
|
||||
self.assertEqual(mo.product_qty, 510)
|
||||
|
||||
so.order_line.product_uom_qty = 510 * 2
|
||||
self.assertEqual(mo.product_uom_id, uom_gram)
|
||||
self.assertEqual(mo.product_qty, 1020)
|
||||
|
||||
def test_sale_mrp_avoid_multiple_pickings(self):
|
||||
"""
|
||||
Test sale of multiple products. Avoid multiple pickings being
|
||||
|
|
@ -294,13 +351,11 @@ class TestSaleMrpProcurement(TransactionCase):
|
|||
'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,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
# -*- 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.fields import Command
|
||||
from odoo.tests import tagged
|
||||
from odoo.tools import html2plaintext
|
||||
|
||||
from odoo.addons.sale.tests.common import TestSaleCommon
|
||||
|
||||
@common.tagged('post_install', '-at_install')
|
||||
class TestSaleMrpInvoices(AccountTestInvoicingCommon):
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSaleMrpInvoices(TestSaleCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.product_by_lot = cls.env['product.product'].create({
|
||||
'name': 'Product By Lot',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'tracking': 'lot',
|
||||
})
|
||||
cls.warehouse = cls.env['stock.warehouse'].search([('company_id', '=', cls.env.company.id)], limit=1)
|
||||
|
|
@ -24,7 +24,6 @@ class TestSaleMrpInvoices(AccountTestInvoicingCommon):
|
|||
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)
|
||||
|
||||
|
|
@ -40,7 +39,6 @@ class TestSaleMrpInvoices(AccountTestInvoicingCommon):
|
|||
'product_qty': 1,
|
||||
})]
|
||||
})
|
||||
cls.partner = cls.env['res.partner'].create({'name': 'Test Partner'})
|
||||
|
||||
def test_deliver_and_invoice_tracked_components(self):
|
||||
"""
|
||||
|
|
@ -50,7 +48,7 @@ class TestSaleMrpInvoices(AccountTestInvoicingCommon):
|
|||
"""
|
||||
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)]})
|
||||
self.env.user.write({'group_ids': [(4, display_lots.id), (4, display_uom.id)]})
|
||||
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
|
|
@ -60,16 +58,14 @@ class TestSaleMrpInvoices(AccountTestInvoicingCommon):
|
|||
})
|
||||
so.action_confirm()
|
||||
|
||||
action = so.picking_ids.button_validate()
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
wizard.process()
|
||||
so.picking_ids.button_validate()
|
||||
|
||||
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)
|
||||
text = html2plaintext(html.decode())
|
||||
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):
|
||||
|
|
@ -81,9 +77,19 @@ class TestSaleMrpInvoices(AccountTestInvoicingCommon):
|
|||
manufacturing_route = self.env.ref('mrp.route_warehouse0_manufacture')
|
||||
product = self.env['product.product'].create({
|
||||
'name': 'SuperProduct',
|
||||
'type': 'product',
|
||||
'is_storable': True,
|
||||
'route_ids': [Command.set((mto_route + manufacturing_route).ids)]
|
||||
})
|
||||
|
||||
product.bom_ids = [Command.create({
|
||||
'product_id': product.id,
|
||||
'product_tmpl_id': product.product_tmpl_id.id,
|
||||
'product_uom_id': product.uom_id.id,
|
||||
'bom_line_ids': [Command.create({
|
||||
'product_id': self.product_by_lot.id,
|
||||
'product_qty': 1,
|
||||
})]
|
||||
})]
|
||||
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)
|
||||
|
|
@ -94,7 +100,6 @@ class TestSaleMrpInvoices(AccountTestInvoicingCommon):
|
|||
'name': product.name,
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 8.0,
|
||||
'product_uom': product.uom_id.id,
|
||||
'price_unit': product.list_price,
|
||||
})]
|
||||
},
|
||||
|
|
@ -104,27 +109,26 @@ class TestSaleMrpInvoices(AccountTestInvoicingCommon):
|
|||
'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']
|
||||
report_lines = self.env['stock.forecasted_product_product'].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))
|
||||
so_1_line = report_lines[0]
|
||||
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_1_line['quantity'], so_1_line['move_out']['id'], so_1_line['replenishment_filled']],
|
||||
[8.0, so_1.picking_ids.move_ids.id, True]
|
||||
)
|
||||
so_2_line = next(filter(lambda line: line.get('document_out') == so_2, report_lines))
|
||||
so_2_line = report_lines[1]
|
||||
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]
|
||||
[so_2_line['quantity'], so_2_line['move_out']['id'], so_2_line['replenishment_filled']],
|
||||
[7.0, so_2.picking_ids.move_ids.id, True]
|
||||
)
|
||||
quant_line = next(filter(lambda line: not line.get('document_out'), report_lines))
|
||||
replenisment_line = report_lines[2]
|
||||
self.assertEqual(
|
||||
[quant_line['document_out'], quant_line['quantity'], quant_line['replenishment_filled']],
|
||||
[False, 2.0, True]
|
||||
[replenisment_line['document_in'], replenisment_line['document_out'], replenisment_line['quantity'], replenisment_line['move_out'], replenisment_line['replenishment_filled']],
|
||||
[False, False, 10.0, None, True]
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue