Initial commit: Mrp packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:50 +02:00
commit 50d736b3bd
739 changed files with 538193 additions and 0 deletions

View file

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_purchase_subcontracting
from . import test_sale_dropshipping
from . import test_anglo_saxon_valuation

View file

@ -0,0 +1,290 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
from odoo import Command
from odoo.tests import tagged, Form
@tagged('post_install', '-at_install')
class TestSubcontractingDropshippingValuation(ValuationReconciliationTestCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
categ_form = Form(cls.env['product.category'])
categ_form.name = 'fifo auto'
categ_form.parent_id = cls.env.ref('product.product_category_all')
categ_form.property_cost_method = 'fifo'
categ_form.property_valuation = 'real_time'
cls.categ_fifo_auto = categ_form.save()
categ_form = Form(cls.env['product.category'])
categ_form.name = 'avco auto'
categ_form.parent_id = cls.env.ref('product.product_category_all')
categ_form.property_cost_method = 'average'
categ_form.property_valuation = 'real_time'
cls.categ_avco_auto = categ_form.save()
cls.dropship_route = cls.env.ref('stock_dropshipping.route_drop_shipping')
cls.dropship_subcontractor_route = cls.env.ref('mrp_subcontracting_dropshipping.route_subcontracting_dropshipping')
(cls.product_a | cls.product_b).type = 'product'
cls.bom_a = cls.env['mrp.bom'].create({
'product_tmpl_id': cls.product_a.product_tmpl_id.id,
'type': 'subcontract',
'subcontractor_ids': [(6, 0, cls.partner_a.ids)],
'bom_line_ids': [
(0, 0, {'product_id': cls.product_b.id, 'product_qty': 1.0}),
],
})
def test_valuation_subcontracted_and_dropshipped(self):
"""
Product:
- FIFO + Auto
- Subcontracted
Purchase 2 from Subcontractor to a customer (dropship).
Then return 1 to subcontractor and one to stock
It should generate the correct valuations AMLs
"""
# pylint: disable=bad-whitespace
all_amls_ids = self.env['account.move.line'].search_read([], ['id'])
grp_multi_loc = self.env.ref('stock.group_stock_multi_locations')
self.env.user.write({'groups_id': [(4, grp_multi_loc.id)]})
(self.product_a | self.product_b).categ_id = self.categ_fifo_auto
self.product_b.standard_price = 10
dropship_picking_type = self.env['stock.picking.type'].search([
('company_id', '=', self.env.company.id),
('default_location_src_id.usage', '=', 'supplier'),
('default_location_dest_id.usage', '=', 'customer'),
], limit=1, order='sequence')
po = self.env['purchase.order'].create({
"partner_id": self.partner_a.id,
"picking_type_id": dropship_picking_type.id,
"dest_address_id": self.partner_b.id,
"order_line": [(0, 0, {
'product_id': self.product_a.id,
'name': self.product_a.name,
'product_qty': 2.0,
'price_unit': 100,
'taxes_id': False,
})],
})
po.button_confirm()
delivery = po.picking_ids
res = delivery.button_validate()
Form(self.env['stock.immediate.transfer'].with_context(res['context'])).save().process()
stock_in_acc_id = self.categ_fifo_auto.property_stock_account_input_categ_id.id
stock_out_acc_id = self.categ_fifo_auto.property_stock_account_output_categ_id.id
stock_valu_acc_id = self.categ_fifo_auto.property_stock_valuation_account_id.id
amls = self.env['account.move.line'].search([('id', 'not in', all_amls_ids)])
all_amls_ids += amls.ids
self.assertRecordValues(amls, [
# Compensation of dropshipping value
{'account_id': stock_valu_acc_id, 'product_id': self.product_a.id, 'debit': 0.0, 'credit': 20.0},
{'account_id': stock_out_acc_id, 'product_id': self.product_a.id, 'debit': 20.0, 'credit': 0.0},
# Receipt from subcontractor
{'account_id': stock_in_acc_id, 'product_id': self.product_a.id, 'debit': 0.0, 'credit': 220.0},
{'account_id': stock_valu_acc_id, 'product_id': self.product_a.id, 'debit': 220.0, 'credit': 0.0},
# Delivery to subcontractor
{'account_id': stock_valu_acc_id, 'product_id': self.product_b.id, 'debit': 0.0, 'credit': 20.0},
{'account_id': stock_out_acc_id, 'product_id': self.product_b.id, 'debit': 20.0, 'credit': 0.0},
# Initial dropshipped value
{'account_id': stock_valu_acc_id, 'product_id': self.product_a.id, 'debit': 0.0, 'credit': 200.0},
{'account_id': stock_out_acc_id, 'product_id': self.product_a.id, 'debit': 200.0, 'credit': 0.0},
])
# return to subcontracting location
sbc_location = self.env.company.subcontracting_location_id
return_form = Form(self.env['stock.return.picking'].with_context(active_id=delivery.id, active_model='stock.picking'))
return_form.location_id = sbc_location
with return_form.product_return_moves.edit(0) as line:
line.quantity = 1
return_wizard = return_form.save()
return_id, _ = return_wizard._create_returns()
return_picking = self.env['stock.picking'].browse(return_id)
return_picking.move_ids.quantity_done = 1
return_picking.button_validate()
amls = self.env['account.move.line'].search([('id', 'not in', all_amls_ids)])
all_amls_ids += amls.ids
self.assertRecordValues(amls, [
{'account_id': stock_valu_acc_id, 'product_id': self.product_a.id, 'debit': 0.0, 'credit': 110.0},
{'account_id': stock_in_acc_id, 'product_id': self.product_a.id, 'debit': 110.0, 'credit': 0.0},
])
# return to stock location
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
stock_location = warehouse.lot_stock_id
stock_location.return_location = True
return_form = Form(self.env['stock.return.picking'].with_context(active_id=delivery.id, active_model='stock.picking'))
return_form.location_id = stock_location
with return_form.product_return_moves.edit(0) as line:
line.quantity = 1
return_wizard = return_form.save()
return_id, _ = return_wizard._create_returns()
return_picking = self.env['stock.picking'].browse(return_id)
return_picking.move_ids.quantity_done = 1
return_picking.button_validate()
amls = self.env['account.move.line'].search([('id', 'not in', all_amls_ids)])
all_amls_ids += amls.ids
self.assertRecordValues(amls, [
{'account_id': stock_out_acc_id, 'product_id': self.product_a.id, 'debit': 0.0, 'credit': 110.0},
{'account_id': stock_valu_acc_id, 'product_id': self.product_a.id, 'debit': 110.0, 'credit': 0.0},
])
def test_avco_valuation_subcontract_and_dropshipped_and_backorder(self):
""" Splitting a dropship transfer via backorder and invoicing for delivered quantities
should result in SVL records which have accurate values based on the portion of the total
order-picking sequence for which they were generated.
"""
final_product = self.product_a
final_product.write({
'categ_id': self.categ_avco_auto.id,
'invoice_policy': 'delivery',
})
comp_product = self.product_b
comp_product.write({
'categ_id': self.categ_avco_auto.id,
'route_ids': [(4, self.dropship_subcontractor_route.id)],
})
self.env['product.supplierinfo'].create({
'product_tmpl_id': final_product.product_tmpl_id.id,
'partner_id': self.partner_a.id,
'price': 10,
})
self.env['product.supplierinfo'].create({
'product_tmpl_id': comp_product.product_tmpl_id.id,
'partner_id': self.partner_a.id,
'price': 1,
})
sale_order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [(0, 0, {
'product_id': final_product.id,
'route_id': self.dropship_route.id,
'product_uom_qty': 100,
})],
})
sale_order.action_confirm()
purchase_order = sale_order._get_purchase_orders()[0]
purchase_order.button_confirm()
dropship_transfer = purchase_order.picking_ids[0]
dropship_transfer.move_ids[0].quantity_done = 50
dropship_transfer.with_context(cancel_backorder=False)._action_done()
account_move_1 = sale_order._create_invoices()
account_move_1.action_post()
dropship_backorder = dropship_transfer.backorder_ids[0]
dropship_backorder.action_set_quantities_to_reservation()
dropship_backorder._action_done()
account_move_2 = sale_order._create_invoices()
account_move_2.action_post()
self.assertRecordValues(
self.env['stock.valuation.layer'].search([('product_id', '=', final_product.id)]),
[
# DS/01
{'reference': dropship_transfer.name, 'quantity': -50, 'value': -500},
{'reference': dropship_transfer.move_ids.move_orig_ids[0].name, 'quantity': 50, 'value': 8500},
{'reference': dropship_transfer.name, 'quantity': 0, 'value': -8000},
# DS/02 - backorder
{'reference': dropship_backorder.name, 'quantity': -50, 'value': -500},
{'reference': dropship_backorder.move_ids.move_orig_ids[1].name, 'quantity': 50, 'value': 8500},
{'reference': dropship_backorder.name, 'quantity': 0, 'value': -8000},
]
)
def test_account_line_entry_kit_bom_dropship(self):
""" An order delivered via dropship for some kit bom product variant should result in
accurate journal entries in the expense and stock output accounts if the cost on the
purchase order line has been manually edited.
"""
kit_final_prod = self.product_a
product_c = self.env['product.product'].create({
'name': 'product_c',
'uom_id': self.env.ref('uom.product_uom_dozen').id,
'uom_po_id': self.env.ref('uom.product_uom_dozen').id,
'lst_price': 120.0,
'standard_price': 100.0,
'property_account_income_id': self.copy_account(self.company_data['default_account_revenue']).id,
'property_account_expense_id': self.copy_account(self.company_data['default_account_expense']).id,
'taxes_id': [Command.set((self.tax_sale_a + self.tax_sale_b).ids)],
'supplier_taxes_id': [Command.set((self.tax_purchase_a + self.tax_purchase_b).ids)],
})
kit_bom = self.env['mrp.bom'].create({
'product_tmpl_id': kit_final_prod.product_tmpl_id.id,
'product_uom_id': kit_final_prod.uom_id.id,
'product_qty': 1.0,
'type': 'phantom',
})
kit_bom.bom_line_ids = [(0, 0, {
'product_id': self.product_b.id,
'product_qty': 4,
}), (0, 0, {
'product_id': product_c.id,
'product_qty': 2,
})]
self.env['product.supplierinfo'].create({
'product_id': self.product_b.id,
'partner_id': self.partner_a.id,
'price': 160,
})
self.env['product.supplierinfo'].create({
'product_id': product_c.id,
'partner_id': self.partner_a.id,
'price': 100,
})
(kit_final_prod + self.product_b).categ_id.write({
'property_cost_method': 'fifo',
'property_valuation': 'real_time',
})
sale_order = self.env['sale.order'].create({
'partner_id': self.partner_b.id,
'order_line': [(0, 0, {
'price_unit': 900,
'product_id': kit_final_prod.id,
'route_id': self.dropship_route.id,
'product_uom_qty': 2.0,
})],
})
sale_order.action_confirm()
purchase_order = sale_order._get_purchase_orders()[0]
purchase_order.button_confirm()
dropship_transfer = purchase_order.picking_ids[0]
dropship_transfer.move_ids[0].quantity_done = 2.0
dropship_transfer.button_validate()
account_move = sale_order._create_invoices()
account_move.action_post()
# Each product_a should cost:
# 4x product_b = 160 * 4 = 640 +
# 2x product_c = 100 * 2 = 200
# = 840
self.assertRecordValues(
account_move.line_ids,
[
{'name': 'product_a', 'debit': 0.0, 'credit': 1800.0},
{'name': 'Tax 15% (Copy)', 'debit': 0.0, 'credit': 270.0},
{'name': account_move.name, 'debit': 621.0, 'credit': 0.0},
{'name': account_move.name, 'debit': 1449.0, 'credit': 0.0},
{'name': 'product_a', 'debit': 0.0, 'credit': 840 * 2},
{'name': 'product_a', 'debit': 840 * 2, 'credit': 0.0},
]
)

