[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:
@@ -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': {
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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': ['&', ('state', '!=', 'draft'), ('is_locked', '=', True)]}">
|
||||
<tree delete="0" default_order="is_done,sequence" decoration-muted="is_done" decoration-warning="quantity_done>product_uom_qty" decoration-success="not is_done and quantity_done==product_uom_qty" decoration-danger="not is_done and reserved_availability < 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': ['&', ('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', '=', [])]}">
|
||||
|
||||
@@ -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>product_uom_qty" decoration-success="not is_done and quantity_done==product_uom_qty" decoration-danger="not is_done and reserved_availability < 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>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,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,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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+5
-4
@@ -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
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
-11
@@ -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
@@ -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
-6
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user