[MIG] mrp_subcontracting_purchase: Adapt to 14.0

This commit is contained in:
geomer198
2023-10-13 14:08:59 +04:00
parent 89fd156c01
commit 93cd12b38c
34 changed files with 2148 additions and 42 deletions

View File

@@ -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.

View File

@@ -1,3 +1 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models from . import models

View File

@@ -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",

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -0,0 +1,5 @@
<odoo>
<record id="product_category_subcontracting_product" model="product.category">
<field name="name">Subcontracting products</field>
</record>
</odoo>

View 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>

View File

@@ -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>

View 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>

View 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>

View File

View 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

View 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)

View File

@@ -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()

View 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

View 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

View 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

View File

@@ -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

View 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

View 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

View 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.

View File

@@ -0,0 +1,4 @@
- Cetmix <@cetmix.com>
- Ooops404 <https://ooops404.com>
- Dessan Hemrayev <dessanhemrayev@gmail.com>
- Maksim Shurupov <geomer198@gmail.com>

View 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”

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -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

View 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)

View File

@@ -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")

View 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)

View 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)

View 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")

View File

@@ -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>

View File

@@ -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>