View file

@ -0,0 +1,495 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.tests import Form
from odoo.addons.mrp_subcontracting.tests.common import TestMrpSubcontractingCommon
class TestSubcontractingDropshippingFlows(TestMrpSubcontractingCommon):
def test_mrp_subcontracting_dropshipping_1(self):
""" Mark the subcontracted product with the route dropship and add the
subcontractor as seller. The component has the routes 'MTO', 'Replenish
on order' and 'Buy'. Also another partner is set as vendor on the comp.
Create a SO and check that:
- Delivery between subcontractor and customer for subcontracted product.
- Delivery for the component to the subcontractor for the specified wh.
- Po created for the component.
"""
self.env.ref('stock.route_warehouse0_mto').active = True
mto_route = self.env['stock.route'].search([('name', '=', 'Replenish on Order (MTO)')])
resupply_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
buy_route = self.env['stock.route'].search([('name', '=', 'Buy')])
dropship_route = self.env['stock.route'].search([('name', '=', 'Dropship')])
self.comp2.write({'route_ids': [(4, buy_route.id), (4, mto_route.id), (4, resupply_route.id)]})
self.finished.write({'route_ids': [(4, dropship_route.id)]})
warehouse = self.env['stock.warehouse'].create({
'name': 'Warehouse For subcontract',
'code': 'WFS'
})
self.env['product.supplierinfo'].create({
'product_tmpl_id': self.finished.product_tmpl_id.id,
'partner_id': self.subcontractor_partner1.id
})
partner = self.env['res.partner'].create({
'name': 'Toto'
})
self.env['product.supplierinfo'].create({
'product_tmpl_id': self.comp2.product_tmpl_id.id,
'partner_id': partner.id
})
# Create a receipt picking from the subcontractor
so_form = Form(self.env['sale.order'])
so_form.partner_id = partner
so_form.warehouse_id = warehouse
with so_form.order_line.new() as line:
line.product_id = self.finished
line.product_uom_qty = 1
so = so_form.save()
so.action_confirm()
# Pickings should directly be created
po = self.env['purchase.order'].search([('origin', 'ilike', so.name)])
self.assertTrue(po)
po.button_approve()
picking_finished = po.picking_ids
self.assertEqual(len(picking_finished), 1.0)
self.assertEqual(picking_finished.location_dest_id, partner.property_stock_customer)
self.assertEqual(picking_finished.location_id, self.subcontractor_partner1.property_stock_supplier)
self.assertEqual(picking_finished.state, 'assigned')
picking_delivery = self.env['stock.move'].search([
('product_id', '=', self.comp2.id),
('location_id', '=', warehouse.lot_stock_id.id),
('location_dest_id', '=', self.subcontractor_partner1.property_stock_subcontractor.id),
]).picking_id
self.assertTrue(picking_delivery)
self.assertEqual(picking_delivery.state, 'waiting')
po = self.env['purchase.order.line'].search([
('product_id', '=', self.comp2.id),
('partner_id', '=', partner.id),
]).order_id
self.assertTrue(po)
def test_mrp_subcontracting_purchase_2(self):
"""Let's consider a subcontracted BOM with 1 component. Tick "Resupply Subcontractor on Order" on the component and set a supplier on it.
Purchase 1 BOM to the subcontractor. Confirm the purchase and change the purchased quantity to 2.
Check that 2 components are delivered to the subcontractor
"""
# Tick "resupply subconractor on order on component"
self.bom.bom_line_ids = [(5, 0, 0)]
self.bom.bom_line_ids = [(0, 0, {'product_id': self.comp1.id, 'product_qty': 1})]
resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
(self.comp1).write({'route_ids': [(4, resupply_sub_on_order_route.id, None)]})
# Create a supplier and set it to component
vendor = self.env['res.partner'].create({'name': 'AAA', 'email': 'from.test@example.com'})
self.env['product.supplierinfo'].create({
'partner_id': vendor.id,
'price': 50,
})
self.comp1.write({'seller_ids': [(0, 0, {'partner_id': vendor.id, 'product_code': 'COMP1'})]})
# Purchase 1 BOM to the subcontractor
po = Form(self.env['purchase.order'])
po.partner_id = self.subcontractor_partner1
with po.order_line.new() as po_line:
po_line.product_id = self.finished
po_line.product_qty = 1
po_line.price_unit = 100
po = po.save()
# Confirm the purchase
po.button_confirm()
# Check one delivery order with the component has been created for the subcontractor
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
self.assertEqual(mo.state, 'confirmed')
# Check that 1 delivery with 1 component for the subcontractor has been created
picking_delivery = mo.picking_ids
wh = picking_delivery.picking_type_id.warehouse_id
origin = picking_delivery.origin
self.assertEqual(len(picking_delivery), 1)
self.assertEqual(len(picking_delivery.move_ids_without_package), 1)
self.assertEqual(picking_delivery.picking_type_id, wh.subcontracting_resupply_type_id)
self.assertEqual(picking_delivery.partner_id, self.subcontractor_partner1)
# Change the purchased quantity to 2
po.order_line.write({'product_qty': 2})
# Check that a single delivery with the two components for the subcontractor have been created
picking_deliveries = self.env['stock.picking'].search([('origin', '=', origin)])
self.assertEqual(len(picking_deliveries), 1)
self.assertEqual(picking_deliveries.picking_type_id, wh.subcontracting_resupply_type_id)
self.assertEqual(picking_deliveries.partner_id, self.subcontractor_partner1)
self.assertTrue(picking_deliveries.state != 'cancel')
move1 = picking_deliveries.move_ids_without_package
self.assertEqual(move1.product_id, self.comp1)
self.assertEqual(move1.product_uom_qty, 2)
def test_dropshipped_component_and_sub_location(self):
"""
Suppose:
- a subcontracted product and a component dropshipped to the subcontractor
- the location of the subcontractor is a sub-location of the main subcontrating location
This test ensures that the PO that brings the component to the subcontractor has a correct
destination address
"""
subcontract_location = self.env.company.subcontracting_location_id
sub_location = self.env['stock.location'].create({
'name': 'Super Location',
'location_id': subcontract_location.id,
'is_subcontracting_location': True,
})
dropship_subcontractor_route = self.env['stock.route'].search([('name', '=', 'Dropship Subcontractor on Order')])
subcontractor, vendor = self.env['res.partner'].create([
{'name': 'SuperSubcontractor', 'property_stock_subcontractor': sub_location.id},
{'name': 'SuperVendor'},
])
p_finished, p_compo = self.env['product.product'].create([{
'name': 'Finished Product',
'type': 'product',
'seller_ids': [(0, 0, {'partner_id': subcontractor.id})],
}, {
'name': 'Component',
'type': 'consu',
'seller_ids': [(0, 0, {'partner_id': vendor.id})],
'route_ids': [(6, 0, dropship_subcontractor_route.ids)]
}])
self.env['mrp.bom'].create({
'product_tmpl_id': p_finished.product_tmpl_id.id,
'product_qty': 1,
'type': 'subcontract',
'subcontractor_ids': [(6, 0, subcontractor.ids)],
'bom_line_ids': [
(0, 0, {'product_id': p_compo.id, 'product_qty': 1}),
],
})
subcontract_po = self.env['purchase.order'].create({
"partner_id": subcontractor.id,
"order_line": [(0, 0, {
'product_id': p_finished.id,
'name': p_finished.name,
'product_qty': 1.0,
})],
})
subcontract_po.button_confirm()
dropship_po = self.env['purchase.order'].search([('partner_id', '=', vendor.id)])
self.assertEqual(dropship_po.dest_address_id, subcontractor)
def test_po_to_customer(self):
"""
Create and confirm a PO with a subcontracted move. The picking type of
the PO is 'Dropship' and the delivery address a customer. Then, process
a return with the stock location as destination and another return with
the supplier as destination
"""
grp_multi_loc = self.env.ref('stock.group_stock_multi_locations')
self.env.user.write({'groups_id': [(4, grp_multi_loc.id)]})
subcontractor, client = self.env['res.partner'].create([
{'name': 'SuperSubcontractor'},
{'name': 'SuperClient'},
])
p_finished, p_compo = self.env['product.product'].create([{
'name': 'Finished Product',
'type': 'product',
'seller_ids': [(0, 0, {'partner_id': subcontractor.id})],
}, {
'name': 'Component',
'type': 'consu',
}])
bom = self.env['mrp.bom'].create({
'product_tmpl_id': p_finished.product_tmpl_id.id,
'product_qty': 1,
'type': 'subcontract',
'subcontractor_ids': [(6, 0, subcontractor.ids)],
'bom_line_ids': [
(0, 0, {'product_id': p_compo.id, 'product_qty': 1}),
],
})
dropship_picking_type = self.env['stock.picking.type'].search([
('company_id', '=', self.env.company.id),
('default_location_src_id.usage', '=', 'supplier'),
('default_location_dest_id.usage', '=', 'customer'),
], limit=1, order='sequence')
po = self.env['purchase.order'].create({
"partner_id": subcontractor.id,
"picking_type_id": dropship_picking_type.id,
"dest_address_id": client.id,
"order_line": [(0, 0, {
'product_id': p_finished.id,
'name': p_finished.name,
'product_qty': 2.0,
})],
})
po.button_confirm()
mo = self.env['mrp.production'].search([('bom_id', '=', bom.id)])
self.assertEqual(mo.picking_type_id, self.warehouse.subcontracting_type_id)
delivery = po.picking_ids
delivery.move_line_ids.qty_done = 2.0
delivery.button_validate()
self.assertEqual(delivery.state, 'done')
self.assertEqual(mo.state, 'done')
self.assertEqual(po.order_line.qty_received, 2)
# return 1 x P_finished to the stock location
stock_location = self.warehouse.lot_stock_id
stock_location.return_location = True
return_form = Form(self.env['stock.return.picking'].with_context(active_ids=delivery.ids, active_id=delivery.id, active_model='stock.picking'))
with return_form.product_return_moves.edit(0) as line:
line.quantity = 1.0
return_form.location_id = stock_location
return_wizard = return_form.save()
return_picking_id, _pick_type_id = return_wizard._create_returns()
delivery_return01 = self.env['stock.picking'].browse(return_picking_id)
delivery_return01.move_line_ids.qty_done = 1.0
delivery_return01.button_validate()
self.assertEqual(delivery_return01.state, 'done')
self.assertEqual(p_finished.qty_available, 1, 'One product has been returned to the stock location, so it should be available')
self.assertEqual(po.order_line.qty_received, 2, 'One product has been returned to the stock location, so we should still consider it as received')
# return 1 x P_finished to the supplier location
supplier_location = dropship_picking_type.default_location_src_id
return_form = Form(self.env['stock.return.picking'].with_context(active_ids=delivery.ids, active_id=delivery.id, active_model='stock.picking'))
with return_form.product_return_moves.edit(0) as line:
line.quantity = 1.0
return_form.location_id = supplier_location
return_wizard = return_form.save()
return_picking_id, _pick_type_id = return_wizard._create_returns()
delivery_return02 = self.env['stock.picking'].browse(return_picking_id)
delivery_return02.move_line_ids.qty_done = 1.0
delivery_return02.button_validate()
self.assertEqual(delivery_return02.state, 'done')
self.assertEqual(po.order_line.qty_received, 1)
def test_po_to_subcontractor(self):
"""
Create and confirm a PO with a subcontracted move. The bought product is
also a component of another subcontracted product. The picking type of
the PO is 'Dropship' and the delivery address is the other subcontractor
"""
subcontractor, super_subcontractor = self.env['res.partner'].create([
{'name': 'Subcontractor'},
{'name': 'SuperSubcontractor'},
])
super_product, product, component = self.env['product.product'].create([{
'name': 'Super Product',
'type': 'product',
'seller_ids': [(0, 0, {'partner_id': super_subcontractor.id})],
}, {
'name': 'Product',
'type': 'product',
'seller_ids': [(0, 0, {'partner_id': subcontractor.id})],
}, {
'name': 'Component',
'type': 'consu',
}])
_, bom_product = self.env['mrp.bom'].create([{
'product_tmpl_id': super_product.product_tmpl_id.id,
'product_qty': 1,
'type': 'subcontract',
'subcontractor_ids': [(6, 0, super_subcontractor.ids)],
'bom_line_ids': [
(0, 0, {'product_id': product.id, 'product_qty': 1}),
],
}, {
'product_tmpl_id': product.product_tmpl_id.id,
'product_qty': 1,
'type': 'subcontract',
'subcontractor_ids': [(6, 0, subcontractor.ids)],
'bom_line_ids': [
(0, 0, {'product_id': component.id, 'product_qty': 1}),
],
}])
po = self.env['purchase.order'].create({
"partner_id": subcontractor.id,
"picking_type_id": self.env.company.dropship_subcontractor_pick_type_id.id,
"dest_address_id": super_subcontractor.id,
"order_line": [(0, 0, {
'product_id': product.id,
'name': product.name,
'product_qty': 1.0,
})],
})
po.button_confirm()
mo = self.env['mrp.production'].search([('bom_id', '=', bom_product.id)])
self.assertEqual(mo.picking_type_id, self.warehouse.subcontracting_type_id)
delivery = po.picking_ids
self.assertEqual(delivery.location_dest_id, super_subcontractor.property_stock_subcontractor)
self.assertTrue(delivery.is_dropship)
delivery.move_line_ids.qty_done = 1.0
delivery.button_validate()
self.assertEqual(po.order_line.qty_received, 1.0)
def test_two_boms_same_component_supplier(self):
"""
The "Dropship Subcontractor" route is modified: the propagation of the
buy rule is set to "Leave Empty".
Two subcontracted products (different subcontractor) that use the same
component. The component has its own supplier. Confirm one PO for each
subcontrated product. It should generate two PO from component's
supplier to each subcontractor.
"""
dropship_subcontractor_route = self.env.ref('mrp_subcontracting_dropshipping.route_subcontracting_dropshipping')
dropship_subcontractor_route.rule_ids.filtered(lambda r: r.action == 'buy').group_propagation_option = 'none'
subcontractor01, subcontractor02, component_supplier = self.env['res.partner'].create([{
'name': 'Super Partner %d' % i
} for i in range(3)])
product01, product02, component = self.env['product.product'].create([{
'name': name,
'type': 'product',
'seller_ids': [(0, 0, {'partner_id': vendor.id})],
'route_ids': [(6, 0, routes)],
} for name, vendor, routes in [
('SuperProduct 01', subcontractor01, []),
('SuperProduct 02', subcontractor02, []),
('Component', component_supplier, dropship_subcontractor_route.ids),
]])
self.env['mrp.bom'].create([{
'product_tmpl_id': finished.product_tmpl_id.id,
'type': 'subcontract',
'subcontractor_ids': [(4, subcontractor.id)],
'bom_line_ids': [(0, 0, {'product_id': component.id})]
} for finished, subcontractor in [
(product01, subcontractor01),
(product02, subcontractor02),
]])
for (partner, product) in [(subcontractor01, product01), (subcontractor02, product02)]:
po_form = Form(self.env['purchase.order'])
po_form.partner_id = partner
with po_form.order_line.new() as line:
line.product_id = product
po = po_form.save()
po.button_confirm()
supplier_orders = self.env['purchase.order'].search([('partner_id', '=', component_supplier.id)])
self.assertEqual(supplier_orders.dest_address_id, subcontractor01 | subcontractor02)
self.assertRecordValues(supplier_orders.order_line, [
{'product_id': component.id, 'product_qty': 1.0},
{'product_id': component.id, 'product_qty': 1.0},
])
def test_subcontracted_bom_routes(self):
"""
Take two BoM having those components. One being subcontracted and the other not.
- Compo RR : Buy & Reordering rule to resupply subcontractor.
- Compo DROP : Buy & Dropship subcontractor on order.
Check that depending on the context, the right route is shown on the report.
"""
route_buy = self.env.ref('purchase_stock.route_warehouse0_buy')
route_dropship = self.env['stock.route'].search([('name', '=', 'Dropship Subcontractor on Order')], limit=1)
warehouse = self.env['stock.warehouse'].search([], limit=1)
compo_drop, compo_rr = self.env['product.product'].create([{
'name': name,
'type': 'product',
'seller_ids': [Command.create({'partner_id': self.subcontractor_partner1.parent_id.id})],
'route_ids': [Command.set(routes)],
} for name, routes in [
('Compo DROP', [route_buy.id, route_dropship.id]),
('Compo RR', [route_buy.id]),
]])
route_resupply = self.env['stock.route'].search([('name', '=like', '%Resupply Subcontractor'), ('warehouse_ids', '=', warehouse.id)])
route_resupply.product_selectable = True
self.env['stock.warehouse.orderpoint'].create({
'name': 'Resupply Subcontractor',
'location_id': self.subcontractor_partner1.property_stock_subcontractor.id,
'route_id': route_resupply.id,
'product_id': compo_rr.id,
'product_min_qty': 0,
'product_max_qty': 0,
})
compo_rr.route_ids = [Command.link(route_resupply.id)]
bom_subcontract, bom_local = self.env['mrp.bom'].create([{
'product_tmpl_id': self.comp1.product_tmpl_id.id,
'type': bom_type,
'subcontractor_ids': partner_id,
'bom_line_ids': [
Command.create({'product_id': compo_drop.id, 'product_qty': 1}),
Command.create({'product_id': compo_rr.id, 'product_qty': 1}),
]
} for bom_type, partner_id in [
('subcontract', [Command.link(self.subcontractor_partner1.id)]),
('normal', False),
]])
# Need to add the subcontractor as Vendor to have the bom read as subcontracted.
self.comp1.write({'seller_ids': [Command.create({'partner_id': self.subcontractor_partner1.id})]})
report = self.env['report.mrp.report_bom_structure'].with_context(warehouse=warehouse.id)._get_report_data(bom_subcontract.id)
component_lines = report.get('lines', []).get('components', [])
self.assertEqual(component_lines[0]['product_id'], compo_drop.id)
self.assertEqual(component_lines[0]['route_name'], 'Dropship Subcontractor on Order')
self.assertEqual(component_lines[1]['product_id'], compo_rr.id)
self.assertEqual(component_lines[1]['route_name'], 'Buy', 'Despite the RR linked to it, it should still display the Buy route')
report = self.env['report.mrp.report_bom_structure'].with_context(warehouse=warehouse.id)._get_report_data(bom_local.id)
component_lines = report.get('lines', []).get('components', [])
self.assertEqual(component_lines[0]['product_id'], compo_drop.id)
self.assertEqual(component_lines[0]['route_name'], 'Buy', 'Outside of the subcontracted context, it should try to resupply stock.')
self.assertEqual(component_lines[1]['product_id'], compo_rr.id)
self.assertEqual(component_lines[1]['route_name'], 'Buy')
def test_partner_id_no_overwrite(self):
subcontract_location = self.env.company.subcontracting_location_id
p1, p2 = self.env['res.partner'].create([
{'name': 'partner 1', 'property_stock_subcontractor': subcontract_location.id},
{'name': 'partner 2', 'property_stock_subcontractor': subcontract_location.id},
])
route_resupply = self.env['stock.route'].create({
'name': 'Resupply Subcontractor',
'rule_ids': [(0, False, {
'name': 'Stock -> Subcontractor',
'location_src_id': self.env.ref('stock.stock_location_stock').id,
'location_dest_id': subcontract_location.id,
'company_id': self.env.company.id,
'action': 'pull',
'auto': 'manual',
'picking_type_id': self.env.ref('stock.picking_type_out').id,
'partner_address_id': p1.id,
})],
})
self.env['stock.warehouse.orderpoint'].create({
'name': 'Resupply Subcontractor',
'location_id': subcontract_location.id,
'route_id': route_resupply.id,
'product_id': self.comp1.id,
'product_min_qty': 2,
'product_max_qty': 2,
})
self.env['procurement.group'].run_scheduler()
delivery = self.env["stock.move"].search([("product_id", "=", self.comp1.id)]).picking_id
self.assertEqual(delivery.partner_id, p1)

