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
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
"name": "Purchase and Subcontracting Management",
|
||||
"version": "14.0.1.0.0",
|
||||
"category": "Manufacturing/Purchase",
|
||||
"summary": """
|
||||
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",
|
||||
"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/mrp_subcontracting_dropshipping_data.xml",
|
||||
"views/purchase_order_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,
|
||||
"auto_install": True,
|
||||
"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.
|
||||
|
||||
from . import stock_move
|
||||
from . import stock_warehouse
|
||||
from . import stock_picking
|
||||
from . import stock_rule
|
||||
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 import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class PurchaseOrder(models.Model):
|
||||
@@ -23,8 +22,102 @@ class PurchaseOrder(models.Model):
|
||||
return self._get_action_view_picking(self._get_subcontracting_resupplies())
|
||||
|
||||
def _get_subcontracting_resupplies(self):
|
||||
moves_subcontracted = self.order_line.move_ids.filtered(
|
||||
lambda m: m.is_subcontract
|
||||
return self.order_line.move_ids.filtered(lambda m: m.is_subcontract).mapped(
|
||||
"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.tools.float_utils import float_compare
|
||||
|
||||
|
||||
class StockPicking(models.Model):
|
||||
@@ -14,12 +15,14 @@ class StockPicking(models.Model):
|
||||
|
||||
@api.depends("move_lines.move_dest_ids.raw_material_production_id")
|
||||
def _compute_subcontracting_source_purchase_count(self):
|
||||
"""Compute number of subcontracting Purchase Order Source"""
|
||||
for picking in self:
|
||||
picking.subcontracting_source_purchase_count = len(
|
||||
picking._get_subcontracting_source_purchase()
|
||||
)
|
||||
|
||||
def action_view_subcontracting_source_purchase(self):
|
||||
"""Returns action for subcontracting source purchase"""
|
||||
purchase_order_ids = self._get_subcontracting_source_purchase().ids
|
||||
action = {
|
||||
"res_model": "purchase.order",
|
||||
@@ -35,7 +38,7 @@ class StockPicking(models.Model):
|
||||
else:
|
||||
action.update(
|
||||
{
|
||||
"name": _("Source PO of %s", self.name),
|
||||
"name": _("Source PO of %s") % self.name,
|
||||
"domain": [("id", "in", purchase_order_ids)],
|
||||
"view_mode": "tree,form",
|
||||
}
|
||||
@@ -43,7 +46,91 @@ class StockPicking(models.Model):
|
||||
return action
|
||||
|
||||
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
|
||||
lambda m: m.is_subcontract
|
||||
)
|
||||
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_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 import Command
|
||||
from odoo.tests import Form
|
||||
|
||||
from odoo.addons.mrp_subcontracting.tests.common import 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):
|
||||
resupply_sub_on_order_route = self.env["stock.location.route"].search(
|
||||
[("name", "=", "Resupply Subcontractor on Order")]
|
||||
)
|
||||
(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
|
||||
@@ -19,14 +53,16 @@ class MrpSubcontractingPurchaseTest(TestMrpSubcontractingCommon):
|
||||
{
|
||||
"partner_id": self.subcontractor_partner1.id,
|
||||
"order_line": [
|
||||
Command.create(
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "finished",
|
||||
"product_id": self.finished.id,
|
||||
"product_qty": 1.0,
|
||||
"product_uom": self.finished.uom_id.id,
|
||||
"price_unit": 50.0,
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
@@ -34,10 +70,119 @@ class MrpSubcontractingPurchaseTest(TestMrpSubcontractingCommon):
|
||||
|
||||
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()
|
||||
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()
|
||||
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" ?>
|
||||
<odoo>
|
||||
<record id="purchase_order_form_mrp_subcontracting_purchase" model="ir.ui.view">
|
||||
<field
|
||||
name="name"
|
||||
>purchase.order.inherited.form.mrp.subcontracting.purchase</field>
|
||||
<record id="po_form_mrp_subcontracting_purchase" model="ir.ui.view">
|
||||
<field name="name">
|
||||
purchase.order.inherited.form.mrp.subcontracting.purchase
|
||||
</field>
|
||||
<field name="model">purchase.order</field>
|
||||
<field name="inherit_id" ref="purchase.purchase_order_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath
|
||||
expr="//div[hasclass('oe_button_box')]/button[@name='action_view_picking']"
|
||||
position="before"
|
||||
>
|
||||
<button name="action_view_picking" position='before'>
|
||||
<button
|
||||
class="oe_stat_button"
|
||||
name="action_view_subcontracting_resupply"
|
||||
@@ -20,13 +17,13 @@
|
||||
groups="stock.group_stock_user"
|
||||
>
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value"><field
|
||||
name="subcontracting_resupply_picking_count"
|
||||
/></span>
|
||||
<span class="o_stat_value">
|
||||
<field name="subcontracting_resupply_picking_count" />
|
||||
</span>
|
||||
<span class="o_stat_text">Resupply</span>
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
</button>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
class="oe_stat_button"
|
||||
name="action_view_subcontracting_source_purchase"
|
||||
type="object"
|
||||
icon="fa-shopping-cart"
|
||||
icon="fa-credit-card"
|
||||
attrs="{'invisible': [('subcontracting_source_purchase_count', '=', 0)]}"
|
||||
groups="stock.group_stock_user"
|
||||
>
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value"><field
|
||||
name="subcontracting_source_purchase_count"
|
||||
/></span>
|
||||
<span class="o_stat_value">
|
||||
<field name="subcontracting_source_purchase_count" />
|
||||
</span>
|
||||
<span class="o_stat_text">Source PO</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user