mirror of
https://github.com/OCA/manufacture.git
synced 2025-01-28 16:37:15 +02:00
[MIG] mrp_subcontracting: Adapt to v12 + make tests to pass
This commit is contained in:
87
mrp_subcontracting/README.rst
Normal file
87
mrp_subcontracting/README.rst
Normal file
@@ -0,0 +1,87 @@
|
||||
=======================
|
||||
Subcontract Productions
|
||||
=======================
|
||||
|
||||
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Beta
|
||||
.. |badge2| image:: https://img.shields.io/badge/github-OCA%2Fmanufacture-lightgray.png?logo=github
|
||||
:target: https://github.com/OCA/manufacture/tree/12.0/mrp_subcontracting
|
||||
:alt: OCA/manufacture
|
||||
.. |badge3| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||
:target: https://translation.odoo-community.org/projects/manufacture-12-0/manufacture-12-0-mrp_subcontracting
|
||||
:alt: Translate me on Weblate
|
||||
.. |badge4| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
|
||||
:target: https://runbot.odoo-community.org/runbot/129/12.0
|
||||
:alt: Try me on Runbot
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4|
|
||||
|
||||
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
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/manufacture/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us smashing it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/OCA/manufacture/issues/new?body=module:%20mrp_subcontracting%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Authors
|
||||
~~~~~~~
|
||||
|
||||
* Odoo S.A.
|
||||
* Tecnativa
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Odoo S.A.
|
||||
* `Tecnativa <https://www.tecnativa.com>`__:
|
||||
|
||||
* Alexandre Díaz
|
||||
* Pedro M. Baeza
|
||||
|
||||
Other credits
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
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.
|
||||
|
||||
Maintainers
|
||||
~~~~~~~~~~~
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
This module is part of the `OCA/manufacture <https://github.com/OCA/manufacture/tree/12.0/mrp_subcontracting>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||
@@ -1,2 +1,5 @@
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from . import models
|
||||
from . import wizard
|
||||
from .hooks import uninstall_hook
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
# 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
|
||||
|
||||
{
|
||||
'name': "mrp_subcontracting",
|
||||
'name': "Subcontract Productions",
|
||||
'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',
|
||||
"author": "Odoo S.A., Tecnativa, Odoo Community Association (OCA)",
|
||||
'website': 'https://github.com/OCA/manufacture',
|
||||
'category': 'Manufacturing Orders & BOMs',
|
||||
'depends': ['mrp'],
|
||||
'data': [
|
||||
'data/mrp_subcontracting_data.xml',
|
||||
@@ -21,4 +22,6 @@
|
||||
'demo': [
|
||||
'data/mrp_subcontracting_demo.xml',
|
||||
],
|
||||
"uninstall_hook": "uninstall_hook",
|
||||
"license": "LGPL-3",
|
||||
}
|
||||
|
||||
@@ -11,10 +11,5 @@
|
||||
<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>
|
||||
|
||||
@@ -29,4 +29,4 @@
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from . import mrp_bom
|
||||
from . import mrp_production
|
||||
from . import product
|
||||
from . import res_company
|
||||
from . import res_partner
|
||||
from . import stock_location
|
||||
from . import stock_move
|
||||
from . import stock_move_line
|
||||
from . import stock_picking
|
||||
from . import stock_picking_type
|
||||
from . import stock_rule
|
||||
from . import stock_warehouse
|
||||
from . import stock_production_lot
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# 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 _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo import api, fields, models
|
||||
from odoo.osv.expression import AND
|
||||
|
||||
|
||||
@@ -10,8 +12,7 @@ class MrpBom(models.Model):
|
||||
|
||||
type = fields.Selection(selection_add=[('subcontract', 'Subcontracting')])
|
||||
subcontractor_ids = fields.Many2many(
|
||||
'res.partner', 'mrp_bom_subcontractor', string='Subcontractors',
|
||||
check_company=True)
|
||||
'res.partner', 'mrp_bom_subcontractor', string='Subcontractors')
|
||||
|
||||
def _bom_subcontract_find(self, product_tmpl=None, product=None,
|
||||
picking_type=None, company_id=False,
|
||||
@@ -29,10 +30,12 @@ class MrpBom(models.Model):
|
||||
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):
|
||||
"""Helper method that is present on v13 but not in v12. We recreate
|
||||
it here with v12 conditions.
|
||||
"""
|
||||
if product:
|
||||
if not product_tmpl:
|
||||
product_tmpl = product.product_tmpl_id
|
||||
@@ -43,21 +46,13 @@ class MrpBom(models.Model):
|
||||
]
|
||||
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')),
|
||||
]
|
||||
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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||
# Copyright 2020 Tecnativa - Pedro M. Baeza
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class MrpProduction(models.Model):
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# 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 api, fields, models
|
||||
|
||||
@@ -17,4 +20,5 @@ class SupplierInfo(models.Model):
|
||||
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
|
||||
supplier.is_subcontractor = (
|
||||
supplier.name in boms.mapped('subcontractor_ids'))
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# 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 api, fields, models, _
|
||||
|
||||
@@ -14,9 +17,15 @@ class ResCompany(models.Model):
|
||||
[('subcontracting_location_id', '=', False)])
|
||||
company_without_subcontracting_loc._create_subcontracting_location()
|
||||
|
||||
def _create_per_company_locations(self):
|
||||
super(ResCompany, self)._create_per_company_locations()
|
||||
def create_transit_location(self):
|
||||
"""As there's no standard method for creating locations and we must
|
||||
create the subcontracting location before the warehouse creation, we
|
||||
inherit this method for performing the subcontracting location
|
||||
creation as well.
|
||||
"""
|
||||
res = super().create_transit_location()
|
||||
self._create_subcontracting_location()
|
||||
return res
|
||||
|
||||
def _create_subcontracting_location(self):
|
||||
parent_location = self.env.ref(
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# 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 fields, models
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# 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 collections import defaultdict
|
||||
|
||||
@@ -16,24 +19,22 @@ 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 raw moves 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
|
||||
move.show_subcontracting_details_visible = (
|
||||
move.is_subcontract and
|
||||
move._has_tracked_subcontract_components() and
|
||||
not float_is_zero(
|
||||
move.quantity_done,
|
||||
precision_rounding=move.product_uom.rounding
|
||||
)
|
||||
)
|
||||
|
||||
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()
|
||||
res = super()._compute_show_details_visible()
|
||||
for move in self:
|
||||
if not move.is_subcontract:
|
||||
continue
|
||||
@@ -45,11 +46,11 @@ class StockMove(models.Model):
|
||||
def copy(self, default=None):
|
||||
self.ensure_one()
|
||||
if not self.is_subcontract or 'location_id' in default:
|
||||
return super(StockMove, self).copy(default=default)
|
||||
return super().copy(default=default)
|
||||
if not default:
|
||||
default = {}
|
||||
default['location_id'] = self.picking_id.location_id.id
|
||||
return super(StockMove, self).copy(default=default)
|
||||
return super().copy(default=default)
|
||||
|
||||
def write(self, values):
|
||||
""" If the initial demand is updated then also update the linked
|
||||
@@ -57,10 +58,12 @@ 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'])
|
||||
return super(StockMove, self).write(values)
|
||||
return super().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'])
|
||||
return super().write(values)
|
||||
|
||||
def action_show_details(self):
|
||||
""" Open the produce wizard in order to register tracked components for
|
||||
@@ -77,8 +80,8 @@ class StockMove(models.Model):
|
||||
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 = super().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'),
|
||||
]
|
||||
@@ -104,6 +107,12 @@ 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:
|
||||
@@ -138,10 +147,23 @@ class StockMove(models.Model):
|
||||
*list(subcontract_details_per_picking.keys())).action_assign()
|
||||
return res
|
||||
|
||||
def _action_assign(self):
|
||||
"""As we don't have the bypass reservation method in v12 at stock.move
|
||||
level, we have to trick this method for splitting the assign in
|
||||
2 steps, classifying previously the subcontract moves and then
|
||||
faking location_id.should_bypass_reservation method through
|
||||
context.
|
||||
"""
|
||||
subcontract_moves = self.filtered('is_subcontract')
|
||||
res = super(StockMove, self - subcontract_moves)._action_assign()
|
||||
super(StockMove, subcontract_moves.with_context(
|
||||
mrp_subcontracting_bypass_reservation=True))._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,
|
||||
active_id=self.move_orig_ids.production_id.id,
|
||||
default_subcontract_move_id=self.id
|
||||
)
|
||||
return action
|
||||
@@ -161,9 +183,11 @@ class StockMove(models.Model):
|
||||
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,
|
||||
sum(move.move_orig_ids.mapped('production_id.qty_produced')),
|
||||
precision_rounding=rounding
|
||||
) > 0:
|
||||
overprocessed_moves |= move
|
||||
if overprocessed_moves:
|
||||
raise UserError(_("""
|
||||
@@ -177,19 +201,18 @@ operations.""") % ('\n'.join(overprocessed_moves.mapped(
|
||||
|
||||
def _get_subcontract_bom(self):
|
||||
self.ensure_one()
|
||||
bom = self.env['mrp.bom'].sudo()._bom_subcontract_find(
|
||||
return self.env['mrp.bom'].sudo()._bom_subcontract_find(
|
||||
product=self.product_id,
|
||||
picking_type=self.picking_type_id,
|
||||
company_id=self.company_id.id,
|
||||
bom_type='subcontract',
|
||||
subcontractor=self.picking_id.partner_id,
|
||||
)
|
||||
return bom
|
||||
|
||||
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)
|
||||
self.move_orig_ids.mapped('production_id.move_raw_ids'))
|
||||
|
||||
def _prepare_extra_move_vals(self, qty):
|
||||
vals = super()._prepare_extra_move_vals(qty)
|
||||
@@ -201,21 +224,12 @@ operations.""") % ('\n'.join(overprocessed_moves.mapped(
|
||||
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()._should_bypass_reservation()
|
||||
if not should_bypass_reservation and self.is_subcontract:
|
||||
return True
|
||||
return should_bypass_reservation
|
||||
|
||||
def _update_subcontract_order_qty(self, quantity):
|
||||
for move in self:
|
||||
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'].create({
|
||||
'mo_id': production.id,
|
||||
'product_qty': production.product_uom_qty + quantity_change
|
||||
}).change_prod_qty()
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# 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 api, models
|
||||
|
||||
@@ -9,20 +12,38 @@ 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).mapped(
|
||||
'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()
|
||||
# Same explanation as for stock.move.action_assign()
|
||||
subcontract_amls = self.filtered(lambda x: x.move_id.is_subcontract)
|
||||
if (self - subcontract_amls) or not subcontract_amls:
|
||||
res = super(StockMoveLine, self - subcontract_amls).write(values)
|
||||
if subcontract_amls:
|
||||
res = super(StockMoveLine, subcontract_amls.with_context(
|
||||
mrp_subcontracting_bypass_reservation=True)).write(values)
|
||||
self.filtered(lambda ml: ml.move_id.is_subcontract).mapped(
|
||||
'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()._should_bypass_reservation(
|
||||
location)
|
||||
if not should_bypass_reservation and self.move_id.is_subcontract:
|
||||
return True
|
||||
return should_bypass_reservation
|
||||
def unlink(self):
|
||||
# Same explanation as for stock.move.action_assign()
|
||||
subcontract_amls = self.filtered(lambda x: x.move_id.is_subcontract)
|
||||
if (self - subcontract_amls) or not subcontract_amls:
|
||||
res = super(StockMoveLine, self - subcontract_amls).unlink()
|
||||
if subcontract_amls:
|
||||
res = super(StockMoveLine, subcontract_amls.with_context(
|
||||
mrp_subcontracting_bypass_reservation=True)).unlink()
|
||||
return res
|
||||
|
||||
def _action_done(self):
|
||||
# Same explanation as for stock.move.action_assign()
|
||||
subcontract_amls = self.filtered(lambda x: x.move_id.is_subcontract)
|
||||
if (self - subcontract_amls) or not subcontract_amls:
|
||||
res = super(StockMoveLine, self - subcontract_amls)._action_done()
|
||||
if subcontract_amls:
|
||||
res = super(StockMoveLine, subcontract_amls.with_context(
|
||||
mrp_subcontracting_bypass_reservation=True))._action_done()
|
||||
return res
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# 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 datetime import timedelta
|
||||
|
||||
@@ -31,8 +34,9 @@ class StockPicking(models.Model):
|
||||
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: (
|
||||
not mo.check_to_done and mo.state != 'done'
|
||||
)):
|
||||
picking.display_action_record_components = False
|
||||
continue
|
||||
picking.display_action_record_components = True
|
||||
@@ -42,17 +46,16 @@ class StockPicking(models.Model):
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def action_done(self):
|
||||
res = super(StockPicking, self).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
|
||||
production = move.move_orig_ids.mapped('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()
|
||||
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:
|
||||
@@ -72,31 +75,33 @@ class StockPicking(models.Model):
|
||||
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_id': production.product_id.id,
|
||||
'product_qty': move_line.qty_done,
|
||||
'product_uom_id': move_line.product_uom_id.id,
|
||||
'finished_lot_id': move_line.lot_id.id,
|
||||
'consumption': 'strict',
|
||||
'lot_id': move_line.lot_id.id,
|
||||
})
|
||||
produce._generate_produce_lines()
|
||||
produce._record_production()
|
||||
produce._onchange_product_qty()
|
||||
produce.do_produce()
|
||||
productions |= production
|
||||
for subcontracted_production in productions:
|
||||
if subcontracted_production.state == 'progress':
|
||||
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)
|
||||
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),
|
||||
})
|
||||
for subcontracted_production in productions:
|
||||
if subcontracted_production.check_to_done:
|
||||
subcontracted_production.button_mark_done()
|
||||
else:
|
||||
subcontracted_production.post_inventory()
|
||||
res = super(StockPicking, self).action_done()
|
||||
for subcontracted_production in productions:
|
||||
# For consistency, set the date on production move before the
|
||||
# date on picking. (Traceability report + Product Moves menu
|
||||
# item)
|
||||
minimum_date = min(picking.move_line_ids.mapped('date'))
|
||||
production_moves = subcontracted_production.move_raw_ids\
|
||||
| subcontracted_production.move_finished_ids
|
||||
production_moves.write({
|
||||
'date': minimum_date - timedelta(seconds=1),
|
||||
})
|
||||
production_moves.mapped('move_line_ids').write({
|
||||
'date': minimum_date - timedelta(seconds=1),
|
||||
})
|
||||
return res
|
||||
|
||||
def action_record_components(self):
|
||||
@@ -156,12 +161,10 @@ class StockPicking(models.Model):
|
||||
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))
|
||||
self.env['stock.move'].create(mo._get_moves_raw_values())
|
||||
|
||||
force_company=move.company_id.id
|
||||
).create(self._prepare_subcontract_mo_vals(move, bom))
|
||||
# Link the finished to the receipt move.
|
||||
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._generate_moves()
|
||||
mo.action_assign()
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# 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 fields, models, _
|
||||
|
||||
@@ -116,10 +119,7 @@ class StockWarehouse(models.Model):
|
||||
'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,
|
||||
},
|
||||
})
|
||||
return data, max_sequence + 4
|
||||
@@ -131,7 +131,7 @@ class StockWarehouse(models.Model):
|
||||
'name': self.name + ' ' + _('Sequence subcontracting'),
|
||||
'prefix': self.code + '/SBC/',
|
||||
'padding': 5,
|
||||
#'company_id': self.company_id.id,
|
||||
'company_id': self.company_id.id,
|
||||
},
|
||||
})
|
||||
return values
|
||||
|
||||
435
mrp_subcontracting/static/description/index.html
Normal file
435
mrp_subcontracting/static/description/index.html
Normal file
@@ -0,0 +1,435 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
|
||||
<title>Subcontract Productions</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
|
||||
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: grey; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document" id="subcontract-productions">
|
||||
<h1 class="title">Subcontract Productions</h1>
|
||||
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="https://github.com/OCA/manufacture/tree/12.0/mrp_subcontracting"><img alt="OCA/manufacture" src="https://img.shields.io/badge/github-OCA%2Fmanufacture-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/manufacture-12-0/manufacture-12-0-mrp_subcontracting"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/129/12.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
|
||||
<p>This module is a backport of the one found in official Odoo 13.0, adapted
|
||||
for this version.</p>
|
||||
<p>For the configuration and usage, see Odoo documentation:</p>
|
||||
<p><a class="reference external" href="https://www.odoo.com/documentation/user/13.0/manufacturing/management/subcontracting.html">https://www.odoo.com/documentation/user/13.0/manufacturing/management/subcontracting.html</a></p>
|
||||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#bug-tracker" id="id1">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="id2">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="id3">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="id4">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#other-credits" id="id5">Other credits</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="id6">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#id1">Bug Tracker</a></h1>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/manufacture/issues">GitHub Issues</a>.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us smashing it by providing a detailed and welcomed
|
||||
<a class="reference external" href="https://github.com/OCA/manufacture/issues/new?body=module:%20mrp_subcontracting%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h1><a class="toc-backref" href="#id2">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#id3">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Odoo S.A.</li>
|
||||
<li>Tecnativa</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h2><a class="toc-backref" href="#id4">Contributors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Odoo S.A.</li>
|
||||
<li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:<ul>
|
||||
<li>Alexandre Díaz</li>
|
||||
<li>Pedro M. Baeza</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="other-credits">
|
||||
<h2><a class="toc-backref" href="#id5">Other credits</a></h2>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#id6">Maintainers</a></h2>
|
||||
<p>This module is maintained by the OCA.</p>
|
||||
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
|
||||
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.</p>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/manufacture/tree/12.0/mrp_subcontracting">OCA/manufacture</a> project on GitHub.</p>
|
||||
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +1,3 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from . import test_subcontracting
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
# 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.tests.common import Form, SavepointCase
|
||||
|
||||
|
||||
class TestMrpSubcontractingCommon(SavepointCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUp()
|
||||
super().setUpClass()
|
||||
# 1: Create a subcontracting partner
|
||||
main_partner = cls.env['res.partner'].create({'name': 'main_partner'})
|
||||
cls.subcontractor_partner1 = cls.env['res.partner'].create({
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# 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.tests import Form
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.addons.mrp_subcontracting.tests.common import (
|
||||
TestMrpSubcontractingCommon)
|
||||
from .common import TestMrpSubcontractingCommon
|
||||
|
||||
from odoo.tests import tagged
|
||||
|
||||
@@ -33,10 +35,6 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
component and run the scheduler to resupply. Checks if the resupplying
|
||||
actually works
|
||||
"""
|
||||
# Check subcontracting picking Type
|
||||
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')
|
||||
@@ -46,14 +44,12 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
move.product_uom_qty = 1
|
||||
picking_receipt = picking_form.save()
|
||||
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)
|
||||
|
||||
# Check the created manufacturing order
|
||||
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
|
||||
self.assertEqual(len(mo), 1)
|
||||
@@ -61,7 +57,6 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
wh = picking_receipt.picking_type_id.warehouse_id
|
||||
self.assertEquals(mo.picking_type_id, wh.subcontracting_type_id)
|
||||
self.assertFalse(mo.picking_type_id.active)
|
||||
|
||||
# Create a RR
|
||||
pg1 = self.env['procurement.group'].create({})
|
||||
self.env['stock.warehouse.orderpoint'].create({
|
||||
@@ -73,7 +68,6 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
self.env.user.company_id.subcontracting_location_id.id,
|
||||
'group_id': pg1.id,
|
||||
})
|
||||
|
||||
# Run the scheduler and check the created picking
|
||||
self.env['procurement.group'].run_scheduler()
|
||||
picking = self.env['stock.picking'].search([('group_id', '=', pg1.id)])
|
||||
@@ -82,49 +76,68 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
picking_receipt.move_lines.quantity_done = 1
|
||||
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, 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)
|
||||
# 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_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_wizard = self.env['stock.return.picking'].with_context(
|
||||
active_id=picking_receipt.id, active_model='stock.picking',
|
||||
).create({})
|
||||
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)
|
||||
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. Also set a different subcontracting location on the partner.
|
||||
""" 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. 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)]})
|
||||
# Tick "resupply subcontractor 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)]})
|
||||
# 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,
|
||||
'location_id': self.env.ref(
|
||||
'stock.stock_location_locations_partner').id,
|
||||
'usage': 'internal',
|
||||
'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.user.company_id.subcontracting_location_id)
|
||||
resupply_rule.copy({'location_src_id': partner_subcontract_location.id})
|
||||
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)
|
||||
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.user.company_id.subcontracting_location_id))
|
||||
resupply_rule.copy({
|
||||
'location_src_id': partner_subcontract_location.id})
|
||||
resupply_warehouse_rule = (
|
||||
self.warehouse.mapped('route_ids.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)))
|
||||
for warehouse_rule in resupply_warehouse_rule:
|
||||
warehouse_rule.copy({'location_id': partner_subcontract_location.id})
|
||||
|
||||
warehouse_rule.copy({
|
||||
'location_id': partner_subcontract_location.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')
|
||||
@@ -134,62 +147,71 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
move.product_uom_qty = 1
|
||||
picking_receipt = picking_form.save()
|
||||
picking_receipt.action_confirm()
|
||||
|
||||
# Nothing should be tracked
|
||||
self.assertFalse(picking_receipt.display_action_record_components)
|
||||
|
||||
# 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, 'confirmed')
|
||||
self.assertEqual(len(mo.picking_ids.move_lines), 2)
|
||||
|
||||
picking = mo.picking_ids
|
||||
wh = picking.picking_type_id.warehouse_id
|
||||
|
||||
# The picking should be a delivery order
|
||||
self.assertEquals(picking.picking_type_id, wh.out_type_id)
|
||||
|
||||
self.assertEquals(mo.picking_type_id, wh.subcontracting_type_id)
|
||||
self.assertFalse(mo.picking_type_id.active)
|
||||
|
||||
# No manufacturing order for `self.comp2`
|
||||
comp2mo = self.env['mrp.production'].search([('bom_id', '=', self.comp2_bom.id)])
|
||||
comp2mo = self.env['mrp.production'].search([
|
||||
('bom_id', '=', self.comp2_bom.id)])
|
||||
self.assertEqual(len(comp2mo), 0)
|
||||
|
||||
picking_receipt.move_lines.quantity_done = 1
|
||||
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, 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)
|
||||
# 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_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.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)
|
||||
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)
|
||||
|
||||
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
|
||||
resupplying actually works. One of the component has also "manufacture" set and a BOM
|
||||
linked. Checks that an MO is created for this one.
|
||||
""" 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 resupplying actually works.
|
||||
One of the component has also "manufacture" set and a BOM linked.
|
||||
Checks that an MO is created for this one.
|
||||
"""
|
||||
# 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)]})
|
||||
|
||||
# Tick "resupply subcontractor on order"
|
||||
resupply_sub_on_order_route = self.env.ref(
|
||||
'mrp_subcontracting.route_resupply_subcontractor_mto')
|
||||
(self.comp1 + self.comp2).write({
|
||||
'route_ids': [(4, resupply_sub_on_order_route.id, None)]})
|
||||
# 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')])
|
||||
mto_route = self.env.ref('stock.route_warehouse0_mto')
|
||||
manufacture_route = self.env.ref('mrp.route_warehouse0_manufacture')
|
||||
self.comp2.write({'route_ids': [(4, manufacture_route.id, None)]})
|
||||
self.comp2.write({'route_ids': [(4, mto_route.id, None)]})
|
||||
|
||||
# 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')
|
||||
@@ -199,38 +221,41 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
move.product_uom_qty = 1
|
||||
picking_receipt = picking_form.save()
|
||||
picking_receipt.action_confirm()
|
||||
|
||||
# Nothing should be tracked
|
||||
self.assertFalse(picking_receipt.display_action_record_components)
|
||||
|
||||
# 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.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)
|
||||
|
||||
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
|
||||
self.assertEquals(mo.picking_ids.picking_type_id, wh.out_type_id)
|
||||
|
||||
self.assertEquals(mo.picking_type_id, wh.subcontracting_type_id)
|
||||
self.assertFalse(mo.picking_type_id.active)
|
||||
|
||||
# As well as a manufacturing order for `self.comp2`
|
||||
comp2mo = self.env['mrp.production'].search([('bom_id', '=', self.comp2_bom.id)])
|
||||
comp2mo = self.env['mrp.production'].search([
|
||||
('bom_id', '=', self.comp2_bom.id)])
|
||||
self.assertEqual(len(comp2mo), 1)
|
||||
picking_receipt.move_lines.quantity_done = 1
|
||||
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, 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)
|
||||
# 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_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)
|
||||
@@ -238,23 +263,24 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
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.
|
||||
picking. Checks that the delivery to the subcontractor and MO exist.
|
||||
|
||||
NOTE: This is different from v13, as the MO doesn't have a draft state,
|
||||
and thus, we can't control through scheduler the creation of the rest
|
||||
of the elements.
|
||||
"""
|
||||
# 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')])
|
||||
mto_route = self.env.ref('stock.route_warehouse0_mto')
|
||||
manufacture_route = self.env.ref('mrp.route_warehouse0_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.user.company_id.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
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
@@ -264,60 +290,42 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
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.user.company_id.subcontracting_location_id.id)
|
||||
])
|
||||
self.assertFalse(move)
|
||||
|
||||
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.user.company_id.subcontracting_location_id.id)
|
||||
('location_dest_id', '=',
|
||||
self.env.user.company_id.subcontracting_location_id.id),
|
||||
])
|
||||
self.assertTrue(move)
|
||||
picking_delivery = move.picking_id
|
||||
self.assertTrue(picking_delivery)
|
||||
self.assertEqual(move.product_uom_qty, 11.0)
|
||||
|
||||
self.assertEqual(move.product_uom_qty, 1.0)
|
||||
# As well as a manufacturing order for `self.comp2`
|
||||
comp2mo = self.env['mrp.production'].search([('bom_id', '=', self.comp2_bom.id)])
|
||||
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'})
|
||||
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
|
||||
})
|
||||
|
||||
# We create a different BoM for the same product
|
||||
comp3 = self.env['product.product'].create({
|
||||
'name': 'Component1',
|
||||
'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
|
||||
@@ -328,11 +336,11 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
bom_line.product_id = comp3
|
||||
bom_line.product_qty = 1
|
||||
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)]})
|
||||
|
||||
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')
|
||||
@@ -342,7 +350,6 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
move.product_uom_qty = 1
|
||||
picking_receipt1 = picking_form.save()
|
||||
picking_receipt1.action_confirm()
|
||||
|
||||
# Create a receipt picking from the subcontractor2
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
@@ -352,9 +359,10 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
move.product_uom_qty = 1
|
||||
picking_receipt2 = picking_form.save()
|
||||
picking_receipt2.action_confirm()
|
||||
|
||||
mo_pick1 = picking_receipt1.move_lines.mapped('move_orig_ids.production_id')
|
||||
mo_pick2 = picking_receipt2.move_lines.mapped('move_orig_ids.production_id')
|
||||
mo_pick1 = picking_receipt1.move_lines.mapped(
|
||||
'move_orig_ids.production_id')
|
||||
mo_pick2 = picking_receipt2.move_lines.mapped(
|
||||
'move_orig_ids.production_id')
|
||||
self.assertEquals(len(mo_pick1), 1)
|
||||
self.assertEquals(len(mo_pick2), 1)
|
||||
self.assertEquals(mo_pick1.bom_id, self.bom)
|
||||
@@ -364,7 +372,8 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
""" Extra quantity on the move.
|
||||
"""
|
||||
# We create a second partner of type subcontractor
|
||||
main_partner_2 = self.env['res.partner'].create({'name': 'main_partner'})
|
||||
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,
|
||||
@@ -391,8 +400,10 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
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)]})
|
||||
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'])
|
||||
@@ -407,7 +418,8 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
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_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)
|
||||
@@ -419,8 +431,8 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
|
||||
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
|
||||
finished product. Simulate the register components button.
|
||||
Once the components are registered, try to do a correction on existing
|
||||
move lines and check that the subcontracting document is updated.
|
||||
"""
|
||||
# Create a receipt picking from the subcontractor
|
||||
@@ -434,101 +446,114 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
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_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,
|
||||
'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.user.company_id.id,
|
||||
})
|
||||
lot_f1 = self.env['stock.production.lot'].create({
|
||||
'name': 'LOT F1',
|
||||
'product_id': self.finished.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')
|
||||
|
||||
context = {
|
||||
'active_id': picking_receipt._get_subcontracted_productions().id,
|
||||
'default_subcontract_move_id': picking_receipt.move_lines.id,
|
||||
}
|
||||
register_form = Form(self.env['mrp.product.produce'].with_context(
|
||||
context))
|
||||
register_form.product_qty = 3.0
|
||||
self.assertEqual(
|
||||
len(register_form._values['produce_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['produce_line_ids']),
|
||||
'Register Components Form should contains component.')
|
||||
with register_form.produce_line_ids.edit(0) as pl:
|
||||
pl.lot_id = lot_c1
|
||||
with register_form.produce_line_ids.edit(1) as pl:
|
||||
pl.lot_id = lot_c2
|
||||
register_form.lot_id = lot_f1
|
||||
register_wizard = register_form.save()
|
||||
register_wizard.do_produce()
|
||||
register_form = Form(self.env['mrp.product.produce'].with_context(
|
||||
context))
|
||||
with register_form.produce_line_ids.edit(0) as pl:
|
||||
pl.lot_id = lot_c1
|
||||
with register_form.produce_line_ids.edit(1) as pl:
|
||||
pl.lot_id = lot_c2
|
||||
register_form.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.mapped('lot_id.name')[0], 'LOT C1')
|
||||
self.assertEqual(move_comp2.quantity_done, 5.0)
|
||||
self.assertEqual(
|
||||
move_comp2.move_line_ids.mapped('lot_id.name')[0], 'LOT C2')
|
||||
self.assertEqual(move_finished.quantity_done, 5.0)
|
||||
self.assertEqual(
|
||||
move_finished.move_line_ids.mapped('lot_id.name')[0], 'LOT F1')
|
||||
corrected_final_lot = self.env['stock.production.lot'].create({
|
||||
'name': 'LOT F2',
|
||||
'product_id': self.finished.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'))
|
||||
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'])):
|
||||
orig_moves = picking_receipt.move_lines.move_orig_ids
|
||||
move_raw_comp_1 = orig_moves.production_id.move_raw_ids.filtered(
|
||||
lambda m: m.product_id == self.comp1)
|
||||
move_raw_comp_2 = orig_moves.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)
|
||||
sc.lot_produced_id = 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'])):
|
||||
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)
|
||||
sc.lot_produced_id = 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')
|
||||
self.assertEqual(
|
||||
move_comp1.move_line_ids.mapped('lot_produced_id.name')[0],
|
||||
'LOT F2')
|
||||
self.assertEqual(
|
||||
move_comp2.move_line_ids.mapped('lot_produced_id.name')[0],
|
||||
'LOT F2')
|
||||
|
||||
def test_flow_8(self):
|
||||
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)]})
|
||||
|
||||
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 receipt picking from the subcontractor
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
@@ -538,29 +563,67 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
move.product_uom_qty = 5
|
||||
picking_receipt = picking_form.save()
|
||||
picking_receipt.action_confirm()
|
||||
|
||||
picking_receipt.move_lines.quantity_done = 3
|
||||
backorder_wiz = picking_receipt.button_validate()
|
||||
backorder_wiz = self.env['stock.backorder.confirmation'].browse(backorder_wiz['res_id'])
|
||||
backorder_wiz = self.env['stock.backorder.confirmation'].browse(
|
||||
backorder_wiz['res_id'])
|
||||
backorder_wiz.process()
|
||||
|
||||
backorder = self.env['stock.picking'].search([('backorder_id', '=', picking_receipt.id)])
|
||||
backorder = self.env['stock.picking'].search([
|
||||
('backorder_id', '=', picking_receipt.id)])
|
||||
self.assertTrue(backorder)
|
||||
self.assertEqual(backorder.move_lines.product_uom_qty, 2)
|
||||
subcontract_order = backorder.move_lines.move_orig_ids.production_id.filtered(lambda p: p.state != 'done')
|
||||
orig_moves = backorder.move_lines.move_orig_ids
|
||||
subcontract_order = orig_moves.mapped('production_id').filtered(
|
||||
lambda p: p.state != 'done')
|
||||
self.assertTrue(subcontract_order)
|
||||
self.assertEqual(subcontract_order.product_uom_qty, 5)
|
||||
self.assertEqual(subcontract_order.qty_produced, 3)
|
||||
backorder.move_lines.quantity_done = 2
|
||||
backorder.action_done()
|
||||
self.assertTrue(picking_receipt.move_lines.move_orig_ids.production_id.state == 'done')
|
||||
orig_moves = picking_receipt.move_lines.move_orig_ids
|
||||
self.assertTrue(orig_moves.mapped('production_id').state == 'done')
|
||||
|
||||
def test_flow_9(self):
|
||||
"""Ensure that cancel the subcontract moves will also delete the
|
||||
components need for the subcontractor.
|
||||
"""
|
||||
# TODO: Fix
|
||||
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)
|
||||
]).mapped('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)
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSubcontractingTracking(TransactionCase):
|
||||
def setUp(self):
|
||||
super(TestSubcontractingTracking, self).setUp()
|
||||
# 1: Create a subcontracting partner
|
||||
main_company_1 = self.env['res.partner'].create({'name': 'main_partner'})
|
||||
main_company_1 = self.env['res.partner'].create({
|
||||
'name': 'main_partner'})
|
||||
self.subcontractor_partner1 = self.env['res.partner'].create({
|
||||
'name': 'Subcontractor 1',
|
||||
'parent_id': main_company_1.id,
|
||||
@@ -601,7 +664,8 @@ class TestSubcontractingTracking(TransactionCase):
|
||||
self.bom_tracked = bom_form.save()
|
||||
|
||||
def test_flow_tracked_1(self):
|
||||
""" This test mimics test_flow_1 but with a BoM that has tracking included in it.
|
||||
""" This test mimics test_flow_1 but with a BoM that has tracking
|
||||
included in it.
|
||||
"""
|
||||
# Create a receipt picking from the subcontractor
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
@@ -612,19 +676,17 @@ class TestSubcontractingTracking(TransactionCase):
|
||||
move.product_uom_qty = 1
|
||||
picking_receipt = picking_form.save()
|
||||
picking_receipt.action_confirm()
|
||||
|
||||
# We should be able to call the 'record_components' button
|
||||
self.assertTrue(picking_receipt.display_action_record_components)
|
||||
|
||||
# Check the created manufacturing order
|
||||
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom_tracked.id)])
|
||||
mo = self.env['mrp.production'].search([
|
||||
('bom_id', '=', self.bom_tracked.id)])
|
||||
self.assertEqual(len(mo), 1)
|
||||
self.assertEquals(mo.state, 'confirmed')
|
||||
self.assertEqual(len(mo.picking_ids), 0)
|
||||
wh = picking_receipt.picking_type_id.warehouse_id
|
||||
self.assertEquals(mo.picking_type_id, wh.subcontracting_type_id)
|
||||
self.assertFalse(mo.picking_type_id.active)
|
||||
|
||||
# Create a RR
|
||||
pg1 = self.env['procurement.group'].create({})
|
||||
self.env['stock.warehouse.orderpoint'].create({
|
||||
@@ -632,48 +694,51 @@ class TestSubcontractingTracking(TransactionCase):
|
||||
'product_id': self.comp1_sn.id,
|
||||
'product_min_qty': 0,
|
||||
'product_max_qty': 0,
|
||||
'location_id': self.env.user.company_id.subcontracting_location_id.id,
|
||||
'location_id': (
|
||||
self.env.user.company_id.subcontracting_location_id.id),
|
||||
'group_id': pg1.id,
|
||||
})
|
||||
|
||||
# Run the scheduler and check the created picking
|
||||
self.env['procurement.group'].run_scheduler()
|
||||
picking = self.env['stock.picking'].search([('group_id', '=', pg1.id)])
|
||||
self.assertEqual(len(picking), 1)
|
||||
self.assertEquals(picking.picking_type_id, wh.out_type_id)
|
||||
|
||||
lot_id = self.env['stock.production.lot'].create({
|
||||
'name': 'lot1',
|
||||
'product_id': self.finished_lot.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.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.lot_id = lot_id
|
||||
with produce_form.produce_line_ids.edit(0) as pl:
|
||||
pl.lot_id = serial_id
|
||||
produce_form.lot_id = lot_id
|
||||
wiz_produce = produce_form.save()
|
||||
wiz_produce.do_produce()
|
||||
|
||||
# We should not be able to call the 'record_components' button
|
||||
self.assertFalse(picking_receipt.display_action_record_components)
|
||||
|
||||
picking_receipt.move_lines.quantity_done = 1
|
||||
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_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)
|
||||
# Available quantities should be negative at the subcontracting
|
||||
# location for each components
|
||||
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(
|
||||
self.comp1_sn,
|
||||
self.subcontractor_partner1.property_stock_subcontractor,
|
||||
allow_negative=True)
|
||||
avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(
|
||||
self.comp2,
|
||||
self.subcontractor_partner1.property_stock_subcontractor,
|
||||
allow_negative=True)
|
||||
avail_qty_finished = self.env['stock.quant']._get_available_quantity(
|
||||
self.finished_lot, wh.lot_stock_id)
|
||||
self.assertEquals(avail_qty_comp1, -1)
|
||||
self.assertEquals(avail_qty_comp2, -1)
|
||||
self.assertEquals(avail_qty_finished, 1)
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
<field name="inherit_id" ref="mrp.mrp_bom_form_view" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='type']" position="after">
|
||||
<field name="subcontractor_ids" widget="many2many_tags" attrs="{'invisible': [('type', '!=', 'subcontract')], 'required': [('type', '=', 'subcontract')]}"/>
|
||||
<field name="subcontractor_ids" domain="[('supplier', '=', True)]" widget="many2many_tags" attrs="{'invisible': [('type', '!=', 'subcontract')], 'required': [('type', '=', 'subcontract')]}" context="{'default_supplier': True}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='property_stock_supplier']" position="after">
|
||||
<field name="property_stock_subcontractor"/>
|
||||
<separator/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -13,15 +13,13 @@
|
||||
<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="state" invisible="1"/>
|
||||
<field name="tracking" invisible="1"/>
|
||||
<field name="product_id" readonly="1"/>
|
||||
<!--field name="lot_produced_ids"
|
||||
widget="many2many_tags"
|
||||
<field name="lot_produced_id"
|
||||
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,17 +28,35 @@
|
||||
</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>
|
||||
<field name="mode">primary</field>
|
||||
<field name="inherit_id" ref="mrp.view_stock_move_raw_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//tree" position="attributes">
|
||||
<attribute name="create">0</attribute>
|
||||
<attribute name="delete">0</attribute>
|
||||
</xpath>
|
||||
<tree create="0" delete="0" default_order="is_done,sequence" decoration-muted="is_done" decoration-warning="quantity_done>product_uom_qty" decoration-success="not is_done and quantity_done==product_uom_qty" decoration-danger="not is_done and reserved_availability < product_uom_qty">
|
||||
<field name="product_id" required="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="name" invisible="1"/>
|
||||
<field name="unit_factor" invisible="1"/>
|
||||
<field name="product_uom" groups="uom.group_uom"/>
|
||||
<field name="date" invisible="1"/>
|
||||
<field name="date_expected" invisible="1"/>
|
||||
<field name="picking_type_id" invisible="1"/>
|
||||
<field name="has_tracking" invisible="1"/>
|
||||
<field name="operation_id" invisible="1"/>
|
||||
<field name="needs_lots" readonly="1" groups="stock.group_production_lot"/>
|
||||
<field name="is_done" invisible="1"/>
|
||||
<field name="bom_line_id" invisible="1"/>
|
||||
<field name="sequence" invisible="1"/>
|
||||
<field name="location_id" invisible="1"/>
|
||||
<field name="warehouse_id" invisible="1"/>
|
||||
<field name="location_dest_id" domain="[('id', 'child_of', parent.location_dest_id)]" invisible="1"/>
|
||||
<field name="state" invisible="1" force_save="1"/>
|
||||
<field name="product_uom_qty" string="To Consume"/>
|
||||
<field name="reserved_availability" attrs="{'invisible': [('is_done', '=', True)], 'column_invisible': [('parent.state', 'in', ('draft', 'done'))]}" string="Reserved"/>
|
||||
<field name="quantity_done" string="Consumed" attrs="{'column_invisible': [('parent.state', '=', 'draft')]}" readonly="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record-->
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -17,19 +17,4 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<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,5 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from . import mrp_product_produce
|
||||
from . import stock_backorder_confirmation
|
||||
from . import stock_picking_return
|
||||
|
||||
@@ -1,52 +1,54 @@
|
||||
# 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 fields, models
|
||||
from odoo.tools.float_utils import float_is_zero
|
||||
|
||||
|
||||
class MrpProductProduce(models.TransientModel):
|
||||
_inherit = 'mrp.product.produce'
|
||||
|
||||
subcontract_move_id = fields.Many2one('stock.move', 'stock move from the subcontract picking', check_company=True)
|
||||
subcontract_move_id = fields.Many2one(
|
||||
'stock.move', 'stock move from the subcontract picking')
|
||||
|
||||
def continue_production(self):
|
||||
action = super(MrpProductProduce, self).continue_production()
|
||||
action['context'] = dict(action['context'], default_subcontract_move_id=self.subcontract_move_id.id)
|
||||
return action
|
||||
def _get_todo(self, production):
|
||||
"""This method will return remaining todo quantity of production."""
|
||||
main_product_moves = production.move_finished_ids.filtered(
|
||||
lambda x: x.product_id == production.product_id)
|
||||
todo_quantity = production.product_qty - sum(
|
||||
main_product_moves.mapped('quantity_done'))
|
||||
return todo_quantity if (todo_quantity > 0) else 0
|
||||
|
||||
def _generate_produce_lines(self):
|
||||
""" When the wizard is called in backend, the onchange that create the
|
||||
produce lines is not trigger. This method generate them and is used with
|
||||
_record_production to appropriately set the lot_produced_id and
|
||||
appropriately create raw stock move lines.
|
||||
"""
|
||||
self.ensure_one()
|
||||
moves = (self.move_raw_ids | self.move_finished_ids).filtered(
|
||||
lambda move: move.state not in ('done', 'cancel')
|
||||
)
|
||||
for move in moves:
|
||||
qty_to_consume = self._prepare_component_quantity(move, self.qty_producing)
|
||||
line_values = self._generate_lines_values(move, qty_to_consume)
|
||||
self.env['mrp.product.produce.line'].create(line_values)
|
||||
|
||||
def _update_finished_move(self):
|
||||
def do_produce(self):
|
||||
""" After producing, set the move line on the subcontract picking. """
|
||||
res = super(MrpProductProduce, self)._update_finished_move()
|
||||
res = super().do_produce()
|
||||
if self.subcontract_move_id:
|
||||
self.env['stock.move.line'].create({
|
||||
'move_id': self.subcontract_move_id.id,
|
||||
'picking_id': self.subcontract_move_id.picking_id.id,
|
||||
'product_id': self.product_id.id,
|
||||
'location_id': self.subcontract_move_id.location_id.id,
|
||||
'location_dest_id': self.subcontract_move_id.location_dest_id.id,
|
||||
'location_dest_id': (
|
||||
self.subcontract_move_id.location_dest_id.id),
|
||||
'product_uom_qty': 0,
|
||||
'product_uom_id': self.product_uom_id.id,
|
||||
'qty_done': self.qty_producing,
|
||||
'lot_id': self.finished_lot_id and self.finished_lot_id.id,
|
||||
'qty_done': self.product_qty,
|
||||
'lot_id': self.lot_id.id,
|
||||
})
|
||||
if not self._get_todo(self.production_id):
|
||||
ml_reserved = self.subcontract_move_id.move_line_ids.filtered(lambda ml:
|
||||
float_is_zero(ml.qty_done, precision_rounding=ml.product_uom_id.rounding) and
|
||||
not float_is_zero(ml.product_uom_qty, precision_rounding=ml.product_uom_id.rounding))
|
||||
ml_reserved = self.subcontract_move_id.move_line_ids.filtered(
|
||||
lambda ml: (
|
||||
float_is_zero(
|
||||
ml.qty_done,
|
||||
precision_rounding=ml.product_uom_id.rounding
|
||||
) and not float_is_zero(
|
||||
ml.product_uom_qty,
|
||||
precision_rounding=ml.product_uom_id.rounding
|
||||
)
|
||||
)
|
||||
)
|
||||
ml_reserved.unlink()
|
||||
for ml in self.subcontract_move_id.move_line_ids:
|
||||
ml.product_uom_qty = ml.qty_done
|
||||
|
||||
@@ -1,29 +1,17 @@
|
||||
# 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
|
||||
|
||||
from odoo import api, models
|
||||
from odoo.osv.expression import OR
|
||||
|
||||
class ReturnPicking(models.TransientModel):
|
||||
_inherit = 'stock.return.picking'
|
||||
|
||||
@api.onchange('picking_id')
|
||||
def _onchange_picking_id(self):
|
||||
res = super(ReturnPicking, self)._onchange_picking_id()
|
||||
if not any(self.product_return_moves.filtered(lambda r: r.quantity > 0).move_id.mapped('is_subcontract')):
|
||||
return res
|
||||
subcontract_location = self.picking_id.partner_id.with_context(force_company=self.picking_id.company_id.id).property_stock_subcontractor
|
||||
self.location_id = subcontract_location.id
|
||||
domain_location = OR([
|
||||
['|', ('id', '=', self.original_location_id.id), ('return_location', '=', True)],
|
||||
[('id', '=', subcontract_location.id)]
|
||||
])
|
||||
if not res:
|
||||
res = {'domain': {'location_id': domain_location}}
|
||||
else:
|
||||
res['domain'] = {'location_id': domain_location}
|
||||
return res
|
||||
|
||||
def _prepare_move_default_values(self, return_line, new_picking):
|
||||
vals = super(ReturnPicking, self)._prepare_move_default_values(return_line, new_picking)
|
||||
vals = super()._prepare_move_default_values(return_line, new_picking)
|
||||
vals['is_subcontract'] = False
|
||||
if return_line.move_id.is_subcontract:
|
||||
vals['location_dest_id'] = return_line.move_id.location_id.id
|
||||
return vals
|
||||
|
||||
Reference in New Issue
Block a user