View file

@ -0,0 +1,293 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import Form
from odoo.addons.mrp_subcontracting.tests.common import TestMrpSubcontractingCommon
class TestSaleDropshippingFlows(TestMrpSubcontractingCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.supplier = cls.env["res.partner"].create({"name": "Supplier"})
cls.customer = cls.env["res.partner"].create({"name": "Customer"})
cls.dropship_route = cls.env.ref('stock_dropshipping.route_drop_shipping')
def test_dropship_with_different_suppliers(self):
"""
Suppose a kit with 3 components supplied by 3 vendors
When dropshipping this kit, if 2 components are delivered and if the last
picking is cancelled, we should consider the kit as fully delivered.
"""
partners = self.env['res.partner'].create([{'name': 'Vendor %s' % i} for i in range(4)])
compo01, compo02, compo03, kit = self.env['product.product'].create([{
'name': name,
'type': 'consu',
'route_ids': [(6, 0, [self.dropship_route.id])],
'seller_ids': [(0, 0, {'partner_id': seller.id})],
} for name, seller in zip(['Compo01', 'Compo02', 'Compo03', 'Kit'], partners)])
self.env['mrp.bom'].create({
'product_tmpl_id': kit.product_tmpl_id.id,
'product_qty': 1,
'type': 'phantom',
'bom_line_ids': [
(0, 0, {'product_id': compo01.id, 'product_qty': 1}),
(0, 0, {'product_id': compo02.id, 'product_qty': 1}),
(0, 0, {'product_id': compo03.id, 'product_qty': 1}),
],
})
sale_order = self.env['sale.order'].create({
'partner_id': self.customer.id,
'picking_policy': 'direct',
'order_line': [
(0, 0, {'name': kit.name, 'product_id': kit.id, 'product_uom_qty': 1}),
],
})
sale_order.action_confirm()
self.assertEqual(sale_order.order_line.qty_delivered, 0)
purchase_orders = self.env['purchase.order'].search([('partner_id', 'in', partners.ids)])
purchase_orders.button_confirm()
self.assertEqual(sale_order.order_line.qty_delivered, 0)
# Deliver the first one
picking = sale_order.picking_ids.filtered(lambda p: p.partner_id == partners[0])
action = picking.button_validate()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
self.assertEqual(sale_order.order_line.qty_delivered, 0)
# Deliver the third one
picking = sale_order.picking_ids.filtered(lambda p: p.partner_id == partners[2])
action = picking.button_validate()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
self.assertEqual(sale_order.order_line.qty_delivered, 0)
# Cancel the second one
sale_order.picking_ids[1].action_cancel()
self.assertEqual(sale_order.order_line.qty_delivered, 1)
def test_return_kit_and_delivered_qty(self):
"""
Sell a kit thanks to the dropshipping route, return it then deliver it again
The delivered quantity should be correctly computed
"""
compo, kit = self.env['product.product'].create([{
'name': n,
'type': 'consu',
'route_ids': [(6, 0, [self.dropship_route.id])],
'seller_ids': [(0, 0, {'partner_id': self.supplier.id})],
} for n in ['Compo', 'Kit']])
self.env['mrp.bom'].create({
'product_tmpl_id': kit.product_tmpl_id.id,
'product_qty': 1,
'type': 'phantom',
'bom_line_ids': [
(0, 0, {'product_id': compo.id, 'product_qty': 1}),
],
})
sale_order = self.env['sale.order'].create({
'partner_id': self.customer.id,
'picking_policy': 'direct',
'order_line': [
(0, 0, {'name': kit.name, 'product_id': kit.id, 'product_uom_qty': 1}),
],
})
sale_order.action_confirm()
self.env['purchase.order'].search([], order='id desc', limit=1).button_confirm()
self.assertEqual(sale_order.order_line.qty_delivered, 0.0)
picking = sale_order.picking_ids
action = picking.button_validate()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
self.assertEqual(sale_order.order_line.qty_delivered, 1.0)
for case in ['return', 'deliver again']:
delivered_before_case = 1.0 if case == 'return' else 0.0
delivered_after_case = 0.0 if case == 'return' else 1.0
return_form = Form(self.env['stock.return.picking'].with_context(active_ids=[picking.id], active_id=picking.id, active_model='stock.picking'))
return_wizard = return_form.save()
action = return_wizard.create_returns()
picking = self.env['stock.picking'].browse(action['res_id'])
self.assertEqual(sale_order.order_line.qty_delivered, delivered_before_case, "Incorrect delivered qty for case '%s'" % case)
action = picking.button_validate()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
self.assertEqual(sale_order.order_line.qty_delivered, delivered_after_case, "Incorrect delivered qty for case '%s'" % case)
def test_partial_return_kit_and_delivered_qty(self):
"""
Suppose a kit with 4x the same dropshipped component
Suppose a complex delivery process:
- Deliver 2 (with backorder)
- Return 2
- Deliver 1 (with backorder)
- Deliver 1 (process "done")
- Deliver 1 (from the return)
- Deliver 1 (from the return)
The test checks the all-or-nothing policy of the delivered quantity
This quantity should be 1.0 after the last delivery
"""
compo, kit = self.env['product.product'].create([{
'name': n,
'type': 'consu',
'route_ids': [(6, 0, [self.dropship_route.id])],
'seller_ids': [(0, 0, {'partner_id': self.supplier.id})],
} for n in ['Compo', 'Kit']])
self.env['mrp.bom'].create({
'product_tmpl_id': kit.product_tmpl_id.id,
'product_qty': 1,
'type': 'phantom',
'bom_line_ids': [
(0, 0, {'product_id': compo.id, 'product_qty': 4}),
],
})
sale_order = self.env['sale.order'].create({
'partner_id': self.customer.id,
'picking_policy': 'direct',
'order_line': [
(0, 0, {'name': kit.name, 'product_id': kit.id, 'product_uom_qty': 1}),
],
})
sale_order.action_confirm()
self.env['purchase.order'].search([], order='id desc', limit=1).button_confirm()
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 0/4")
picking01 = sale_order.picking_ids
picking01.move_ids.quantity_done = 2
action = picking01.button_validate()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 2/4")
# Create a return of picking01 (with both components)
return_form = Form(self.env['stock.return.picking'].with_context(active_id=picking01.id, active_model='stock.picking'))
wizard = return_form.save()
wizard.product_return_moves.write({'quantity': 2.0})
res = wizard.create_returns()
return01 = self.env['stock.picking'].browse(res['res_id'])
return01.move_ids.quantity_done = 2
return01.button_validate()
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 0/4")
picking02 = picking01.backorder_ids
picking02.move_ids.quantity_done = 1
action = picking02.button_validate()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 1/4")
picking03 = picking02.backorder_ids
picking03.move_ids.quantity_done = 1
picking03.button_validate()
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 2/4")
# Create a return of return01 (with 1 component)
return_form = Form(self.env['stock.return.picking'].with_context(active_id=return01.id, active_model='stock.picking'))
wizard = return_form.save()
wizard.product_return_moves.write({'quantity': 1.0})
res = wizard.create_returns()
picking04 = self.env['stock.picking'].browse(res['res_id'])
picking04.move_ids.quantity_done = 1
picking04.button_validate()
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 3/4")
# Create a second return of return01 (with 1 component, the last one)
return_form = Form(self.env['stock.return.picking'].with_context(active_id=return01.id, active_model='stock.picking'))
wizard = return_form.save()
wizard.product_return_moves.write({'quantity': 1.0})
res = wizard.create_returns()
picking04 = self.env['stock.picking'].browse(res['res_id'])
picking04.move_ids.quantity_done = 1
picking04.button_validate()
self.assertEqual(sale_order.order_line.qty_delivered, 1, "Delivered components: 4/4")
def test_cancelled_picking_and_delivered_qty(self):
"""
The delivered quantity should be zero if all SM are cancelled
"""
compo, kit = self.env['product.product'].create([{
'name': n,
'type': 'consu',
'route_ids': [(6, 0, [self.dropship_route.id])],
'seller_ids': [(0, 0, {'partner_id': self.supplier.id})],
} for n in ['Compo', 'Kit']])
self.env['mrp.bom'].create({
'product_tmpl_id': kit.product_tmpl_id.id,
'product_qty': 1,
'type': 'phantom',
'bom_line_ids': [
(0, 0, {'product_id': compo.id, 'product_qty': 1}),
],
})
sale_order = self.env['sale.order'].create({
'partner_id': self.customer.id,
'picking_policy': 'direct',
'order_line': [
(0, 0, {'name': kit.name, 'product_id': kit.id, 'product_uom_qty': 1}),
],
})
sale_order.action_confirm()
self.env['purchase.order'].search([], order='id desc', limit=1).button_confirm()
self.assertEqual(sale_order.order_line.qty_delivered, 0.0)
sale_order.picking_ids.action_cancel()
self.assertEqual(sale_order.order_line.qty_delivered, 0.0)
def test_sale_kit_with_dropshipped_component(self):
"""
The test checks the delivered quantity of a kit when one of the
components is dropshipped
"""
compo01, compo02, kit = self.env['product.product'].create([{
'name': n,
'type': 'consu',
} for n in ['compo01', 'compo02', 'super kit']])
compo02.write({
'route_ids': [(6, 0, [self.dropship_route.id])],
'seller_ids': [(0, 0, {'partner_id': self.supplier.id})],
})
self.env['mrp.bom'].create({
'product_tmpl_id': kit.product_tmpl_id.id,
'product_qty': 1,
'type': 'phantom',
'bom_line_ids': [
(0, 0, {'product_id': compo01.id, 'product_qty': 1}),
(0, 0, {'product_id': compo02.id, 'product_qty': 1}),
],
})
sale_order = self.env['sale.order'].create({
'partner_id': self.customer.id,
'picking_policy': 'direct',
'order_line': [
(0, 0, {'name': kit.name, 'product_id': kit.id, 'product_uom_qty': 1}),
],
})
sale_order.action_confirm()
self.env['purchase.order'].search([], order='id desc', limit=1).button_confirm()
sale_order.picking_ids.move_ids.quantity_done = 1
sale_order.picking_ids[0].button_validate()
sale_order.picking_ids[1].button_validate()
self.assertEqual(sale_order.order_line.qty_delivered, 1.0)