diff --git a/mrp_subcontracting_purchase/README.rst b/mrp_subcontracting_purchase/README.rst
index e69de29bb2d..b0c8aa6b183 100644
--- a/mrp_subcontracting_purchase/README.rst
+++ b/mrp_subcontracting_purchase/README.rst
@@ -0,0 +1,2 @@
+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.
diff --git a/mrp_subcontracting_purchase/__init__.py b/mrp_subcontracting_purchase/__init__.py
index d6210b1285d..0650744f6bc 100644
--- a/mrp_subcontracting_purchase/__init__.py
+++ b/mrp_subcontracting_purchase/__init__.py
@@ -1,3 +1 @@
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
from . import models
diff --git a/mrp_subcontracting_purchase/__manifest__.py b/mrp_subcontracting_purchase/__manifest__.py
index 283cc06bb4c..ef10b8c57d1 100644
--- a/mrp_subcontracting_purchase/__manifest__.py
+++ b/mrp_subcontracting_purchase/__manifest__.py
@@ -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/0001_demo_product_category.xml",
+ "demo/0011_demo_stock_location.xml",
+ "demo/0002_demo_partner_subcontract.xml",
+ "demo/0003_demo_product_test_product.xml",
+ "demo/0005_demo_product_supplierinfo.xml",
+ "demo/0004_demo_bom_subcontract.xml",
+ "demo/0006_demo_stock_rules.xml",
+ ],
"installable": True,
"auto_install": True,
"license": "LGPL-3",
diff --git a/mrp_subcontracting_purchase/data/mrp_subcontracting_dropshipping_data.xml b/mrp_subcontracting_purchase/data/mrp_subcontracting_dropshipping_data.xml
new file mode 100644
index 00000000000..855b9d91559
--- /dev/null
+++ b/mrp_subcontracting_purchase/data/mrp_subcontracting_dropshipping_data.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ Dropship Subcontractor on Order
+ 5
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mrp_subcontracting_purchase/demo/0001_demo_product_category.xml b/mrp_subcontracting_purchase/demo/0001_demo_product_category.xml
new file mode 100644
index 00000000000..cb1d54a4b33
--- /dev/null
+++ b/mrp_subcontracting_purchase/demo/0001_demo_product_category.xml
@@ -0,0 +1,5 @@
+
+
+ Subcontracting products
+
+
diff --git a/mrp_subcontracting_purchase/demo/0002_demo_partner_subcontract.xml b/mrp_subcontracting_purchase/demo/0002_demo_partner_subcontract.xml
new file mode 100644
index 00000000000..f66140c0f62
--- /dev/null
+++ b/mrp_subcontracting_purchase/demo/0002_demo_partner_subcontract.xml
@@ -0,0 +1,15 @@
+
+
+ Sub 1
+
+
+
+
+ Sub 2
+
+
+
+
+ Vendor
+
+
diff --git a/mrp_subcontracting_purchase/demo/0003_demo_product_test_product.xml b/mrp_subcontracting_purchase/demo/0003_demo_product_test_product.xml
new file mode 100644
index 00000000000..600f4c31cad
--- /dev/null
+++ b/mrp_subcontracting_purchase/demo/0003_demo_product_test_product.xml
@@ -0,0 +1,40 @@
+
+
+ Finished
+ product
+ 10
+
+
+
+
+
+
+
+ Component
+ product
+ 10
+
+
+
+
+
+
+
+ Element
+ product
+ 10
+
+
+
+
+
+
diff --git a/mrp_subcontracting_purchase/demo/0004_demo_bom_subcontract.xml b/mrp_subcontracting_purchase/demo/0004_demo_bom_subcontract.xml
new file mode 100644
index 00000000000..5a932a1b2d5
--- /dev/null
+++ b/mrp_subcontracting_purchase/demo/0004_demo_bom_subcontract.xml
@@ -0,0 +1,58 @@
+
+
+
+
+ subcontract
+
+ 1
+
+
+
+
+
+ subcontract
+
+ 1
+
+
+
+
+ 1
+
+ 5
+
+
+
+
+
+ 1
+
+ 5
+
+
+
+
diff --git a/mrp_subcontracting_purchase/demo/0005_demo_product_supplierinfo.xml b/mrp_subcontracting_purchase/demo/0005_demo_product_supplierinfo.xml
new file mode 100644
index 00000000000..da7c78cbe1b
--- /dev/null
+++ b/mrp_subcontracting_purchase/demo/0005_demo_product_supplierinfo.xml
@@ -0,0 +1,32 @@
+
+
+
+
+ 10
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+ 10
+
+
+
+
diff --git a/mrp_subcontracting_purchase/demo/0006_demo_stock_rules.xml b/mrp_subcontracting_purchase/demo/0006_demo_stock_rules.xml
new file mode 100644
index 00000000000..e92055fb530
--- /dev/null
+++ b/mrp_subcontracting_purchase/demo/0006_demo_stock_rules.xml
@@ -0,0 +1,68 @@
+
+
+
+ Vendors — Sub 1
+ buy
+
+
+
+
+
+
+
+ Vendors — Sub 2
+ buy
+
+
+
+
+
+
+
+ Sub 1 — Production
+ pull
+
+
+
+
+
+ make_to_order
+
+
+
+
+ Sub 2 — Production
+ pull
+
+
+
+
+
+ make_to_order
+
+
+
diff --git a/mrp_subcontracting_purchase/demo/0011_demo_stock_location.xml b/mrp_subcontracting_purchase/demo/0011_demo_stock_location.xml
new file mode 100644
index 00000000000..b87c5bfcb4d
--- /dev/null
+++ b/mrp_subcontracting_purchase/demo/0011_demo_stock_location.xml
@@ -0,0 +1,21 @@
+
+
+
+ sub1
+
+
+
+
+ sub2
+
+
+
+
diff --git a/mrp_subcontracting_purchase/i18n/it.po b/mrp_subcontracting_purchase/i18n/it.po
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/mrp_subcontracting_purchase/models/__init__.py b/mrp_subcontracting_purchase/models/__init__.py
index 00f4d8ade64..352f8d71f73 100644
--- a/mrp_subcontracting_purchase/models/__init__.py
+++ b/mrp_subcontracting_purchase/models/__init__.py
@@ -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
diff --git a/mrp_subcontracting_purchase/models/mrp_production.py b/mrp_subcontracting_purchase/models/mrp_production.py
new file mode 100644
index 00000000000..1653727995f
--- /dev/null
+++ b/mrp_subcontracting_purchase/models/mrp_production.py
@@ -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)
diff --git a/mrp_subcontracting_purchase/models/purchase_order.py b/mrp_subcontracting_purchase/models/purchase_order.py
index 31323bbf27e..260263a02df 100644
--- a/mrp_subcontracting_purchase/models/purchase_order.py
+++ b/mrp_subcontracting_purchase/models/purchase_order.py
@@ -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 @@ def action_view_subcontracting_resupply(self):
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"
+ )
+
+ @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
)
- subcontracted_productions = moves_subcontracted.move_orig_ids.production_id
- return subcontracted_productions.picking_ids
+
+ 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()
diff --git a/mrp_subcontracting_purchase/models/purchase_order_line.py b/mrp_subcontracting_purchase/models/purchase_order_line.py
new file mode 100644
index 00000000000..0044f86cd57
--- /dev/null
+++ b/mrp_subcontracting_purchase/models/purchase_order_line.py
@@ -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
diff --git a/mrp_subcontracting_purchase/models/res_company.py b/mrp_subcontracting_purchase/models/res_company.py
new file mode 100644
index 00000000000..2afbb488a70
--- /dev/null
+++ b/mrp_subcontracting_purchase/models/res_company.py
@@ -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
diff --git a/mrp_subcontracting_purchase/models/stock_move.py b/mrp_subcontracting_purchase/models/stock_move.py
new file mode 100644
index 00000000000..b6caf1a262b
--- /dev/null
+++ b/mrp_subcontracting_purchase/models/stock_move.py
@@ -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
diff --git a/mrp_subcontracting_purchase/models/stock_picking.py b/mrp_subcontracting_purchase/models/stock_picking.py
index 5fdca7c0a82..309923c72ea 100644
--- a/mrp_subcontracting_purchase/models/stock_picking.py
+++ b/mrp_subcontracting_purchase/models/stock_picking.py
@@ -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 @@ def action_view_subcontracting_source_purchase(self):
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 @@ def action_view_subcontracting_source_purchase(self):
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
diff --git a/mrp_subcontracting_purchase/models/stock_rule.py b/mrp_subcontracting_purchase/models/stock_rule.py
new file mode 100644
index 00000000000..d865cd10b96
--- /dev/null
+++ b/mrp_subcontracting_purchase/models/stock_rule.py
@@ -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
diff --git a/mrp_subcontracting_purchase/models/stock_warehouse.py b/mrp_subcontracting_purchase/models/stock_warehouse.py
new file mode 100644
index 00000000000..e9e7004171e
--- /dev/null
+++ b/mrp_subcontracting_purchase/models/stock_warehouse.py
@@ -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
diff --git a/mrp_subcontracting_purchase/readme/CONFIGURATION.md b/mrp_subcontracting_purchase/readme/CONFIGURATION.md
new file mode 100644
index 00000000000..ba1a1570073
--- /dev/null
+++ b/mrp_subcontracting_purchase/readme/CONFIGURATION.md
@@ -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.
diff --git a/mrp_subcontracting_purchase/readme/CONTRIBUTORS.md b/mrp_subcontracting_purchase/readme/CONTRIBUTORS.md
new file mode 100644
index 00000000000..204dc397154
--- /dev/null
+++ b/mrp_subcontracting_purchase/readme/CONTRIBUTORS.md
@@ -0,0 +1,4 @@
+- Cetmix <@cetmix.com>
+- Ooops404
+- Dessan Hemrayev
+- Maksim Shurupov
diff --git a/mrp_subcontracting_purchase/readme/DESCRIPTION.md b/mrp_subcontracting_purchase/readme/DESCRIPTION.md
new file mode 100644
index 00000000000..308d12103d2
--- /dev/null
+++ b/mrp_subcontracting_purchase/readme/DESCRIPTION.md
@@ -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”
diff --git a/mrp_subcontracting_purchase/readme/HISTORY.md b/mrp_subcontracting_purchase/readme/HISTORY.md
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/mrp_subcontracting_purchase/static/description/icon.png b/mrp_subcontracting_purchase/static/description/icon.png
new file mode 100644
index 00000000000..3a0328b516c
Binary files /dev/null and b/mrp_subcontracting_purchase/static/description/icon.png differ
diff --git a/mrp_subcontracting_purchase/tests/__init__.py b/mrp_subcontracting_purchase/tests/__init__.py
index 6296cfac695..f627f03fc2d 100644
--- a/mrp_subcontracting_purchase/tests/__init__.py
+++ b/mrp_subcontracting_purchase/tests/__init__.py
@@ -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
diff --git a/mrp_subcontracting_purchase/tests/test_mrp_production.py b/mrp_subcontracting_purchase/tests/test_mrp_production.py
new file mode 100644
index 00000000000..d3619d63d22
--- /dev/null
+++ b/mrp_subcontracting_purchase/tests/test_mrp_production.py
@@ -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)
diff --git a/mrp_subcontracting_purchase/tests/test_mrp_subcontracting_purchase.py b/mrp_subcontracting_purchase/tests/test_mrp_subcontracting_purchase.py
index ee8fa37a52b..165f1bee0a9 100644
--- a/mrp_subcontracting_purchase/tests/test_mrp_subcontracting_purchase.py
+++ b/mrp_subcontracting_purchase/tests/test_mrp_subcontracting_purchase.py
@@ -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 @@ def test_count_smart_buttons(self):
{
"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 @@ def test_count_smart_buttons(self):
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")
diff --git a/mrp_subcontracting_purchase/tests/test_purchase_order.py b/mrp_subcontracting_purchase/tests/test_purchase_order.py
new file mode 100644
index 00000000000..14421723669
--- /dev/null
+++ b/mrp_subcontracting_purchase/tests/test_purchase_order.py
@@ -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)
diff --git a/mrp_subcontracting_purchase/tests/test_res_company.py b/mrp_subcontracting_purchase/tests/test_res_company.py
new file mode 100644
index 00000000000..ef0f6069901
--- /dev/null
+++ b/mrp_subcontracting_purchase/tests/test_res_company.py
@@ -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)
diff --git a/mrp_subcontracting_purchase/tests/test_stock_move.py b/mrp_subcontracting_purchase/tests/test_stock_move.py
new file mode 100644
index 00000000000..b8502abd36c
--- /dev/null
+++ b/mrp_subcontracting_purchase/tests/test_stock_move.py
@@ -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")
diff --git a/mrp_subcontracting_purchase/views/purchase_order_views.xml b/mrp_subcontracting_purchase/views/purchase_order_views.xml
index 56125168afc..7b1c240c67f 100644
--- a/mrp_subcontracting_purchase/views/purchase_order_views.xml
+++ b/mrp_subcontracting_purchase/views/purchase_order_views.xml
@@ -1,16 +1,13 @@
-
- purchase.order.inherited.form.mrp.subcontracting.purchase
+
+
+ purchase.order.inherited.form.mrp.subcontracting.purchase
+
purchase.order
-
+
+
diff --git a/mrp_subcontracting_purchase/views/stock_picking_views.xml b/mrp_subcontracting_purchase/views/stock_picking_views.xml
index 7cca96dfa39..da93972dddb 100644
--- a/mrp_subcontracting_purchase/views/stock_picking_views.xml
+++ b/mrp_subcontracting_purchase/views/stock_picking_views.xml
@@ -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"
>
-
+
+
+
Source PO