[MIG] mrp_subcontracting: Adapt to v12 + make tests to pass

This commit is contained in:
Pedro M. Baeza
2020-05-10 12:56:44 +02:00
parent 0426acf030
commit f32ce8ba46
26 changed files with 1059 additions and 426 deletions

View 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.

View File

@@ -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

View File

@@ -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",
}

View File

@@ -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>

View File

@@ -29,4 +29,4 @@
</record>
</data>
</odoo>
</odoo>

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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'))

View File

@@ -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(

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View 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>

View File

@@ -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

View File

@@ -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({

View File

@@ -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)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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&gt;product_uom_qty" decoration-success="not is_done and quantity_done==product_uom_qty" decoration-danger="not is_done and reserved_availability &lt; product_uom_qty">
<field name="product_id" required="1"/>
<field name="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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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