mirror of
https://github.com/OCA/manufacture.git
synced 2025-01-28 16:37:15 +02:00
[12.0][ADD] stock_picking_product_kit_helper
This commit is contained in:
4
stock_picking_product_kit_helper/__init__.py
Normal file
4
stock_picking_product_kit_helper/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Copyright 2019 Kitti U. - Ecosoft <kittiu@ecosoft.co.th>
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from . import models
|
||||||
20
stock_picking_product_kit_helper/__manifest__.py
Normal file
20
stock_picking_product_kit_helper/__manifest__.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Copyright 2019 Kitti U. - Ecosoft <kittiu@ecosoft.co.th>
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
{
|
||||||
|
'name': 'Stock Picking Product Kit Helper',
|
||||||
|
'summary': 'Set quanity in picking line based on product kit quantity',
|
||||||
|
'version': '12.0.1.0.0',
|
||||||
|
'category': 'Stock',
|
||||||
|
'website': 'https://github.com/OCA/manufacture',
|
||||||
|
'author': 'Ecosoft, Odoo Community Association (OCA)',
|
||||||
|
'license': 'AGPL-3',
|
||||||
|
'installable': True,
|
||||||
|
'depends': [
|
||||||
|
'sale_mrp',
|
||||||
|
],
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'views/stock_view.xml',
|
||||||
|
],
|
||||||
|
'maintainers': ['kittiu']
|
||||||
|
}
|
||||||
4
stock_picking_product_kit_helper/models/__init__.py
Normal file
4
stock_picking_product_kit_helper/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Copyright 2019 Kitti U. - Ecosoft <kittiu@ecosoft.co.th>
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from . import stock_picking
|
||||||
133
stock_picking_product_kit_helper/models/stock_picking.py
Normal file
133
stock_picking_product_kit_helper/models/stock_picking.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# Copyright 2019 Kitti U. - Ecosoft <kittiu@ecosoft.co.th>
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class StockPicking(models.Model):
|
||||||
|
_inherit = 'stock.picking'
|
||||||
|
|
||||||
|
product_kit_helper_ids = fields.One2many(
|
||||||
|
comodel_name='stock.picking.product.kit.helper',
|
||||||
|
string='Product Kit Helper Lines',
|
||||||
|
inverse_name='picking_id',
|
||||||
|
readonly=False,
|
||||||
|
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
|
||||||
|
)
|
||||||
|
has_product_kit = fields.Boolean(
|
||||||
|
string='Has Product Kit',
|
||||||
|
compute='_compute_has_product_kit',
|
||||||
|
help="True if there is at least 1 product kit in the sales order",
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _is_product_kit(self, product, company):
|
||||||
|
BOM = self.env['mrp.bom'].sudo()
|
||||||
|
bom = BOM._bom_find(product=product,
|
||||||
|
company_id=company.id)
|
||||||
|
return bom and bom.type == 'phantom'
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _compute_has_product_kit(self):
|
||||||
|
for picking in self:
|
||||||
|
if any(self._is_product_kit(line.product_id, line.company_id)
|
||||||
|
for line in picking.move_lines.mapped('sale_line_id')):
|
||||||
|
picking.has_product_kit = True
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def show_product_kit(self):
|
||||||
|
"""Find move_lines with product kit to create helper line."""
|
||||||
|
self.ensure_one()
|
||||||
|
BOM = self.env['mrp.bom'].sudo()
|
||||||
|
helpers = []
|
||||||
|
for sale_line in self.move_lines.mapped('sale_line_id'):
|
||||||
|
bom = BOM._bom_find(product=sale_line.product_id,
|
||||||
|
company_id=sale_line.company_id.id)
|
||||||
|
if bom and bom.type == 'phantom': # Create product kit line
|
||||||
|
helpers.append((0, 0, {'sale_line_id': sale_line.id,
|
||||||
|
'product_id': sale_line.product_id.id,
|
||||||
|
'product_uom_qty': 0.0,
|
||||||
|
}))
|
||||||
|
self.product_kit_helper_ids.unlink()
|
||||||
|
self.write({'product_kit_helper_ids': helpers})
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def action_product_kit_helper(self):
|
||||||
|
"""Assign product kit's quantity to stock move."""
|
||||||
|
self.ensure_one()
|
||||||
|
if self.state in ('done', 'cancel'):
|
||||||
|
raise ValidationError(
|
||||||
|
_('Product Kit Helper is not allowed on current state'))
|
||||||
|
for helper in self.product_kit_helper_ids:
|
||||||
|
helper.action_explode_helper()
|
||||||
|
|
||||||
|
|
||||||
|
class StockPickingProductKitHelper(models.Model):
|
||||||
|
_name = 'stock.picking.product.kit.helper'
|
||||||
|
_description = """
|
||||||
|
Product Kit Helper, allow user to specify quantity of product kit,
|
||||||
|
to explode as product quantity in operations tab
|
||||||
|
"""
|
||||||
|
|
||||||
|
picking_id = fields.Many2one(
|
||||||
|
comodel_name='stock.picking',
|
||||||
|
string='Picking',
|
||||||
|
required=True,
|
||||||
|
index=True,
|
||||||
|
ondelete='cascade',
|
||||||
|
)
|
||||||
|
sale_line_id = fields.Many2one(
|
||||||
|
comodel_name='sale.order.line',
|
||||||
|
string='Sales Order Line',
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
product_id = fields.Many2one(
|
||||||
|
comodel_name='product.product',
|
||||||
|
string='Product',
|
||||||
|
required=True,
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
product_uom_qty = fields.Float(
|
||||||
|
string='Quantity',
|
||||||
|
)
|
||||||
|
product_uom = fields.Many2one(
|
||||||
|
comodel_name='uom.uom',
|
||||||
|
string='Unit of Measure',
|
||||||
|
related='sale_line_id.product_uom',
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def action_explode_helper(self):
|
||||||
|
"""Explodes product kit quantity to detailed product in stock move."""
|
||||||
|
self.ensure_one()
|
||||||
|
# Mock stock.move, in order to resue stock.move's action_explode
|
||||||
|
StockMove = self.env['stock.move']
|
||||||
|
mock_loc = self.env['stock.location'].sudo().search([], limit=1)
|
||||||
|
mock_pt = self.env['stock.picking.type'].sudo().search([], limit=1)
|
||||||
|
mock_stock_move = StockMove.sudo().create({
|
||||||
|
'name': '/',
|
||||||
|
'product_id': self.product_id.id,
|
||||||
|
'product_uom': self.product_uom.id,
|
||||||
|
'product_uom_qty': self.product_uom_qty,
|
||||||
|
'picking_type_id': mock_pt.id,
|
||||||
|
'location_id': mock_loc.id,
|
||||||
|
'location_dest_id': mock_loc.id,
|
||||||
|
})
|
||||||
|
# Reuse explode function and assign quantity_done in stock.move
|
||||||
|
mock_processed_moves = mock_stock_move.action_explode()
|
||||||
|
for mock_move in mock_processed_moves:
|
||||||
|
stock_move = StockMove.search([
|
||||||
|
('picking_id', '=', self.picking_id.id),
|
||||||
|
('sale_line_id', '=', self.sale_line_id.id),
|
||||||
|
('product_id', '=', mock_move.product_id.id)])
|
||||||
|
if not stock_move:
|
||||||
|
continue
|
||||||
|
if len(stock_move) != 1:
|
||||||
|
raise ValidationError(
|
||||||
|
_('No matching detailed product %s for product kit %s') %
|
||||||
|
(mock_move.product_id.display_name,
|
||||||
|
self.product_id.display_name))
|
||||||
|
stock_move.write({'quantity_done': mock_move.product_uom_qty})
|
||||||
|
mock_processed_moves.sudo().unlink()
|
||||||
1
stock_picking_product_kit_helper/readme/CONTRIBUTORS.rst
Normal file
1
stock_picking_product_kit_helper/readme/CONTRIBUTORS.rst
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* Kitti Upariphutthiphong <kittiu@ecosoft.co.th>
|
||||||
6
stock_picking_product_kit_helper/readme/DESCRIPTION.rst
Normal file
6
stock_picking_product_kit_helper/readme/DESCRIPTION.rst
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
When sales order contain product kits (product with BOM of type kit),
|
||||||
|
the delivery order (stock.move) created by it will be exploded to multiple product lines.
|
||||||
|
|
||||||
|
Normally, to partially deliver, user will calculate manually the quantity of each product lines to delivery.
|
||||||
|
|
||||||
|
This module add new tab "Product Kit Help" to help in calculate quantity in product line with ease.
|
||||||
9
stock_picking_product_kit_helper/readme/USAGE.rst
Normal file
9
stock_picking_product_kit_helper/readme/USAGE.rst
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
When origin sales order of the underlining delivery order contains at least 1 product kit,
|
||||||
|
the tab "Product Kit Helper" will appear.
|
||||||
|
|
||||||
|
To use the helper, go to the "Product Kit Helper" tab,
|
||||||
|
|
||||||
|
#. Click => Show Product Kit
|
||||||
|
#. Edit and fill in required quantity
|
||||||
|
#. Click Assign Operation Quantity
|
||||||
|
#. Check result in Operations tab
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_stock_picking_product_kit_helper_user,stock.picking.product.kit.helper user,model_stock_picking_product_kit_helper,stock.group_stock_user,1,1,1,1
|
||||||
|
BIN
stock_picking_product_kit_helper/static/description/icon.png
Normal file
BIN
stock_picking_product_kit_helper/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
79
stock_picking_product_kit_helper/static/description/icon.svg
Normal file
79
stock_picking_product_kit_helper/static/description/icon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 45 KiB |
4
stock_picking_product_kit_helper/tests/__init__.py
Normal file
4
stock_picking_product_kit_helper/tests/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Copyright 2019 Kitti U. - Ecosoft <kittiu@ecosoft.co.th>
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from . import test_stock_picking_product_kit_helper
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Copyright 2019 Kitti U. - Ecosoft <kittiu@ecosoft.co.th>
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo.tests import common, Form
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class TestStockPickingProductKitHelper(common.TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestStockPickingProductKitHelper, self).setUp()
|
||||||
|
self.partner = self.env.ref('base.res_partner_2')
|
||||||
|
self.table_kit = self.env.ref('mrp.product_product_table_kit')
|
||||||
|
|
||||||
|
def test_00_sale_product_kit_helper(self):
|
||||||
|
"""Test sale order with product kit, I expect,
|
||||||
|
- Product is exploded on picking
|
||||||
|
- Use helper, will assign quantity to stock.move correctly
|
||||||
|
- After picking is done, do not allow to use helper
|
||||||
|
"""
|
||||||
|
# Create sales order of 10 table kit
|
||||||
|
order_form = Form(self.env['sale.order'])
|
||||||
|
order_form.partner_id = self.partner
|
||||||
|
with order_form.order_line.new() as line:
|
||||||
|
line.product_id = self.table_kit
|
||||||
|
line.product_uom_qty = 10
|
||||||
|
order = order_form.save()
|
||||||
|
order.action_confirm()
|
||||||
|
# In the picking, product line is exploded.
|
||||||
|
picking = order.mapped('picking_ids')
|
||||||
|
self.assertEqual(len(picking), 1)
|
||||||
|
stock_moves = picking.move_lines
|
||||||
|
# 1 SO line exploded to 2 moves
|
||||||
|
moves = [{'product': x.product_id.name, 'qty': x.product_uom_qty}
|
||||||
|
for x in stock_moves]
|
||||||
|
self.assertEqual(moves,
|
||||||
|
[{'product': 'Wood Panel', 'qty': 10.0},
|
||||||
|
{'product': 'Bolt', 'qty': 40.0}])
|
||||||
|
self.assertTrue(picking.has_product_kit)
|
||||||
|
self.assertFalse(picking.product_kit_helper_ids) # Not show yet
|
||||||
|
picking.show_product_kit()
|
||||||
|
self.assertEqual(len(picking.product_kit_helper_ids), 1)
|
||||||
|
# Assign product set 4 qty and test that it apply to stock.move
|
||||||
|
picking.product_kit_helper_ids[0].write({'product_uom_qty': 4.0})
|
||||||
|
picking.action_product_kit_helper()
|
||||||
|
moves = [{'product': x.product_id.name, 'qty': x.quantity_done}
|
||||||
|
for x in stock_moves]
|
||||||
|
self.assertEqual(moves,
|
||||||
|
[{'product': 'Wood Panel', 'qty': 4.0},
|
||||||
|
{'product': 'Bolt', 'qty': 16.0}])
|
||||||
|
# Assign again to 10 qty
|
||||||
|
picking.product_kit_helper_ids[0].write({'product_uom_qty': 10.0})
|
||||||
|
picking.action_product_kit_helper()
|
||||||
|
moves = [{'product': x.product_id.name, 'qty': x.quantity_done}
|
||||||
|
for x in stock_moves]
|
||||||
|
self.assertEqual(moves,
|
||||||
|
[{'product': 'Wood Panel', 'qty': 10.0},
|
||||||
|
{'product': 'Bolt', 'qty': 40.0}])
|
||||||
|
# Validate Picking
|
||||||
|
picking.button_validate()
|
||||||
|
self.assertEqual(picking.state, 'done')
|
||||||
|
# After done state, block the helper
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
picking.action_product_kit_helper()
|
||||||
41
stock_picking_product_kit_helper/views/stock_view.xml
Normal file
41
stock_picking_product_kit_helper/views/stock_view.xml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright 2019 Kitti U. - Ecosoft <carlos.dauden@tecnativa.com>
|
||||||
|
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
|
||||||
|
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_picking_form" model="ir.ui.view">
|
||||||
|
<field name="name">stock.picking.form</field>
|
||||||
|
<field name="model">stock.picking</field>
|
||||||
|
<field name="inherit_id" ref="stock.view_picking_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="/form/sheet/notebook/page[@name='extra']" position="before">
|
||||||
|
<page string="Kit Helper" attrs="{'invisible': [('has_product_kit', '=', False)]}">
|
||||||
|
<p class="oe_grey">
|
||||||
|
To deliver partial product kits, you can use this tab to help calculate
|
||||||
|
quantity and auto fill in "Done" column in Operations tab.<br/>
|
||||||
|
<ol>
|
||||||
|
<li>Click => Show Product Kit</li>
|
||||||
|
<li>Edit and fill in required quantity</li>
|
||||||
|
<li>Click Assign Operation Quantity</li>
|
||||||
|
<li>Check result in Operations tab</li>
|
||||||
|
</ol>
|
||||||
|
</p>
|
||||||
|
<field name="has_product_kit" invisible="1"/>
|
||||||
|
<button name="show_product_kit" type="object" string="⇒ Show Product Kit" class="oe_link"
|
||||||
|
attrs="{'invisible': ['|', ('product_kit_helper_ids', '!=', []), ('state', 'in', ['done', 'cancel'])]}"/>
|
||||||
|
<button name="action_product_kit_helper" type="object" string="Assign Operation Quantity"
|
||||||
|
class="oe_highlight" attrs="{'invisible': ['|', ('product_kit_helper_ids', '=', []), ('state', 'in', ['done', 'cancel'])]}"/>
|
||||||
|
<field name="product_kit_helper_ids" attrs="{'invisible': [('product_kit_helper_ids', '=', [])]}">
|
||||||
|
<tree editable="bottom" create="0" delete="0">
|
||||||
|
<field name="product_id"/>
|
||||||
|
<field name="product_uom_qty"/>
|
||||||
|
<field name="product_uom" groups="uom.group_uom"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
Reference in New Issue
Block a user