mirror of
https://github.com/OCA/manufacture.git
synced 2025-01-28 16:37:15 +02:00
ADD module mrp_unbuild_tracked_raw_material
This commit is contained in:
5
mrp_unbuild_tracked_raw_material/README.rst
Normal file
5
mrp_unbuild_tracked_raw_material/README.rst
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
!! This file is generated by oca-gen-addon-readme !!
|
||||||
|
!! changes will be overwritten. !!
|
||||||
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
|
||||||
1
mrp_unbuild_tracked_raw_material/__init__.py
Normal file
1
mrp_unbuild_tracked_raw_material/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
19
mrp_unbuild_tracked_raw_material/__manifest__.py
Normal file
19
mrp_unbuild_tracked_raw_material/__manifest__.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Copyright (C) 2019 Akretion (http://www.akretion.com). All Rights Reserved
|
||||||
|
# @author David BEAL <david.beal@akretion.com>
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Mrp Unbuild Tracked Raw Material",
|
||||||
|
"summary": "Allow to unbuild tracked purchased products",
|
||||||
|
"author": "Akretion, Odoo Community Association (OCA)",
|
||||||
|
"website": "https://github.com/OCA/manufacture",
|
||||||
|
"category": "Manufacturing",
|
||||||
|
"version": "12.0.1.0.0",
|
||||||
|
"license": "AGPL-3",
|
||||||
|
"depends": ["mrp"],
|
||||||
|
"maintainers": ["bealdav"],
|
||||||
|
"data": [
|
||||||
|
"views/product_view.xml",
|
||||||
|
],
|
||||||
|
"installable": True,
|
||||||
|
}
|
||||||
2
mrp_unbuild_tracked_raw_material/models/__init__.py
Normal file
2
mrp_unbuild_tracked_raw_material/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from . import product
|
||||||
|
from . import unbuild
|
||||||
14
mrp_unbuild_tracked_raw_material/models/product.py
Normal file
14
mrp_unbuild_tracked_raw_material/models/product.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Copyright (C) 2019 Akretion (http://www.akretion.com). All Rights Reserved
|
||||||
|
# @author David BEAL <david.beal@akretion.com>
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class ProductTemplate(models.Model):
|
||||||
|
_inherit = "product.template"
|
||||||
|
|
||||||
|
allow_unbuild_purchased = fields.Boolean(
|
||||||
|
help="If checked, unbuild orders doesn't assume a previous "
|
||||||
|
"manufacturing order have built this product.\n"
|
||||||
|
"In this case it's a purchased product and you want unbuild it")
|
||||||
117
mrp_unbuild_tracked_raw_material/models/unbuild.py
Normal file
117
mrp_unbuild_tracked_raw_material/models/unbuild.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Copyright (C) 2019 Akretion (http://www.akretion.com). All Rights Reserved
|
||||||
|
# @author David BEAL <david.beal@akretion.com>
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from odoo import _, models
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT as DT_FORMAT
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
MESSAGE = "Some of your components are tracked, you have to specify " \
|
||||||
|
"a manufacturing order in order to retrieve " \
|
||||||
|
"the correct components."
|
||||||
|
ALTER_MESSAGE = "Alternatively, you may unbuild '%s' tracked product " \
|
||||||
|
"if you set it as '%s'.\n" \
|
||||||
|
"In this case lots'll be automatically created for you."
|
||||||
|
|
||||||
|
|
||||||
|
class MrpUnbuild(models.Model):
|
||||||
|
_inherit = "mrp.unbuild"
|
||||||
|
|
||||||
|
def action_unbuild(self):
|
||||||
|
""" We need to catch raise behavior when tracked products
|
||||||
|
are unbuild without an original manufacturing order and
|
||||||
|
go on with another workflow with
|
||||||
|
_bypass_tracked_product_without_mo()
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
res = super().action_unbuild()
|
||||||
|
except UserError as err:
|
||||||
|
# Unbuild is impossible because of MESSAGE
|
||||||
|
# original condition of this raise is :
|
||||||
|
# any(produce_move.has_tracking != 'none'
|
||||||
|
# and not self.mo_id for produce_move in produce_moves)
|
||||||
|
if hasattr(err, 'name') and err.name == _(MESSAGE):
|
||||||
|
if self.product_id.allow_unbuild_purchased:
|
||||||
|
# In this case it becomes possible to unbuild
|
||||||
|
return self._bypass_tracked_product_without_mo()
|
||||||
|
# Here other option to resolve unbuild conditions
|
||||||
|
new_message = _(ALTER_MESSAGE) % (
|
||||||
|
self.product_id.name, _("Unbuild Purchased"))
|
||||||
|
# We teach user the 2 conditions that make unbuild possible
|
||||||
|
raise UserError(_("%s \n\n%s") % (err.name, new_message))
|
||||||
|
# Here is the Odoo native raise
|
||||||
|
raise err
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _bypass_tracked_product_without_mo(self):
|
||||||
|
# These moves are already generated with call to
|
||||||
|
# super().action_unbuild(). We catch them
|
||||||
|
consume_move = self.env["stock.move"].search(
|
||||||
|
[("consume_unbuild_id", "=", self.id)])
|
||||||
|
produce_moves = self.env["stock.move"].search(
|
||||||
|
[("unbuild_id", "=", self.id)])
|
||||||
|
|
||||||
|
# Comes from
|
||||||
|
# https://github.com/OCA/ocb/blob/12.0/addons/mrp/models/...
|
||||||
|
# mrp_unbuild.py#L117
|
||||||
|
if consume_move.has_tracking != 'none':
|
||||||
|
self.env['stock.move.line'].create({
|
||||||
|
'move_id': consume_move.id,
|
||||||
|
'lot_id': self.lot_id.id,
|
||||||
|
'qty_done': consume_move.product_uom_qty,
|
||||||
|
'product_id': consume_move.product_id.id,
|
||||||
|
'product_uom_id': consume_move.product_uom.id,
|
||||||
|
'location_id': consume_move.location_id.id,
|
||||||
|
'location_dest_id': consume_move.location_dest_id.id,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
consume_move.quantity_done = consume_move.product_uom_qty
|
||||||
|
consume_move._action_done()
|
||||||
|
|
||||||
|
# Comment from odoo original module:
|
||||||
|
# TODO: Will fail if user do more than one unbuild with lot
|
||||||
|
# on the same MO. Need to check what other unbuild has aready took
|
||||||
|
for produce_move in produce_moves:
|
||||||
|
if produce_move.has_tracking != 'none':
|
||||||
|
if produce_move.product_id.tracking == "serial":
|
||||||
|
# TODO
|
||||||
|
raise UserError(_(
|
||||||
|
"Unbuild of component of tracked as serial "
|
||||||
|
"is not supported for now: contact maintainers of "
|
||||||
|
"'mrp_unbuild_tracked_raw_material' module "
|
||||||
|
"if you want this feature"))
|
||||||
|
lot = self.env['stock.production.lot'].create(
|
||||||
|
self._prepare_lots_for_purchased_unbuild(
|
||||||
|
produce_move.product_id))
|
||||||
|
self.env['stock.move.line'].create({
|
||||||
|
'move_id': produce_move.id,
|
||||||
|
'lot_id': lot.id,
|
||||||
|
'qty_done': produce_move.product_uom_qty,
|
||||||
|
'product_id': produce_move.product_id.id,
|
||||||
|
'product_uom_id': produce_move.product_uom.id,
|
||||||
|
'location_id': produce_move.location_id.id,
|
||||||
|
'location_dest_id': produce_move.location_dest_id.id,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
produce_move.quantity_done = produce_move.product_uom_qty
|
||||||
|
# comes from native code
|
||||||
|
produce_moves._action_done()
|
||||||
|
produced_move_line_ids = produce_moves.mapped(
|
||||||
|
'move_line_ids').filtered(lambda ml: ml.qty_done > 0)
|
||||||
|
consume_move.move_line_ids.write(
|
||||||
|
{'produce_line_ids': [(6, 0, produced_move_line_ids.ids)]})
|
||||||
|
self.message_post(
|
||||||
|
body=_("Product has been unbuilt without previous "
|
||||||
|
"manufacturing order"))
|
||||||
|
return self.write({'state': 'done'})
|
||||||
|
|
||||||
|
def _prepare_lots_for_purchased_unbuild(self, product):
|
||||||
|
# Customize your data lot with your own code
|
||||||
|
return {
|
||||||
|
"name": datetime.now().strftime(DT_FORMAT),
|
||||||
|
"product_id": product.id,
|
||||||
|
}
|
||||||
1
mrp_unbuild_tracked_raw_material/readme/CONFIGURE.rst
Normal file
1
mrp_unbuild_tracked_raw_material/readme/CONFIGURE.rst
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Customize method `_prepare_lots_for_purchased_unbuild()` to define your own data lot
|
||||||
3
mrp_unbuild_tracked_raw_material/readme/CONTRIBUTORS.rst
Normal file
3
mrp_unbuild_tracked_raw_material/readme/CONTRIBUTORS.rst
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Akretion:
|
||||||
|
|
||||||
|
* David Béal <david.beal@akretion.com>
|
||||||
17
mrp_unbuild_tracked_raw_material/readme/DESCRIPTION.rst
Normal file
17
mrp_unbuild_tracked_raw_material/readme/DESCRIPTION.rst
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
Odoo has a limitation on tracked product's components
|
||||||
|
which are not manufactured in the ERP.
|
||||||
|
|
||||||
|
When you try to do it, you get this warning:
|
||||||
|
|
||||||
|
Some of your components are tracked, you have to specify a manufacturing order in order to retrieve the correct components.
|
||||||
|
|
||||||
|
Unfortunately, it doesn't cover all the use cases.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
You receive eggs and you want to unbuild them in 2 parts:
|
||||||
|
|
||||||
|
- yellow part
|
||||||
|
- white part
|
||||||
|
|
||||||
|
Each of the parts are tracked and not linked to a previous manufacturing order
|
||||||
|
because, you don't build the eggs yourself, you subcontract it to a chicken.
|
||||||
3
mrp_unbuild_tracked_raw_material/readme/ROADMAP.rst
Normal file
3
mrp_unbuild_tracked_raw_material/readme/ROADMAP.rst
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
This module doesn't take account product `allow_unbuild_purchased` checked
|
||||||
|
which use `serial` tracking
|
||||||
|
|
||||||
6
mrp_unbuild_tracked_raw_material/readme/USAGE.rst
Normal file
6
mrp_unbuild_tracked_raw_material/readme/USAGE.rst
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Check 'Allow Unbuild Purchased' field in Inventory tab on product form
|
||||||
|
for any product with a bom you didn't manufactured but you want unbuild.
|
||||||
|
|
||||||
|
# Go to Manufacturing > Operations > Unbuild Orders
|
||||||
|
|
||||||
|
# Encode an unbuild order with the product used in first step
|
||||||
1
mrp_unbuild_tracked_raw_material/tests/__init__.py
Normal file
1
mrp_unbuild_tracked_raw_material/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import test_unbuild
|
||||||
71
mrp_unbuild_tracked_raw_material/tests/test_unbuild.py
Normal file
71
mrp_unbuild_tracked_raw_material/tests/test_unbuild.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Copyright (C) 2019 Akretion (http://www.akretion.com). All Rights Reserved
|
||||||
|
# @author David BEAL <david.beal@akretion.com>
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
# from odoo.tests.common import TransactionCase
|
||||||
|
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class TestUnbuildUnmanufacturedProduct(TestMrpCommon):
|
||||||
|
|
||||||
|
def setUp(self, *args, **kwargs):
|
||||||
|
self.loc = self.env.ref("stock.stock_location_stock")
|
||||||
|
super().setUp(*args, **kwargs)
|
||||||
|
|
||||||
|
def create_data(self):
|
||||||
|
prd_to_build = self.env["product.product"].create({
|
||||||
|
"name": "To unbuild",
|
||||||
|
"type": "product",
|
||||||
|
"allow_unbuild_purchased": True,
|
||||||
|
"tracking": "lot",
|
||||||
|
})
|
||||||
|
prd_to_use1 = self.env["product.product"].create({
|
||||||
|
"name": "component 1",
|
||||||
|
"type": "product",
|
||||||
|
"tracking": "lot",
|
||||||
|
})
|
||||||
|
prd_to_use2 = self.env["product.product"].create({
|
||||||
|
"name": "component 2",
|
||||||
|
"type": "product",
|
||||||
|
"tracking": "none",
|
||||||
|
})
|
||||||
|
bom = self.env["mrp.bom"].create({
|
||||||
|
"product_id": prd_to_build.id,
|
||||||
|
"product_tmpl_id": prd_to_build.product_tmpl_id.id,
|
||||||
|
"product_uom_id": self.uom_unit.id,
|
||||||
|
"product_qty": 1.0,
|
||||||
|
"type": "normal",
|
||||||
|
"bom_line_ids": [
|
||||||
|
(0, 0, {"product_id": prd_to_use2.id,
|
||||||
|
"product_qty": 1}),
|
||||||
|
(0, 0, {"product_id": prd_to_use1.id,
|
||||||
|
"product_qty": 2})
|
||||||
|
]})
|
||||||
|
return (bom, prd_to_build, prd_to_use1, prd_to_use2)
|
||||||
|
|
||||||
|
def test_unbuild(self):
|
||||||
|
""" """
|
||||||
|
bom, prd_to_build, prd_to_use1, prd_to_use2 = self.create_data()
|
||||||
|
lot = self.env['stock.production.lot'].create(
|
||||||
|
{"name": "%s" % datetime.now(),
|
||||||
|
"product_id": prd_to_build.id})
|
||||||
|
self.env["stock.quant"]._update_available_quantity(
|
||||||
|
prd_to_build, self.loc, 10, lot_id=lot)
|
||||||
|
unbuild = self.env["mrp.unbuild"].create({
|
||||||
|
"product_id": prd_to_build.id,
|
||||||
|
"bom_id": bom.id,
|
||||||
|
"product_qty": 1.0,
|
||||||
|
"lot_id": lot.id,
|
||||||
|
"product_uom_id": self.uom_unit.id,
|
||||||
|
})
|
||||||
|
unbuild.action_validate()
|
||||||
|
self._check_qty(9, prd_to_build)
|
||||||
|
self._check_qty(2, prd_to_use1)
|
||||||
|
self._check_qty(1, prd_to_use2)
|
||||||
|
|
||||||
|
def _check_qty(self, qty, product):
|
||||||
|
self.assertEqual(self.env["stock.quant"]._get_available_quantity(
|
||||||
|
product, self.loc), qty,
|
||||||
|
"You should have the %s product '%s' in stock" % (
|
||||||
|
qty, product.name))
|
||||||
16
mrp_unbuild_tracked_raw_material/views/product_view.xml
Normal file
16
mrp_unbuild_tracked_raw_material/views/product_view.xml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_template_property_form" model="ir.ui.view">
|
||||||
|
<field name="model">product.template</field>
|
||||||
|
<field name="inherit_id" ref="stock.view_template_property_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<field name="tracking" position="after">
|
||||||
|
<field name="allow_unbuild_purchased"
|
||||||
|
attrs="{'invisible': [('tracking', '=', 'none')]}"/>
|
||||||
|
</field>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
Reference in New Issue
Block a user