[REF] mrp_subcontracting

This PR modify the subcontracting process:
- Support extra quantity
- No more interaction with the subcontract order. Everything is directly accessible on the operation
- Dropshipping with subcontracting
- Bypass reservation process
- The partner type subcontractor is removed and every partner could be set on the BoM
- Update the initial demand will trigger the rules.
- Demo data
- Subcontractor is required on the BoM (no more BoM for all partners of type subcontractor)

Visually:
- Remove the stat button for linked operation.
- Register Components is only available when a subcontract component is tracked
- Button on operations in order to correct tracked components
- Show Operation button will ask to record components until the total quantity is provided(correction on finish lines are allowed after)

More details available in each commit
Task 1962187

closes odoo/odoo#35738

Signed-off-by: Simon Lejeune (sle) <sle@openerp.com>
This commit is contained in:
Robot Odoo
2019-08-19 18:48:35 +02:00
committed by GitHub
47 changed files with 1086 additions and 633 deletions
+21
View File
@@ -13,6 +13,8 @@ class StockWarehouse(models.Model):
help="When products are manufactured, they can be manufactured in this warehouse.")
manufacture_pull_id = fields.Many2one(
'stock.rule', 'Manufacture Rule')
manufacture_mto_pull_id = fields.Many2one(
'stock.rule', 'Manufacture MTO Rule')
pbm_mto_pull_id = fields.Many2one(
'stock.rule', 'Picking Before Manufacturing MTO Rule')
sam_rule_id = fields.Many2one(
@@ -101,6 +103,8 @@ class StockWarehouse(models.Model):
def _get_global_route_rules_values(self):
rules = super(StockWarehouse, self)._get_global_route_rules_values()
location_src = self.manufacture_steps == 'mrp_one_step' and self.lot_stock_id or self.pbm_loc_id
production_location = self._get_production_location()
location_id = self.manufacture_steps == 'pbm_sam' and self.sam_loc_id or self.lot_stock_id
rules.update({
'manufacture_pull_id': {
@@ -119,6 +123,23 @@ class StockWarehouse(models.Model):
'propagate_cancel': self.manufacture_steps == 'pbm_sam'
},
},
'manufacture_mto_pull_id': {
'depends': ['manufacture_steps', 'manufacture_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('stock.route_warehouse0_mto', _('Make To Order')).id,
'location_id': production_location.id,
'location_src_id': location_src.id,
'picking_type_id': self.manu_type_id.id
},
'update_values': {
'name': self._format_rulename(location_src, production_location, 'MTO'),
'active': self.manufacture_to_resupply,
},
},
'pbm_mto_pull_id': {
'depends': ['manufacture_steps', 'manufacture_to_resupply'],
'create_values': {
+1
View File
@@ -516,6 +516,7 @@ class TestMrpOrder(TestMrpCommon):
update_quantity_wizard.change_prod_qty()
self.assertEqual(mo.move_raw_ids.filtered(lambda m: m.product_id == p1).quantity_done, 20, 'Update the produce quantity should not impact already produced quantity.')
self.assertEqual(mo.move_finished_ids.product_uom_qty, 4)
mo.button_mark_done()
def test_product_produce_6(self):
+1 -24
View File
@@ -173,30 +173,7 @@
</group>
<notebook>
<page string="Components">
<field name="move_raw_ids" context="{'final_lots': show_final_lots, 'form_view_ref': 'mrp.view_stock_move_lots', 'default_location_id': location_src_id, 'default_location_dest_id': production_location_id, 'default_state': 'draft', 'default_raw_material_production_id': id, 'default_picking_type_id': picking_type_id}" attrs="{'readonly': ['&amp;', ('state', '!=', 'draft'), ('is_locked', '=', True)]}">
<tree delete="0" default_order="is_done,sequence" decoration-muted="is_done" decoration-warning="quantity_done&gt;product_uom_qty" decoration-success="not is_done and quantity_done==product_uom_qty" decoration-danger="not is_done and reserved_availability &lt; product_uom_qty">
<field name="product_id" required="1"/>
<field name="name" invisible="1"/>
<field name="unit_factor" invisible="1"/>
<field name="product_uom" groups="uom.group_uom"/>
<field name="date" invisible="1"/>
<field name="date_expected" invisible="1"/>
<field name="picking_type_id" invisible="1"/>
<field name="has_tracking" invisible="1"/>
<field name="operation_id" invisible="1"/>
<field name="needs_lots" readonly="1" groups="stock.group_production_lot"/>
<field name="is_done" invisible="1"/>
<field name="bom_line_id" invisible="1"/>
<field name="sequence" invisible="1"/>
<field name="location_id" invisible="1"/>
<field name="warehouse_id" invisible="1"/>
<field name="location_dest_id" domain="[('id', 'child_of', parent.location_dest_id)]" invisible="1"/>
<field name="state" invisible="1" force_save="1"/>
<field name="product_uom_qty" string="To Consume"/>
<field name="reserved_availability" attrs="{'invisible': [('is_done', '=', True)], 'column_invisible': [('parent.state', 'in', ('draft', 'done'))]}" string="Reserved"/>
<field name="quantity_done" string="Consumed" attrs="{'column_invisible': [('parent.state', '=', 'draft')]}" readonly="1"/>
</tree>
</field>
<field name="move_raw_ids" context="{'final_lots': show_final_lots, 'tree_view_ref': 'mrp.view_stock_move_raw_tree', 'form_view_ref': 'mrp.view_stock_move_lots', 'default_location_id': location_src_id, 'default_location_dest_id': production_location_id, 'default_state': 'draft', 'default_raw_material_production_id': id, 'default_picking_type_id': picking_type_id}" attrs="{'readonly': ['&amp;', ('state', '!=', 'draft'), ('is_locked', '=', True)]}"/>
</page>
<page string="Finished Products">
<field name="finished_move_line_ids" context="{'form_view_ref': 'mrp.view_finisehd_move_line'}" attrs="{'readonly': [('is_locked', '=', True)], 'invisible': [('finished_move_line_ids', '=', [])]}">
+30
View File
@@ -66,6 +66,36 @@
</field>
</record>
<record id="view_stock_move_raw_tree" model="ir.ui.view">
<field name="name">stock.move.raw.tree</field>
<field name="model">stock.move</field>
<field name="priority">1000</field>
<field name="arch" type="xml">
<tree delete="0" default_order="is_done,sequence" decoration-muted="is_done" decoration-warning="quantity_done&gt;product_uom_qty" decoration-success="not is_done and quantity_done==product_uom_qty" decoration-danger="not is_done and reserved_availability &lt; product_uom_qty">
<field name="product_id" required="1"/>
<field name="name" invisible="1"/>
<field name="unit_factor" invisible="1"/>
<field name="product_uom" groups="uom.group_uom"/>
<field name="date" invisible="1"/>
<field name="date_expected" invisible="1"/>
<field name="picking_type_id" invisible="1"/>
<field name="has_tracking" invisible="1"/>
<field name="operation_id" invisible="1"/>
<field name="needs_lots" readonly="1" groups="stock.group_production_lot"/>
<field name="is_done" invisible="1"/>
<field name="bom_line_id" invisible="1"/>
<field name="sequence" invisible="1"/>
<field name="location_id" invisible="1"/>
<field name="warehouse_id" invisible="1"/>
<field name="location_dest_id" domain="[('id', 'child_of', parent.location_dest_id)]" invisible="1"/>
<field name="state" invisible="1" force_save="1"/>
<field name="product_uom_qty" string="To Consume"/>
<field name="reserved_availability" attrs="{'invisible': [('is_done', '=', True)], 'column_invisible': [('parent.state', 'in', ('draft', 'done'))]}" string="Reserved"/>
<field name="quantity_done" string="Consumed" attrs="{'column_invisible': [('parent.state', '=', 'draft')]}" readonly="1"/>
</tree>
</field>
</record>
<record id="view_finisehd_move_line" model="ir.ui.view">
<field name="name">mrp.finished.move.line.form</field>
<field name="priority">1000</field>
+2 -2
View File
@@ -35,8 +35,8 @@ class ChangeProductionQty(models.TransientModel):
modification = {}
for move in production.move_finished_ids.filtered(lambda m: m.state not in ('done', 'cancel')):
qty = (qty - old_qty) * move.unit_factor
modification[move] = (move.product_uom_qty - qty, move.product_uom_qty)
move[0].write({'product_uom_qty': move.product_uom_qty - qty})
modification[move] = (move.product_uom_qty + qty, move.product_uom_qty)
move[0].write({'product_uom_qty': move.product_uom_qty + qty})
return modification
def change_prod_qty(self):
+6 -3
View File
@@ -16,8 +16,11 @@ class MrpProductProduce(models.TransientModel):
@api.model
def default_get(self, fields):
res = super(MrpProductProduce, self).default_get(fields)
if self._context and self._context.get('active_id'):
production = self.env['mrp.production'].browse(self._context['active_id'])
production = self.env['mrp.production']
production_id = self.env.context.get('default_production_id') or self.env.context.get('active_id')
if production_id:
production = self.env['mrp.production'].browse(production_id)
if production.exists():
serial_finished = (production.product_id.tracking == 'serial')
todo_uom = production.product_uom_id.id
todo_quantity = self._get_todo(production)
@@ -68,7 +71,7 @@ class MrpProductProduce(models.TransientModel):
self.ensure_one()
self._record_production()
action = self.production_id.open_produce_product()
action['context'] = {'active_id': self.production_id.id}
action['context'] = {'default_production_id': self.production_id.id}
return action
def action_generate_serial(self):
+1 -22
View File
@@ -1,25 +1,4 @@
# -*- coding: utf-8 -*-
from . import models
from odoo import api, SUPERUSER_ID
def _create_subcontracting_rules(cr, registry):
""" This hook is used to add the default subcontracting rules on every warehouse/company. It is
necessary if the mrp_subcontracting module is installed after some warehouses
were already created.
"""
env = api.Environment(cr, SUPERUSER_ID, {})
warehouse_ids = env['stock.warehouse'].search(['|', ('subcontracting_mto_pull_id', '=', False), ('subcontracting_pull_id', '=', False)])
for warehouse_id in warehouse_ids:
warehouse_id.write({'subcontracting_to_resupply': warehouse_id.subcontracting_to_resupply})
company_ids = env['res.company'].search([('subcontracting_location_id', '!=', False)])
for company_id in company_ids:
wh = env['stock.warehouse'].search([('company_id', '=', company_id.id), ('active', '=', True)], limit=1)
production_location = wh._get_production_location()
rsoo_rule = env['stock.rule'].search([
('location_src_id', '=', company_id.subcontracting_location_id.id),
('location_id', '=', production_location.id),
('picking_type_id', '=', wh.manu_type_id.id)
])
if not rsoo_rule:
company_id._create_resupply_subcontractor_rules()
from . import wizard
+8 -4
View File
@@ -8,13 +8,17 @@
'description': "",
'website': 'https://www.odoo.com/page/manufacturing',
'category': 'Manufacturing/Manufacturing',
'depends': ['mrp', 'contacts'],
'depends': ['mrp'],
'data': [
'data/mrp_subcontracting_data.xml',
'views/stock_warehouse_views.xml',
'views/stock_picking_views.xml',
'views/mrp_bom_views.xml',
'views/res_partner_views.xml',
'views/stock_warehouse_views.xml',
'views/stock_move_views.xml',
'views/stock_picking_views.xml',
'views/supplier_info_views.xml',
],
'demo': [
'data/mrp_subcontracting_demo.xml',
],
'post_init_hook': '_create_subcontracting_rules',
}
@@ -7,6 +7,10 @@
<field name="sequence">5</field>
</record>
<function model="res.company" name="create_missing_subcontracting_location" />
<function model="stock.warehouse" name="write">
<value model="stock.warehouse" eval="obj().env['stock.warehouse'].search([]).ids"/>
<value eval="{'subcontracting_to_resupply': True}"/>
</function>
</data>
</odoo>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="mrp_bom_subcontract" model="mrp.bom">
<field name="product_tmpl_id" ref="product.product_delivery_02"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="type">subcontract</field>
<field name="subcontractor_ids" eval="[(4, ref('base.res_partner_12'))]"/>
</record>
<record id="mrp_bom_line_subcontract" model="mrp.bom.line">
<field name="product_id" ref="mrp.product_product_computer_desk_screw"/>
<field name="product_qty">1</field>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="bom_id" ref="mrp_bom_subcontract"/>
</record>
<record id="product_supplierinfo_subcontracting" model="product.supplierinfo">
<field name="product_tmpl_id" ref="product.product_delivery_02_product_template"/>
<field name="name" ref="base.res_partner_12"/>
<field name="sequence">1</field>
<field name="delay">1</field>
<field name="min_qty">1</field>
<field name="price">27</field>
</record>
<record id="product.product_delivery_02" model="product.product">
<field name="route_ids" eval="[(4,ref('stock.route_warehouse0_mto'))]"></field>
</record>
</data>
</odoo>
@@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-
from . import mrp_bom
from . import product
from . import res_company
from . import res_partner
from . import stock_move
from . import stock_move_line
from . import stock_picking
from . import stock_warehouse
+3 -6
View File
@@ -2,19 +2,16 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo.osv.expression import AND
class MrpBom(models.Model):
_inherit = 'mrp.bom'
type = fields.Selection(selection_add=[('subcontract', 'Subcontracting')])
subcontractor_ids = fields.One2many('res.partner', 'bom_id', domain=[('type', '=', 'subcontractor')], string='Subcontractors')
subcontractor_ids = fields.Many2many('res.partner', 'mrp_bom_subcontractor', string='Subcontractors')
def _bom_subcontract_find(self, product_tmpl=None, product=None, picking_type=None, company_id=False, bom_type='subcontract', subcontractor=False):
domain = self._bom_find_domain(product_tmpl=product_tmpl, product=product, picking_type=picking_type, company_id=company_id, bom_type=bom_type)
if subcontractor:
domain += ['|', ('subcontractor_ids', 'in', subcontractor.id), ('subcontractor_ids', '=', False)]
else:
domain += [('subcontractor_ids', '=', False)]
domain = AND([domain, [('subcontractor_ids', 'in', subcontractor.id)]])
return self.search(domain, order='subcontractor_ids, sequence, product_id', limit=1)
@@ -9,8 +9,9 @@ class SupplierInfo(models.Model):
is_subcontractor = fields.Boolean('Subcontracted', compute='_compute_is_subcontractor', help="Choose a vendor of type subcontractor if you want to subcontract the product")
@api.depends('name')
@api.depends('name', 'product_id', 'product_tmpl_id')
def _compute_is_subcontractor(self):
for supplierinfo in self:
supplierinfo.is_subcontractor = True if supplierinfo.name.type == 'subcontractor' else False
for supplier in self:
boms = supplier.product_id.variant_bom_ids
boms |= supplier.product_tmpl_id.bom_ids.filtered(lambda b: not b.product_id)
supplier.is_subcontractor = supplier.name in boms.subcontractor_ids
+18 -24
View File
@@ -9,39 +9,33 @@ class ResCompany(models.Model):
subcontracting_location_id = fields.Many2one('stock.location')
@api.model
def create_missing_subcontracting_location(self):
company_without_subcontracting_loc = self.env['res.company'].search(
[('subcontracting_location_id', '=', False)])
company_without_subcontracting_loc._create_subcontracting_location()
def _create_per_company_locations(self):
super(ResCompany, self)._create_per_company_locations()
self._create_subcontracting_location()
def _create_subcontracting_location(self):
parent_location = self.env.ref('stock.stock_location_locations_partner', raise_if_not_found=False)
property_stock_subcontractor_res_partner_field = self.env['ir.model.fields'].search([
('model', '=', 'res.partner'),
('name', '=', 'property_stock_subcontractor')
])
for company in self:
parent_location = self.env.ref('stock.stock_location_locations_partner', raise_if_not_found=False)
subcontracting_location = self.env['stock.location'].create({
'name': _('%s: Subcontracting Location') % company.name,
'usage': 'internal',
'location_id': parent_location.id,
'company_id': company.id,
})
company.write({'subcontracting_location_id': subcontracting_location.id})
@api.model
def create_missing_subcontracting_location(self):
company_without_subcontracting_loc = self.env['res.company'].search(
[('subcontracting_location_id', '=', False)])
for company in company_without_subcontracting_loc:
company._create_subcontracting_location()
def _create_resupply_subcontractor_rules(self):
self.ensure_one()
wh = self.env['stock.warehouse'].search([('company_id', '=', self.id), ('active', '=', True)], limit=1)
subcontracting_location = self.subcontracting_location_id
production_location = wh._get_production_location()
resupply_route = wh._find_global_route('mrp_subcontracting.route_resupply_subcontractor_mto', _('Resupply Subcontractor on Order'))
resupply_route.rule_ids = [(0, 0, {
'name': '%s → %s' % (subcontracting_location.name, production_location.name),
'action': 'pull',
'picking_type_id': wh.subcontracting_type_id,
'location_src_id': subcontracting_location,
'location_id': production_location,
})],
self.env['ir.property'].create({
'name': 'property_stock_subcontractor_%s' % company.name,
'fields_id': property_stock_subcontractor_res_partner_field.id,
'company_id': company.id,
'value': 'stock.location,%d' % subcontracting_location.id,
})
company.subcontracting_location_id = subcontracting_location
@@ -1,56 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo import fields, models
class ResPartner(models.Model):
_inherit = 'res.partner'
type = fields.Selection(selection_add=[('subcontractor', 'Subcontractor')])
bom_id = fields.Many2one('mrp.bom', domain=[('type', '=', 'subcontract')])
def _get_name(self):
name = super(ResPartner, self)._get_name()
if self.type == 'subcontractor':
subcontractor = _('Subcontractor')
if name.endswith(', '):
name = "%s%s" % (name, subcontractor)
else:
name = '%s, %s' % (name, subcontractor)
return name
@api.onchange('type')
def _onchange_partner_type(self):
if self.type == 'subcontractor':
company = self.company_id or self.parent_id.company_id
subcontracting_location = company.subcontracting_location_id
self.update({
'property_stock_customer': subcontracting_location,
'property_stock_supplier': subcontracting_location
})
else:
self.update({
'property_stock_customer': self.env.ref('stock.stock_location_customers'),
'property_stock_supplier': self.env.ref('stock.stock_location_suppliers')
})
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('type') == 'subcontractor':
if not vals.get('property_stock_supplier') and not vals.get('property_stock_customer'):
company = vals.get('company_id') and self.env['res.company'].browse(vals['company_id']) or \
self.browse(vals.get('parent_id')).company_id
subcontracting_location = company.subcontracting_location_id
vals['property_stock_supplier'] = subcontracting_location
vals['property_stock_customer'] = subcontracting_location
return super(ResPartner, self).create(vals_list)
def write(self, vals):
if vals.get('type') == 'subcontractor':
subcontracting_location = self.env.company.subcontracting_location_id
if not vals.get('property_stock_supplier') and not vals.get('property_stock_customer'):
vals['property_stock_supplier'] = subcontracting_location.id
vals['property_stock_customer'] = subcontracting_location.id
return super(ResPartner, self).write(vals)
property_stock_subcontractor = fields.Many2one(
'stock.location', string="Subcontractor Location", company_dependent=True,
help="The stock location used as source and destination when sending\
goods to this contact during a subcontracting process.")
+150 -20
View File
@@ -3,16 +3,145 @@
from collections import defaultdict
from odoo import models, _
from odoo import fields, models, _
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_compare, float_is_zero
class StockMove(models.Model):
_inherit = 'stock.move'
is_subcontract = fields.Boolean('The move is a subcontract receipt', copy=False)
show_subcontracting_details_visible = fields.Boolean(
compute='_compute_show_subcontracting_details_visible'
)
def _compute_show_subcontracting_details_visible(self):
""" Compute if the action button in order to see moves raw is visible """
for move in self:
if move.is_subcontract and move._has_tracked_subcontract_components() and\
not float_is_zero(move.quantity_done, precision_rounding=move.product_uom.rounding):
move.show_subcontracting_details_visible = True
else:
move.show_subcontracting_details_visible = False
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
if not move._has_tracked_subcontract_components():
continue
move.show_details_visible = True
return res
def write(self, values):
""" If the initial demand is updated then also update the linked
subcontract order to the new quantity.
"""
res = super(StockMove, self).write(values)
if 'product_uom_qty' in values:
self.filtered(lambda m: m.is_subcontract and
m.state not in ['draft', 'cancel', 'done'])._update_subcontract_order_qty()
return res
def action_show_details(self):
""" Open the produce wizard in order to register tracked components for
subcontracted product. Otherwise use standard behavior.
"""
self.ensure_one()
if self.is_subcontract:
rounding = self.product_uom.rounding
production = self.move_orig_ids.production_id
if self._has_tracked_subcontract_components() and\
float_compare(production.qty_produced, production.product_uom_qty, precision_rounding=rounding) < 0 and\
float_compare(self.quantity_done, self.product_uom_qty, precision_rounding=rounding) < 0:
return self._action_record_components()
action = super(StockMove, self).action_show_details()
if self.is_subcontract:
action['views'] = [(self.env.ref('stock.view_stock_move_operations').id, 'form')]
action['context'].update({
'show_lots_m2o': self.has_tracking != 'none',
'show_lots_text': False,
})
return action
def action_show_subcontract_details(self):
""" Display moves raw for subcontracted product self. """
moves = self.move_orig_ids.production_id.move_raw_ids
tree_view = self.env.ref('mrp_subcontracting.mrp_subcontracting_move_tree_view')
form_view = self.env.ref('mrp_subcontracting.mrp_subcontracting_move_form_view')
return {
'name': _('Raw Materials for %s') % (self.product_id.display_name),
'type': 'ir.actions.act_window',
'res_model': 'stock.move',
'views': [(tree_view.id, 'tree'), (form_view.id, 'form')],
'target': 'current',
'domain': [('id', 'in', moves.ids)],
}
def _action_confirm(self, merge=True, merge_into=False):
subcontract_details_per_picking = defaultdict(list)
for move in self:
if move.location_id.usage != 'supplier' or move.location_dest_id.usage == 'supplier':
continue
if not move.picking_id or self.env.context.get('do_not_create_subcontract_order'):
continue
bom = move._get_subcontract_bom()
if not bom:
continue
subcontract_details_per_picking[move.picking_id].append((move, bom))
move.write({
'is_subcontract': True,
'location_id': self.picking_id.partner_id.property_stock_subcontractor.id
})
for picking, subcontract_details in subcontract_details_per_picking.items():
picking._subcontracted_produce(subcontract_details)
res = super(StockMove, self)._action_confirm(merge=merge, merge_into=merge_into)
if subcontract_details_per_picking:
self.env['stock.picking'].concat(*list(subcontract_details_per_picking.keys())).action_assign()
return res
def _action_record_components(self):
action = self.env.ref('mrp.act_mrp_product_produce').read()[0]
action['context'] = dict(
default_production_id=self.move_orig_ids.production_id.id,
default_subcontract_move_id=self.id
)
return action
def _check_overprocessed_subcontract_qty(self):
""" If a subcontracted move use tracked components. Do not allow to add
quantity without the produce wizard. Instead update the initial demand
and use the register component button. Split or correct a lot/sn is
possible.
"""
overprocessed_moves = self.env['stock.move']
for move in self:
if not move.is_subcontract:
continue
# Extra quantity is allowed when components do not need to be register
if not move._has_tracked_subcontract_components():
continue
rounding = move.product_uom.rounding
if float_compare(move.quantity_done, move.move_orig_ids.production_id.qty_produced, precision_rounding=rounding) > 0:
overprocessed_moves |= move
if overprocessed_moves:
raise UserError(_("""
You have to use 'Records Components' button in order to register quantity for a
subcontracted product(s) with tracked component(s):
%s.
If you want to process more than initially planned, you
can use the edit + unlock buttons in order to adapt the initial demand on the
operations.""") % ('\n'.join(overprocessed_moves.mapped('product_id.display_name'))))
def _get_subcontract_bom(self):
self.ensure_one()
bom = self.env['mrp.bom']._bom_subcontract_find(
bom = self.env['mrp.bom'].sudo()._bom_subcontract_find(
product=self.product_id,
picking_type=self.picking_type_id,
company_id=self.company_id.id,
@@ -21,21 +150,22 @@ class StockMove(models.Model):
)
return bom
def _action_confirm(self, merge=True, merge_into=False):
res = super(StockMove, self)._action_confirm(merge=merge, merge_into=merge_into)
subcontract_details_per_picking = defaultdict(list)
for move in res:
if not move.picking_id:
continue
if not move.picking_id._is_subcontract():
continue
bom = move._get_subcontract_bom()
if not bom:
error_message = _('Please define a BoM of type subcontracting for the product "%s". If you don\'t want to subcontract the product "%s", do not select a partner of type subcontractor.')
error_message += '\n\n'
error_message += _('If there is well a BoM of type subcontracting defined, check if you have set the correct subcontractors on it.')
raise UserError(error_message % (move.product_id.name, move.product_id.name))
subcontract_details_per_picking[move.picking_id].append((move, bom))
for picking, subcontract_details in subcontract_details_per_picking.items():
picking._subcontracted_produce(subcontract_details)
return res
def _has_tracked_subcontract_components(self):
self.ensure_one()
return any(m.has_tracking != 'none' for m in self.move_orig_ids.production_id.move_raw_ids)
def _should_bypass_reservation(self):
""" If the move is subcontracted then ignore the reservation. """
should_bypass_reservation = super(StockMove, self)._should_bypass_reservation()
if not should_bypass_reservation and self.is_subcontract:
return True
return should_bypass_reservation
def _update_subcontract_order_qty(self):
for move in self:
production = move.move_orig_ids.production_id
if production:
self.env['change.production.qty'].with_context(skip_activity=True).create({
'mo_id': production.id,
'product_qty': move.product_uom_qty
}).change_prod_qty()
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class StockMoveLine(models.Model):
_inherit = 'stock.move.line'
def create(self, values):
records = super(StockMoveLine, self).create(values)
records.filtered(lambda ml: ml.move_id.is_subcontract).move_id._check_overprocessed_subcontract_qty()
return records
def write(self, values):
res = super(StockMoveLine, self).write(values)
self.filtered(lambda ml: ml.move_id.is_subcontract).move_id._check_overprocessed_subcontract_qty()
return res
def _should_bypass_reservation(self, location):
""" If the move line is subcontracted then ignore the reservation. """
should_bypass_reservation = super(StockMoveLine, self)._should_bypass_reservation(location)
if not should_bypass_reservation and self.move_id.is_subcontract:
return True
return should_bypass_reservation
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import timedelta
from odoo import api, fields, models
@@ -8,7 +10,6 @@ class StockPicking(models.Model):
_inherit = 'stock.picking'
display_action_record_components = fields.Boolean(compute='_compute_display_action_record_components')
display_view_subcontracted_move_lines = fields.Boolean(compute='_compute_display_view_subcontracted_move_lines')
@api.depends('state')
def _compute_display_action_record_components(self):
@@ -18,10 +19,9 @@ class StockPicking(models.Model):
continue
if not picking._is_subcontract():
continue
# Hide if no move is tracked
# Hide if no components are track
subcontracted_productions = picking._get_subcontracted_productions()
subcontracted_moves = subcontracted_productions.mapped('move_raw_ids')
subcontracted_moves |= subcontracted_productions.mapped('move_finished_ids')
if all(subcontracted_move.has_tracking == 'none' for subcontracted_move in subcontracted_moves):
continue
# Hide if the production is to close
@@ -29,85 +29,102 @@ class StockPicking(models.Model):
continue
picking.display_action_record_components = True
@api.depends('state')
def _compute_display_view_subcontracted_move_lines(self):
for picking in self:
# Hide if not encoding state
if picking.state in ('draft', 'cancel'):
continue
if not picking._is_subcontract():
continue
# Hide until state done if no move is tracked, if tracked until something was produced
subcontracted_productions = picking._get_subcontracted_productions()
subcontracted_moves = subcontracted_productions.mapped('move_raw_ids')
subcontracted_moves |= subcontracted_productions.mapped('move_finished_ids')
if all(subcontracted_move.has_tracking == 'none' for subcontracted_move in subcontracted_moves):
if picking.state != 'done':
continue
# Hide if nothing was produced
if all(subcontracted_move.quantity_done == 0 for subcontracted_move in subcontracted_moves):
continue
picking.display_view_subcontracted_move_lines = True
# -------------------------------------------------------------------------
# Action methods
# -------------------------------------------------------------------------
def action_done(self):
for picking in self:
subcontracted_productions = picking._get_subcontracted_productions()
for subcontracted_production in subcontracted_productions:
subcontracted_production.button_mark_done()
return super(StockPicking, self).action_done()
def action_view_subcontracted_move_lines(self):
""" Returns a list view with the move lines of the subcontracted products. To find them, we
look on the origin moves of the move lines of the picking if there is a manufacturing order.
"""
self.ensure_one()
subcontracted_productions = self._get_subcontracted_productions()
subcontracted_move_lines = self.env['stock.move.line']
for subcontracted_production in subcontracted_productions:
subcontracted_move_lines |= subcontracted_production.move_raw_ids.mapped('move_line_ids')
subcontracted_move_lines |= subcontracted_production.move_finished_ids.mapped('move_line_ids')
action = self.env.ref('stock.stock_move_line_action').read()[0]
action['context'] = {}
action['domain'] = [('id', 'in', subcontracted_move_lines.ids)]
return action
def action_cancel(self):
for picking in self:
picking._get_subcontracted_productions()._action_cancel()
return super(StockPicking, self).action_cancel()
def action_done(self):
# action_done with an extra move will trigger two methods that
# create/modify a subcontracting order, _action_confirm and a write on
# product_uom_qty. This context will be used by _action_confirm and it
# will not create a subcontract order. Instead the existing order will
# be updated in the stock.move write during the merge move.
res = super(StockPicking, self.with_context(do_not_create_subcontract_order=True)).action_done()
productions = self.env['mrp.production']
for picking in self:
for move in picking.move_lines:
if not move.is_subcontract:
continue
production = move.move_orig_ids.production_id
if move._has_tracked_subcontract_components():
move.move_orig_ids.move_line_ids.unlink()
move_finished_ids = move.move_orig_ids
for ml in move.move_line_ids:
ml.copy({
'picking_id': False,
'production_id': move_finished_ids.production_id.id,
'move_id': move_finished_ids.id,
'qty_done': ml.qty_done,
'result_package_id': False,
'location_id': move_finished_ids.location_id.id,
'location_dest_id': move_finished_ids.location_dest_id.id,
})
else:
for move_line in move.move_line_ids:
produce = self.env['mrp.product.produce'].with_context(default_production_id=production.id).create({
'production_id': production.id,
'qty_producing': move_line.qty_done,
'product_uom_id': move_line.product_uom_id.id,
'finished_lot_id': move_line.lot_id.id,
'consumption': 'strict',
})
produce._generate_produce_lines()
produce._record_production()
productions |= production
for subcontracted_production in productions:
subcontracted_production.button_mark_done()
# For concistency, set the date on production move before the date
# on picking. (Tracability report + Product Moves menu item)
minimum_date = min(picking.move_line_ids.mapped('date'))
production_moves = subcontracted_production.move_raw_ids | subcontracted_production.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
def action_record_components(self):
self.ensure_one()
subcontracted_productions = self._get_subcontracted_productions()
to_register = subcontracted_productions.filtered(lambda mo: mo.state not in ('to_close', 'done'))
if to_register:
mo = to_register[0]
action = self.env.ref('mrp.act_mrp_product_produce').read()[0]
action['context'] = dict(self.env.context, active_id=mo.id)
return action
for move in self.move_lines:
if not move._has_tracked_subcontract_components():
continue
production = move.move_orig_ids.production_id
if not production or production.state in ('done', 'to_close'):
continue
return move._action_record_components()
# -------------------------------------------------------------------------
# Subcontract helpers
# -------------------------------------------------------------------------
def _is_subcontract(self):
self.ensure_one()
if self.partner_id.type == 'subcontractor' and \
self.picking_type_id.code == 'incoming':
return True
return False
return self.picking_type_id.code == 'incoming' and any(m.is_subcontract for m in self.move_lines)
def _get_subcontracted_productions(self):
self.ensure_one()
return self.move_lines.mapped('move_orig_ids.production_id')
def _get_warehouse(self, subcontract_move):
return subcontract_move.warehouse_id or self.picking_type_id.warehouse_id
def _prepare_subcontract_mo_vals(self, subcontract_move, bom):
subcontract_move.ensure_one()
group = self.env['procurement.group'].create({
'name': self.name,
'partner_id': self.partner_id.id,
})
product = subcontract_move.product_id
warehouse = subcontract_move.warehouse_id or self.picking_type_id.warehouse_id
warehouse = self._get_warehouse(subcontract_move)
vals = {
'procurement_group_id': group.id,
'product_id': product.id,
'product_uom_id': subcontract_move.product_uom.id,
'bom_id': bom.id,
'location_src_id': subcontract_move.picking_id.partner_id.property_stock_supplier.id,
'location_dest_id': subcontract_move.location_id.id,
'location_src_id': subcontract_move.picking_id.partner_id.property_stock_subcontractor.id,
'location_dest_id': subcontract_move.picking_id.partner_id.property_stock_subcontractor.id,
'product_qty': subcontract_move.product_uom_qty,
'picking_type_id': warehouse.subcontracting_type_id.id
}
@@ -124,10 +141,3 @@ class StockPicking(models.Model):
finished_move = mo.move_finished_ids.filtered(lambda m: m.product_id == move.product_id)
finished_move.write({'move_dest_ids': [(4, move.id, False)]})
mo.action_assign()
# Only skip the produce wizard if no moves is tracked
moves = mo.mapped('move_raw_ids') + mo.mapped('move_finished_ids')
if any(move.has_tracking != 'none' for move in moves):
continue
for m in moves:
m.quantity_done = m.product_uom_qty
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo import fields, models, _
class StockWarehouse(models.Model):
@@ -107,8 +107,7 @@ class StockWarehouse(models.Model):
'subcontracting_type_id': {
'name': _('Subcontracting'),
'code': 'mrp_operation',
'use_create_lots': True,
'use_existing_lots': True,
'use_create_components_lots': True,
'sequence': next_sequence + 2,
'sequence_code': 'SBC',
},
@@ -137,4 +136,3 @@ class StockWarehouse(models.Model):
def _get_subcontracting_location(self):
return self.company_id.subcontracting_location_id
+60
View File
@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import Form, SavepointCase
class TestMrpSubcontractingCommon(SavepointCase):
@classmethod
def setUpClass(cls):
super(TestMrpSubcontractingCommon, cls).setUpClass()
# 1: Create a subcontracting partner
main_partner = cls.env['res.partner'].create({'name': 'main_partner'})
cls.subcontractor_partner1 = cls.env['res.partner'].create({
'name': 'subcontractor_partner',
'parent_id': main_partner.id,
'company_id': cls.env.ref('base.main_company').id,
})
# 2. Create a BOM of subcontracting type
cls.comp1 = cls.env['product.product'].create({
'name': 'Component1',
'type': 'product',
'categ_id': cls.env.ref('product.product_category_all').id,
})
cls.comp2 = cls.env['product.product'].create({
'name': 'Component2',
'type': 'product',
'categ_id': cls.env.ref('product.product_category_all').id,
})
cls.finished = cls.env['product.product'].create({
'name': 'finished',
'type': 'product',
'categ_id': cls.env.ref('product.product_category_all').id,
})
bom_form = Form(cls.env['mrp.bom'])
bom_form.type = 'subcontract'
bom_form.product_tmpl_id = cls.finished.product_tmpl_id
bom_form.subcontractor_ids.add(cls.subcontractor_partner1)
with bom_form.bom_line_ids.new() as bom_line:
bom_line.product_id = cls.comp1
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 = bom_form.save()
# Create a BoM for cls.comp2
cls.comp2comp = cls.env['product.product'].create({
'name': 'component for Component2',
'type': 'product',
'categ_id': cls.env.ref('product.product_category_all').id,
})
bom_form = Form(cls.env['mrp.bom'])
bom_form.product_tmpl_id = cls.comp2.product_tmpl_id
with bom_form.bom_line_ids.new() as bom_line:
bom_line.product_id = cls.comp2comp
bom_line.product_qty = 1
cls.comp2_bom = bom_form.save()
cls.warehouse = cls.env['stock.warehouse'].search([], limit=1)
@@ -2,180 +2,30 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import Form
from odoo.tests.common import SavepointCase, TransactionCase
from odoo.tests.common import TransactionCase
from odoo.addons.mrp_subcontracting.tests.common import TestMrpSubcontractingCommon
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestSubcontractingBasic(TransactionCase):
def test_partner_1(self):
""" Checks the `display_name` of subcontracted partners. """
main_partner = self.env['res.partner'].create({'name': 'main_partner'})
# When a name is set to the subcontracted partner.
subcontractor_partner1 = self.env['res.partner'].create({
'name': 'subcontractor_partner',
'type': 'subcontractor',
'parent_id': main_partner.id,
})
self.assertEqual(subcontractor_partner1.display_name, "main_partner, subcontractor_partner, Subcontractor")
# When a name is not set to the subcontracted partner.
subcontractor_partner1 = self.env['res.partner'].create({
'name': '',
'type': 'subcontractor',
'parent_id': main_partner.id,
})
self.assertEqual(subcontractor_partner1.display_name, "main_partner, Subcontractor")
def test_partner_2(self):
""" Open an existing partner and add a contact of type subcontractor (simple form view).
Checks the customer and supplier locations are set to the subcontracting one onces saved.
"""
that_company = self.env['res.partner'].create({
'is_company': True,
'name': "That Company",
'company_id': self.env.ref('base.main_company').id
})
partner_form = Form(that_company)
with partner_form.child_ids.new() as partner:
partner.name = 'That Guy'
partner.type = 'subcontractor'
partner_form.save()
subcontractor = that_company.child_ids
self.assertEquals(subcontractor.property_stock_supplier, self.env.user.company_id.subcontracting_location_id)
self.assertEquals(subcontractor.property_stock_customer, self.env.user.company_id.subcontracting_location_id)
def test_partner_3(self):
""" Create a new partner, select a parent company, and choose type subcontractor (debug
active). Checks the onchange correctly set the customer and supplier locations to the
subcontracting ones. Change them to another one, save, checks they are rightly applied.
"""
# FIXME sle: this field should not be writeable without the debug mode but it is. ask xmo
group_no_one = self.env.ref('base.group_no_one')
self.env.user.write({'groups_id': [(3, group_no_one.id)]})
partner_form = Form(self.env['res.partner'])
partner_form.name = 'subcontractor partner'
partner_form.company_id = self.env.ref('base.main_company')
self.assertNotEqual(partner_form.property_stock_supplier, self.env.user.company_id.subcontracting_location_id)
self.assertNotEqual(partner_form.property_stock_customer, self.env.user.company_id.subcontracting_location_id)
partner_form.type = 'subcontractor'
self.assertEqual(partner_form.property_stock_supplier, self.env.user.company_id.subcontracting_location_id)
self.assertEqual(partner_form.property_stock_customer, self.env.user.company_id.subcontracting_location_id)
subcontracting_location2 = self.env.user.company_id.subcontracting_location_id.copy()
partner_form.property_stock_supplier = subcontracting_location2
partner_form.property_stock_customer = subcontracting_location2
partner = partner_form.save()
self.assertEqual(partner.property_stock_supplier, subcontracting_location2)
self.assertEqual(partner.property_stock_customer, subcontracting_location2)
def test_partner_4(self):
""" Create a new partner, select a parent company, and choose type subcontractor (debug
not active). Checks the customer and supplier locations are set to the subcontracting one
onces saved.
"""
that_company = self.env['res.partner'].create({
'is_company': True,
'name': "That Company",
'company_id': self.env.ref('base.main_company').id
})
partner_form = Form(self.env['res.partner'])
partner_form.name = 'That Guy'
partner_form.parent_id = that_company
partner_form.type = 'subcontractor'
subcontractor = partner_form.save()
self.assertEquals(subcontractor.property_stock_supplier, self.env.user.company_id.subcontracting_location_id)
self.assertEquals(subcontractor.property_stock_customer, self.env.user.company_id.subcontracting_location_id)
def test_partner_5(self):
""" Create a new partner with a child partner of type contact,
then write the type 'subcontractor' on it
"""
that_company = self.env['res.partner'].create({
'is_company': True,
'name': "That Company",
})
partner_form = Form(that_company)
with partner_form.child_ids.new() as partner:
partner.name = 'That Guy'
partner.type = 'contact'
partner_form.save()
# We write a new type on the created child partner
subcontractor = that_company.child_ids
self.assertEquals(len(that_company.child_ids), 1)
subcontractor.type = 'subcontractor'
self.assertEquals(subcontractor.property_stock_supplier, self.env.user.company_id.subcontracting_location_id)
self.assertEquals(subcontractor.property_stock_customer, self.env.user.company_id.subcontracting_location_id)
def test_subcontracting_location_1(self):
""" Checks the creation and presence of the subcontracting location. """
self.assertTrue(self.env.user.company_id.subcontracting_location_id)
self.assertTrue(self.env.user.company_id.subcontracting_location_id.active)
self.assertTrue(self.env.company.subcontracting_location_id)
self.assertTrue(self.env.company.subcontracting_location_id.active)
company2 = self.env['res.company'].create({'name': 'Test Company'})
self.assertTrue(company2.subcontracting_location_id)
self.assertTrue(self.env.company.subcontracting_location_id != company2.subcontracting_location_id)
class TestSubcontractingFlows(SavepointCase):
@classmethod
def setUpClass(cls):
super(TestSubcontractingFlows, cls).setUpClass()
# 1: Create a subcontracting partner
main_partner = cls.env['res.partner'].create({'name': 'main_partner'})
cls.subcontractor_partner1 = cls.env['res.partner'].create({
'name': 'subcontractor_partner',
'type': 'subcontractor',
'parent_id': main_partner.id,
'company_id': cls.env.ref('base.main_company').id
})
# 2. Create a BOM of subcontracting type
cls.comp1 = cls.env['product.product'].create({
'name': 'Component1',
'type': 'product',
'categ_id': cls.env.ref('product.product_category_all').id,
})
cls.comp2 = cls.env['product.product'].create({
'name': 'Component2',
'type': 'product',
'categ_id': cls.env.ref('product.product_category_all').id,
})
cls.finished = cls.env['product.product'].create({
'name': 'finished',
'type': 'product',
'categ_id': cls.env.ref('product.product_category_all').id,
})
bom_form = Form(cls.env['mrp.bom'])
bom_form.type = 'subcontract'
bom_form.product_tmpl_id = cls.finished.product_tmpl_id
with bom_form.bom_line_ids.new() as bom_line:
bom_line.product_id = cls.comp1
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 = bom_form.save()
# Create a BoM for cls.comp2
cls.comp2comp = cls.env['product.product'].create({
'name': 'component for Component2',
'type': 'product',
'categ_id': cls.env.ref('product.product_category_all').id,
})
bom_form = Form(cls.env['mrp.bom'])
bom_form.product_tmpl_id = cls.comp2.product_tmpl_id
with bom_form.bom_line_ids.new() as bom_line:
bom_line.product_id = cls.comp2comp
bom_line.product_qty = 1
cls.comp2_bom = bom_form.save()
class TestSubcontractingFlows(TestMrpSubcontractingCommon):
def test_flow_1(self):
""" Don't tick any route on the components and trigger the creation of the subcontracting
manufacturing order through a receipt picking. Create a reordering rule in the
subcontracting locations for a component and run the scheduler to resupply. Checks if the
resupplying actually works
"""
# Check subcontracting picking Type
self.assertTrue(all(self.env['stock.warehouse'].search([]).with_context(active_test=False).mapped('subcontracting_type_id.use_create_components_lots')))
# 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')
@@ -187,8 +37,9 @@ class TestSubcontractingFlows(SavepointCase):
picking_receipt.action_confirm()
# Nothing should be tracked
self.assertTrue(all(m.product_uom_qty == m.reserved_availability for m in picking_receipt.move_lines))
self.assertEqual(picking_receipt.state, 'assigned')
self.assertFalse(picking_receipt.display_action_record_components)
self.assertFalse(picking_receipt.display_view_subcontracted_move_lines)
# Check the created manufacturing order
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
@@ -218,25 +69,46 @@ class TestSubcontractingFlows(SavepointCase):
picking_receipt.button_validate()
self.assertEquals(mo.state, 'done')
# Now that the picking is done, the details stat button should be visible
self.assertTrue(picking_receipt.display_view_subcontracted_move_lines)
# Available quantities should be negative at the subcontracting location for each components
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, 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, wh.lot_stock_id)
self.assertEquals(avail_qty_comp1, -1)
self.assertEquals(avail_qty_comp2, -1)
self.assertEquals(avail_qty_finished, 1)
# Ensure returns to subcontractor location
return_form = Form(self.env['stock.return.picking'].with_context(active_id=picking_receipt.id, active_model='stock.picking'))
return_wizard = return_form.save()
return_picking_id, pick_type_id = return_wizard._create_returns()
return_picking = self.env['stock.picking'].browse(return_picking_id)
self.assertEqual(len(return_picking), 1)
self.assertEqual(return_picking.move_lines.location_dest_id, self.subcontractor_partner1.property_stock_subcontractor)
def test_flow_2(self):
""" Tick "Resupply Subcontractor on Order" on the components and trigger the creation of
the subcontracting manufacturing order through a receipt picking. Checks if the resupplying
actually works.
actually works. Also set a different subcontracting location on the partner.
"""
# Tick "resupply subconractor on order"
resupply_sub_on_order_route = self.env['stock.location.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
(self.comp1 + self.comp2).write({'route_ids': [(4, resupply_sub_on_order_route.id, None)]})
# Create a different subcontract location
partner_subcontract_location = self.env['stock.location'].create({
'name': 'Specific partner location',
'location_id': self.env.ref('stock.stock_location_locations_partner').id,
'usage': 'internal',
'company_id': self.env.company.id,
})
self.subcontractor_partner1.property_stock_subcontractor = partner_subcontract_location.id
resupply_rule = resupply_sub_on_order_route.rule_ids.filtered(lambda l:
l.location_id == self.comp1.property_stock_production and
l.location_src_id == self.env.company.subcontracting_location_id)
resupply_rule.copy({'location_src_id': partner_subcontract_location.id})
resupply_warehouse_rule = self.warehouse.route_ids.rule_ids.filtered(lambda l:
l.location_id == self.env.company.subcontracting_location_id and
l.location_src_id == self.warehouse.lot_stock_id)
resupply_warehouse_rule.copy({'location_id': partner_subcontract_location.id})
# Create a receipt picking from the subcontractor
picking_form = Form(self.env['stock.picking'])
@@ -250,12 +122,11 @@ class TestSubcontractingFlows(SavepointCase):
# Nothing should be tracked
self.assertFalse(picking_receipt.display_action_record_components)
self.assertFalse(picking_receipt.display_view_subcontracted_move_lines)
# Pickings should directly be created
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
self.assertEqual(len(mo.picking_ids), 1)
self.assertEquals(mo.state, 'to_close')
self.assertEquals(mo.state, 'confirmed')
self.assertEqual(len(mo.picking_ids.move_lines), 2)
picking = mo.picking_ids
@@ -275,17 +146,19 @@ class TestSubcontractingFlows(SavepointCase):
picking_receipt.button_validate()
self.assertEquals(mo.state, 'done')
# Now that the picking is done, the details stat button should be visible
self.assertTrue(picking_receipt.display_view_subcontracted_move_lines)
# Available quantities should be negative at the subcontracting location for each components
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, 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, wh.lot_stock_id)
self.assertEquals(avail_qty_comp1, -1)
self.assertEquals(avail_qty_comp2, -1)
self.assertEquals(avail_qty_finished, 1)
avail_qty_comp1_in_global_location = self.env['stock.quant']._get_available_quantity(self.comp1, self.env.company.subcontracting_location_id, allow_negative=True)
avail_qty_comp2_in_global_location = self.env['stock.quant']._get_available_quantity(self.comp2, self.env.company.subcontracting_location_id, allow_negative=True)
self.assertEqual(avail_qty_comp1_in_global_location, 0.0)
self.assertEqual(avail_qty_comp2_in_global_location, 0.0)
def test_flow_3(self):
""" Tick "Resupply Subcontractor on Order" and "MTO" on the components and trigger the
creation of the subcontracting manufacturing order through a receipt picking. Checks if the
@@ -314,13 +187,16 @@ class TestSubcontractingFlows(SavepointCase):
# Nothing should be tracked
self.assertFalse(picking_receipt.display_action_record_components)
self.assertFalse(picking_receipt.display_view_subcontracted_move_lines)
# Pickings should directly be created
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
self.assertEqual(len(mo.picking_ids), 1)
self.assertEquals(mo.state, 'to_close')
self.assertEqual(len(mo.picking_ids.move_lines), 2)
self.assertEquals(mo.state, 'confirmed')
picking_delivery = mo.picking_ids
self.assertEqual(len(picking_delivery), 1)
self.assertEqual(len(picking_delivery.move_lines), 2)
self.assertEquals(picking_delivery.origin, picking_receipt.name)
self.assertEquals(picking_delivery.partner_id, picking_receipt.partner_id)
# The picking should be a delivery order
wh = picking_receipt.picking_type_id.warehouse_id
@@ -336,25 +212,86 @@ class TestSubcontractingFlows(SavepointCase):
picking_receipt.button_validate()
self.assertEquals(mo.state, 'done')
# Now that the picking is done, the details stat button should be visible
self.assertTrue(picking_receipt.display_view_subcontracted_move_lines)
# Available quantities should be negative at the subcontracting location for each components
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, 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, wh.lot_stock_id)
self.assertEquals(avail_qty_comp1, -1)
self.assertEquals(avail_qty_comp2, -1)
self.assertEquals(avail_qty_finished, 1)
def test_flow_4(self):
""" Tick "Manufacture" and "MTO" on the components and trigger the
creation of the subcontracting manufacturing order through a receipt
picking. Checks that the delivery to the subcontractor is not created
at the receipt creation. Then run the scheduler and check that
the delivery and MO exist.
"""
# Tick "manufacture" and MTO on self.comp2
mto_route = self.env['stock.location.route'].search([('name', '=', 'Replenish on Order (MTO)')])
manufacture_route = self.env['stock.location.route'].search([('name', '=', 'Manufacture')])
self.comp2.write({'route_ids': [(4, manufacture_route.id, None)]})
self.comp2.write({'route_ids': [(4, mto_route.id, None)]})
orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
orderpoint_form.product_id = self.comp2
orderpoint_form.product_min_qty = 0.0
orderpoint_form.product_max_qty = 10.0
orderpoint_form.location_id = self.env.company.subcontracting_location_id
orderpoint_form.save()
# 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
move.product_uom_qty = 1
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
warehouse = picking_receipt.picking_type_id.warehouse_id
# Pickings should directly be created
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
self.assertEquals(mo.state, 'confirmed')
picking_delivery = mo.picking_ids
self.assertFalse(picking_delivery)
picking_delivery = self.env['stock.picking'].search([('origin', 'ilike', '%' + picking_receipt.name + '%')])
self.assertFalse(picking_delivery)
move = self.env['stock.move'].search([
('product_id', '=', self.comp2.id),
('location_id', '=', warehouse.lot_stock_id.id),
('location_dest_id', '=', self.env.company.subcontracting_location_id.id)
])
self.assertFalse(move)
self.env['procurement.group'].run_scheduler(company_id=self.env.company.id)
move = self.env['stock.move'].search([
('product_id', '=', self.comp2.id),
('location_id', '=', warehouse.lot_stock_id.id),
('location_dest_id', '=', self.env.company.subcontracting_location_id.id)
])
self.assertTrue(move)
picking_delivery = move.picking_id
self.assertTrue(picking_delivery)
self.assertEqual(move.product_uom_qty, 11.0)
# As well as a manufacturing order for `self.comp2`
comp2mo = self.env['mrp.production'].search([('bom_id', '=', self.comp2_bom.id)])
self.assertEqual(len(comp2mo), 1)
def test_flow_5(self):
""" Check that the correct BoM is chosen accordingly to the partner
"""
# We create a second partner of type subcontractor
main_partner_2 = self.env['res.partner'].create({'name': 'main_partner'})
subcontractor_partner2 = self.env['res.partner'].create({
'name': 'subcontractor_partner',
'type': 'subcontractor',
'parent_id': main_partner_2.id,
'company_id': self.env.ref('base.main_company').id
})
@@ -408,6 +345,166 @@ class TestSubcontractingFlows(SavepointCase):
self.assertEquals(mo_pick1.bom_id, self.bom)
self.assertEquals(mo_pick2.bom_id, bom2)
def test_flow_6(self):
""" Extra quantity on the move.
"""
# We create a second partner of type subcontractor
main_partner_2 = self.env['res.partner'].create({'name': 'main_partner'})
subcontractor_partner2 = self.env['res.partner'].create({
'name': 'subcontractor_partner',
'parent_id': main_partner_2.id,
'company_id': self.env.ref('base.main_company').id,
})
self.env.cache.invalidate()
# We create a different BoM for the same product
comp3 = self.env['product.product'].create({
'name': 'Component3',
'type': 'product',
'categ_id': self.env.ref('product.product_category_all').id,
})
bom_form = Form(self.env['mrp.bom'])
bom_form.type = 'subcontract'
bom_form.product_tmpl_id = self.finished.product_tmpl_id
with bom_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.comp1
bom_line.product_qty = 1
with bom_form.bom_line_ids.new() as bom_line:
bom_line.product_id = comp3
bom_line.product_qty = 2
bom2 = bom_form.save()
# We assign the second BoM to the new partner
self.bom.write({'subcontractor_ids': [(4, self.subcontractor_partner1.id, None)]})
bom2.write({'subcontractor_ids': [(4, subcontractor_partner2.id, None)]})
# Create a receipt picking from the subcontractor1
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = subcontractor_partner2
with picking_form.move_ids_without_package.new() as move:
move.product_id = self.finished
move.product_uom_qty = 1
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
picking_receipt.move_lines.quantity_done = 3.0
picking_receipt.action_done()
mo = picking_receipt._get_subcontracted_productions()
move_comp1 = mo.move_raw_ids.filtered(lambda m: m.product_id == self.comp1)
move_comp3 = mo.move_raw_ids.filtered(lambda m: m.product_id == comp3)
self.assertEqual(sum(move_comp1.mapped('product_uom_qty')), 3.0)
self.assertEqual(sum(move_comp3.mapped('product_uom_qty')), 6.0)
self.assertEqual(sum(move_comp1.mapped('quantity_done')), 3.0)
self.assertEqual(sum(move_comp3.mapped('quantity_done')), 6.0)
move_finished = mo.move_finished_ids
self.assertEqual(sum(move_finished.mapped('product_uom_qty')), 3.0)
self.assertEqual(sum(move_finished.mapped('quantity_done')), 3.0)
def test_flow_7(self):
""" Process a subcontracting receipt with tracked component and
finished product. Simulate the regiter components button.
Once the components are registered, try to do a correction on exisiting
move lines and check that the subcontracting document is updated.
"""
# Create a receipt picking from the subcontractor
(self.comp1 | self.comp2 | self.finished).write({'tracking': 'lot'})
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
move.product_uom_qty = 5
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
mo = picking_receipt.move_lines.move_orig_ids.production_id
move_comp1 = mo.move_raw_ids.filtered(lambda m: m.product_id == self.comp1)
move_comp2 = mo.move_raw_ids.filtered(lambda m: m.product_id == self.comp2)
# move_finished is linked to receipt and not MO finished move.
move_finished = picking_receipt.move_lines
self.assertEqual(move_comp1.quantity_done, 0)
self.assertEqual(move_comp2.quantity_done, 0)
lot_c1 = self.env['stock.production.lot'].create({
'name': 'LOT C1',
'product_id': self.comp1.id
})
lot_c2 = self.env['stock.production.lot'].create({
'name': 'LOT C2',
'product_id': self.comp2.id
})
lot_f1 = self.env['stock.production.lot'].create({
'name': 'LOT F1',
'product_id': self.finished.id
})
register_form = Form(self.env['mrp.product.produce'].with_context(
active_id=picking_receipt._get_subcontracted_productions().id,
default_subcontract_move_id=picking_receipt.move_lines.id
))
register_form.qty_producing = 3.0
self.assertEqual(len(register_form._values['raw_workorder_line_ids']), 2,
'Register Components Form should contains one line per component.')
self.assertTrue(all(p[2]['product_id'] in (self.comp1 | self.comp2).ids for p in register_form._values['raw_workorder_line_ids']),
'Register Components Form should contains component.')
with register_form.raw_workorder_line_ids.edit(0) as pl:
pl.lot_id = lot_c1
with register_form.raw_workorder_line_ids.edit(1) as pl:
pl.lot_id = lot_c2
register_form.finished_lot_id = lot_f1
register_wizard = register_form.save()
action = register_wizard.continue_production()
register_form = Form(self.env['mrp.product.produce'].with_context(
**action['context']
))
with register_form.raw_workorder_line_ids.edit(0) as pl:
pl.lot_id = lot_c1
with register_form.raw_workorder_line_ids.edit(1) as pl:
pl.lot_id = lot_c2
register_form.finished_lot_id = lot_f1
register_wizard = register_form.save()
register_wizard.do_produce()
self.assertEqual(move_comp1.quantity_done, 5.0)
self.assertEqual(move_comp1.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).lot_id.name, 'LOT C1')
self.assertEqual(move_comp2.quantity_done, 5.0)
self.assertEqual(move_comp2.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).lot_id.name, 'LOT C2')
self.assertEqual(move_finished.quantity_done, 5.0)
self.assertEqual(move_finished.move_line_ids.filtered(lambda ml: ml.product_uom_qty).lot_id.name, 'LOT F1')
corrected_final_lot = self.env['stock.production.lot'].create({
'name': 'LOT F2',
'product_id': self.finished.id
})
details_operation_form = Form(picking_receipt.move_lines, view=self.env.ref('stock.view_stock_move_operations'))
for i in range(len(details_operation_form._values['move_line_ids'])):
with details_operation_form.move_line_ids.edit(i) as ml:
if ml._values['qty_done']:
ml.lot_id = corrected_final_lot
details_operation_form.save()
move_raw_comp_1 = picking_receipt.move_lines.move_orig_ids.production_id.move_raw_ids.filtered(lambda m: m.product_id == self.comp1)
move_raw_comp_2 = picking_receipt.move_lines.move_orig_ids.production_id.move_raw_ids.filtered(lambda m: m.product_id == self.comp2)
details_subcontract_moves_form = Form(move_raw_comp_1, view=self.env.ref('mrp_subcontracting.mrp_subcontracting_move_form_view'))
for i in range(len(details_subcontract_moves_form._values['move_line_ids'])):
with details_subcontract_moves_form.move_line_ids.edit(i) as sc:
if sc._values['qty_done']:
sc.lot_produced_ids.remove(index=0)
sc.lot_produced_ids.add(corrected_final_lot)
details_subcontract_moves_form.save()
details_subcontract_moves_form = Form(move_raw_comp_2, view=self.env.ref('mrp_subcontracting.mrp_subcontracting_move_form_view'))
for i in range(len(details_subcontract_moves_form._values['move_line_ids'])):
with details_subcontract_moves_form.move_line_ids.edit(i) as sc:
if sc._values['qty_done']:
sc.lot_produced_ids.remove(index=0)
sc.lot_produced_ids.add(corrected_final_lot)
details_subcontract_moves_form.save()
self.assertEqual(move_comp1.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).lot_produced_ids.name, 'LOT F2')
self.assertEqual(move_comp2.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).lot_produced_ids.name, 'LOT F2')
class TestSubcontractingTracking(TransactionCase):
def setUp(self):
@@ -416,7 +513,6 @@ class TestSubcontractingTracking(TransactionCase):
main_company_1 = self.env['res.partner'].create({'name': 'main_partner'})
self.subcontractor_partner1 = self.env['res.partner'].create({
'name': 'Subcontractor 1',
'type': 'subcontractor',
'parent_id': main_company_1.id,
'company_id': self.env.ref('base.main_company').id
})
@@ -444,6 +540,7 @@ class TestSubcontractingTracking(TransactionCase):
})
bom_form = Form(self.env['mrp.bom'])
bom_form.type = 'subcontract'
bom_form.subcontractor_ids.add(self.subcontractor_partner1)
bom_form.product_tmpl_id = self.finished_lot.product_tmpl_id
with bom_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.comp1_sn
@@ -468,7 +565,6 @@ class TestSubcontractingTracking(TransactionCase):
# We should be able to call the 'record_components' button
self.assertTrue(picking_receipt.display_action_record_components)
self.assertFalse(picking_receipt.display_view_subcontracted_move_lines)
# Check the created manufacturing order
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom_tracked.id)])
@@ -515,17 +611,15 @@ class TestSubcontractingTracking(TransactionCase):
# We should not be able to call the 'record_components' button
self.assertFalse(picking_receipt.display_action_record_components)
# We should see the move lines stat button
self.assertTrue(picking_receipt.display_view_subcontracted_move_lines)
picking_receipt.move_lines.quantity_done = 1
picking_receipt.move_lines.move_line_ids.lot_name = 'lot00001test'
picking_receipt.move_lines.move_line_ids.lot_id = lot_id.id
picking_receipt.button_validate()
self.assertEquals(mo.state, 'done')
# 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_supplier, allow_negative=True)
avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
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_lot, wh.lot_stock_id)
self.assertEquals(avail_qty_comp1, -1)
self.assertEquals(avail_qty_comp2, -1)
@@ -6,7 +6,7 @@
<field name="inherit_id" ref="mrp.mrp_bom_form_view" />
<field name="arch" type="xml">
<xpath expr="//field[@name='type']" position="after">
<field name="subcontractor_ids" widget="many2many_tags" context="{'default_type': 'subcontractor'}" attrs="{'invisible': [('type', '!=', 'subcontract')]}"/>
<field name="subcontractor_ids" widget="many2many_tags" context="{'default_type': 'subcontractor'}" attrs="{'invisible': [('type', '!=', 'subcontract')], 'required': [('type', '=', 'subcontract')]}"/>
</xpath>
</field>
</record>
@@ -1,14 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_partner_form_inherit_mrp" model="ir.ui.view">
<field name="name">res.partner.form.inherit.mrp</field>
<record id="view_partner_mrp_subcontracting_form" model="ir.ui.view">
<field name="name">res.partner.mrp_subcontracting.property.form.inherit</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form" />
<field name="inherit_id" ref="stock.view_partner_stock_form"/>
<field name="arch" type="xml">
<xpath expr="//span[@name='address_name']" position="inside">
<b attrs="{'invisible': ['|', ('parent_id', '=', False), ('type', '!=', 'subcontractor')]}">Subcontractor Address</b>
<xpath expr="//field[@name='property_stock_supplier']" position="after">
<field name="property_stock_subcontractor"/>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="mrp_subcontracting_move_form_view" model="ir.ui.view">
<field name="name">mrp.subcontracting.move.form.view</field>
<field name="model">stock.move</field>
<field name="priority">1000</field>
<field name="arch" type="xml">
<form create="0" delete="0">
<sheet>
<field name="product_id" invisible="1"/>
<field name="finished_lots_exist" invisible="1"/>
<field name="state" invisible="1"/>
<field name="move_line_ids" context="{'default_product_id': product_id}" attrs="{'readonly': [('state', 'in', ['done', 'cancel'])]}">
<tree editable="bottom" decoration-muted="state in ('done', 'cancel')">
<field name="state" invisible="1"/>
<field name="tracking" invisible="1"/>
<field name="product_id" readonly="1"/>
<field name="lot_produced_ids"
widget="many2many_tags"
context="{'default_product_id': parent.product_id}"
attrs="{'column_invisible': [('parent.finished_lots_exist', '!=', True)]}"
/>
<field name="qty_done"/>
<field name="lot_id" context="{'default_product_id': product_id}"/>
</tree>
</field>
</sheet>
</form>
</field>
</record>
<record id="mrp_subcontracting_move_tree_view" model="ir.ui.view">
<field name="name">mrp.subcontracting.move.tree.view</field>
<field name="model">stock.move</field>
<field name="priority">1000</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="mrp.view_stock_move_raw_tree"/>
<field name="arch" type="xml">
<xpath expr="//tree" position="attributes">
<attribute name="create">0</attribute>
<attribute name="delete">0</attribute>
</xpath>
</field>
</record>
</odoo>
@@ -5,18 +5,15 @@
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form" />
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<field name="display_view_subcontracted_move_lines" invisible="1" />
<button string="Subcontracted Moves" type="object"
name="action_view_subcontracted_move_lines"
class="oe_stat_button" icon="fa-exchange"
attrs="{'invisible': [('display_view_subcontracted_move_lines', '=', False)]}" />
</xpath>
<xpath expr="//button[@name='button_validate'][hasclass('o_btn_validate')]" position="before">
<field name="display_action_record_components" invisible="1" />
<button name="action_record_components" class="oe_highlight" attrs="{'invisible': [('display_action_record_components', '=', False)]}" string="Record components" type="object"/>
</xpath>
<xpath expr="//field[@name='move_ids_without_package']//tree//button[@name='action_show_details']" position="after">
<field name="show_subcontracting_details_visible" invisible="1"/>
<button name="action_show_subcontract_details" string="Register components for subcontracted product" type="object" icon="fa-sitemap"
attrs="{'invisible': [('show_subcontracting_details_visible', '=', False)]}"/>
</xpath>
</field>
</record>
</odoo>
@@ -10,16 +10,5 @@
</xpath>
</field>
</record>
<record id="product_supplierinfo_subcontracting_form_view" model="ir.ui.view">
<field name="name">product.supplierinfo.subcontracting.form.view</field>
<field name="model">product.supplierinfo</field>
<field name="inherit_id" ref="product.product_supplierinfo_form_view"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='vendor']" position="inside">
<field name="is_subcontractor" readonly="1"/>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mrp_product_produce
from . import stock_picking_return
@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo.tools.float_utils import float_is_zero
class MrpProductProduce(models.TransientModel):
_inherit = 'mrp.product.produce'
subcontract_move_id = fields.Many2one('stock.move', 'stock move from the subcontract picking')
def continue_production(self):
action = super(MrpProductProduce, self).continue_production()
action['context'] = dict(action['context'], default_subcontract_move_id=self.subcontract_move_id.id)
return action
def _generate_produce_lines(self):
""" When the wizard is called in backend, the onchange that create the
produce lines is not trigger. This method generate them and is used with
_record_production to appropriately set the lot_produced_id and
appropriately create raw stock move lines.
"""
self.ensure_one()
moves = (self.move_raw_ids | self.move_finished_ids).filtered(
lambda move: move.state not in ('done', 'cancel')
)
for move in moves:
qty_to_consume = self._prepare_component_quantity(move, self.qty_producing)
line_values = self._generate_lines_values(move, qty_to_consume)
self.env['mrp.product.produce.line'].create(line_values)
def _update_finished_move(self):
""" After producing, set the move line on the subcontract picking. """
res = super(MrpProductProduce, self)._update_finished_move()
if self.subcontract_move_id:
self.env['stock.move.line'].create({
'move_id': self.subcontract_move_id.id,
'picking_id': self.subcontract_move_id.picking_id.id,
'product_id': self.product_id.id,
'location_id': self.subcontract_move_id.location_id.id,
'location_dest_id': self.subcontract_move_id.location_dest_id.id,
'product_uom_qty': 0,
'product_uom_id': self.product_uom_id.id,
'qty_done': self.qty_producing,
'lot_id': self.finished_lot_id and self.finished_lot_id.id,
})
if not self._get_todo(self.production_id):
ml_reserved = self.subcontract_move_id.move_line_ids.filtered(lambda ml:
float_is_zero(ml.qty_done, precision_rounding=ml.product_uom_id.rounding) and
not float_is_zero(ml.product_uom_qty, precision_rounding=ml.product_uom_id.rounding))
ml_reserved.unlink()
for ml in self.subcontract_move_id.move_line_ids:
ml.product_uom_qty = ml.qty_done
self.subcontract_move_id._recompute_state()
return res
@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class ReturnPicking(models.TransientModel):
_inherit = 'stock.return.picking'
def _prepare_move_default_values(self, return_line, new_picking):
vals = super(ReturnPicking, self)._prepare_move_default_values(return_line, new_picking)
if return_line.move_id.is_subcontract:
vals['location_dest_id'] = self.picking_id.partner_id.property_stock_subcontractor.id
return vals
@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Subcontracting Management with Stock Valuation',
'version': '0.1',
'category': 'Hidden',
'description': """
This bridge module allows to manage subcontracting with valuation.
""",
'depends': ['mrp_subcontracting', 'mrp_account'],
'installable': True,
'auto_install': True,
}
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import product
from . import stock_picking
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo.osv.expression import OR
class StockPicking(models.Model):
_inherit = 'stock.picking'
def action_view_stock_valuation_layers(self):
action = super(StockPicking, self).action_view_stock_valuation_layers()
subcontracted_productions = self._get_subcontracted_productions()
if not subcontracted_productions:
return action
domain = action['domain']
domain_subcontracting = [('id', 'in', (subcontracted_productions.move_raw_ids | subcontracted_productions.move_finished_ids).stock_valuation_layer_ids.ids)]
domain = OR([domain, domain_subcontracting])
return dict(action, domain=domain)
def _prepare_subcontract_mo_vals(self, subcontract_move, bom):
vals = super(StockPicking, self)._prepare_subcontract_mo_vals(subcontract_move, bom)
if bom.product_tmpl_id.cost_method in ('fifo', 'average'):
vals = dict(vals, extra_cost=subcontract_move._get_price_unit())
return vals
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_subcontracting_account
@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.mrp_subcontracting.tests.common import TestMrpSubcontractingCommon
from odoo.tests.common import Form
class TestAccountSubcontractingFlows(TestMrpSubcontractingCommon):
def test_subcontracting_account_flow_1(self):
self.stock_location = self.env.ref('stock.stock_location_stock')
self.customer_location = self.env.ref('stock.stock_location_customers')
self.supplier_location = self.env.ref('stock.stock_location_suppliers')
self.uom_unit = self.env.ref('uom.product_uom_unit')
self.env.ref('product.product_category_all').property_cost_method = 'fifo'
# IN 10@10 comp1 10@20 comp2
move1 = self.env['stock.move'].create({
'name': 'IN 10 units @ 10.00 per unit',
'location_id': self.supplier_location.id,
'location_dest_id': self.env.company.subcontracting_location_id.id,
'product_id': self.comp1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 10.0,
'price_unit': 10.0,
})
move1._action_confirm()
move1._action_assign()
move1.move_line_ids.qty_done = 10.0
move1._action_done()
move2 = self.env['stock.move'].create({
'name': 'IN 10 units @ 20.00 per unit',
'location_id': self.supplier_location.id,
'location_dest_id': self.env.company.subcontracting_location_id.id,
'product_id': self.comp2.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 10.0,
'price_unit': 20.0,
})
move2._action_confirm()
move2._action_assign()
move2.move_line_ids.qty_done = 10.0
move2._action_done()
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
move.product_uom_qty = 1
picking_receipt = picking_form.save()
picking_receipt.move_lines.price_unit = 30.0
picking_receipt.action_confirm()
picking_receipt.move_lines.quantity_done = 1.0
picking_receipt.action_done()
mo = picking_receipt._get_subcontracted_productions()
self.assertEqual(mo.move_finished_ids.stock_valuation_layer_ids.value, 60)
self.assertEqual(mo.move_finished_ids.product_id.value_svl, 60)
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
@@ -3,16 +3,13 @@
{
'name': 'Purchase and Subcontracting Management',
'name': 'Dropship and Subcontracting Management',
'version': '0.1',
'category': 'Hidden',
'description': """
This bridge module allows to manage subcontracting with the purchase module.
This bridge module allows to manage subcontracting with the dropshipping module.
""",
'depends': ['purchase_stock', 'mrp_subcontracting'],
'data': [
'views/supplier_info_views.xml',
],
'depends': ['mrp_subcontracting', 'stock_dropshipping'],
'installable': True,
'auto_install': True,
}
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import stock_picking
@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class StockPicking(models.Model):
_inherit = 'stock.picking'
def _get_warehouse(self, subcontract_move):
if subcontract_move.sale_line_id:
return subcontract_move.sale_line_id.order_id.warehouse_id
return super(StockPicking, self)._get_warehouse(subcontract_move)
@@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import Form
from odoo.addons.mrp_subcontracting.tests.common import TestMrpSubcontractingCommon
class TestSubcontractingDropshippingFlows(TestMrpSubcontractingCommon):
def test_mrp_subcontracting_dropshipping_1(self):
""" Mark the subcontracted product with the route dropship and add the
subcontractor as seller. The component has the routes 'MTO', 'Replenish
on order' and 'Buy'. Also another partner is set as vendor on the comp.
Create a SO and check that:
- Delivery between subcontractor and customer for subcontracted product.
- Delivery for the component to the subcontractor for the specified wh.
- Po created for the component.
"""
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)
@@ -1,37 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * purchase_mrp_subcontracting
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~12.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
"PO-Revision-Date: 2019-08-12 11:32+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: purchase_mrp_subcontracting
#: model:ir.model.fields,help:purchase_mrp_subcontracting.field_product_supplierinfo__is_subcontractor
msgid ""
"Choose a vendor of type subcontractor if you want to subcontract the product"
msgstr ""
#. module: purchase_mrp_subcontracting
#: model:ir.model.fields,field_description:purchase_mrp_subcontracting.field_product_supplierinfo__is_subcontractor
msgid "Subcontracted"
msgstr ""
#. module: purchase_mrp_subcontracting
#: model:ir.model,name:purchase_mrp_subcontracting.model_product_supplierinfo
msgid "Supplier Pricelist"
msgstr ""
#. module: purchase_mrp_subcontracting
#: model:ir.model,name:purchase_mrp_subcontracting.model_stock_picking
msgid "Transfer"
msgstr ""
@@ -1,16 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class StockPicking(models.Model):
_inherit = 'stock.picking'
def _prepare_subcontract_mo_vals(self, subcontract_move, bom):
vals = super(StockPicking, self)._prepare_subcontract_mo_vals(subcontract_move, bom)
if bom.product_tmpl_id.cost_method in ('fifo', 'average'):
vals = dict(vals, extra_cost=subcontract_move._get_price_unit())
return vals
@@ -1,113 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from odoo.tests import Form
from odoo.tests.common import TransactionCase
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
class TestPurchaseSubcontractingFlows(TransactionCase):
def setUp(self):
super(TestPurchaseSubcontractingFlows, self).setUp()
# 1. Create a BOM of subcontracting type
self.comp1 = self.env['product.product'].create({
'name': 'Component1',
'type': 'product',
'categ_id': self.env.ref('product.product_category_all').id,
})
self.comp2 = self.env['product.product'].create({
'name': 'Component2',
'type': 'product',
'categ_id': self.env.ref('product.product_category_all').id,
})
self.finished_product = self.env['product.product'].create({
'name': 'finished',
'type': 'product',
'categ_id': self.env.ref('product.product_category_all').id,
})
bom_form = Form(self.env['mrp.bom'])
bom_form.type = 'subcontract'
bom_form.product_tmpl_id = self.finished_product.product_tmpl_id
with bom_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.comp1
bom_line.product_qty = 1
with bom_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.comp2
bom_line.product_qty = 1
self.bom_finished_product = bom_form.save()
# 2: Create a subcontracting partner
company_partner = self.env['res.partner'].create({'name': 'main_partner', 'is_company': True})
self.subcontractor_partner1 = self.env['res.partner'].create({
'name': 'subcontractor_partner',
'type': 'subcontractor',
'parent_id': company_partner.id,
'company_id': self.env.ref('base.main_company').id
})
self.stock_location = self.env.ref('stock.stock_location_stock')
self.customer_location = self.env.ref('stock.stock_location_customers')
self.supplier_location = self.env.ref('stock.stock_location_suppliers')
self.uom_unit = self.env.ref('uom.product_uom_unit')
def test_purchase_1(self):
self.env.ref('product.product_category_all').property_cost_method = 'fifo'
# IN 10@10 comp1 10@20 comp2
move1 = self.env['stock.move'].create({
'name': 'IN 10 units @ 10.00 per unit',
'location_id': self.supplier_location.id,
'location_dest_id': self.env.user.company_id.subcontracting_location_id.id,
'product_id': self.comp1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 10.0,
'price_unit': 10.0,
})
move1._action_confirm()
move1._action_assign()
move1.move_line_ids.qty_done = 10.0
move1._action_done()
move1 = self.env['stock.move'].create({
'name': 'IN 10 units @ 20.00 per unit',
'location_id': self.supplier_location.id,
'location_dest_id': self.env.user.company_id.subcontracting_location_id.id,
'product_id': self.comp2.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 10.0,
'price_unit': 20.0,
})
move1._action_confirm()
move1._action_assign()
move1.move_line_ids.qty_done = 10.0
move1._action_done()
po1 = self.env['purchase.order'].create({
'partner_id': self.subcontractor_partner1.id,
'order_line': [
(0, 0, {
'name': self.finished_product.name,
'product_id': self.finished_product.id,
'product_qty': 1,
'product_uom': self.finished_product.uom_po_id.id,
'price_unit': 30.0,
'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
}),
],
})
po1.button_confirm()
picking1 = po1.picking_ids[0]
# Check the created manufacturing order
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom_finished_product.id)])
self.assertEqual(len(mo), 1)
move1 = picking1.move_lines[0]
move1.quantity_done = 1
picking1.button_validate()
self.assertEqual(mo.move_finished_ids.stock_valuation_layer_ids.value, 60)
self.assertEqual(mo.move_finished_ids.product_id.value_svl, 60)
+7 -12
View File
@@ -872,8 +872,8 @@ class StockMove(models.Model):
""" return create values for new picking that will be linked with group
of moves in self.
"""
origins = self.filtered(lambda m: m.origin).mapped('origin')
origin = len(origins) == 1 and origins[0] or False
origins = set(self.filtered(lambda m: m.origin).mapped('origin'))
origin = len(origins) == 1 and origins.pop() or False
partners = self.mapped('partner_id')
partner = len(partners) == 1 and partners.id or False
return {
@@ -1048,6 +1048,10 @@ class StockMove(models.Model):
self.env['stock.move.line'].create(self._prepare_move_line_vals(quantity=quantity, reserved_quant=reserved_quant))
return taken_quantity
def _should_bypass_reservation(self):
self.ensure_one()
return self.location_id.should_bypass_reservation() or self.product_id.type != 'product'
def _action_assign(self):
""" Reserve stock moves by creating their stock move lines. A stock move is
considered reserved once the sum of `product_qty` for all its move lines is
@@ -1064,8 +1068,7 @@ class StockMove(models.Model):
rounding = roundings[move]
missing_reserved_uom_quantity = move.product_uom_qty - reserved_availability[move]
missing_reserved_quantity = move.product_uom._compute_quantity(missing_reserved_uom_quantity, move.product_id.uom_id, rounding_method='HALF-UP')
if move.location_id.should_bypass_reservation()\
or move.product_id.type == 'consu':
if move._should_bypass_reservation():
# create the move line(s) but do not impact quants
if move.product_id.tracking == 'serial' and (move.picking_type_id.use_create_lots or move.picking_type_id.use_existing_lots):
for i in range(0, int(missing_reserved_quantity)):
@@ -1453,11 +1456,6 @@ class StockMove(models.Model):
""" This method will try to apply the procure method MTO on some moves if
a compatible MTO route is found. Else the procure method will be set to MTS
"""
try:
mto_route = self.env['stock.warehouse']._find_global_route('stock.route_warehouse0_mto',
'Make To Order')
except ValueError:
mto_route = False
for move in self:
product_id = move.product_id
domain = [
@@ -1466,10 +1464,7 @@ class StockMove(models.Model):
('action', '!=', 'push')
]
rules = self.env['procurement.group']._search_rule(False, product_id, move.warehouse_id, domain)
routes = product_id.route_ids + product_id.route_from_categ_ids + move.warehouse_id.route_ids
if rules and (rules.procure_method == 'make_to_order'):
move.procure_method = rules.procure_method
elif mto_route and mto_route.id in [x.id for x in routes]:
move.procure_method = 'make_to_order'
else:
move.procure_method = 'make_to_stock'
+12 -8
View File
@@ -224,7 +224,7 @@ class StockMoveLine(models.Model):
# TODO Move me in the update
if 'product_uom_qty' in vals:
for ml in self.filtered(lambda m: m.state in ('partially_available', 'assigned') and m.product_id.type == 'product'):
if not ml.location_id.should_bypass_reservation():
if not ml._should_bypass_reservation(ml.location_id):
qty_to_decrease = ml.product_qty - ml.product_uom_id._compute_quantity(vals['product_uom_qty'], ml.product_id.uom_id, rounding_method='HALF-UP')
try:
Quant._update_reserved_quantity(ml.product_id, ml.location_id, -qty_to_decrease, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
@@ -249,7 +249,7 @@ class StockMoveLine(models.Model):
if updates:
for ml in self.filtered(lambda ml: ml.state in ['partially_available', 'assigned'] and ml.product_id.type == 'product'):
if not ml.location_id.should_bypass_reservation():
if not ml._should_bypass_reservation(ml.location_id):
try:
Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
except UserError:
@@ -258,7 +258,7 @@ class StockMoveLine(models.Model):
else:
raise
if not updates.get('location_id', ml.location_id).should_bypass_reservation():
if not ml._should_bypass_reservation(updates.get('location_id', ml.location_id)):
new_product_qty = 0
try:
q = Quant._update_reserved_quantity(ml.product_id, updates.get('location_id', ml.location_id), ml.product_qty, lot_id=updates.get('lot_id', ml.lot_id),
@@ -301,7 +301,7 @@ class StockMoveLine(models.Model):
result_package_id = updates.get('result_package_id', ml.result_package_id)
owner_id = updates.get('owner_id', ml.owner_id)
quantity = ml.move_id.product_uom._compute_quantity(qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP')
if not location_id.should_bypass_reservation():
if not ml._should_bypass_reservation(location_id):
ml._free_reservation(product_id, location_id, quantity, lot_id=lot_id, package_id=package_id, owner_id=owner_id)
if not float_is_zero(quantity, precision_digits=precision):
available_qty, in_date = Quant._update_available_quantity(product_id, location_id, -quantity, lot_id=lot_id, package_id=package_id, owner_id=owner_id)
@@ -312,7 +312,7 @@ class StockMoveLine(models.Model):
taken_from_untracked_qty = min(untracked_qty, abs(available_qty))
Quant._update_available_quantity(product_id, location_id, -taken_from_untracked_qty, lot_id=False, package_id=package_id, owner_id=owner_id)
Quant._update_available_quantity(product_id, location_id, taken_from_untracked_qty, lot_id=lot_id, package_id=package_id, owner_id=owner_id)
if not location_id.should_bypass_reservation():
if not ml._should_bypass_reservation(location_id):
ml._free_reservation(ml.product_id, location_id, untracked_qty, lot_id=False, package_id=package_id, owner_id=owner_id)
Quant._update_available_quantity(product_id, location_dest_id, quantity, lot_id=lot_id, package_id=result_package_id, owner_id=owner_id, in_date=in_date)
@@ -348,7 +348,7 @@ class StockMoveLine(models.Model):
if ml.state in ('done', 'cancel'):
raise UserError(_('You can not delete product moves if the picking is done. You can only correct the done quantities.'))
# Unlinking a move line should unreserve.
if ml.product_id.type == 'product' and not ml.location_id.should_bypass_reservation() and not float_is_zero(ml.product_qty, precision_digits=precision):
if ml.product_id.type == 'product' and not ml._should_bypass_reservation(ml.location_id) and not float_is_zero(ml.product_qty, precision_digits=precision):
try:
self.env['stock.quant']._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
except UserError:
@@ -428,11 +428,11 @@ class StockMoveLine(models.Model):
rounding = ml.product_uom_id.rounding
# if this move line is force assigned, unreserve elsewhere if needed
if not ml.location_id.should_bypass_reservation() and float_compare(ml.qty_done, ml.product_qty, precision_rounding=rounding) > 0:
if not ml._should_bypass_reservation(ml.location_id) and float_compare(ml.qty_done, ml.product_qty, precision_rounding=rounding) > 0:
extra_qty = ml.qty_done - ml.product_qty
ml._free_reservation(ml.product_id, ml.location_id, extra_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, ml_to_ignore=done_ml)
# unreserve what's been reserved
if not ml.location_id.should_bypass_reservation() and ml.product_id.type == 'product' and ml.product_qty:
if not ml._should_bypass_reservation(ml.location_id) and ml.product_id.type == 'product' and ml.product_qty:
try:
Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
except UserError:
@@ -541,3 +541,7 @@ class StockMoveLine(models.Model):
move_to_recompute_state |= candidate.move_id
break
move_to_recompute_state._recompute_state()
def _should_bypass_reservation(self, location):
self.ensure_one()
return location.should_bypass_reservation() or self.product_id.type != 'product'
+2
View File
@@ -912,6 +912,8 @@ class Picking(models.Model):
stream is 'DOWN', it should sort/group by tuple(object on
which the activity is log, the responsible for this object)
"""
if self.env.context.get('skip_activity'):
return {}
move_to_orig_object_rel = {co: ooc for ooc in orig_obj_changes.keys() for co in ooc[stream_field]}
origin_objects = self.env[list(orig_obj_changes.keys())[0]._name].concat(*list(orig_obj_changes.keys()))
# The purpose here is to group each destination object by