mirror of
https://github.com/OCA/manufacture.git
synced 2025-01-28 16:37:15 +02:00
[MIG] mrp_subcontracting: Adapt to OCA guidelines + initial work
This commit is contained in:
committed by
Pedro M. Baeza
parent
f3a0fc97fc
commit
0426acf030
@@ -1,19 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import SUPERUSER_ID, api
|
||||
|
||||
from . import models
|
||||
from . import wizard
|
||||
|
||||
|
||||
def uninstall_hook(cr, registry):
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
warehouses = env["stock.warehouse"].search([])
|
||||
subcontracting_routes = warehouses.mapped("subcontracting_route_id")
|
||||
warehouses.write({"subcontracting_route_id": False})
|
||||
# Fail unlink means that the route is used somewhere (e.g. route_id on stock.rule). In this case
|
||||
# we don't try to do anything.
|
||||
try:
|
||||
subcontracting_routes.unlink()
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': "mrp_subcontracting",
|
||||
'version': '0.1',
|
||||
'version': '12.0.1.0.0',
|
||||
'summary': "Subcontract Productions",
|
||||
'description': "",
|
||||
"author": "Odoo S.A., Odoo Community Association (OCA)",
|
||||
'website': 'https://www.odoo.com/page/manufacturing',
|
||||
'category': 'Manufacturing/Manufacturing',
|
||||
'depends': ['mrp'],
|
||||
@@ -21,5 +21,4 @@
|
||||
'demo': [
|
||||
'data/mrp_subcontracting_demo.xml',
|
||||
],
|
||||
'uninstall_hook': 'uninstall_hook',
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
<value model="stock.warehouse" eval="obj().env['stock.warehouse'].search([]).ids"/>
|
||||
<value eval="{'subcontracting_to_resupply': True}"/>
|
||||
</function>
|
||||
|
||||
<function model="stock.picking.type" name="write">
|
||||
<value model="stock.picking.type" eval="obj().env['stock.picking.type'].search([('code', '=', 'mrp_operation')]).ids"/>
|
||||
<value eval="{'use_create_components_lots': True}"/>
|
||||
</function>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
|
||||
19
mrp_subcontracting/hooks.py
Normal file
19
mrp_subcontracting/hooks.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||
# Copyright 2019 Odoo
|
||||
# Copyright 2020 Tecnativa - Alexandre Díaz
|
||||
# Copyright 2020 Tecnativa - Pedro M. Baeza
|
||||
|
||||
from odoo import SUPERUSER_ID, api
|
||||
|
||||
|
||||
def uninstall_hook(cr, registry):
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
warehouses = env["stock.warehouse"].search([])
|
||||
subcontracting_routes = warehouses.mapped("subcontracting_route_id")
|
||||
warehouses.write({"subcontracting_route_id": False})
|
||||
# Fail unlink means that the route is used somewhere (e.g. route_id on
|
||||
# stock.rule). In this case, we don't try to do anything.
|
||||
try:
|
||||
subcontracting_routes.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -1,12 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mrp_bom
|
||||
from . import mrp_production
|
||||
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_rule
|
||||
from . import stock_picking_type
|
||||
from . import stock_warehouse
|
||||
|
||||
from . import stock_production_lot
|
||||
|
||||
@@ -1,19 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.osv.expression import AND
|
||||
|
||||
|
||||
class MrpBom(models.Model):
|
||||
_inherit = 'mrp.bom'
|
||||
|
||||
type = fields.Selection(selection_add=[('subcontract', 'Subcontracting')])
|
||||
subcontractor_ids = fields.Many2many('res.partner', 'mrp_bom_subcontractor', string='Subcontractors', check_company=True)
|
||||
subcontractor_ids = fields.Many2many(
|
||||
'res.partner', 'mrp_bom_subcontractor', string='Subcontractors',
|
||||
check_company=True)
|
||||
|
||||
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)
|
||||
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 = AND([domain, [('subcontractor_ids', 'parent_of', subcontractor.ids)]])
|
||||
domain = AND([domain, [
|
||||
('subcontractor_ids', 'parent_of', subcontractor.ids),
|
||||
]])
|
||||
return self.search(domain, order='sequence, product_id', limit=1)
|
||||
else:
|
||||
return self.env['mrp.bom']
|
||||
|
||||
# This is a copy from mrp v13.0
|
||||
@api.model
|
||||
def _bom_find_domain(self, product_tmpl=None, product=None,
|
||||
picking_type=None, company_id=False, bom_type=False):
|
||||
if product:
|
||||
if not product_tmpl:
|
||||
product_tmpl = product.product_tmpl_id
|
||||
domain = [
|
||||
'|', ('product_id', '=', product.id),
|
||||
'&', ('product_id', '=', False),
|
||||
('product_tmpl_id', '=', product_tmpl.id),
|
||||
]
|
||||
elif product_tmpl:
|
||||
domain = [('product_tmpl_id', '=', product_tmpl.id)]
|
||||
else:
|
||||
# neither product nor template, makes no sense to search
|
||||
raise UserError(_(
|
||||
'You should provide either a product or a product template to\
|
||||
search a BoM'))
|
||||
if picking_type:
|
||||
domain += ['|', ('picking_type_id', '=', picking_type.id),
|
||||
('picking_type_id', '=', False)]
|
||||
if company_id or self.env.context.get('company_id'):
|
||||
domain = domain + [
|
||||
'|', ('company_id', '=', False),
|
||||
('company_id', '=',
|
||||
company_id or self.env.context.get('company_id')),
|
||||
]
|
||||
if bom_type:
|
||||
domain += [('type', '=', bom_type)]
|
||||
# order to prioritize bom with product_id over the one without
|
||||
return domain
|
||||
|
||||
18
mrp_subcontracting/models/mrp_production.py
Normal file
18
mrp_subcontracting/models/mrp_production.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||
# Copyright 2020 Tecnativa - Pedro M. Baeza
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class MrpProduction(models.Model):
|
||||
_inherit = 'mrp.production'
|
||||
|
||||
@api.depends(
|
||||
'workorder_ids.state',
|
||||
'move_finished_ids',
|
||||
'move_finished_ids.quantity_done',
|
||||
'is_locked',
|
||||
)
|
||||
def _get_produced_qty(self):
|
||||
"""Add workorder_ids.state to depends list."""
|
||||
return super()._get_produced_qty()
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
@@ -7,11 +6,15 @@ from odoo import api, fields, models
|
||||
class SupplierInfo(models.Model):
|
||||
_inherit = 'product.supplierinfo'
|
||||
|
||||
is_subcontractor = fields.Boolean('Subcontracted', compute='_compute_is_subcontractor', help="Choose a vendor of type subcontractor if you want to subcontract the product")
|
||||
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', 'product_id', 'product_tmpl_id')
|
||||
def _compute_is_subcontractor(self):
|
||||
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)
|
||||
boms |= supplier.product_tmpl_id.bom_ids.filtered(
|
||||
lambda b: not b.product_id)
|
||||
supplier.is_subcontractor = supplier.name in boms.subcontractor_ids
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
@@ -20,11 +19,13 @@ class ResCompany(models.Model):
|
||||
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')
|
||||
])
|
||||
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:
|
||||
subcontracting_location = self.env['stock.location'].create({
|
||||
'name': _('%s: Subcontracting Location') % company.name,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
@@ -8,6 +7,7 @@ class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
property_stock_subcontractor = fields.Many2one(
|
||||
'stock.location', string="Subcontractor Location", company_dependent=True,
|
||||
'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.")
|
||||
|
||||
13
mrp_subcontracting/models/stock_location.py
Normal file
13
mrp_subcontracting/models/stock_location.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||
# Copyright 2020 Tecnativa - Pedro M. Baeza
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class StockLocation(models.Model):
|
||||
_inherit = 'stock.location'
|
||||
|
||||
def should_bypass_reservation(self):
|
||||
if self.env.context.get('mrp_subcontracting_bypass_reservation'):
|
||||
return True
|
||||
return super().should_bypass_reservation()
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
@@ -17,10 +16,15 @@ class StockMove(models.Model):
|
||||
)
|
||||
|
||||
def _compute_show_subcontracting_details_visible(self):
|
||||
""" Compute if the action button in order to see moves raw is visible """
|
||||
"""
|
||||
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):
|
||||
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
|
||||
@@ -54,8 +58,8 @@ class StockMove(models.Model):
|
||||
if 'product_uom_qty' in values:
|
||||
if self.env.context.get('cancel_backorder') is False:
|
||||
return super(StockMove, self).write(values)
|
||||
self.filtered(lambda m: m.is_subcontract and
|
||||
m.state not in ['draft', 'cancel', 'done'])._update_subcontract_order_qty(values['product_uom_qty'])
|
||||
self.filtered(lambda m: m.is_subcontract
|
||||
and m.state not in ['draft', 'cancel', 'done'])._update_subcontract_order_qty(values['product_uom_qty'])
|
||||
return super(StockMove, self).write(values)
|
||||
|
||||
def action_show_details(self):
|
||||
@@ -67,12 +71,17 @@ class StockMove(models.Model):
|
||||
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:
|
||||
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 and self._has_tracked_subcontract_components():
|
||||
action['views'] = [(self.env.ref('stock.view_stock_move_operations').id, 'form')]
|
||||
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,
|
||||
@@ -82,8 +91,10 @@ class StockMove(models.Model):
|
||||
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')
|
||||
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',
|
||||
@@ -93,36 +104,38 @@ class StockMove(models.Model):
|
||||
'domain': [('id', 'in', moves.ids)],
|
||||
}
|
||||
|
||||
def _action_cancel(self):
|
||||
for move in self:
|
||||
if move.is_subcontract:
|
||||
move.move_orig_ids.production_id._action_cancel()
|
||||
return super()._action_cancel()
|
||||
|
||||
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':
|
||||
if move.location_id.usage != 'supplier' \
|
||||
or move.location_dest_id.usage == 'supplier':
|
||||
continue
|
||||
if move.move_orig_ids.production_id:
|
||||
continue
|
||||
bom = move._get_subcontract_bom()
|
||||
if not bom:
|
||||
continue
|
||||
if float_is_zero(move.product_qty, precision_rounding=move.product_uom.rounding) and\
|
||||
if float_is_zero(move.product_qty,
|
||||
precision_rounding=move.product_uom.rounding) and\
|
||||
move.picking_id.immediate_transfer is True:
|
||||
raise UserError(_("To subcontract, use a planned transfer."))
|
||||
subcontract_details_per_picking[move.picking_id].append((move, bom))
|
||||
subcontract_details_per_picking[move.picking_id].append(
|
||||
(move, bom))
|
||||
move.write({
|
||||
'is_subcontract': True,
|
||||
'location_id': move.picking_id.partner_id.with_context(force_company=move.company_id.id).property_stock_subcontractor.id
|
||||
'location_id': move.picking_id.partner_id.with_context(
|
||||
force_company=move.company_id.id)
|
||||
.property_stock_subcontractor.id
|
||||
})
|
||||
for picking, subcontract_details in subcontract_details_per_picking.items():
|
||||
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)
|
||||
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()
|
||||
self.env['stock.picking'].concat(
|
||||
*list(subcontract_details_per_picking.keys())).action_assign()
|
||||
return res
|
||||
|
||||
def _action_record_components(self):
|
||||
@@ -143,11 +156,14 @@ class StockMove(models.Model):
|
||||
for move in self:
|
||||
if not move.is_subcontract:
|
||||
continue
|
||||
# Extra quantity is allowed when components do not need to be register
|
||||
# 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:
|
||||
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(_("""
|
||||
@@ -156,7 +172,8 @@ 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'))))
|
||||
operations.""") % ('\n'.join(overprocessed_moves.mapped(
|
||||
'product_id.display_name'))))
|
||||
|
||||
def _get_subcontract_bom(self):
|
||||
self.ensure_one()
|
||||
@@ -171,21 +188,22 @@ operations.""") % ('\n'.join(overprocessed_moves.mapped('product_id.display_name
|
||||
|
||||
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)
|
||||
return any(m.has_tracking != 'none' for m in
|
||||
self.move_orig_ids.production_id.move_raw_ids)
|
||||
|
||||
def _prepare_extra_move_vals(self, qty):
|
||||
vals = super(StockMove, self)._prepare_extra_move_vals(qty)
|
||||
vals = super()._prepare_extra_move_vals(qty)
|
||||
vals['location_id'] = self.location_id.id
|
||||
return vals
|
||||
|
||||
def _prepare_move_split_vals(self, qty):
|
||||
vals = super(StockMove, self)._prepare_move_split_vals(qty)
|
||||
vals = super()._prepare_move_split_vals(qty)
|
||||
vals['location_id'] = self.location_id.id
|
||||
return vals
|
||||
|
||||
def _should_bypass_reservation(self):
|
||||
""" If the move is subcontracted then ignore the reservation. """
|
||||
should_bypass_reservation = super(StockMove, self)._should_bypass_reservation()
|
||||
should_bypass_reservation = super()._should_bypass_reservation()
|
||||
if not should_bypass_reservation and self.is_subcontract:
|
||||
return True
|
||||
return should_bypass_reservation
|
||||
@@ -195,8 +213,9 @@ operations.""") % ('\n'.join(overprocessed_moves.mapped('product_id.display_name
|
||||
quantity_change = quantity - move.product_uom_qty
|
||||
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': production.product_uom_qty + quantity_change
|
||||
}).change_prod_qty()
|
||||
|
||||
self.env['change.production.qty'].with_context(
|
||||
skip_activity=True).create({
|
||||
'mo_id': production.id,
|
||||
'product_qty': production.product_uom_qty
|
||||
+ quantity_change,
|
||||
}).change_prod_qty()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
@@ -10,17 +9,20 @@ class StockMoveLine(models.Model):
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super(StockMoveLine, self).create(vals_list)
|
||||
records.filtered(lambda ml: ml.move_id.is_subcontract).move_id._check_overprocessed_subcontract_qty()
|
||||
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()
|
||||
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)
|
||||
should_bypass_reservation = super()._should_bypass_reservation(
|
||||
location)
|
||||
if not should_bypass_reservation and self.move_id.is_subcontract:
|
||||
return True
|
||||
return should_bypass_reservation
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import timedelta
|
||||
@@ -9,7 +8,8 @@ from odoo import api, fields, models
|
||||
class StockPicking(models.Model):
|
||||
_inherit = 'stock.picking'
|
||||
|
||||
display_action_record_components = fields.Boolean(compute='_compute_display_action_record_components')
|
||||
display_action_record_components = fields.Boolean(
|
||||
compute='_compute_display_action_record_components')
|
||||
|
||||
@api.depends('state')
|
||||
def _compute_display_action_record_components(self):
|
||||
@@ -22,13 +22,17 @@ class StockPicking(models.Model):
|
||||
picking.display_action_record_components = False
|
||||
continue
|
||||
# Hide if no components are track
|
||||
subcontracted_productions = picking._get_subcontracted_productions()
|
||||
subcontracted_moves = subcontracted_productions.mapped('move_raw_ids')
|
||||
if all(subcontracted_move.has_tracking == 'none' for subcontracted_move in subcontracted_moves):
|
||||
subcontracted_productions = picking\
|
||||
._get_subcontracted_productions()
|
||||
subcontracted_moves = subcontracted_productions.mapped(
|
||||
'move_raw_ids')
|
||||
if all(subcontracted_move.has_tracking == 'none'
|
||||
for subcontracted_move in subcontracted_moves):
|
||||
picking.display_action_record_components = False
|
||||
continue
|
||||
# Hide if the production is to close
|
||||
if not subcontracted_productions.filtered(lambda mo: mo.state not in ('to_close', 'done')):
|
||||
if not subcontracted_productions.filtered(
|
||||
lambda mo: mo.state not in ('to_close', 'done')):
|
||||
picking.display_action_record_components = False
|
||||
continue
|
||||
picking.display_action_record_components = True
|
||||
@@ -46,27 +50,33 @@ class StockPicking(models.Model):
|
||||
continue
|
||||
production = move.move_orig_ids.production_id
|
||||
if move._has_tracked_subcontract_components():
|
||||
move.move_orig_ids.filtered(lambda m: m.state not in ('done', 'cancel')).move_line_ids.unlink()
|
||||
move_finished_ids = move.move_orig_ids.filtered(lambda m: m.state not in ('done', 'cancel'))
|
||||
move.move_orig_ids.filtered(
|
||||
lambda m: m.state not in ('done', 'cancel'))\
|
||||
.move_line_ids.unlink()
|
||||
move_finished_ids = move.move_orig_ids.filtered(
|
||||
lambda m: m.state not in ('done', 'cancel'))
|
||||
for ml in move.move_line_ids:
|
||||
ml.copy({
|
||||
'picking_id': False,
|
||||
'production_id': move_finished_ids.production_id.id,
|
||||
'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,
|
||||
'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 = 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
|
||||
@@ -75,12 +85,18 @@ class StockPicking(models.Model):
|
||||
subcontracted_production.post_inventory()
|
||||
else:
|
||||
subcontracted_production.button_mark_done()
|
||||
# For concistency, set the date on production move before the date
|
||||
# on picking. (Tracability report + Product Moves menu item)
|
||||
# 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)})
|
||||
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):
|
||||
@@ -98,14 +114,16 @@ class StockPicking(models.Model):
|
||||
# -------------------------------------------------------------------------
|
||||
def _is_subcontract(self):
|
||||
self.ensure_one()
|
||||
return self.picking_type_id.code == 'incoming' and any(m.is_subcontract for m in self.move_lines)
|
||||
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
|
||||
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()
|
||||
@@ -121,8 +139,14 @@ class StockPicking(models.Model):
|
||||
'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.with_context(force_company=subcontract_move.company_id.id).property_stock_subcontractor.id,
|
||||
'location_dest_id': subcontract_move.picking_id.partner_id.with_context(force_company=subcontract_move.company_id.id).property_stock_subcontractor.id,
|
||||
'location_src_id':
|
||||
subcontract_move.picking_id.partner_id.with_context(
|
||||
force_company=subcontract_move.company_id.id)
|
||||
.property_stock_subcontractor.id,
|
||||
'location_dest_id':
|
||||
subcontract_move.picking_id.partner_id.with_context(
|
||||
force_company=subcontract_move.company_id.id)
|
||||
.property_stock_subcontractor.id,
|
||||
'product_qty': subcontract_move.product_uom_qty,
|
||||
'picking_type_id': warehouse.subcontracting_type_id.id
|
||||
}
|
||||
@@ -131,11 +155,13 @@ class StockPicking(models.Model):
|
||||
def _subcontracted_produce(self, subcontract_details):
|
||||
self.ensure_one()
|
||||
for move, bom in subcontract_details:
|
||||
mo = self.env['mrp.production'].with_context(force_company=move.company_id.id).create(self._prepare_subcontract_mo_vals(move, bom))
|
||||
mo = self.env['mrp.production'].with_context(
|
||||
force_company=move.company_id.id)\
|
||||
.create(self._prepare_subcontract_mo_vals(move, bom))
|
||||
self.env['stock.move'].create(mo._get_moves_raw_values())
|
||||
mo.action_confirm()
|
||||
|
||||
# Link the finished to the receipt move.
|
||||
finished_move = mo.move_finished_ids.filtered(lambda m: m.product_id == move.product_id)
|
||||
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()
|
||||
mo._generate_moves()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||
# Copyright 2019 Odoo
|
||||
# Copyright 2020 Tecnativa - Alexandre Díaz
|
||||
# Copyright 2020 Tecnativa - Pedro M. Baeza
|
||||
|
||||
from odoo import models
|
||||
|
||||
@@ -8,6 +10,7 @@ class StockRule(models.Model):
|
||||
_inherit = "stock.rule"
|
||||
|
||||
def _push_prepare_move_copy_values(self, move_to_copy, new_date):
|
||||
new_move_vals = super(StockRule, self)._push_prepare_move_copy_values(move_to_copy, new_date)
|
||||
new_move_vals = super(StockRule, self)._push_prepare_move_copy_values(
|
||||
move_to_copy, new_date)
|
||||
new_move_vals["is_subcontract"] = False
|
||||
return new_move_vals
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, _
|
||||
@@ -17,7 +16,9 @@ class StockWarehouse(models.Model):
|
||||
'stock.rule', 'Subcontracting MTS Rule'
|
||||
)
|
||||
|
||||
subcontracting_route_id = fields.Many2one('stock.location.route', 'Resupply Subcontractor', ondelete='restrict')
|
||||
subcontracting_route_id = fields.Many2one('stock.location.route',
|
||||
'Resupply Subcontractor',
|
||||
ondelete='restrict')
|
||||
|
||||
subcontracting_type_id = fields.Many2one(
|
||||
'stock.picking.type', 'Subcontracting Operation Type',
|
||||
@@ -29,7 +30,9 @@ class StockWarehouse(models.Model):
|
||||
for warehouse in self:
|
||||
result[warehouse.id].update({
|
||||
'subcontract': [
|
||||
self.Routing(warehouse.lot_stock_id, subcontract_location_id, warehouse.out_type_id, 'pull'),
|
||||
self.Routing(warehouse.lot_stock_id,
|
||||
subcontract_location_id,
|
||||
warehouse.out_type_id, 'pull'),
|
||||
]
|
||||
})
|
||||
return result
|
||||
@@ -46,7 +49,8 @@ class StockWarehouse(models.Model):
|
||||
'product_selectable': False,
|
||||
'company_id': self.company_id.id,
|
||||
'sequence': 10,
|
||||
'name': self._format_routename(name=_('Resupply Subcontractor'))
|
||||
'name': self._format_routename(
|
||||
name=_('Resupply Subcontractor'))
|
||||
},
|
||||
'route_update_values': {
|
||||
'active': self.subcontracting_to_resupply,
|
||||
@@ -70,8 +74,10 @@ class StockWarehouse(models.Model):
|
||||
'company_id': self.company_id.id,
|
||||
'action': 'pull',
|
||||
'auto': 'manual',
|
||||
'route_id': self._find_global_route('stock.route_warehouse0_mto', _('Make To Order')).id,
|
||||
'name': self._format_rulename(self.lot_stock_id, subcontract_location_id, 'MTO'),
|
||||
'route_id': self._find_global_route(
|
||||
'stock.route_warehouse0_mto', _('Make To Order')).id,
|
||||
'name': self._format_rulename(
|
||||
self.lot_stock_id, subcontract_location_id, 'MTO'),
|
||||
'location_id': subcontract_location_id.id,
|
||||
'location_src_id': self.lot_stock_id.id,
|
||||
'picking_type_id': self.out_type_id.id
|
||||
@@ -87,9 +93,11 @@ class StockWarehouse(models.Model):
|
||||
'company_id': self.company_id.id,
|
||||
'action': 'pull',
|
||||
'auto': 'manual',
|
||||
'route_id': self._find_global_route('mrp_subcontracting.route_resupply_subcontractor_mto',
|
||||
_('Resupply Subcontractor on Order')).id,
|
||||
'name': self._format_rulename(self.lot_stock_id, subcontract_location_id, False),
|
||||
'route_id': self._find_global_route(
|
||||
'mrp_subcontracting.route_resupply_subcontractor_mto',
|
||||
_('Resupply Subcontractor on Order')).id,
|
||||
'name': self._format_rulename(
|
||||
self.lot_stock_id, subcontract_location_id, False),
|
||||
'location_id': production_location_id.id,
|
||||
'location_src_id': subcontract_location_id.id,
|
||||
'picking_type_id': self.out_type_id.id
|
||||
@@ -102,23 +110,29 @@ class StockWarehouse(models.Model):
|
||||
return rules
|
||||
|
||||
def _get_picking_type_create_values(self, max_sequence):
|
||||
data, next_sequence = super(StockWarehouse, self)._get_picking_type_create_values(max_sequence)
|
||||
data, next_sequence = super()._get_picking_type_create_values(
|
||||
max_sequence)
|
||||
data.update({
|
||||
'subcontracting_type_id': {
|
||||
'name': _('Subcontracting'),
|
||||
'code': 'mrp_operation',
|
||||
'use_create_components_lots': True,
|
||||
'sequence': next_sequence + 2,
|
||||
'sequence_code': 'SBC',
|
||||
'company_id': self.company_id.id,
|
||||
#'sequence_code': 'SBC',
|
||||
#'company_id': self.company_id.id,
|
||||
},
|
||||
})
|
||||
return data, max_sequence + 4
|
||||
|
||||
def _get_sequence_values(self):
|
||||
values = super(StockWarehouse, self)._get_sequence_values()
|
||||
values = super()._get_sequence_values()
|
||||
values.update({
|
||||
'subcontracting_type_id': {'name': self.name + ' ' + _('Sequence subcontracting'), 'prefix': self.code + '/SBC/', 'padding': 5, 'company_id': self.company_id.id},
|
||||
'subcontracting_type_id': {
|
||||
'name': self.name + ' ' + _('Sequence subcontracting'),
|
||||
'prefix': self.code + '/SBC/',
|
||||
'padding': 5,
|
||||
#'company_id': self.company_id.id,
|
||||
},
|
||||
})
|
||||
return values
|
||||
|
||||
|
||||
5
mrp_subcontracting/readme/CONTRIBUTORS.rst
Normal file
5
mrp_subcontracting/readme/CONTRIBUTORS.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
* Odoo S.A.
|
||||
* `Tecnativa <https://www.tecnativa.com>`__:
|
||||
|
||||
* Alexandre Díaz
|
||||
* Pedro M. Baeza
|
||||
3
mrp_subcontracting/readme/CREDITS.rst
Normal file
3
mrp_subcontracting/readme/CREDITS.rst
Normal file
@@ -0,0 +1,3 @@
|
||||
This module is a backport from Odoo SA and as such, it is not included in the
|
||||
OCA CLA. That means we do not have a copy of the copyright on it like all other
|
||||
OCA modules.
|
||||
6
mrp_subcontracting/readme/DESCRIPTION.rst
Normal file
6
mrp_subcontracting/readme/DESCRIPTION.rst
Normal file
@@ -0,0 +1,6 @@
|
||||
This module is a backport of the one found in official Odoo 13.0, adapted
|
||||
for this version.
|
||||
|
||||
For the configuration and usage, see Odoo documentation:
|
||||
|
||||
https://www.odoo.com/documentation/user/13.0/manufacturing/management/subcontracting.html
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_subcontracting
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests.common import Form, SavepointCase
|
||||
@@ -7,7 +6,7 @@ class TestMrpSubcontractingCommon(SavepointCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMrpSubcontractingCommon, cls).setUpClass()
|
||||
super().setUp()
|
||||
# 1: Create a subcontracting partner
|
||||
main_partner = cls.env['res.partner'].create({'name': 'main_partner'})
|
||||
cls.subcontractor_partner1 = cls.env['res.partner'].create({
|
||||
|
||||
@@ -1,31 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import Form
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.addons.mrp_subcontracting.tests.common import TestMrpSubcontractingCommon
|
||||
from odoo.addons.mrp_subcontracting.tests.common import (
|
||||
TestMrpSubcontractingCommon)
|
||||
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSubcontractingBasic(TransactionCase):
|
||||
def test_subcontracting_location_1(self):
|
||||
""" Checks the creation and presence of the subcontracting location. """
|
||||
self.assertTrue(self.env.company.subcontracting_location_id)
|
||||
self.assertTrue(self.env.company.subcontracting_location_id.active)
|
||||
"""
|
||||
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)
|
||||
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)
|
||||
self.assertTrue(
|
||||
self.env.user.company_id.subcontracting_location_id
|
||||
!= company2.subcontracting_location_id)
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
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
|
||||
""" 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')))
|
||||
warehouses = self.env['stock.warehouse'].search([])
|
||||
self.assertTrue(all(warehouses.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')
|
||||
@@ -37,7 +48,9 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
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.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)
|
||||
|
||||
@@ -56,7 +69,8 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
'product_id': self.comp1.id,
|
||||
'product_min_qty': 0,
|
||||
'product_max_qty': 0,
|
||||
'location_id': self.env.user.company_id.subcontracting_location_id.id,
|
||||
'location_id':
|
||||
self.env.user.company_id.subcontracting_location_id.id,
|
||||
'group_id': pg1.id,
|
||||
})
|
||||
|
||||
@@ -98,17 +112,18 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
'name': 'Specific partner location',
|
||||
'location_id': self.env.ref('stock.stock_location_locations_partner').id,
|
||||
'usage': 'internal',
|
||||
'company_id': self.env.company.id,
|
||||
'company_id': self.env.user.company_id.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)
|
||||
l.location_src_id == self.env.user.company_id.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
|
||||
resupply_warehouse_rule = self.warehouse.route_ids[0].rule_ids.filtered(lambda l:
|
||||
l.location_id == self.env.user.company_id.subcontracting_location_id and
|
||||
l.location_src_id == self.warehouse.lot_stock_id)
|
||||
resupply_warehouse_rule.copy({'location_id': partner_subcontract_location.id})
|
||||
for warehouse_rule in resupply_warehouse_rule:
|
||||
warehouse_rule.copy({'location_id': partner_subcontract_location.id})
|
||||
|
||||
# Create a receipt picking from the subcontractor
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
@@ -154,8 +169,8 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
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)
|
||||
avail_qty_comp1_in_global_location = self.env['stock.quant']._get_available_quantity(self.comp1, self.env.user.company_id.subcontracting_location_id, allow_negative=True)
|
||||
avail_qty_comp2_in_global_location = self.env['stock.quant']._get_available_quantity(self.comp2, self.env.user.company_id.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)
|
||||
|
||||
@@ -237,7 +252,7 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
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.location_id = self.env.user.company_id.subcontracting_location_id
|
||||
orderpoint_form.save()
|
||||
|
||||
# Create a receipt picking from the subcontractor
|
||||
@@ -265,16 +280,16 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
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)
|
||||
('location_dest_id', '=', self.env.user.company_id.subcontracting_location_id.id)
|
||||
])
|
||||
self.assertFalse(move)
|
||||
|
||||
self.env['procurement.group'].run_scheduler(company_id=self.env.company.id)
|
||||
self.env['procurement.group'].run_scheduler(company_id=self.env.user.company_id.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)
|
||||
('location_dest_id', '=', self.env.user.company_id.subcontracting_location_id.id)
|
||||
])
|
||||
self.assertTrue(move)
|
||||
picking_delivery = move.picking_id
|
||||
@@ -430,57 +445,57 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
lot_c1 = self.env['stock.production.lot'].create({
|
||||
'name': 'LOT C1',
|
||||
'product_id': self.comp1.id,
|
||||
'company_id': self.env.company.id,
|
||||
'company_id': self.env.user.company_id.id,
|
||||
})
|
||||
lot_c2 = self.env['stock.production.lot'].create({
|
||||
'name': 'LOT C2',
|
||||
'product_id': self.comp2.id,
|
||||
'company_id': self.env.company.id,
|
||||
'company_id': self.env.user.company_id.id,
|
||||
})
|
||||
lot_f1 = self.env['stock.production.lot'].create({
|
||||
'name': 'LOT F1',
|
||||
'product_id': self.finished.id,
|
||||
'company_id': self.env.company.id,
|
||||
'company_id': self.env.user.company_id.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')
|
||||
# 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,
|
||||
'company_id': self.env.company.id,
|
||||
'company_id': self.env.user.company_id.id,
|
||||
})
|
||||
|
||||
details_operation_form = Form(picking_receipt.move_lines, view=self.env.ref('stock.view_stock_move_operations'))
|
||||
@@ -540,63 +555,7 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
backorder.action_done()
|
||||
self.assertTrue(picking_receipt.move_lines.move_orig_ids.production_id.state == 'done')
|
||||
|
||||
def test_flow_9(self):
|
||||
"""Ensure that cancel the subcontract moves will also delete the
|
||||
components need for the subcontractor.
|
||||
"""
|
||||
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)]
|
||||
})
|
||||
|
||||
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()
|
||||
|
||||
picking_delivery = self.env['stock.move'].search([
|
||||
('product_id', 'in', (self.comp1 | self.comp2).ids)
|
||||
]).picking_id
|
||||
self.assertTrue(picking_delivery)
|
||||
self.assertEqual(picking_delivery.state, 'confirmed')
|
||||
self.assertEqual(self.comp1.virtual_available, -5)
|
||||
self.assertEqual(self.comp2.virtual_available, -5)
|
||||
# action_cancel is not call on the picking in order
|
||||
# to test behavior from other source than picking (e.g. puchase).
|
||||
picking_receipt.move_lines._action_cancel()
|
||||
self.assertEqual(picking_delivery.state, 'cancel')
|
||||
self.assertEqual(self.comp1.virtual_available, 0.0)
|
||||
self.assertEqual(self.comp1.virtual_available, 0.0)
|
||||
|
||||
def test_flow_10(self):
|
||||
"""Receipts from a children contact of a subcontractor are properly
|
||||
handled.
|
||||
"""
|
||||
# Create a children contact
|
||||
subcontractor_contact = self.env['res.partner'].create({
|
||||
'name': 'Test children subcontractor contact',
|
||||
'parent_id': self.subcontractor_partner1.id,
|
||||
})
|
||||
# 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 = subcontractor_contact
|
||||
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()
|
||||
# Check that a manufacturing order is created
|
||||
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
|
||||
self.assertEqual(len(mo), 1)
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSubcontractingTracking(TransactionCase):
|
||||
def setUp(self):
|
||||
super(TestSubcontractingTracking, self).setUp()
|
||||
@@ -686,19 +645,20 @@ class TestSubcontractingTracking(TransactionCase):
|
||||
lot_id = self.env['stock.production.lot'].create({
|
||||
'name': 'lot1',
|
||||
'product_id': self.finished_lot.id,
|
||||
'company_id': self.env.company.id,
|
||||
'company_id': self.env.user.company_id.id,
|
||||
})
|
||||
serial_id = self.env['stock.production.lot'].create({
|
||||
'name': 'lot1',
|
||||
'product_id': self.comp1_sn.id,
|
||||
'company_id': self.env.company.id,
|
||||
'company_id': self.env.user.company_id.id,
|
||||
})
|
||||
produce_form = Form(self.env['mrp.product.produce'].with_context({
|
||||
'active_id': mo.id,
|
||||
'active_ids': [mo.id],
|
||||
}))
|
||||
produce_form.finished_lot_id = lot_id
|
||||
produce_form.raw_workorder_line_ids._records[0]['lot_id'] = serial_id.id
|
||||
#produce_form.finished_lot_id = lot_id
|
||||
#produce_form.raw_workorder_line_ids._records[0]['lot_id'] = serial_id.id
|
||||
produce_form.lot_id = lot_id
|
||||
wiz_produce = produce_form.save()
|
||||
wiz_produce.do_produce()
|
||||
|
||||
|
||||
@@ -13,15 +13,15 @@
|
||||
<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="company_id" invisible="1"/>
|
||||
<!--field name="company_id" invisible="1"/-->
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="tracking" invisible="1"/>
|
||||
<field name="product_id" readonly="1"/>
|
||||
<field name="lot_produced_ids"
|
||||
<!--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>
|
||||
@@ -30,7 +30,7 @@
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="mrp_subcontracting_move_tree_view" model="ir.ui.view">
|
||||
<!--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>
|
||||
@@ -42,5 +42,5 @@
|
||||
<attribute name="delete">0</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</record-->
|
||||
</odoo>
|
||||
|
||||
@@ -16,5 +16,20 @@
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
<record id="view_picking_type_form_inherit_mrp" model="ir.ui.view">
|
||||
<field name="name">Operation Types</field>
|
||||
<field name="model">stock.picking.type</field>
|
||||
<field name="inherit_id" ref="stock.view_picking_type_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="show_operations" position="attributes">
|
||||
<attribute name="attrs">{"invisible": [("code", "=", "mrp_operation")]}</attribute>
|
||||
</field>
|
||||
<xpath expr="//group[@groups='stock.group_production_lot']" position="after">
|
||||
<group attrs='{"invisible": [("code", "!=", "mrp_operation")]}' string="Traceability" groups="stock.group_production_lot">
|
||||
<field name="use_create_components_lots"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mrp_product_produce
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
@@ -53,4 +52,3 @@ class MrpProductProduce(models.TransientModel):
|
||||
ml.product_uom_qty = ml.qty_done
|
||||
self.subcontract_move_id._recompute_state()
|
||||
return res
|
||||
|
||||
|
||||
19
mrp_subcontracting/wizard/stock_backorder_confirmation.py
Normal file
19
mrp_subcontracting/wizard/stock_backorder_confirmation.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||
# Copyright 2019 Odoo
|
||||
# Copyright 2020 Tecnativa - Alexandre Díaz
|
||||
# Copyright 2020 Tecnativa - Pedro M. Baeza
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class StockBackorderConfirmation(models.TransientModel):
|
||||
_inherit = 'stock.backorder.confirmation'
|
||||
|
||||
def _process(self, cancel_backorder=False):
|
||||
"""Needed for passing the cancel_backorder context that allows to
|
||||
not automatically change the produced quantity in subcontracting.
|
||||
"""
|
||||
return super(
|
||||
StockBackorderConfirmation,
|
||||
self.with_context(cancel_backorder=cancel_backorder),
|
||||
)._process(cancel_backorder=cancel_backorder)
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
Reference in New Issue
Block a user