mirror of
https://github.com/OCA/manufacture.git
synced 2025-01-28 16:37:15 +02:00
[MIG] mrp_subcontracting_purchase: Adapt to 14.0
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
This bridge module adds some smart buttons between Purchase and Subcontracting
|
||||||
|
|
||||||
|
**DISCLAIMER:** This module is a backport from Odoo SA and as such, it is not included in the OCA CLA.
|
||||||
|
|
||||||
|
That means we do not have a copy of the copyright on it like all other OCA modules.
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|||||||
@@ -1,19 +1,28 @@
|
|||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "Purchase and Subcontracting Management",
|
"name": "Purchase and Subcontracting Management",
|
||||||
"version": "14.0.1.0.0",
|
|
||||||
"category": "Manufacturing/Purchase",
|
|
||||||
"summary": """
|
"summary": """
|
||||||
This bridge module adds some smart buttons between Purchase and Subcontracting
|
This bridge module adds some smart buttons between Purchase and Subcontracting
|
||||||
""",
|
""",
|
||||||
"author": "Odoo S.A., Odoo Community Association (OCA)",
|
|
||||||
"website": "https://github.com/OCA/manufacture",
|
"website": "https://github.com/OCA/manufacture",
|
||||||
"depends": ["mrp_subcontracting", "purchase"],
|
"version": "14.0.1.0.0",
|
||||||
|
"author": "Odoo S.A., Ooops, Cetmix, Odoo Community Association (OCA)",
|
||||||
|
"maintainers": ["dessanhemrayev", "CetmixGitDrone", "Volodiay622", "geomer198"],
|
||||||
|
"category": "Manufacturing/Purchase",
|
||||||
|
"depends": ["mrp_subcontracting", "purchase_mrp", "stock_dropshipping"],
|
||||||
"data": [
|
"data": [
|
||||||
|
"data/mrp_subcontracting_dropshipping_data.xml",
|
||||||
"views/purchase_order_views.xml",
|
"views/purchase_order_views.xml",
|
||||||
"views/stock_picking_views.xml",
|
"views/stock_picking_views.xml",
|
||||||
],
|
],
|
||||||
|
"demo": [
|
||||||
|
"demo/product_category_demo.xml",
|
||||||
|
"demo/stock_location_demo.xml",
|
||||||
|
"demo/partner_subcontract_demo.xml",
|
||||||
|
"demo/product_product_demo.xml",
|
||||||
|
"demo/product_supplierinfo_demo.xml",
|
||||||
|
"demo/bom_subcontract_demo.xml",
|
||||||
|
"demo/stock_rules_demo.xml",
|
||||||
|
],
|
||||||
"installable": True,
|
"installable": True,
|
||||||
"auto_install": True,
|
"auto_install": True,
|
||||||
"license": "LGPL-3",
|
"license": "LGPL-3",
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" ?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="route_subcontracting_dropshipping" model='stock.location.route'>
|
||||||
|
<field name="name">Dropship Subcontractor on Order</field>
|
||||||
|
<field name="sequence">5</field>
|
||||||
|
<field name="company_id" />
|
||||||
|
<field name="product_selectable" eval="True" />
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<function
|
||||||
|
model="res.company"
|
||||||
|
name="create_missing_subcontracting_dropshipping_rules"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<function model="stock.warehouse" name="write">
|
||||||
|
<value
|
||||||
|
model="stock.warehouse"
|
||||||
|
eval="obj().env['stock.warehouse'].search([]).ids"
|
||||||
|
/>
|
||||||
|
<value eval="{'subcontracting_dropshipping_to_resupply': True}" />
|
||||||
|
</function>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
58
mrp_subcontracting_purchase/demo/bom_subcontract_demo.xml
Normal file
58
mrp_subcontracting_purchase/demo/bom_subcontract_demo.xml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<odoo noupdate="1">
|
||||||
|
<record id="mrp_bom_subcontract_1" model="mrp.bom">
|
||||||
|
<field
|
||||||
|
name="product_tmpl_id"
|
||||||
|
ref="demo_product_product_finished_product_template"
|
||||||
|
/>
|
||||||
|
<field name="product_uom_id" ref="uom.product_uom_unit" />
|
||||||
|
<field name="type">subcontract</field>
|
||||||
|
<field
|
||||||
|
name="subcontractor_ids"
|
||||||
|
eval="[(6, 0,[ref('res_partner_subcontracting_1')] )]"
|
||||||
|
/>
|
||||||
|
<field name="sequence">1</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="mrp_bom_subcontract_2" model="mrp.bom">
|
||||||
|
<field
|
||||||
|
name="product_tmpl_id"
|
||||||
|
ref="demo_product_product_component_product_template"
|
||||||
|
/>
|
||||||
|
<field name="product_uom_id" ref="uom.product_uom_unit" />
|
||||||
|
<field name="type">subcontract</field>
|
||||||
|
<field
|
||||||
|
name="subcontractor_ids"
|
||||||
|
eval="[(6, 0,[ref('res_partner_subcontracting_2')] )]"
|
||||||
|
/>
|
||||||
|
<field name="sequence">1</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="mrp_bom_subcontract_1_manufacture_line_1" model="mrp.bom.line">
|
||||||
|
<field
|
||||||
|
name="product_id"
|
||||||
|
ref="mrp_subcontracting_purchase.demo_product_product_component"
|
||||||
|
/>
|
||||||
|
<field name="product_qty">1</field>
|
||||||
|
<field name="product_uom_id" ref="uom.product_uom_unit" />
|
||||||
|
<field name="sequence">5</field>
|
||||||
|
<field
|
||||||
|
name="bom_id"
|
||||||
|
ref="mrp_subcontracting_purchase.mrp_bom_subcontract_1"
|
||||||
|
/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="mrp_bom_subcontract_2_manufacture_line_1" model="mrp.bom.line">
|
||||||
|
<field
|
||||||
|
name="product_id"
|
||||||
|
ref="mrp_subcontracting_purchase.demo_product_product_element"
|
||||||
|
/>
|
||||||
|
<field name="product_qty">1</field>
|
||||||
|
<field name="product_uom_id" ref="uom.product_uom_unit" />
|
||||||
|
<field name="sequence">5</field>
|
||||||
|
<field
|
||||||
|
name="bom_id"
|
||||||
|
ref="mrp_subcontracting_purchase.mrp_bom_subcontract_2"
|
||||||
|
/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<odoo>
|
||||||
|
<record id="res_partner_subcontracting_1" model="res.partner">
|
||||||
|
<field name="name">Sub 1</field>
|
||||||
|
<field name="property_stock_subcontractor" ref="demo_stock_location_sub_1" />
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="res_partner_subcontracting_2" model="res.partner">
|
||||||
|
<field name="name">Sub 2</field>
|
||||||
|
<field name="property_stock_subcontractor" ref="demo_stock_location_sub_2" />
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="res_partner_subcontracting_vendor" model="res.partner">
|
||||||
|
<field name="name">Vendor</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<odoo>
|
||||||
|
<record id="product_category_subcontracting_product" model="product.category">
|
||||||
|
<field name="name">Subcontracting products</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
40
mrp_subcontracting_purchase/demo/product_product_demo.xml
Normal file
40
mrp_subcontracting_purchase/demo/product_product_demo.xml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<odoo>
|
||||||
|
<record id="demo_product_product_finished" model="product.product">
|
||||||
|
<field name="name">Finished</field>
|
||||||
|
<field name="type">product</field>
|
||||||
|
<field name="list_price">10</field>
|
||||||
|
<field name="taxes_id" eval="False" />
|
||||||
|
<field name="sale_ok" eval="False" />
|
||||||
|
<field name="categ_id" ref="product_category_subcontracting_product" />
|
||||||
|
<field
|
||||||
|
name="route_ids"
|
||||||
|
eval="[(6, 0,[ref('purchase_stock.route_warehouse0_buy')])]"
|
||||||
|
/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_product_product_component" model="product.product">
|
||||||
|
<field name="name">Component</field>
|
||||||
|
<field name="type">product</field>
|
||||||
|
<field name="list_price">10</field>
|
||||||
|
<field name="taxes_id" eval="False" />
|
||||||
|
<field name="sale_ok" eval="False" />
|
||||||
|
<field name="categ_id" ref="product_category_subcontracting_product" />
|
||||||
|
<field
|
||||||
|
name="route_ids"
|
||||||
|
eval="[(6, 0,[ref('route_subcontracting_dropshipping')])]"
|
||||||
|
/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_product_product_element" model="product.product">
|
||||||
|
<field name="name">Element</field>
|
||||||
|
<field name="type">product</field>
|
||||||
|
<field name="list_price">10</field>
|
||||||
|
<field name="taxes_id" eval="False" />
|
||||||
|
<field name="sale_ok" eval="False" />
|
||||||
|
<field name="categ_id" ref="product_category_subcontracting_product" />
|
||||||
|
<field
|
||||||
|
name="route_ids"
|
||||||
|
eval="[(6, 0,[ref('route_subcontracting_dropshipping')])]"
|
||||||
|
/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8' ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="product_supplierinfo_subcontract_1" model="product.supplierinfo">
|
||||||
|
<field
|
||||||
|
name="product_tmpl_id"
|
||||||
|
ref="demo_product_product_finished_product_template"
|
||||||
|
/>
|
||||||
|
<field name="price">10</field>
|
||||||
|
<field name="name" ref="res_partner_subcontracting_1" />
|
||||||
|
<field name="currency_id" ref="base.USD" />
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="product_supplierinfo_subcontract_2" model="product.supplierinfo">
|
||||||
|
<field
|
||||||
|
name="product_tmpl_id"
|
||||||
|
ref="demo_product_product_component_product_template"
|
||||||
|
/>
|
||||||
|
<field name="price">10</field>
|
||||||
|
<field name="name" ref="res_partner_subcontracting_2" />
|
||||||
|
<field name="currency_id" ref="base.USD" />
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="product_supplierinfo_subcontract_3" model="product.supplierinfo">
|
||||||
|
<field
|
||||||
|
name="product_tmpl_id"
|
||||||
|
ref="demo_product_product_element_product_template"
|
||||||
|
/>
|
||||||
|
<field name="price">10</field>
|
||||||
|
<field name="name" ref="res_partner_subcontracting_vendor" />
|
||||||
|
<field name="currency_id" ref="base.USD" />
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
21
mrp_subcontracting_purchase/demo/stock_location_demo.xml
Normal file
21
mrp_subcontracting_purchase/demo/stock_location_demo.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8' ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="demo_stock_location_sub_1" model="stock.location">
|
||||||
|
<field name="name">sub1</field>
|
||||||
|
<field
|
||||||
|
name="location_id"
|
||||||
|
model="stock.location"
|
||||||
|
eval="obj().with_context(active_test=False).search([('name','=','Subcontracting Location'),('company_id','=',obj().env.company.id)],limit=1)"
|
||||||
|
/>
|
||||||
|
<field name="company_id" ref="base.main_company" />
|
||||||
|
</record>
|
||||||
|
<record id="demo_stock_location_sub_2" model="stock.location">
|
||||||
|
<field name="name">sub2</field>
|
||||||
|
<field
|
||||||
|
name="location_id"
|
||||||
|
model="stock.location"
|
||||||
|
eval="obj().with_context(active_test=False).search([('name','=','Subcontracting Location'),('company_id','=',obj().env.company.id)],limit=1)"
|
||||||
|
/>
|
||||||
|
<field name="company_id" ref="base.main_company" />
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
68
mrp_subcontracting_purchase/demo/stock_rules_demo.xml
Normal file
68
mrp_subcontracting_purchase/demo/stock_rules_demo.xml
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8' ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="demo_stock_rules_1" model="stock.rule">
|
||||||
|
<field name="name">Vendors — Sub 1</field>
|
||||||
|
<field name="action">buy</field>
|
||||||
|
<field name="location_id" ref="demo_stock_location_sub_1" />
|
||||||
|
<field name="route_id" ref="route_subcontracting_dropshipping" />
|
||||||
|
<field name="company_id" ref="base.main_company" />
|
||||||
|
<field
|
||||||
|
name="picking_type_id"
|
||||||
|
model="stock.picking.type"
|
||||||
|
eval="obj().with_context(active_test=False).search([('name','=','Dropship'),('sequence_code','=','DS'),('company_id','=',obj().env.company.id)],limit=1)"
|
||||||
|
/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_stock_rules_2" model="stock.rule">
|
||||||
|
<field name="name">Vendors — Sub 2</field>
|
||||||
|
<field name="action">buy</field>
|
||||||
|
<field name="location_id" ref="demo_stock_location_sub_2" />
|
||||||
|
<field name="route_id" ref="route_subcontracting_dropshipping" />
|
||||||
|
<field name="company_id" ref="base.main_company" />
|
||||||
|
<field
|
||||||
|
name="picking_type_id"
|
||||||
|
model="stock.picking.type"
|
||||||
|
eval="obj().with_context(active_test=False).search([('name','=','Dropship'),('sequence_code','=','DS'),('company_id','=',obj().env.company.id)],limit=1)"
|
||||||
|
/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_stock_rules_3" model="stock.rule">
|
||||||
|
<field name="name">Sub 1 — Production</field>
|
||||||
|
<field name="action">pull</field>
|
||||||
|
<field
|
||||||
|
name="picking_type_id"
|
||||||
|
model="stock.picking.type"
|
||||||
|
eval="obj().with_context(active_test=False).search([('name','=','Subcontracting'),('sequence_code','=','SBC')],limit=1)"
|
||||||
|
/>
|
||||||
|
<field name="location_src_id" ref="demo_stock_location_sub_1" />
|
||||||
|
<field
|
||||||
|
name="location_id"
|
||||||
|
model="stock.location"
|
||||||
|
eval="obj().search([('name','=','Production')],limit=1)"
|
||||||
|
/>
|
||||||
|
<field name="route_id" ref="route_subcontracting_dropshipping" />
|
||||||
|
<field name="warehouse_id" ref="stock.warehouse0" />
|
||||||
|
<field name="procure_method">make_to_order</field>
|
||||||
|
<field name="partner_address_id" ref="res_partner_subcontracting_1" />
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="demo_stock_rules_4" model="stock.rule">
|
||||||
|
<field name="name">Sub 2 — Production</field>
|
||||||
|
<field name="action">pull</field>
|
||||||
|
<field
|
||||||
|
name="picking_type_id"
|
||||||
|
model="stock.picking.type"
|
||||||
|
eval="obj().with_context(active_test=False).search([('name','=','Subcontracting'),('sequence_code','=','SBC')],limit=1)"
|
||||||
|
/>
|
||||||
|
<field name="location_src_id" ref="demo_stock_location_sub_2" />
|
||||||
|
<field
|
||||||
|
name="location_id"
|
||||||
|
model="stock.location"
|
||||||
|
eval="obj().search([('name','=','Production')],limit=1)"
|
||||||
|
/>
|
||||||
|
<field name="route_id" ref="route_subcontracting_dropshipping" />
|
||||||
|
<field name="warehouse_id" ref="stock.warehouse0" />
|
||||||
|
<field name="procure_method">make_to_order</field>
|
||||||
|
<field name="partner_address_id" ref="res_partner_subcontracting_2" />
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
0
mrp_subcontracting_purchase/i18n/it.po
Normal file
0
mrp_subcontracting_purchase/i18n/it.po
Normal file
@@ -1,4 +1,10 @@
|
|||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import stock_move
|
||||||
|
from . import stock_warehouse
|
||||||
from . import stock_picking
|
from . import stock_picking
|
||||||
|
from . import stock_rule
|
||||||
from . import purchase_order
|
from . import purchase_order
|
||||||
|
from . import purchase_order_line
|
||||||
|
from . import res_company
|
||||||
|
from . import mrp_production
|
||||||
|
|||||||
117
mrp_subcontracting_purchase/models/mrp_production.py
Normal file
117
mrp_subcontracting_purchase/models/mrp_production.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import _, fields, models
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
from odoo.tools.float_utils import float_is_zero
|
||||||
|
|
||||||
|
|
||||||
|
class MrpProduction(models.Model):
|
||||||
|
_inherit = "mrp.production"
|
||||||
|
|
||||||
|
subcontracting_has_been_recorded = fields.Boolean("Has been recorded", copy=False)
|
||||||
|
|
||||||
|
def subcontracting_record_component(self):
|
||||||
|
"""Returns subcontracting issues
|
||||||
|
|
||||||
|
Since we don't have a subcontracting_has_been_recorded field in version 14.0,
|
||||||
|
we need to add functionality related to this field
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
self._check_exception_subcontracting_record_component()
|
||||||
|
consumption_issues = self._get_consumption_issues()
|
||||||
|
if consumption_issues:
|
||||||
|
return self._action_generate_consumption_wizard(consumption_issues)
|
||||||
|
self._update_finished_move()
|
||||||
|
self.subcontracting_has_been_recorded = True
|
||||||
|
if self._get_quantity_produced_issues():
|
||||||
|
return self._has_quantity_issues()
|
||||||
|
return {"type": "ir.actions.act_window_close"}
|
||||||
|
|
||||||
|
def _check_exception_subcontracting_record_component(self):
|
||||||
|
"""Check exceptions subcontracting component"""
|
||||||
|
if not self._get_subcontract_move():
|
||||||
|
raise UserError(_("This MO isn't related to a subcontracted move"))
|
||||||
|
if float_is_zero(
|
||||||
|
self.qty_producing, precision_rounding=self.product_uom_id.rounding
|
||||||
|
):
|
||||||
|
return {"type": "ir.actions.act_window_close"}
|
||||||
|
if self.product_tracking != "none" and not self.lot_producing_id:
|
||||||
|
raise UserError(
|
||||||
|
_("You must enter a serial number for %s") % self.product_id.name
|
||||||
|
)
|
||||||
|
smls = self.move_raw_ids.move_line_ids.filtered(
|
||||||
|
lambda s: s.tracking != "none" and not s.lot_id
|
||||||
|
)
|
||||||
|
if smls:
|
||||||
|
sml = fields.first(smls)
|
||||||
|
raise UserError(
|
||||||
|
_("You must enter a serial number for each line of %s")
|
||||||
|
% sml.product_id.display_name
|
||||||
|
)
|
||||||
|
if self.move_raw_ids and not any(self.move_raw_ids.mapped("quantity_done")):
|
||||||
|
raise UserError(
|
||||||
|
_(
|
||||||
|
"""You must indicate a non-zero amount
|
||||||
|
consumed for at least one of your components"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _has_quantity_issues(self):
|
||||||
|
"""Returns action with issues"""
|
||||||
|
backorder = self._generate_backorder_productions(close_mo=False)
|
||||||
|
# No qty to consume to avoid propagate additional move
|
||||||
|
# TODO avoid : stock move created in backorder with 0 as qty
|
||||||
|
backorder.move_raw_ids.filtered(lambda m: m.additional).product_uom_qty = 0.0
|
||||||
|
backorder.qty_producing = backorder.product_qty
|
||||||
|
backorder._set_qty_producing()
|
||||||
|
self.product_qty = self.qty_producing
|
||||||
|
action = (
|
||||||
|
self._get_subcontract_move()
|
||||||
|
.filtered(lambda m: m.state not in ("done", "cancel"))
|
||||||
|
._action_record_components()
|
||||||
|
)
|
||||||
|
action.update(res_id=backorder.id)
|
||||||
|
return action
|
||||||
|
|
||||||
|
def _pre_button_mark_done(self):
|
||||||
|
return (
|
||||||
|
True
|
||||||
|
if self._get_subcontract_move()
|
||||||
|
else super(MrpProduction, self)._pre_button_mark_done()
|
||||||
|
)
|
||||||
|
|
||||||
|
def _subcontracting_filter_to_done(self):
|
||||||
|
# OVERRIDE, to add condition 'not mo.subcontracting_has_been_recorded',
|
||||||
|
# which checks whether the subcontracting has been recorded or not
|
||||||
|
|
||||||
|
def filter_in(mo):
|
||||||
|
return not (
|
||||||
|
mo.state in ("done", "cancel")
|
||||||
|
or not mo.subcontracting_has_been_recorded
|
||||||
|
or float_is_zero(
|
||||||
|
mo.qty_producing, precision_rounding=mo.product_uom_id.rounding
|
||||||
|
)
|
||||||
|
or not all(
|
||||||
|
line.lot_id
|
||||||
|
for line in mo.move_raw_ids.filtered(
|
||||||
|
lambda sm: sm.has_tracking != "none"
|
||||||
|
).move_line_ids
|
||||||
|
)
|
||||||
|
or mo.product_tracking != "none"
|
||||||
|
and not mo.lot_producing_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.filtered(filter_in)
|
||||||
|
|
||||||
|
def _has_been_recorded(self):
|
||||||
|
"""Checks for records in subcontracting production"""
|
||||||
|
self.ensure_one()
|
||||||
|
return self.state in ("cancel", "done") or self.subcontracting_has_been_recorded
|
||||||
|
|
||||||
|
def _has_tracked_component(self):
|
||||||
|
"""Checks the component for tracking in the stock"""
|
||||||
|
return any(m.has_tracking != "none" for m in self.move_raw_ids)
|
||||||
|
|
||||||
|
def _get_subcontract_move(self):
|
||||||
|
"""Returns destination for subcontract"""
|
||||||
|
return self.move_finished_ids.move_dest_ids.filtered(lambda m: m.is_subcontract)
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
from odoo import _, api, fields, models
|
||||||
|
from odoo.exceptions import UserError
|
||||||
from odoo import api, fields, models
|
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrder(models.Model):
|
class PurchaseOrder(models.Model):
|
||||||
@@ -23,8 +22,102 @@ class PurchaseOrder(models.Model):
|
|||||||
return self._get_action_view_picking(self._get_subcontracting_resupplies())
|
return self._get_action_view_picking(self._get_subcontracting_resupplies())
|
||||||
|
|
||||||
def _get_subcontracting_resupplies(self):
|
def _get_subcontracting_resupplies(self):
|
||||||
moves_subcontracted = self.order_line.move_ids.filtered(
|
return self.order_line.move_ids.filtered(lambda m: m.is_subcontract).mapped(
|
||||||
lambda m: m.is_subcontract
|
"move_orig_ids.production_id.picking_ids"
|
||||||
)
|
)
|
||||||
subcontracted_productions = moves_subcontracted.move_orig_ids.production_id
|
|
||||||
return subcontracted_productions.picking_ids
|
@api.depends("order_line.move_dest_ids.group_id.mrp_production_ids")
|
||||||
|
def _compute_mrp_production_count(self):
|
||||||
|
for purchase in self:
|
||||||
|
purchase.mrp_production_count = len(purchase._get_mrp_productions())
|
||||||
|
|
||||||
|
def _get_mrp_productions(self, **kwargs):
|
||||||
|
productions = (
|
||||||
|
self.order_line.move_dest_ids.group_id.mrp_production_ids
|
||||||
|
| self.order_line.move_ids.move_dest_ids.group_id.mrp_production_ids
|
||||||
|
)
|
||||||
|
if kwargs.get("remove_archived_picking_types", True):
|
||||||
|
productions = productions.filtered(
|
||||||
|
lambda production: production.with_context(
|
||||||
|
active_test=False
|
||||||
|
).picking_type_id.active
|
||||||
|
)
|
||||||
|
return productions
|
||||||
|
|
||||||
|
def action_view_picking(self):
|
||||||
|
return self._get_action_view_picking(self.picking_ids)
|
||||||
|
|
||||||
|
def _get_action_view_picking(self, pickings):
|
||||||
|
"""This function returns an action that display existing picking orders
|
||||||
|
of given purchase order ids. When only one found, show the picking immediately.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
result = self.env["ir.actions.actions"]._for_xml_id(
|
||||||
|
"stock.action_picking_tree_all"
|
||||||
|
)
|
||||||
|
# override the context to get rid of the default filtering on operation type
|
||||||
|
result["context"] = {
|
||||||
|
"default_partner_id": self.partner_id.id,
|
||||||
|
"default_origin": self.name,
|
||||||
|
"default_picking_type_id": self.picking_type_id.id,
|
||||||
|
}
|
||||||
|
# choose the view_mode accordingly
|
||||||
|
if not pickings or len(pickings) > 1:
|
||||||
|
result["domain"] = [("id", "in", pickings.ids)]
|
||||||
|
elif len(pickings) == 1:
|
||||||
|
res = self.env.ref("stock.view_picking_form", False)
|
||||||
|
form_view = [(res and res.id or False, "form")]
|
||||||
|
result.update(
|
||||||
|
{
|
||||||
|
"views": form_view
|
||||||
|
+ [
|
||||||
|
(state, view)
|
||||||
|
for state, view in result.get("views", [])
|
||||||
|
if view != "form"
|
||||||
|
],
|
||||||
|
"res_id": pickings.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _get_destination_location(self):
|
||||||
|
"""Returns destination location for subcontractor"""
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.dest_address_id or self.sale_order_count:
|
||||||
|
return super(PurchaseOrder, self)._get_destination_location()
|
||||||
|
|
||||||
|
mrp_production_ids = self._get_mrp_productions(
|
||||||
|
remove_archived_picking_types=False
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
mrp_production_ids
|
||||||
|
and self.dest_address_id in mrp_production_ids.bom_id.subcontractor_ids
|
||||||
|
):
|
||||||
|
return self.dest_address_id.property_stock_subcontractor.id
|
||||||
|
|
||||||
|
in_bom_products = False
|
||||||
|
not_in_bom_products = self.env["purchase.order.line"]
|
||||||
|
for order_line in self.order_line:
|
||||||
|
if any(
|
||||||
|
bom_line.bom_id.type == "subcontract"
|
||||||
|
and self.dest_address_id in bom_line.bom_id.subcontractor_ids
|
||||||
|
for bom_line in order_line.product_id.bom_line_ids.filtered(
|
||||||
|
lambda line: line.company_id == self.company_id
|
||||||
|
)
|
||||||
|
):
|
||||||
|
in_bom_products = True
|
||||||
|
elif not order_line.display_type:
|
||||||
|
not_in_bom_products |= order_line
|
||||||
|
if in_bom_products and not_in_bom_products:
|
||||||
|
raise UserError(
|
||||||
|
_(
|
||||||
|
"""It appears some components in this RFQ are not meant for subcontracting.
|
||||||
|
Please create a separate order for these."""
|
||||||
|
)
|
||||||
|
+ "\n\n"
|
||||||
|
+ "\n".join(not_in_bom_products.mapped("name"))
|
||||||
|
)
|
||||||
|
elif in_bom_products:
|
||||||
|
return self.dest_address_id.property_stock_subcontractor.id
|
||||||
|
return super()._get_destination_location()
|
||||||
|
|||||||
42
mrp_subcontracting_purchase/models/purchase_order_line.py
Normal file
42
mrp_subcontracting_purchase/models/purchase_order_line.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from odoo import api, models
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseOrderLine(models.Model):
|
||||||
|
_inherit = "purchase.order.line"
|
||||||
|
|
||||||
|
def _compute_qty_received(self):
|
||||||
|
"""Returns the quantity comes for moves"""
|
||||||
|
pol_obj = self.env["purchase.order.line"]
|
||||||
|
for line in self.filtered(
|
||||||
|
lambda l: l.qty_received_method == "stock_moves"
|
||||||
|
and l.move_ids.filtered(lambda m: m.state != "cancel")
|
||||||
|
):
|
||||||
|
kit_bom = self.env["mrp.bom"]._bom_find(
|
||||||
|
product=line.product_id,
|
||||||
|
company_id=line.company_id.id,
|
||||||
|
bom_type="phantom",
|
||||||
|
)
|
||||||
|
if kit_bom:
|
||||||
|
pol_obj |= self._set_qty_received(kit_bom, line)
|
||||||
|
super(PurchaseOrderLine, self - pol_obj)._compute_qty_received()
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _set_qty_received(self, kit_bom, line):
|
||||||
|
"""Set qty received on the basis of the bom"""
|
||||||
|
moves = line.move_ids.filtered(lambda m: m.state == "done" and not m.scrapped)
|
||||||
|
order_qty = line.product_uom._compute_quantity(
|
||||||
|
line.product_uom_qty, kit_bom.product_uom_id
|
||||||
|
)
|
||||||
|
filters = {
|
||||||
|
"incoming_moves": lambda m: m.location_id.usage == "supplier"
|
||||||
|
and (
|
||||||
|
not m.origin_returned_move_id
|
||||||
|
or (m.origin_returned_move_id and m.to_refund)
|
||||||
|
),
|
||||||
|
"outgoing_moves": lambda m: m.location_id.usage != "supplier"
|
||||||
|
and m.to_refund,
|
||||||
|
}
|
||||||
|
line.qty_received = moves._compute_kit_quantities(
|
||||||
|
line.product_id, order_qty, kit_bom, filters
|
||||||
|
)
|
||||||
|
return line
|
||||||
70
mrp_subcontracting_purchase/models/res_company.py
Normal file
70
mrp_subcontracting_purchase/models/res_company.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
from odoo import api, models
|
||||||
|
|
||||||
|
|
||||||
|
class ResCompany(models.Model):
|
||||||
|
_inherit = "res.company"
|
||||||
|
|
||||||
|
def _create_subcontracting_dropshipping_rules(self):
|
||||||
|
"""Adds new dropshipping stock rules for subcontracting"""
|
||||||
|
route = self.env.ref(
|
||||||
|
"mrp_subcontracting_purchase.route_subcontracting_dropshipping",
|
||||||
|
raise_if_not_found=False,
|
||||||
|
)
|
||||||
|
supplier_location = self.env.ref(
|
||||||
|
"stock.stock_location_suppliers", raise_if_not_found=False
|
||||||
|
)
|
||||||
|
vals = self._prepared_subcontracting_dropshipping_rules(
|
||||||
|
route, supplier_location
|
||||||
|
)
|
||||||
|
if vals:
|
||||||
|
self.env["stock.rule"].create(vals)
|
||||||
|
|
||||||
|
def _prepared_subcontracting_dropshipping_rules(self, route, supplier_location):
|
||||||
|
vals = []
|
||||||
|
for company in self:
|
||||||
|
subcontracting_location = company.subcontracting_location_id
|
||||||
|
dropship_picking_type = self.env["stock.picking.type"].search(
|
||||||
|
[
|
||||||
|
("company_id", "=", company.id),
|
||||||
|
("default_location_src_id.usage", "=", "supplier"),
|
||||||
|
("default_location_dest_id.usage", "=", "customer"),
|
||||||
|
],
|
||||||
|
limit=1,
|
||||||
|
order="sequence",
|
||||||
|
)
|
||||||
|
if dropship_picking_type:
|
||||||
|
vals.append(
|
||||||
|
{
|
||||||
|
"name": "%s → %s"
|
||||||
|
% (supplier_location.name, subcontracting_location.name),
|
||||||
|
"action": "buy",
|
||||||
|
"location_id": subcontracting_location.id,
|
||||||
|
"location_src_id": supplier_location.id,
|
||||||
|
"procure_method": "make_to_stock",
|
||||||
|
"route_id": route.id,
|
||||||
|
"picking_type_id": dropship_picking_type.id,
|
||||||
|
"company_id": company.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return vals
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create_missing_subcontracting_dropshipping_rules(self):
|
||||||
|
"""Adds new stock rules for missing subcontracting dropshipping"""
|
||||||
|
route = self.env.ref(
|
||||||
|
"mrp_subcontracting_purchase.route_subcontracting_dropshipping",
|
||||||
|
raise_if_not_found=False,
|
||||||
|
)
|
||||||
|
companies = self.env["res.company"].search([])
|
||||||
|
company_has_rules = (
|
||||||
|
self.env["stock.rule"]
|
||||||
|
.search([("route_id", "=", route.id)])
|
||||||
|
.mapped("company_id")
|
||||||
|
)
|
||||||
|
company_todo_rules = companies - company_has_rules
|
||||||
|
company_todo_rules._create_subcontracting_dropshipping_rules()
|
||||||
|
|
||||||
|
def _create_per_company_rules(self):
|
||||||
|
res = super(ResCompany, self)._create_per_company_rules()
|
||||||
|
self.create_missing_subcontracting_dropshipping_rules()
|
||||||
|
return res
|
||||||
86
mrp_subcontracting_purchase/models/stock_move.py
Normal file
86
mrp_subcontracting_purchase/models/stock_move.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
from odoo import _, api, models
|
||||||
|
from odoo.tools.float_utils import float_is_zero
|
||||||
|
|
||||||
|
|
||||||
|
class StockMove(models.Model):
|
||||||
|
_inherit = "stock.move"
|
||||||
|
|
||||||
|
def _get_subcontract_production(self):
|
||||||
|
"""Gets "Production orders" from the previous stock move when chaining them"""
|
||||||
|
return self.filtered(lambda m: m.is_subcontract).move_orig_ids.production_id
|
||||||
|
|
||||||
|
def _compute_display_assign_serial(self):
|
||||||
|
"""Generate multiple serial number and assigns them to stock move lines."""
|
||||||
|
super(StockMove, self)._compute_display_assign_serial()
|
||||||
|
for move in self:
|
||||||
|
if not move.is_subcontract:
|
||||||
|
continue
|
||||||
|
productions = move._get_subcontract_production()
|
||||||
|
if not productions or move.has_tracking != "serial":
|
||||||
|
continue
|
||||||
|
if (
|
||||||
|
productions._has_tracked_component()
|
||||||
|
or productions[:1].consumption != "strict"
|
||||||
|
):
|
||||||
|
move.display_assign_serial = False
|
||||||
|
|
||||||
|
def _compute_show_subcontracting_details_visible(self):
|
||||||
|
"""Compute if the action button in order to see moves raw is visible"""
|
||||||
|
self.show_subcontracting_details_visible = False
|
||||||
|
for move in self:
|
||||||
|
if not move.is_subcontract and float_is_zero(
|
||||||
|
move.quantity_done, precision_rounding=move.product_uom.rounding
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
productions = move._get_subcontract_production()
|
||||||
|
if not productions or (
|
||||||
|
productions[:1].consumption == "strict"
|
||||||
|
and not productions[:1]._has_tracked_component()
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
move.show_subcontracting_details_visible = True
|
||||||
|
|
||||||
|
def _compute_show_details_visible(self):
|
||||||
|
"""If the move is subcontract and the components are tracked. Then the
|
||||||
|
show details button is visible.
|
||||||
|
"""
|
||||||
|
res = super(StockMove, self)._compute_show_details_visible()
|
||||||
|
for move in self:
|
||||||
|
if not move.is_subcontract:
|
||||||
|
continue
|
||||||
|
productions = move._get_subcontract_production()
|
||||||
|
if (
|
||||||
|
not productions._has_tracked_component()
|
||||||
|
and productions[:1].consumption == "strict"
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
move.show_details_visible = True
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class StockMoveLine(models.Model):
|
||||||
|
_inherit = "stock.move.line"
|
||||||
|
|
||||||
|
@api.onchange("lot_name", "lot_id")
|
||||||
|
def _onchange_serial_number(self):
|
||||||
|
"""Checks the correctness of the original location"""
|
||||||
|
current_location_id = self.location_id
|
||||||
|
res = super()._onchange_serial_number()
|
||||||
|
subcontracting_location_id = self.company_id.subcontracting_location_id
|
||||||
|
if (
|
||||||
|
res
|
||||||
|
and not self.lot_name
|
||||||
|
and subcontracting_location_id == current_location_id
|
||||||
|
):
|
||||||
|
# we want to avoid auto-updating source location in
|
||||||
|
# this case + change the warning message
|
||||||
|
self.location_id = current_location_id
|
||||||
|
res["warning"]["message"] = (
|
||||||
|
_(
|
||||||
|
"""%s\n\nMake sure you validate or adapt the related resupply picking
|
||||||
|
to your subcontractor in order to avoid inconsistencies in your stock.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
% res["warning"]["message"].split("\n\n", 1)[0]
|
||||||
|
)
|
||||||
|
return res
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
from datetime import timedelta
|
||||||
|
|
||||||
from odoo import _, api, fields, models
|
from odoo import _, api, fields, models
|
||||||
|
from odoo.tools.float_utils import float_compare
|
||||||
|
|
||||||
|
|
||||||
class StockPicking(models.Model):
|
class StockPicking(models.Model):
|
||||||
@@ -14,12 +15,14 @@ class StockPicking(models.Model):
|
|||||||
|
|
||||||
@api.depends("move_lines.move_dest_ids.raw_material_production_id")
|
@api.depends("move_lines.move_dest_ids.raw_material_production_id")
|
||||||
def _compute_subcontracting_source_purchase_count(self):
|
def _compute_subcontracting_source_purchase_count(self):
|
||||||
|
"""Compute number of subcontracting Purchase Order Source"""
|
||||||
for picking in self:
|
for picking in self:
|
||||||
picking.subcontracting_source_purchase_count = len(
|
picking.subcontracting_source_purchase_count = len(
|
||||||
picking._get_subcontracting_source_purchase()
|
picking._get_subcontracting_source_purchase()
|
||||||
)
|
)
|
||||||
|
|
||||||
def action_view_subcontracting_source_purchase(self):
|
def action_view_subcontracting_source_purchase(self):
|
||||||
|
"""Returns action for subcontracting source purchase"""
|
||||||
purchase_order_ids = self._get_subcontracting_source_purchase().ids
|
purchase_order_ids = self._get_subcontracting_source_purchase().ids
|
||||||
action = {
|
action = {
|
||||||
"res_model": "purchase.order",
|
"res_model": "purchase.order",
|
||||||
@@ -35,7 +38,7 @@ class StockPicking(models.Model):
|
|||||||
else:
|
else:
|
||||||
action.update(
|
action.update(
|
||||||
{
|
{
|
||||||
"name": _("Source PO of %s", self.name),
|
"name": _("Source PO of %s") % self.name,
|
||||||
"domain": [("id", "in", purchase_order_ids)],
|
"domain": [("id", "in", purchase_order_ids)],
|
||||||
"view_mode": "tree,form",
|
"view_mode": "tree,form",
|
||||||
}
|
}
|
||||||
@@ -43,7 +46,91 @@ class StockPicking(models.Model):
|
|||||||
return action
|
return action
|
||||||
|
|
||||||
def _get_subcontracting_source_purchase(self):
|
def _get_subcontracting_source_purchase(self):
|
||||||
|
"""Returns the source purchase order associated with a subcontracted operation."""
|
||||||
moves_subcontracted = self.move_lines.move_dest_ids.raw_material_production_id.move_finished_ids.move_dest_ids.filtered( # noqa
|
moves_subcontracted = self.move_lines.move_dest_ids.raw_material_production_id.move_finished_ids.move_dest_ids.filtered( # noqa
|
||||||
lambda m: m.is_subcontract
|
lambda m: m.is_subcontract
|
||||||
)
|
)
|
||||||
return moves_subcontracted.purchase_line_id.order_id
|
return moves_subcontracted.purchase_line_id.order_id
|
||||||
|
|
||||||
|
def _get_subcontract_production(self):
|
||||||
|
"""Returns subcontract production in stock picking line"""
|
||||||
|
return self.move_lines._get_subcontract_production()
|
||||||
|
|
||||||
|
def _action_done(self):
|
||||||
|
# parent function with a subcontract record line added
|
||||||
|
res = super(StockPicking, self)._action_done()
|
||||||
|
|
||||||
|
for move in self.move_lines.filtered(lambda move: move.is_subcontract):
|
||||||
|
# Auto set qty_producing/lot_producing_id of MO if there isn't tracked component
|
||||||
|
# If there is tracked component,
|
||||||
|
# the flow use subcontracting_record_component instead
|
||||||
|
if move._has_tracked_subcontract_components():
|
||||||
|
continue
|
||||||
|
production = move.move_orig_ids.production_id.filtered(
|
||||||
|
lambda p: p.state not in ("done", "cancel")
|
||||||
|
)[-1:]
|
||||||
|
if not production:
|
||||||
|
continue
|
||||||
|
# Manage additional quantities
|
||||||
|
quantity_done_move = move.product_uom._compute_quantity(
|
||||||
|
move.quantity_done, production.product_uom_id
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
float_compare(
|
||||||
|
production.product_qty,
|
||||||
|
quantity_done_move,
|
||||||
|
precision_rounding=production.product_uom_id.rounding,
|
||||||
|
)
|
||||||
|
== -1
|
||||||
|
):
|
||||||
|
change_qty = self.env["change.production.qty"].create(
|
||||||
|
{"mo_id": production.id, "product_qty": quantity_done_move}
|
||||||
|
)
|
||||||
|
change_qty.with_context(skip_activity=True).change_prod_qty()
|
||||||
|
# Create backorder MO for each move lines
|
||||||
|
for move_line in move.move_line_ids:
|
||||||
|
if move_line.lot_id:
|
||||||
|
production.lot_producing_id = move_line.lot_id
|
||||||
|
production.qty_producing = move_line.product_uom_id._compute_quantity(
|
||||||
|
move_line.qty_done, production.product_uom_id
|
||||||
|
)
|
||||||
|
production._set_qty_producing()
|
||||||
|
production.subcontracting_has_been_recorded = True
|
||||||
|
if move_line != move.move_line_ids[-1]:
|
||||||
|
backorder = production._generate_backorder_productions(
|
||||||
|
close_mo=False
|
||||||
|
)
|
||||||
|
# The move_dest_ids won't be set because the _split filter out done move
|
||||||
|
backorder.move_finished_ids.filtered(
|
||||||
|
lambda mo: mo.product_id == move.product_id
|
||||||
|
).move_dest_ids = production.move_finished_ids.filtered(
|
||||||
|
lambda mo: mo.product_id == move.product_id
|
||||||
|
).move_dest_ids
|
||||||
|
production.product_qty = production.qty_producing
|
||||||
|
production = backorder
|
||||||
|
|
||||||
|
for picking in self:
|
||||||
|
productions_to_done = (
|
||||||
|
picking._get_subcontracted_productions()._subcontracting_filter_to_done()
|
||||||
|
)
|
||||||
|
if not productions_to_done:
|
||||||
|
continue
|
||||||
|
production_ids_backorder = []
|
||||||
|
if not self.env.context.get("cancel_backorder"):
|
||||||
|
production_ids_backorder = productions_to_done.filtered(
|
||||||
|
lambda mo: mo.state == "progress"
|
||||||
|
).ids
|
||||||
|
productions_to_done.with_context(
|
||||||
|
subcontract_move_id=True, mo_ids_to_backorder=production_ids_backorder
|
||||||
|
).button_mark_done()
|
||||||
|
# For concistency, set the date on production move before the date
|
||||||
|
# on picking. (Traceability report + Product Moves menu item)
|
||||||
|
minimum_date = min(picking.move_line_ids.mapped("date"))
|
||||||
|
production_moves = (
|
||||||
|
productions_to_done.move_raw_ids | productions_to_done.move_finished_ids
|
||||||
|
)
|
||||||
|
production_moves.write({"date": minimum_date - timedelta(seconds=1)})
|
||||||
|
production_moves.move_line_ids.write(
|
||||||
|
{"date": minimum_date - timedelta(seconds=1)}
|
||||||
|
)
|
||||||
|
return res
|
||||||
|
|||||||
214
mrp_subcontracting_purchase/models/stock_rule.py
Normal file
214
mrp_subcontracting_purchase/models/stock_rule.py
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from itertools import groupby
|
||||||
|
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
from odoo import SUPERUSER_ID, _, api, fields, models
|
||||||
|
from odoo.tools import float_compare
|
||||||
|
|
||||||
|
from odoo.addons.stock.models.stock_rule import ProcurementException
|
||||||
|
|
||||||
|
|
||||||
|
class StockPicking(models.Model):
|
||||||
|
_inherit = "stock.rule"
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _run_buy(self, procurements):
|
||||||
|
"""Launching a purchase group with required/custom
|
||||||
|
fields generated by a sales order line"""
|
||||||
|
procurements_by_po_domain = defaultdict(list)
|
||||||
|
errors = []
|
||||||
|
message = _(
|
||||||
|
"""There is no matching vendor price to generate the purchase order for
|
||||||
|
product %s (no vendor defined, minimum quantity not reached,
|
||||||
|
dates not valid, ...).
|
||||||
|
Go on the product form and complete the list of vendors."""
|
||||||
|
)
|
||||||
|
for procurement, rule in procurements:
|
||||||
|
supplier = self._get_supplier(procurement)
|
||||||
|
if not supplier:
|
||||||
|
errors.append(
|
||||||
|
(procurement, message % (procurement.product_id.display_name))
|
||||||
|
)
|
||||||
|
partner = supplier.name
|
||||||
|
# we put `supplier_info` in values for extensibility purposes
|
||||||
|
procurement.values.update(
|
||||||
|
{"supplier": supplier, "propagate_cancel": rule.propagate_cancel}
|
||||||
|
)
|
||||||
|
domain = rule._make_po_get_domain(
|
||||||
|
procurement.company_id, procurement.values, partner
|
||||||
|
)
|
||||||
|
procurements_by_po_domain[domain].append((procurement, rule))
|
||||||
|
if errors:
|
||||||
|
raise ProcurementException(errors)
|
||||||
|
self._create_po_not_exist(procurements_by_po_domain)
|
||||||
|
|
||||||
|
def _prepare_purchase_order(self, company_id, origins, values):
|
||||||
|
"""Returns prepared data for create PO"""
|
||||||
|
if (
|
||||||
|
"partner_id" not in values[0]
|
||||||
|
and company_id.subcontracting_location_id.parent_path
|
||||||
|
in self.location_id.parent_path
|
||||||
|
):
|
||||||
|
values[0]["partner_id"] = values[0]["group_id"].partner_id.id
|
||||||
|
return super()._prepare_purchase_order(company_id, origins, values)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_supplier(self, procurement):
|
||||||
|
"""Return valid supplier"""
|
||||||
|
supplier = False
|
||||||
|
# Get the schedule date in order to find a valid seller
|
||||||
|
procurement_date_planned = fields.Datetime.from_string(
|
||||||
|
procurement.values["date_planned"]
|
||||||
|
)
|
||||||
|
if procurement.values.get("supplierinfo_id"):
|
||||||
|
supplier = procurement.values["supplierinfo_id"]
|
||||||
|
elif (
|
||||||
|
procurement.values.get("orderpoint_id")
|
||||||
|
and procurement.values["orderpoint_id"].supplier_id
|
||||||
|
):
|
||||||
|
supplier = procurement.values["orderpoint_id"].supplier_id
|
||||||
|
else:
|
||||||
|
supplier = procurement.product_id.with_company(
|
||||||
|
procurement.company_id.id
|
||||||
|
)._select_seller(
|
||||||
|
partner_id=procurement.values.get("supplierinfo_name"),
|
||||||
|
quantity=procurement.product_qty,
|
||||||
|
date=procurement_date_planned.date(),
|
||||||
|
uom_id=procurement.product_uom,
|
||||||
|
)
|
||||||
|
# Fall back on a supplier for which no price may be defined.
|
||||||
|
# Not ideal, but better than blocking the user.
|
||||||
|
supplier = (
|
||||||
|
supplier
|
||||||
|
or procurement.product_id._prepare_sellers(False).filtered(
|
||||||
|
lambda s: not s.company_id or s.company_id == procurement.company_id
|
||||||
|
)[:1]
|
||||||
|
)
|
||||||
|
return supplier
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _create_po_not_exist(self, procurements_by_po_domain):
|
||||||
|
pol_obj = self.env["purchase.order.line"]
|
||||||
|
for domain, procurements_rules in procurements_by_po_domain.items():
|
||||||
|
# Get the procurements for the current domain.
|
||||||
|
# Get the rules for the current domain. Their only use is to create
|
||||||
|
# the PO if it does not exist.
|
||||||
|
procurements, rules = zip(*procurements_rules)
|
||||||
|
# Check if a PO exists for the current domain.
|
||||||
|
company_id = procurements[0].company_id
|
||||||
|
po = self._check_po_exists(domain, procurements, rules, company_id)
|
||||||
|
procurements_to_merge = self._get_procurements_to_merge(procurements)
|
||||||
|
procurements = self._merge_procurements(procurements_to_merge)
|
||||||
|
po_lines_by_product = {}
|
||||||
|
grouped_po_lines = groupby(
|
||||||
|
po.order_line.filtered(
|
||||||
|
lambda l: not l.display_type
|
||||||
|
and l.product_uom == l.product_id.uom_po_id
|
||||||
|
).sorted(lambda l: l.product_id.id),
|
||||||
|
key=lambda l: l.product_id.id,
|
||||||
|
)
|
||||||
|
for product, po_lines in grouped_po_lines:
|
||||||
|
po_lines_by_product[product] = pol_obj.concat(*list(po_lines))
|
||||||
|
po_line_values = []
|
||||||
|
for procurement in procurements:
|
||||||
|
po_lines = po_lines_by_product.get(procurement.product_id.id, pol_obj)
|
||||||
|
po_line = po_lines._find_candidate(*procurement)
|
||||||
|
|
||||||
|
if po_line:
|
||||||
|
# If the procurement can be merge in an existing line. Directly
|
||||||
|
# write the new values on it.
|
||||||
|
vals = self._update_purchase_order_line(
|
||||||
|
procurement.product_id,
|
||||||
|
procurement.product_qty,
|
||||||
|
procurement.product_uom,
|
||||||
|
company_id,
|
||||||
|
procurement.values,
|
||||||
|
po_line,
|
||||||
|
)
|
||||||
|
po_line.write(vals)
|
||||||
|
else:
|
||||||
|
if (
|
||||||
|
float_compare(
|
||||||
|
procurement.product_qty,
|
||||||
|
0,
|
||||||
|
precision_rounding=procurement.product_uom.rounding,
|
||||||
|
)
|
||||||
|
<= 0
|
||||||
|
):
|
||||||
|
# If procurement contains negative quantity,
|
||||||
|
# don't create a new line that would contain negative qty
|
||||||
|
continue
|
||||||
|
# If it does not exist a PO line for current procurement.
|
||||||
|
# Generate the create values for it and add it to a list in
|
||||||
|
# order to create it in batch.
|
||||||
|
po_line_values.append(
|
||||||
|
pol_obj._prepare_purchase_order_line_from_procurement(
|
||||||
|
procurement.product_id,
|
||||||
|
procurement.product_qty,
|
||||||
|
procurement.product_uom,
|
||||||
|
procurement.company_id,
|
||||||
|
procurement.values,
|
||||||
|
po,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Check if we need to advance the order date for the new line
|
||||||
|
date_planned = procurement.values["date_planned"]
|
||||||
|
order_date_planned = date_planned - relativedelta(
|
||||||
|
days=procurement.values["supplier"].delay
|
||||||
|
)
|
||||||
|
if fields.Date.to_date(order_date_planned) < fields.Date.to_date(
|
||||||
|
po.date_order
|
||||||
|
):
|
||||||
|
po.date_order = order_date_planned
|
||||||
|
pol_obj.sudo().create(po_line_values)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _check_po_exists(self, domain, procurements, rules, company_id):
|
||||||
|
"""Check if a PO exists for the current domain"""
|
||||||
|
po_obj = self.env["purchase.order"]
|
||||||
|
origins = {p.origin for p in procurements}
|
||||||
|
po = po_obj.sudo().search([dom for dom in domain], limit=1)
|
||||||
|
# Get the set of procurement origin for the current domain.
|
||||||
|
if not po:
|
||||||
|
positive_values = [
|
||||||
|
p.values
|
||||||
|
for p in procurements
|
||||||
|
if float_compare(
|
||||||
|
p.product_qty, 0.0, precision_rounding=p.product_uom.rounding
|
||||||
|
)
|
||||||
|
>= 0
|
||||||
|
]
|
||||||
|
if positive_values:
|
||||||
|
# We need a rule to generate the PO. However the rule generated
|
||||||
|
# the same domain for PO and the _prepare_purchase_order method
|
||||||
|
# should only uses the common rules's fields.
|
||||||
|
vals = rules[0]._prepare_purchase_order(
|
||||||
|
company_id, origins, positive_values
|
||||||
|
)
|
||||||
|
# The company_id is the same for all procurements since
|
||||||
|
# _make_po_get_domain add the company in the domain.
|
||||||
|
# We use SUPERUSER_ID since
|
||||||
|
# we don't want the current user to be follower of the PO.
|
||||||
|
# Indeed, the current user may be a user without access to Purchase,
|
||||||
|
# or even be a portal user.
|
||||||
|
po = (
|
||||||
|
po_obj.with_company(company_id).with_user(SUPERUSER_ID).create(vals)
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# If a purchase order is found, adapt its `origin` field.
|
||||||
|
if po.origin:
|
||||||
|
missing_origins = origins - set(po.origin.split(", "))
|
||||||
|
if missing_origins:
|
||||||
|
po.write(
|
||||||
|
{
|
||||||
|
"origin": "{} {}".format(
|
||||||
|
po.origin, ", ".join(missing_origins)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
new_origin = ", ".join(origins)
|
||||||
|
po.write({"origin": f"{new_origin}"})
|
||||||
|
return po
|
||||||
49
mrp_subcontracting_purchase/models/stock_warehouse.py
Normal file
49
mrp_subcontracting_purchase/models/stock_warehouse.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from odoo import _, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class StockWarehouse(models.Model):
|
||||||
|
_inherit = "stock.warehouse"
|
||||||
|
|
||||||
|
subcontracting_dropshipping_to_resupply = fields.Boolean(
|
||||||
|
"Dropship Subcontractors",
|
||||||
|
default=True,
|
||||||
|
help="Dropship subcontractors with components",
|
||||||
|
)
|
||||||
|
|
||||||
|
subcontracting_dropshipping_pull_id = fields.Many2one(
|
||||||
|
"stock.rule", "Subcontracting-Dropshipping MTS Rule"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_global_route_rules_values(self):
|
||||||
|
"""Returns route rules values"""
|
||||||
|
rules = super()._get_global_route_rules_values()
|
||||||
|
subcontract_location_id = self._get_subcontracting_location()
|
||||||
|
production_location_id = self._get_production_location()
|
||||||
|
rsd = "mrp_subcontracting_purchase.route_subcontracting_dropshipping"
|
||||||
|
rules.update(
|
||||||
|
{
|
||||||
|
"subcontracting_dropshipping_pull_id": {
|
||||||
|
"depends": ["subcontracting_dropshipping_to_resupply"],
|
||||||
|
"create_values": {
|
||||||
|
"procure_method": "make_to_order",
|
||||||
|
"company_id": self.company_id.id,
|
||||||
|
"action": "pull",
|
||||||
|
"auto": "manual",
|
||||||
|
"route_id": self._find_global_route(
|
||||||
|
rsd,
|
||||||
|
_("Dropship Subcontractor on Order"),
|
||||||
|
).id,
|
||||||
|
"name": self._format_rulename(
|
||||||
|
subcontract_location_id, production_location_id, False
|
||||||
|
),
|
||||||
|
"location_id": production_location_id.id,
|
||||||
|
"location_src_id": subcontract_location_id.id,
|
||||||
|
"picking_type_id": self.subcontracting_type_id.id,
|
||||||
|
},
|
||||||
|
"update_values": {
|
||||||
|
"active": self.subcontracting_dropshipping_to_resupply
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return rules
|
||||||
21
mrp_subcontracting_purchase/readme/CONFIGURATION.md
Normal file
21
mrp_subcontracting_purchase/readme/CONFIGURATION.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
- Enable Multi-step Routes in Inventory > settings
|
||||||
|
- Unarchive operation type “Subcontracting”
|
||||||
|
|
||||||
|
For each subcontracting partner:
|
||||||
|
|
||||||
|
- Create a subcontracting location with parent location “Physical
|
||||||
|
Locations/Subcontracting Location”
|
||||||
|
- Set created location in subcontracting partner > tab Sales & Purchase >
|
||||||
|
“Subcontracting location” field
|
||||||
|
- Create two rules for Route “Dropship Subcontractor on Order:
|
||||||
|
- Action: Buy, Operation Type: Dropship, Destination location: partner subcontracting
|
||||||
|
location
|
||||||
|
- Action: Pull From, Operation Type: Subcontracting, Source Location: partner
|
||||||
|
subcontracting location, Destination location: Virtual Locations/Production, Supply
|
||||||
|
Method: Trigger Another Rule, Partner Address: subcontracting partner
|
||||||
|
|
||||||
|
For each product:
|
||||||
|
|
||||||
|
- Create a Vendor Pricelist and a Subcontracting BoM.
|
||||||
|
- In Inventory tab, set Route “Buy” for Finished Product, and “Dropship Subcontractor on
|
||||||
|
order” for products needed for its production.
|
||||||
4
mrp_subcontracting_purchase/readme/CONTRIBUTORS.md
Normal file
4
mrp_subcontracting_purchase/readme/CONTRIBUTORS.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
- Cetmix <@cetmix.com>
|
||||||
|
- Ooops404 <https://ooops404.com>
|
||||||
|
- Dessan Hemrayev <dessanhemrayev@gmail.com>
|
||||||
|
- Maksim Shurupov <geomer198@gmail.com>
|
||||||
28
mrp_subcontracting_purchase/readme/DESCRIPTION.md
Normal file
28
mrp_subcontracting_purchase/readme/DESCRIPTION.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
This is a backporting of features from mrp_subcontracting modules from v15 allowing to
|
||||||
|
setup a flow addressing the following use case:
|
||||||
|
|
||||||
|
Vendor 1 manufactures and sells “Finished Product”
|
||||||
|
|
||||||
|
Vendor 2 manufactures and sells “Component Product” (used to manufacture “Finished
|
||||||
|
Product”)
|
||||||
|
|
||||||
|
Vendor 3 sells “Element Product” (used to manufacture “Component Product”)
|
||||||
|
|
||||||
|
As an example, in the case where there is no available qty for each of these three
|
||||||
|
products, creating a PO purchasing “Finished product” for Vendor 1 generates:
|
||||||
|
|
||||||
|
- The standard receipt picking from Vendor 1 to our warehouse
|
||||||
|
- A PO for Vendor 2 for product “Component Product”
|
||||||
|
- A subcontracting order for Vendor 1 for “Finished Product”, with component location:
|
||||||
|
Vendor 1 subcontracting location
|
||||||
|
|
||||||
|
Once this PO is confirmed, this generates:
|
||||||
|
|
||||||
|
- A dropship picking for Vendor 1 from Vendor 2 for “Component Product”
|
||||||
|
- A subcontracting order for Vendor 2 for “Component Product”, with component location:
|
||||||
|
Vendor 2 subcontracting location
|
||||||
|
- A PO for Vendor 3 for product “Element Product”
|
||||||
|
|
||||||
|
Once this PO is confirmed, this generates:
|
||||||
|
|
||||||
|
- A dropship picking for Vendor 2 from Vendor 3 for “Element Product”
|
||||||
0
mrp_subcontracting_purchase/readme/HISTORY.md
Normal file
0
mrp_subcontracting_purchase/readme/HISTORY.md
Normal file
BIN
mrp_subcontracting_purchase/static/description/icon.png
Normal file
BIN
mrp_subcontracting_purchase/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
@@ -1 +1,5 @@
|
|||||||
from . import test_mrp_subcontracting_purchase
|
from . import test_mrp_subcontracting_purchase
|
||||||
|
from . import test_mrp_production
|
||||||
|
from . import test_purchase_order
|
||||||
|
from . import test_stock_move
|
||||||
|
from . import test_res_company
|
||||||
|
|||||||
173
mrp_subcontracting_purchase/tests/test_mrp_production.py
Normal file
173
mrp_subcontracting_purchase/tests/test_mrp_production.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
from odoo.tests import Form
|
||||||
|
|
||||||
|
from odoo.addons.mrp_subcontracting.tests.common import TestMrpSubcontractingCommon
|
||||||
|
|
||||||
|
|
||||||
|
class TestMrpProductionSubcontracting(TestMrpSubcontractingCommon):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super(TestMrpProductionSubcontracting, cls).setUpClass()
|
||||||
|
cls.comp1_sn = cls.env["product.product"].create(
|
||||||
|
{
|
||||||
|
"name": "Component1",
|
||||||
|
"type": "product",
|
||||||
|
"categ_id": cls.env.ref("product.product_category_all").id,
|
||||||
|
"tracking": "serial",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cls.finished_product = cls.env["product.product"].create(
|
||||||
|
{
|
||||||
|
"name": "finished",
|
||||||
|
"type": "product",
|
||||||
|
"categ_id": cls.env.ref("product.product_category_all").id,
|
||||||
|
"tracking": "lot",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
bom_form = Form(cls.env["mrp.bom"])
|
||||||
|
bom_form.type = "subcontract"
|
||||||
|
bom_form.subcontractor_ids.add(cls.subcontractor_partner1)
|
||||||
|
bom_form.product_tmpl_id = cls.finished_product.product_tmpl_id
|
||||||
|
with bom_form.bom_line_ids.new() as bom_line:
|
||||||
|
bom_line.product_id = cls.comp1_sn
|
||||||
|
bom_line.product_qty = 1
|
||||||
|
with bom_form.bom_line_ids.new() as bom_line:
|
||||||
|
bom_line.product_id = cls.comp2
|
||||||
|
bom_line.product_qty = 1
|
||||||
|
cls.bom_tracked = bom_form.save()
|
||||||
|
|
||||||
|
def test_subcontracting_record_component(self):
|
||||||
|
"""This test uses tracked (serial and lot) component
|
||||||
|
|
||||||
|
and tracked (serial) finished product
|
||||||
|
"""
|
||||||
|
todo_nb = 4
|
||||||
|
self.comp2.tracking = "lot"
|
||||||
|
self.finished_product.tracking = "serial"
|
||||||
|
|
||||||
|
# Create a receipt picking from the subcontractor
|
||||||
|
picking_form = Form(self.env["stock.picking"])
|
||||||
|
picking_form.picking_type_id = self.env.ref("stock.picking_type_in")
|
||||||
|
picking_form.partner_id = self.subcontractor_partner1
|
||||||
|
with picking_form.move_ids_without_package.new() as move:
|
||||||
|
move.product_id = self.finished_product
|
||||||
|
move.product_uom_qty = todo_nb
|
||||||
|
picking_receipt = picking_form.save()
|
||||||
|
picking_receipt.action_confirm()
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
picking_receipt.display_action_record_components,
|
||||||
|
msg="We should be able to call the 'record_components' button",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check the created manufacturing order
|
||||||
|
mo = self.env["mrp.production"].search([("bom_id", "=", self.bom_tracked.id)])
|
||||||
|
result = mo._check_exception_subcontracting_record_component()
|
||||||
|
self.assertDictEqual(result, {"type": "ir.actions.act_window_close"})
|
||||||
|
self.assertFalse(mo._has_been_recorded())
|
||||||
|
self.assertEqual(len(mo), 1)
|
||||||
|
self.assertEqual(len(mo.picking_ids), 0)
|
||||||
|
wh = picking_receipt.picking_type_id.warehouse_id
|
||||||
|
self.assertEqual(mo.picking_type_id, wh.subcontracting_type_id)
|
||||||
|
self.assertFalse(mo.picking_type_id.active)
|
||||||
|
|
||||||
|
# Create a RR
|
||||||
|
pg1 = self.env["procurement.group"].create({})
|
||||||
|
self.env["stock.warehouse.orderpoint"].create(
|
||||||
|
{
|
||||||
|
"name": "xxx",
|
||||||
|
"product_id": self.comp1_sn.id,
|
||||||
|
"product_min_qty": 0,
|
||||||
|
"product_max_qty": 0,
|
||||||
|
"location_id": self.env.user.company_id.subcontracting_location_id.id,
|
||||||
|
"group_id": pg1.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Run the scheduler and check the created picking
|
||||||
|
self.env["procurement.group"].run_scheduler()
|
||||||
|
picking = self.env["stock.picking"].search([("group_id", "=", pg1.id)])
|
||||||
|
self.assertEqual(len(picking), 1)
|
||||||
|
self.assertEqual(picking.picking_type_id, wh.out_type_id)
|
||||||
|
|
||||||
|
lot_comp2 = self.env["stock.production.lot"].create(
|
||||||
|
{
|
||||||
|
"name": "lot_comp2",
|
||||||
|
"product_id": self.comp2.id,
|
||||||
|
"company_id": self.env.company.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
serials_finished = []
|
||||||
|
serials_comp1 = []
|
||||||
|
for i in range(todo_nb):
|
||||||
|
serials_finished.append(
|
||||||
|
self.env["stock.production.lot"].create(
|
||||||
|
{
|
||||||
|
"name": "serial_fin_%s" % i,
|
||||||
|
"product_id": self.finished_product.id,
|
||||||
|
"company_id": self.env.company.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
serials_comp1.append(
|
||||||
|
self.env["stock.production.lot"].create(
|
||||||
|
{
|
||||||
|
"name": "serials_comp1_%s" % i,
|
||||||
|
"product_id": self.comp1_sn.id,
|
||||||
|
"company_id": self.env.company.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
mo_ids = self.env["mrp.production"]
|
||||||
|
for i in range(todo_nb):
|
||||||
|
action = picking_receipt.action_record_components()
|
||||||
|
mo = self.env["mrp.production"].browse(action["res_id"])
|
||||||
|
mo_form = Form(mo.with_context(**action["context"]), view=action["view_id"])
|
||||||
|
mo_form.lot_producing_id = serials_finished[i]
|
||||||
|
mo_form.qty_producing = 1
|
||||||
|
with mo_form.move_line_raw_ids.edit(0) as ml:
|
||||||
|
self.assertEqual(ml.product_id, self.comp1_sn)
|
||||||
|
ml.lot_id = serials_comp1[i]
|
||||||
|
with mo_form.move_line_raw_ids.edit(1) as ml:
|
||||||
|
self.assertEqual(ml.product_id, self.comp2)
|
||||||
|
ml.lot_id = lot_comp2
|
||||||
|
mo = mo_form.save()
|
||||||
|
mo.subcontracting_record_component()
|
||||||
|
mo_ids |= mo
|
||||||
|
self.assertTrue(mo._has_been_recorded())
|
||||||
|
|
||||||
|
self.assertFalse(
|
||||||
|
picking_receipt.display_action_record_components,
|
||||||
|
msg="We should not be able to call the 'record_components' button",
|
||||||
|
)
|
||||||
|
|
||||||
|
picking_receipt.button_validate()
|
||||||
|
self.assertEqual(mo.state, "done")
|
||||||
|
self.assertEqual(
|
||||||
|
mo.procurement_group_id.mrp_production_ids.mapped("state"),
|
||||||
|
["done"] * todo_nb,
|
||||||
|
)
|
||||||
|
self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), todo_nb)
|
||||||
|
self.assertEqual(
|
||||||
|
mo.procurement_group_id.mrp_production_ids.mapped("qty_produced"),
|
||||||
|
[1] * todo_nb,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Available quantities should be negative at the
|
||||||
|
# subcontracting location for each components
|
||||||
|
avail_qty_comp1 = self.env["stock.quant"]._get_available_quantity(
|
||||||
|
self.comp1_sn,
|
||||||
|
self.subcontractor_partner1.property_stock_subcontractor,
|
||||||
|
allow_negative=True,
|
||||||
|
)
|
||||||
|
avail_qty_comp2 = self.env["stock.quant"]._get_available_quantity(
|
||||||
|
self.comp2,
|
||||||
|
self.subcontractor_partner1.property_stock_subcontractor,
|
||||||
|
allow_negative=True,
|
||||||
|
)
|
||||||
|
avail_qty_finished = self.env["stock.quant"]._get_available_quantity(
|
||||||
|
self.finished_product, wh.lot_stock_id
|
||||||
|
)
|
||||||
|
self.assertEqual(avail_qty_comp1, -todo_nb)
|
||||||
|
self.assertEqual(avail_qty_comp2, -todo_nb)
|
||||||
|
self.assertEqual(avail_qty_finished, todo_nb)
|
||||||
@@ -1,17 +1,51 @@
|
|||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
from odoo.tests import Form
|
||||||
|
|
||||||
from odoo import Command
|
|
||||||
|
|
||||||
from odoo.addons.mrp_subcontracting.tests.common import TestMrpSubcontractingCommon
|
from odoo.addons.mrp_subcontracting.tests.common import TestMrpSubcontractingCommon
|
||||||
|
|
||||||
|
|
||||||
class MrpSubcontractingPurchaseTest(TestMrpSubcontractingCommon):
|
class MrpSubcontractingPurchaseTest(TestMrpSubcontractingCommon):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
if "purchase.order" not in self.env:
|
||||||
|
self.skipTest("`purchase` is not installed")
|
||||||
|
|
||||||
|
self.finished2, self.comp3 = self.env["product.product"].create(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "SuperProduct",
|
||||||
|
"type": "product",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Component",
|
||||||
|
"type": "consu",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.bom_finished2 = self.env["mrp.bom"].create(
|
||||||
|
{
|
||||||
|
"product_tmpl_id": self.finished2.product_tmpl_id.id,
|
||||||
|
"type": "subcontract",
|
||||||
|
"subcontractor_ids": [(6, 0, self.subcontractor_partner1.ids)],
|
||||||
|
"bom_line_ids": [
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
"product_id": self.comp3.id,
|
||||||
|
"product_qty": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def test_count_smart_buttons(self):
|
def test_count_smart_buttons(self):
|
||||||
resupply_sub_on_order_route = self.env["stock.location.route"].search(
|
resupply_sub_on_order_route = self.env["stock.location.route"].search(
|
||||||
[("name", "=", "Resupply Subcontractor on Order")]
|
[("name", "=", "Resupply Subcontractor on Order")]
|
||||||
)
|
)
|
||||||
(self.comp1 + self.comp2).write(
|
(self.comp1 + self.comp2).write(
|
||||||
{"route_ids": [Command.link(resupply_sub_on_order_route.id)]}
|
{"route_ids": [4, (resupply_sub_on_order_route.id)]}
|
||||||
)
|
)
|
||||||
|
|
||||||
# I create a draft Purchase Order for first in move for 10 kg at 50 euro
|
# I create a draft Purchase Order for first in move for 10 kg at 50 euro
|
||||||
@@ -19,14 +53,16 @@ class MrpSubcontractingPurchaseTest(TestMrpSubcontractingCommon):
|
|||||||
{
|
{
|
||||||
"partner_id": self.subcontractor_partner1.id,
|
"partner_id": self.subcontractor_partner1.id,
|
||||||
"order_line": [
|
"order_line": [
|
||||||
Command.create(
|
(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
{
|
{
|
||||||
"name": "finished",
|
"name": "finished",
|
||||||
"product_id": self.finished.id,
|
"product_id": self.finished.id,
|
||||||
"product_qty": 1.0,
|
"product_qty": 1.0,
|
||||||
"product_uom": self.finished.uom_id.id,
|
"product_uom": self.finished.uom_id.id,
|
||||||
"price_unit": 50.0,
|
"price_unit": 50.0,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -34,10 +70,119 @@ class MrpSubcontractingPurchaseTest(TestMrpSubcontractingCommon):
|
|||||||
|
|
||||||
po.button_confirm()
|
po.button_confirm()
|
||||||
|
|
||||||
self.assertEqual(po.subcontracting_resupply_picking_count, 1)
|
self.assertEqual(po.subcontracting_resupply_picking_count, 1, "Must be equal 1")
|
||||||
action1 = po.action_view_subcontracting_resupply()
|
action1 = po.action_view_subcontracting_resupply()
|
||||||
picking = self.env[action1["res_model"]].browse(action1["res_id"])
|
picking = self.env[action1["res_model"]].browse(action1["res_id"])
|
||||||
self.assertEqual(picking.subcontracting_source_purchase_count, 1)
|
self.assertEqual(
|
||||||
|
picking.subcontracting_source_purchase_count, 1, "Must be equal 1"
|
||||||
|
)
|
||||||
action2 = picking.action_view_subcontracting_source_purchase()
|
action2 = picking.action_view_subcontracting_source_purchase()
|
||||||
po_action2 = self.env[action2["res_model"]].browse(action2["res_id"])
|
po_action2 = self.env[action2["res_model"]].browse(action2["res_id"])
|
||||||
self.assertEqual(po_action2, po)
|
self.assertEqual(po_action2, po, "Should be equal")
|
||||||
|
|
||||||
|
def test_purchase_and_return01(self):
|
||||||
|
"""
|
||||||
|
The user buys 10 x a subcontracted product P. He receives the 10
|
||||||
|
products and then does a return with 3 x P. The test ensures that the
|
||||||
|
final received quantity is correctly computed
|
||||||
|
"""
|
||||||
|
po = self.env["purchase.order"].create(
|
||||||
|
{
|
||||||
|
"partner_id": self.subcontractor_partner1.id,
|
||||||
|
"order_line": [
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
"name": self.finished2.name,
|
||||||
|
"product_id": self.finished2.id,
|
||||||
|
"product_uom_qty": 10,
|
||||||
|
"product_uom": self.finished2.uom_id.id,
|
||||||
|
"price_unit": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
po.button_confirm()
|
||||||
|
|
||||||
|
mo = self.env["mrp.production"].search([("bom_id", "=", self.bom_finished2.id)])
|
||||||
|
self.assertTrue(mo, "Must be equal 'True'")
|
||||||
|
|
||||||
|
receipt = po.picking_ids
|
||||||
|
receipt.move_lines.quantity_done = 10
|
||||||
|
receipt.button_validate()
|
||||||
|
|
||||||
|
return_form = Form(
|
||||||
|
self.env["stock.return.picking"].with_context(
|
||||||
|
active_id=receipt.id, active_model="stock.picking"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
with return_form.product_return_moves.edit(0) as line:
|
||||||
|
line.quantity = 3
|
||||||
|
line.to_refund = True
|
||||||
|
return_wizard = return_form.save()
|
||||||
|
return_id, _ = return_wizard._create_returns()
|
||||||
|
|
||||||
|
return_picking = self.env["stock.picking"].browse(return_id)
|
||||||
|
return_picking.move_lines.quantity_done = 3
|
||||||
|
return_picking.button_validate()
|
||||||
|
|
||||||
|
self.assertEqual(self.finished2.qty_available, 7.0, "Must be equal 7.0")
|
||||||
|
self.assertEqual(po.order_line.qty_received, 7.0, "Must be equal 7.0")
|
||||||
|
|
||||||
|
def test_purchase_and_return02(self):
|
||||||
|
"""
|
||||||
|
The user buys 10 x a subcontracted product P. He receives the 10
|
||||||
|
products and then does a return with 3 x P (with the flag to_refund
|
||||||
|
disabled and the subcontracting location as return location). The test
|
||||||
|
ensures that the final received quantity is correctly computed
|
||||||
|
"""
|
||||||
|
grp_multi_loc = self.env.ref("stock.group_stock_multi_locations")
|
||||||
|
self.env.user.write({"groups_id": [(4, grp_multi_loc.id)]})
|
||||||
|
|
||||||
|
po = self.env["purchase.order"].create(
|
||||||
|
{
|
||||||
|
"partner_id": self.subcontractor_partner1.id,
|
||||||
|
"order_line": [
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
"name": self.finished2.name,
|
||||||
|
"product_id": self.finished2.id,
|
||||||
|
"product_uom_qty": 10,
|
||||||
|
"product_uom": self.finished2.uom_id.id,
|
||||||
|
"price_unit": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
po.button_confirm()
|
||||||
|
|
||||||
|
mo = self.env["mrp.production"].search([("bom_id", "=", self.bom_finished2.id)])
|
||||||
|
self.assertTrue(mo, "Must be equal 'True'")
|
||||||
|
|
||||||
|
receipt = po.picking_ids
|
||||||
|
receipt.move_lines.quantity_done = 10
|
||||||
|
receipt.button_validate()
|
||||||
|
|
||||||
|
return_form = Form(
|
||||||
|
self.env["stock.return.picking"].with_context(
|
||||||
|
active_id=receipt.id, active_model="stock.picking"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return_form.location_id = self.env.company.subcontracting_location_id
|
||||||
|
with return_form.product_return_moves.edit(0) as line:
|
||||||
|
line.quantity = 3
|
||||||
|
line.to_refund = False
|
||||||
|
return_wizard = return_form.save()
|
||||||
|
return_id, _ = return_wizard._create_returns()
|
||||||
|
|
||||||
|
return_picking = self.env["stock.picking"].browse(return_id)
|
||||||
|
return_picking.move_lines.quantity_done = 3
|
||||||
|
return_picking.button_validate()
|
||||||
|
|
||||||
|
self.assertEqual(self.finished2.qty_available, 7.0, "Must be equal 7.0")
|
||||||
|
self.assertEqual(po.order_line.qty_received, 10.0, "Must be equal 10.0")
|
||||||
|
|||||||
542
mrp_subcontracting_purchase/tests/test_purchase_order.py
Normal file
542
mrp_subcontracting_purchase/tests/test_purchase_order.py
Normal file
@@ -0,0 +1,542 @@
|
|||||||
|
from odoo.tests import Form
|
||||||
|
|
||||||
|
from odoo.addons.mrp_subcontracting.tests.common import TestMrpSubcontractingCommon
|
||||||
|
|
||||||
|
|
||||||
|
class TestPurchaseOrder(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.location.route"].search(
|
||||||
|
[("name", "=", "Replenish on Order (MTO)")]
|
||||||
|
)
|
||||||
|
resupply_route = self.env["stock.location.route"].search(
|
||||||
|
[("name", "=", "Resupply Subcontractor on Order")]
|
||||||
|
)
|
||||||
|
buy_route = self.env["stock.location.route"].search([("name", "=", "Buy")])
|
||||||
|
dropship_route = self.env["stock.location.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,
|
||||||
|
"name": 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, "name": 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.location.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(
|
||||||
|
{
|
||||||
|
"name": vendor.id,
|
||||||
|
"price": 50,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.comp1.write(
|
||||||
|
{"seller_ids": [(0, 0, {"name": 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
|
||||||
|
origin = picking_delivery.origin
|
||||||
|
self.assertEqual(len(picking_delivery), 1)
|
||||||
|
self.assertEqual(len(picking_delivery.move_ids_without_package), 1)
|
||||||
|
self.assertEqual(picking_delivery.partner_id, self.subcontractor_partner1)
|
||||||
|
|
||||||
|
# Change the purchased quantity to 2
|
||||||
|
po.order_line.write({"product_qty": 2})
|
||||||
|
# Check that two deliveries with 1 component for the subcontractor have been created
|
||||||
|
picking_deliveries = self.env["stock.picking"].search([("origin", "=", origin)])
|
||||||
|
self.assertEqual(len(picking_deliveries), 2)
|
||||||
|
self.assertEqual(picking_deliveries[0].partner_id, self.subcontractor_partner1)
|
||||||
|
self.assertTrue(picking_deliveries[0].state != "cancel")
|
||||||
|
move1 = picking_deliveries[0].move_ids_without_package
|
||||||
|
self.assertEqual(picking_deliveries[1].partner_id, self.subcontractor_partner1)
|
||||||
|
self.assertTrue(picking_deliveries[1].state != "cancel")
|
||||||
|
move2 = picking_deliveries[1].move_ids_without_package
|
||||||
|
self.assertEqual(move1.product_id, self.comp1)
|
||||||
|
self.assertEqual(move1.product_uom_qty, 1)
|
||||||
|
self.assertEqual(move2.product_id, self.comp1)
|
||||||
|
self.assertEqual(move2.product_uom_qty, 1)
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dropship_subcontractor_route = self.env["stock.location.route"].search(
|
||||||
|
[("name", "=", "Dropship Subcontractor on Order")]
|
||||||
|
)
|
||||||
|
# first_rule = dropship_subcontractor_route.rule_ids.filtered(
|
||||||
|
# lambda rule: rule.location_id == subcontract_location)[0]
|
||||||
|
|
||||||
|
# Set a value for the first rule
|
||||||
|
# first_rule.write({'location_id': sub_location.id})
|
||||||
|
|
||||||
|
# Copy the first rule with the updated value
|
||||||
|
# copied_rule = first_rule.copy(default={'location_id': sub_location.id})
|
||||||
|
|
||||||
|
dropship_subcontractor_route.rule_ids.filtered(
|
||||||
|
lambda rule: rule.location_id == subcontract_location
|
||||||
|
).copy(default={"location_id": sub_location.id})
|
||||||
|
dropship_subcontractor_route.rule_ids.filtered(
|
||||||
|
lambda rule: rule.location_src_id == subcontract_location
|
||||||
|
).copy(default={"location_src_id": sub_location.id})
|
||||||
|
|
||||||
|
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, {"name": subcontractor.id})],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Component",
|
||||||
|
"type": "consu",
|
||||||
|
"seller_ids": [(0, 0, {"name": 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
|
||||||
|
"""
|
||||||
|
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, {"name": 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, {"name": super_subcontractor.id})],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Product",
|
||||||
|
"type": "product",
|
||||||
|
"seller_ids": [(0, 0, {"name": 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}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
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": 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
|
||||||
|
)
|
||||||
|
|
||||||
|
delivery.move_line_ids.qty_done = 1.0
|
||||||
|
delivery.button_validate()
|
||||||
|
|
||||||
|
self.assertEqual(po.order_line.qty_received, 1.0)
|
||||||
|
|
||||||
|
def test_action_view_picking(self):
|
||||||
|
self.product_component = self.env["product.product"].create(
|
||||||
|
{
|
||||||
|
"name": "Component",
|
||||||
|
"type": "consu",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.env["mrp.bom"].create(
|
||||||
|
{
|
||||||
|
"product_tmpl_id": self.finished.product_tmpl_id.id,
|
||||||
|
"product_qty": 1,
|
||||||
|
"type": "phantom",
|
||||||
|
"bom_line_ids": [
|
||||||
|
(0, 0, {"product_id": self.product_component.id, "product_qty": 1}),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
result = po.action_view_picking()
|
||||||
|
context = result.get("context")
|
||||||
|
self.assertEqual(po.name, context.get("default_origin"))
|
||||||
|
self.assertEqual(po.partner_id.id, context.get("default_partner_id"))
|
||||||
|
self.assertEqual(po.picking_type_id.id, context.get("default_picking_type_id"))
|
||||||
|
self.assertListEqual(result.get("domain"), [("id", "in", [])])
|
||||||
|
po.button_confirm()
|
||||||
|
po.order_line._compute_qty_received()
|
||||||
|
self.assertFalse(po.order_line.qty_received)
|
||||||
11
mrp_subcontracting_purchase/tests/test_res_company.py
Normal file
11
mrp_subcontracting_purchase/tests/test_res_company.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from odoo.tests import TransactionCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestResCompany(TransactionCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestResCompany, self).setUp()
|
||||||
|
|
||||||
|
def test_create_per_company_rules(self):
|
||||||
|
company = self.env.company
|
||||||
|
result = company._create_per_company_rules()
|
||||||
|
self.assertIsNone(result)
|
||||||
142
mrp_subcontracting_purchase/tests/test_stock_move.py
Normal file
142
mrp_subcontracting_purchase/tests/test_stock_move.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
from odoo.tests import Form, TransactionCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestStockMove(TransactionCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestStockMove, self).setUp()
|
||||||
|
self.stock_location = self.env.ref("stock.stock_location_stock")
|
||||||
|
self.product = self.env["product.product"].create(
|
||||||
|
{
|
||||||
|
"name": "Product no BoM",
|
||||||
|
"type": "product",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
mo_form = Form(self.env["mrp.production"])
|
||||||
|
mo_form.product_id = self.product
|
||||||
|
self.mo = mo_form.save()
|
||||||
|
|
||||||
|
self.uom_kg = self.env.ref("uom.product_uom_kgm")
|
||||||
|
self.warehouse = self.env["stock.warehouse"].search(
|
||||||
|
[("lot_stock_id", "=", self.stock_location.id)], limit=1
|
||||||
|
)
|
||||||
|
self.picking01 = self.env["stock.move"].create(
|
||||||
|
{
|
||||||
|
"name": "mrp_move",
|
||||||
|
"product_id": self.product.id,
|
||||||
|
"product_uom": self.ref("uom.product_uom_unit"),
|
||||||
|
"production_id": self.mo.id,
|
||||||
|
"location_id": self.ref("stock.stock_location_stock"),
|
||||||
|
"location_dest_id": self.ref("stock.stock_location_output"),
|
||||||
|
"product_uom_qty": 0,
|
||||||
|
"quantity_done": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.subcontracor = self.env["res.partner"].create(
|
||||||
|
{
|
||||||
|
"name": "Subc Partner",
|
||||||
|
"property_stock_subcontractor": self.ref("stock.stock_location_stock"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.vendor = self.env["res.partner"].create({"name": "vendor #1"})
|
||||||
|
|
||||||
|
dropship_subcontractor_route = self.env["stock.location.route"].search(
|
||||||
|
[("name", "=", "Dropship Subcontractor on Order")]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.product_component = self.env["product.product"].create(
|
||||||
|
{
|
||||||
|
"name": "Component",
|
||||||
|
"type": "consu",
|
||||||
|
"seller_ids": [(0, 0, {"name": self.vendor.id})],
|
||||||
|
"route_ids": [(6, 0, dropship_subcontractor_route.ids)],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.env["stock.location"].create(
|
||||||
|
{
|
||||||
|
"name": "Super Location",
|
||||||
|
"location_id": self.ref("stock.stock_location_stock"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.customer_location = self.env.ref("stock.stock_location_customers")
|
||||||
|
|
||||||
|
def test_get_subcontract_production(self):
|
||||||
|
result = self.picking01._get_subcontract_production()
|
||||||
|
self.assertFalse(result)
|
||||||
|
|
||||||
|
self.env["mrp.bom"].create(
|
||||||
|
{
|
||||||
|
"product_tmpl_id": self.product.product_tmpl_id.id,
|
||||||
|
"product_qty": 1,
|
||||||
|
"type": "subcontract",
|
||||||
|
"subcontractor_ids": [(6, 0, self.subcontracor.ids)],
|
||||||
|
"bom_line_ids": [
|
||||||
|
(0, 0, {"product_id": self.product_component.id, "product_qty": 1})
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.picking01.write(
|
||||||
|
{
|
||||||
|
"is_subcontract": True,
|
||||||
|
"move_orig_ids": [
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
"name": "orig_move",
|
||||||
|
"product_id": self.product.id,
|
||||||
|
"product_uom": self.ref("uom.product_uom_unit"),
|
||||||
|
"production_id": self.mo.id,
|
||||||
|
"location_id": self.ref("stock.stock_location_stock"),
|
||||||
|
"location_dest_id": self.ref("stock.stock_location_output"),
|
||||||
|
"product_uom_qty": 0,
|
||||||
|
"quantity_done": 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
picking_ship = self.env["stock.picking"].create(
|
||||||
|
{
|
||||||
|
"partner_id": self.env["res.partner"].create({"name": "A partner"}).id,
|
||||||
|
"picking_type_id": self.warehouse.out_type_id.id,
|
||||||
|
"location_id": self.stock_location.id,
|
||||||
|
"location_dest_id": self.customer_location.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.picking01.write(
|
||||||
|
{
|
||||||
|
"move_line_ids": [
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
"product_id": self.product_component.id,
|
||||||
|
"product_uom_id": self.uom_kg.id,
|
||||||
|
"picking_id": picking_ship.id,
|
||||||
|
"qty_done": 5,
|
||||||
|
"location_id": self.stock_location.id,
|
||||||
|
"location_dest_id": self.customer_location.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = self.picking01._get_subcontract_production()
|
||||||
|
self.assertTrue(result)
|
||||||
|
self.assertTrue(self.picking01.show_details_visible)
|
||||||
|
self.assertTrue(self.picking01.show_subcontracting_details_visible)
|
||||||
|
self.assertFalse(self.picking01.display_assign_serial)
|
||||||
|
|
||||||
|
result = picking_ship._get_subcontract_production()
|
||||||
|
self.assertFalse(result)
|
||||||
|
result = picking_ship.action_view_subcontracting_source_purchase()
|
||||||
|
self.assertEqual(result.get("res_model"), "purchase.order")
|
||||||
|
self.assertEqual(result.get("type"), "ir.actions.act_window")
|
||||||
|
self.assertEqual(
|
||||||
|
result.get("name"), "Source PO of {}".format(picking_ship.name)
|
||||||
|
)
|
||||||
|
self.assertListEqual(result.get("domain"), [("id", "in", [])])
|
||||||
|
self.assertEqual(result.get("view_mode"), "tree,form")
|
||||||
@@ -1,16 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<record id="purchase_order_form_mrp_subcontracting_purchase" model="ir.ui.view">
|
<record id="po_form_mrp_subcontracting_purchase" model="ir.ui.view">
|
||||||
<field
|
<field name="name">
|
||||||
name="name"
|
purchase.order.inherited.form.mrp.subcontracting.purchase
|
||||||
>purchase.order.inherited.form.mrp.subcontracting.purchase</field>
|
</field>
|
||||||
<field name="model">purchase.order</field>
|
<field name="model">purchase.order</field>
|
||||||
<field name="inherit_id" ref="purchase.purchase_order_form" />
|
<field name="inherit_id" ref="purchase.purchase_order_form" />
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath
|
<button name="action_view_picking" position='before'>
|
||||||
expr="//div[hasclass('oe_button_box')]/button[@name='action_view_picking']"
|
|
||||||
position="before"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
class="oe_stat_button"
|
class="oe_stat_button"
|
||||||
name="action_view_subcontracting_resupply"
|
name="action_view_subcontracting_resupply"
|
||||||
@@ -20,13 +17,13 @@
|
|||||||
groups="stock.group_stock_user"
|
groups="stock.group_stock_user"
|
||||||
>
|
>
|
||||||
<div class="o_field_widget o_stat_info">
|
<div class="o_field_widget o_stat_info">
|
||||||
<span class="o_stat_value"><field
|
<span class="o_stat_value">
|
||||||
name="subcontracting_resupply_picking_count"
|
<field name="subcontracting_resupply_picking_count" />
|
||||||
/></span>
|
</span>
|
||||||
<span class="o_stat_text">Resupply</span>
|
<span class="o_stat_text">Resupply</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</xpath>
|
</button>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@@ -10,14 +10,14 @@
|
|||||||
class="oe_stat_button"
|
class="oe_stat_button"
|
||||||
name="action_view_subcontracting_source_purchase"
|
name="action_view_subcontracting_source_purchase"
|
||||||
type="object"
|
type="object"
|
||||||
icon="fa-shopping-cart"
|
icon="fa-credit-card"
|
||||||
attrs="{'invisible': [('subcontracting_source_purchase_count', '=', 0)]}"
|
attrs="{'invisible': [('subcontracting_source_purchase_count', '=', 0)]}"
|
||||||
groups="stock.group_stock_user"
|
groups="stock.group_stock_user"
|
||||||
>
|
>
|
||||||
<div class="o_field_widget o_stat_info">
|
<div class="o_field_widget o_stat_info">
|
||||||
<span class="o_stat_value"><field
|
<span class="o_stat_value">
|
||||||
name="subcontracting_source_purchase_count"
|
<field name="subcontracting_source_purchase_count" />
|
||||||
/></span>
|
</span>
|
||||||
<span class="o_stat_text">Source PO</span>
|
<span class="o_stat_text">Source PO</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user