diff --git a/addons/mrp/models/stock_warehouse.py b/addons/mrp/models/stock_warehouse.py index 40d6b6bb4b9..98e8cada056 100644 --- a/addons/mrp/models/stock_warehouse.py +++ b/addons/mrp/models/stock_warehouse.py @@ -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': { diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index 2ad70c21f4e..d21e246215e 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -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): diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml index 09a9c79bf3d..f49312a6c41 100644 --- a/addons/mrp/views/mrp_production_views.xml +++ b/addons/mrp/views/mrp_production_views.xml @@ -173,30 +173,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/addons/mrp/views/stock_move_views.xml b/addons/mrp/views/stock_move_views.xml index 1fb4c2248fd..cf0a37472fd 100644 --- a/addons/mrp/views/stock_move_views.xml +++ b/addons/mrp/views/stock_move_views.xml @@ -66,6 +66,36 @@ + + stock.move.raw.tree + stock.move + 1000 + + + + + + + + + + + + + + + + + + + + + + + + + + mrp.finished.move.line.form 1000 diff --git a/addons/mrp/wizard/change_production_qty.py b/addons/mrp/wizard/change_production_qty.py index 56bba899044..f0ab200582a 100644 --- a/addons/mrp/wizard/change_production_qty.py +++ b/addons/mrp/wizard/change_production_qty.py @@ -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): diff --git a/addons/mrp/wizard/mrp_product_produce.py b/addons/mrp/wizard/mrp_product_produce.py index 1c8c26fbd96..6cdde0f8ae4 100644 --- a/addons/mrp/wizard/mrp_product_produce.py +++ b/addons/mrp/wizard/mrp_product_produce.py @@ -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): diff --git a/addons/mrp_subcontracting/__init__.py b/addons/mrp_subcontracting/__init__.py index be29e5c5628..35e7c9600c5 100644 --- a/addons/mrp_subcontracting/__init__.py +++ b/addons/mrp_subcontracting/__init__.py @@ -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 diff --git a/addons/mrp_subcontracting/__manifest__.py b/addons/mrp_subcontracting/__manifest__.py index 2ae49d5351b..2a3152636f9 100644 --- a/addons/mrp_subcontracting/__manifest__.py +++ b/addons/mrp_subcontracting/__manifest__.py @@ -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', } diff --git a/addons/mrp_subcontracting/data/mrp_subcontracting_data.xml b/addons/mrp_subcontracting/data/mrp_subcontracting_data.xml index 3966da5b9f2..bd87182e2e9 100644 --- a/addons/mrp_subcontracting/data/mrp_subcontracting_data.xml +++ b/addons/mrp_subcontracting/data/mrp_subcontracting_data.xml @@ -7,6 +7,10 @@ 5 + + + + diff --git a/addons/mrp_subcontracting/data/mrp_subcontracting_demo.xml b/addons/mrp_subcontracting/data/mrp_subcontracting_demo.xml new file mode 100644 index 00000000000..2cc21105207 --- /dev/null +++ b/addons/mrp_subcontracting/data/mrp_subcontracting_demo.xml @@ -0,0 +1,32 @@ + + + + + + + subcontract + + + + + + 1 + + + + + + + + 1 + 1 + 1 + 27 + + + + + + + + \ No newline at end of file diff --git a/addons/mrp_subcontracting/models/__init__.py b/addons/mrp_subcontracting/models/__init__.py index 98238bd9a1b..8dddd8d9090 100644 --- a/addons/mrp_subcontracting/models/__init__.py +++ b/addons/mrp_subcontracting/models/__init__.py @@ -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 diff --git a/addons/mrp_subcontracting/models/mrp_bom.py b/addons/mrp_subcontracting/models/mrp_bom.py index 8f0d87d0b73..a0a1d054cb9 100644 --- a/addons/mrp_subcontracting/models/mrp_bom.py +++ b/addons/mrp_subcontracting/models/mrp_bom.py @@ -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) - diff --git a/addons/purchase_mrp_subcontracting/models/product.py b/addons/mrp_subcontracting/models/product.py similarity index 57% rename from addons/purchase_mrp_subcontracting/models/product.py rename to addons/mrp_subcontracting/models/product.py index 18bd7a53bd5..88e969cc24b 100644 --- a/addons/purchase_mrp_subcontracting/models/product.py +++ b/addons/mrp_subcontracting/models/product.py @@ -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 diff --git a/addons/mrp_subcontracting/models/res_company.py b/addons/mrp_subcontracting/models/res_company.py index 93687fa4d27..74cd5555591 100644 --- a/addons/mrp_subcontracting/models/res_company.py +++ b/addons/mrp_subcontracting/models/res_company.py @@ -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 diff --git a/addons/mrp_subcontracting/models/res_partner.py b/addons/mrp_subcontracting/models/res_partner.py index 925296fee0c..603d5dbbe93 100644 --- a/addons/mrp_subcontracting/models/res_partner.py +++ b/addons/mrp_subcontracting/models/res_partner.py @@ -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.") diff --git a/addons/mrp_subcontracting/models/stock_move.py b/addons/mrp_subcontracting/models/stock_move.py index daf847891bf..80f8badbd24 100644 --- a/addons/mrp_subcontracting/models/stock_move.py +++ b/addons/mrp_subcontracting/models/stock_move.py @@ -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() diff --git a/addons/mrp_subcontracting/models/stock_move_line.py b/addons/mrp_subcontracting/models/stock_move_line.py new file mode 100644 index 00000000000..43f2c8a978d --- /dev/null +++ b/addons/mrp_subcontracting/models/stock_move_line.py @@ -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 diff --git a/addons/mrp_subcontracting/models/stock_picking.py b/addons/mrp_subcontracting/models/stock_picking.py index b3c93fb8b7e..00a6fbbd11a 100644 --- a/addons/mrp_subcontracting/models/stock_picking.py +++ b/addons/mrp_subcontracting/models/stock_picking.py @@ -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 diff --git a/addons/mrp_subcontracting/models/stock_warehouse.py b/addons/mrp_subcontracting/models/stock_warehouse.py index a6c1dc6983b..41e5d704bc1 100644 --- a/addons/mrp_subcontracting/models/stock_warehouse.py +++ b/addons/mrp_subcontracting/models/stock_warehouse.py @@ -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 - diff --git a/addons/mrp_subcontracting/tests/common.py b/addons/mrp_subcontracting/tests/common.py new file mode 100644 index 00000000000..224dfa08504 --- /dev/null +++ b/addons/mrp_subcontracting/tests/common.py @@ -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) diff --git a/addons/mrp_subcontracting/tests/test_subcontracting.py b/addons/mrp_subcontracting/tests/test_subcontracting.py index 30d24418c64..448eda56547 100644 --- a/addons/mrp_subcontracting/tests/test_subcontracting.py +++ b/addons/mrp_subcontracting/tests/test_subcontracting.py @@ -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) diff --git a/addons/mrp_subcontracting/views/mrp_bom_views.xml b/addons/mrp_subcontracting/views/mrp_bom_views.xml index d384f1ab4ea..6f7ab944e13 100644 --- a/addons/mrp_subcontracting/views/mrp_bom_views.xml +++ b/addons/mrp_subcontracting/views/mrp_bom_views.xml @@ -6,7 +6,7 @@ - + diff --git a/addons/mrp_subcontracting/views/res_partner_views.xml b/addons/mrp_subcontracting/views/res_partner_views.xml index 4be30a1d03f..ad54ee15c0e 100644 --- a/addons/mrp_subcontracting/views/res_partner_views.xml +++ b/addons/mrp_subcontracting/views/res_partner_views.xml @@ -1,14 +1,13 @@ - - res.partner.form.inherit.mrp + + res.partner.mrp_subcontracting.property.form.inherit res.partner - + - - Subcontractor Address + + - diff --git a/addons/mrp_subcontracting/views/stock_move_views.xml b/addons/mrp_subcontracting/views/stock_move_views.xml new file mode 100644 index 00000000000..565dece1ad6 --- /dev/null +++ b/addons/mrp_subcontracting/views/stock_move_views.xml @@ -0,0 +1,44 @@ + + + + mrp.subcontracting.move.form.view + stock.move + 1000 + +
+ + + + + + + + + + + + + + + +
+
+
+ + mrp.subcontracting.move.tree.view + stock.move + 1000 + primary + + + + 0 + 0 + + + +
\ No newline at end of file diff --git a/addons/mrp_subcontracting/views/stock_picking_views.xml b/addons/mrp_subcontracting/views/stock_picking_views.xml index 23145eeabbf..29a6e908953 100644 --- a/addons/mrp_subcontracting/views/stock_picking_views.xml +++ b/addons/mrp_subcontracting/views/stock_picking_views.xml @@ -5,18 +5,15 @@ stock.picking - - -