feat: Introduce mrp.packaging model and integrate packaging quantity management into MRP Production, BOM, and Production Schedule, along with production tracking and move line merging.
This commit is contained in:
parent
c305b0f113
commit
076fb7ee0b
@ -8,6 +8,9 @@ This module extends the Odoo Manufacturing app to allow creating Manufacturing O
|
|||||||
- **Quantity Packaging**: Adds a `Quantity Packaging` field to specify the number of packages to produce.
|
- **Quantity Packaging**: Adds a `Quantity Packaging` field to specify the number of packages to produce.
|
||||||
- **Auto-Calculation**: Automatically calculates the total `Quantity` (to produce) based on the selected packaging and the number of packages (Quantity = Packaging Size * Quantity Packaging).
|
- **Auto-Calculation**: Automatically calculates the total `Quantity` (to produce) based on the selected packaging and the number of packages (Quantity = Packaging Size * Quantity Packaging).
|
||||||
- **Manual Override**: Allows users to manually adjust the Quantity if needed, or create MOs without using packaging.
|
- **Manual Override**: Allows users to manually adjust the Quantity if needed, or create MOs without using packaging.
|
||||||
|
- **Traceability Logic**: Automatically consolidates split stock move lines into a single line per Lot/Location when producing more than planned, preventing duplicate entries in Traceability Reports.
|
||||||
|
- **Packaging Reset**: Automatically resets `Quantity Packaging` to 0.0 when the Packaging field is cleared, allowing seamless transition back to standard UOM input.
|
||||||
|
- **Clean Reports**: Uses advanced logic (SQL Delete) to remove duplicate 'Done' lines that cannot be unlinked via ORM, ensuring Traceability Reports are accurate and free of zero-quantity lines.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|||||||
@ -8,11 +8,17 @@
|
|||||||
It allows users to define the quantity to produce based on the selected packaging and its quantity.
|
It allows users to define the quantity to produce based on the selected packaging and its quantity.
|
||||||
""",
|
""",
|
||||||
'author': 'Suherdy Yacob',
|
'author': 'Suherdy Yacob',
|
||||||
'depends': ['mrp', 'product'],
|
'depends': ['mrp', 'product', 'mrp_mps'],
|
||||||
'data': [
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'views/mrp_packaging_views.xml',
|
||||||
'views/mrp_production_views.xml',
|
'views/mrp_production_views.xml',
|
||||||
|
'views/mrp_bom_views.xml',
|
||||||
|
'views/mrp_mps_views.xml',
|
||||||
],
|
],
|
||||||
|
|
||||||
'installable': True,
|
'installable': True,
|
||||||
'application': False,
|
'application': False,
|
||||||
'license': 'LGPL-3',
|
'license': 'LGPL-3',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1 +1,6 @@
|
|||||||
|
|
||||||
from . import mrp_production
|
from . import mrp_production
|
||||||
|
from . import mrp_bom
|
||||||
|
from . import mrp_production_schedule
|
||||||
|
from . import mrp_packaging
|
||||||
|
|
||||||
|
|||||||
22
models/mrp_bom.py
Normal file
22
models/mrp_bom.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
class MrpBom(models.Model):
|
||||||
|
_inherit = 'mrp.bom'
|
||||||
|
|
||||||
|
packaging_id = fields.Many2one('mrp.packaging', string='Packaging', domain="[('product_tmpl_id', '=', product_tmpl_id)]", check_company=True)
|
||||||
|
packaging_qty = fields.Float('Quantity Packaging', default=0.0)
|
||||||
|
|
||||||
|
@api.onchange('packaging_qty')
|
||||||
|
def _onchange_packaging_qty(self):
|
||||||
|
if self.packaging_id and self.packaging_qty:
|
||||||
|
self.product_qty = self.packaging_id.qty * self.packaging_qty
|
||||||
|
|
||||||
|
@api.onchange('packaging_id')
|
||||||
|
def _onchange_packaging_id(self):
|
||||||
|
if self.packaging_id and self.packaging_id.qty:
|
||||||
|
self.packaging_qty = self.product_qty / self.packaging_id.qty
|
||||||
|
|
||||||
|
@api.onchange('product_qty')
|
||||||
|
def _onchange_product_qty(self):
|
||||||
|
if self.packaging_id and self.packaging_id.qty:
|
||||||
|
self.packaging_qty = self.product_qty / self.packaging_id.qty
|
||||||
11
models/mrp_packaging.py
Normal file
11
models/mrp_packaging.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
class MrpPackaging(models.Model):
|
||||||
|
_name = "mrp.packaging"
|
||||||
|
_description = "MRP Packaging"
|
||||||
|
|
||||||
|
name = fields.Char(required=True)
|
||||||
|
product_tmpl_id = fields.Many2one('product.template', string='Product', required=True, check_company=True)
|
||||||
|
qty = fields.Float('Quantity', default=1.0, required=True)
|
||||||
|
barcode = fields.Char('Barcode')
|
||||||
|
company_id = fields.Many2one('res.company', 'Company', index=True)
|
||||||
@ -3,10 +3,127 @@ from odoo import api, fields, models
|
|||||||
class MrpProduction(models.Model):
|
class MrpProduction(models.Model):
|
||||||
_inherit = 'mrp.production'
|
_inherit = 'mrp.production'
|
||||||
|
|
||||||
packaging_id = fields.Many2one('product.uom', string='Packaging', domain="[('product_id', '=', product_id)]", check_company=True)
|
|
||||||
packaging_qty = fields.Float('Quantity Packaging', default=0.0, digits='Product Unit of Measure')
|
|
||||||
|
|
||||||
@api.onchange('packaging_id', 'packaging_qty')
|
packaging_id = fields.Many2one('mrp.packaging', string='Packaging', domain="[('product_tmpl_id', '=', product_tmpl_id)]", check_company=True)
|
||||||
|
packaging_qty = fields.Float('Quantity Packaging', default=0.0)
|
||||||
|
|
||||||
|
@api.onchange('packaging_qty')
|
||||||
def _onchange_packaging_qty(self):
|
def _onchange_packaging_qty(self):
|
||||||
if self.packaging_id and self.packaging_qty:
|
if self.packaging_id and self.packaging_qty:
|
||||||
self.product_qty = self.packaging_id.uom_id._compute_quantity(self.packaging_qty, self.product_uom_id)
|
self.product_qty = self.packaging_id.qty * self.packaging_qty
|
||||||
|
|
||||||
|
@api.onchange('packaging_id')
|
||||||
|
def _onchange_packaging_id(self):
|
||||||
|
if self.packaging_id and self.packaging_id.qty:
|
||||||
|
self.packaging_qty = self.product_qty / self.packaging_id.qty
|
||||||
|
else:
|
||||||
|
self.packaging_qty = 0.0
|
||||||
|
|
||||||
|
qty_producing_packaging = fields.Float('Quantity Producing Packaging', compute='_compute_qty_producing_packaging', inverse='_inverse_qty_producing_packaging', digits=(16, 2))
|
||||||
|
|
||||||
|
@api.depends('qty_producing', 'packaging_id', 'packaging_id.qty')
|
||||||
|
def _compute_qty_producing_packaging(self):
|
||||||
|
for record in self:
|
||||||
|
if record.packaging_id and record.packaging_id.qty:
|
||||||
|
record.qty_producing_packaging = record.qty_producing / record.packaging_id.qty
|
||||||
|
else:
|
||||||
|
record.qty_producing_packaging = 0.0
|
||||||
|
|
||||||
|
def _inverse_qty_producing_packaging(self):
|
||||||
|
for record in self:
|
||||||
|
if record.packaging_id and record.packaging_id.qty:
|
||||||
|
record.qty_producing = record.qty_producing_packaging * record.packaging_id.qty
|
||||||
|
record._merge_finished_move_lines()
|
||||||
|
|
||||||
|
@api.onchange('qty_producing_packaging')
|
||||||
|
def _onchange_qty_producing_packaging(self):
|
||||||
|
if self.packaging_id and self.packaging_id.qty:
|
||||||
|
self.qty_producing = self.qty_producing_packaging * self.packaging_id.qty
|
||||||
|
self._onchange_qty_producing()
|
||||||
|
self._merge_finished_move_lines()
|
||||||
|
|
||||||
|
@api.onchange('qty_producing')
|
||||||
|
def _onchange_qty_producing(self):
|
||||||
|
super()._onchange_qty_producing()
|
||||||
|
self._merge_finished_move_lines()
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
res = super().write(vals)
|
||||||
|
if 'qty_producing' in vals or 'qty_producing_packaging' in vals:
|
||||||
|
self._merge_finished_move_lines()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _set_qty_producing(self, *args, **kwargs):
|
||||||
|
res = super()._set_qty_producing(*args, **kwargs)
|
||||||
|
self._merge_finished_move_lines()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _post_inventory(self, *args, **kwargs):
|
||||||
|
res = super()._post_inventory(*args, **kwargs)
|
||||||
|
self._merge_finished_move_lines()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _merge_finished_move_lines(self):
|
||||||
|
for production in self:
|
||||||
|
# Process ALL finished moves, even DONE ones, to catch splits that happened during validation
|
||||||
|
moves = production.move_finished_ids.filtered(lambda m: m.product_id == production.product_id and m.state != 'cancel')
|
||||||
|
for move in moves:
|
||||||
|
# print(f"DEBUG MERGE: Processing move {move.id}, lines found: {len(move.move_line_ids)}")
|
||||||
|
lines_by_key = {}
|
||||||
|
# track lines to unlink or zero-out (if done)
|
||||||
|
|
||||||
|
# Keep the original line (smallest ID) to preserve consistency
|
||||||
|
sorted_lines = move.move_line_ids.sorted('id')
|
||||||
|
for line in sorted_lines:
|
||||||
|
# Use IDs for the key to ensure reliable grouping
|
||||||
|
# Include lot_name in case lot_id is not yet set
|
||||||
|
key = (
|
||||||
|
line.lot_id.id,
|
||||||
|
line.lot_name,
|
||||||
|
line.location_id.id,
|
||||||
|
line.location_dest_id.id,
|
||||||
|
line.package_id.id,
|
||||||
|
line.result_package_id.id,
|
||||||
|
line.owner_id.id,
|
||||||
|
line.product_id.id
|
||||||
|
)
|
||||||
|
|
||||||
|
if key in lines_by_key:
|
||||||
|
target_line = lines_by_key[key]
|
||||||
|
# print(f"DEBUG MERGE: Merging line {line.id} into {target_line.id}")
|
||||||
|
|
||||||
|
# Merge Quantities
|
||||||
|
target_line.quantity += line.quantity
|
||||||
|
|
||||||
|
# Preserve Traceability Links
|
||||||
|
if line.consume_line_ids:
|
||||||
|
target_line.consume_line_ids |= line.consume_line_ids
|
||||||
|
if line.produce_line_ids:
|
||||||
|
target_line.produce_line_ids |= line.produce_line_ids
|
||||||
|
|
||||||
|
# If the line is DONE, we cannot unlink it via ORM.
|
||||||
|
# But the user wants it gone from the report (0 qty is not enough).
|
||||||
|
# So we use SQL Delete (Hard Delete).
|
||||||
|
if line.state == 'done':
|
||||||
|
# print(f"DEBUG MERGE: SQL Deleting DONE line {line.id}")
|
||||||
|
self.env.cr.execute("DELETE FROM stock_move_line WHERE id = %s", (line.id,))
|
||||||
|
line.invalidate_recordset()
|
||||||
|
else:
|
||||||
|
# print(f"DEBUG MERGE: Unlinking line {line.id}")
|
||||||
|
line.unlink()
|
||||||
|
else:
|
||||||
|
lines_by_key[key] = line
|
||||||
|
|
||||||
|
@api.onchange('product_qty')
|
||||||
|
def _onchange_product_qty(self):
|
||||||
|
if self.packaging_id and self.packaging_id.qty:
|
||||||
|
self.packaging_qty = self.product_qty / self.packaging_id.qty
|
||||||
|
|
||||||
|
@api.onchange('bom_id')
|
||||||
|
def _onchange_bom_id(self):
|
||||||
|
super()._onchange_bom_id()
|
||||||
|
if self.bom_id and self.bom_id.packaging_id:
|
||||||
|
self.packaging_id = self.bom_id.packaging_id
|
||||||
|
self.packaging_qty = self.bom_id.packaging_qty
|
||||||
|
# Trigger qty calculation
|
||||||
|
self._onchange_packaging_qty()
|
||||||
|
|||||||
38
models/mrp_production_schedule.py
Normal file
38
models/mrp_production_schedule.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from odoo import api, fields, models
|
||||||
|
from odoo.tools.float_utils import float_round
|
||||||
|
|
||||||
|
class MrpProductionSchedule(models.Model):
|
||||||
|
_inherit = 'mrp.production.schedule'
|
||||||
|
|
||||||
|
packaging_id = fields.Many2one('mrp.packaging', string='Packaging', domain="[('product_tmpl_id', '=', product_tmpl_id)]", check_company=True)
|
||||||
|
|
||||||
|
def get_production_schedule_view_state(self, period_scale=False, use_all_schedules=False):
|
||||||
|
res = super().get_production_schedule_view_state(period_scale, use_all_schedules)
|
||||||
|
for state in res:
|
||||||
|
mps = self.browse(state['id'])
|
||||||
|
if mps.packaging_id:
|
||||||
|
packaging_qty = mps.packaging_id.qty
|
||||||
|
if packaging_qty:
|
||||||
|
# Adjust header details if needed, but mostly we modify forecast_ids
|
||||||
|
for forecast in state['forecast_ids']:
|
||||||
|
forecast['forecast_qty'] = forecast['forecast_qty'] / packaging_qty
|
||||||
|
forecast['replenish_qty'] = forecast['replenish_qty'] / packaging_qty
|
||||||
|
forecast['safety_stock_qty'] = forecast['safety_stock_qty'] / packaging_qty
|
||||||
|
forecast['starting_inventory_qty'] = forecast['starting_inventory_qty'] / packaging_qty
|
||||||
|
forecast['incoming_qty'] = forecast['incoming_qty'] / packaging_qty
|
||||||
|
forecast['outgoing_qty'] = forecast['outgoing_qty'] / packaging_qty
|
||||||
|
forecast['indirect_demand_qty'] = forecast['indirect_demand_qty'] / packaging_qty
|
||||||
|
|
||||||
|
if state.get('product_uom_id'):
|
||||||
|
state['product_uom_id'] = (state['product_uom_id'][0], mps.packaging_id.name)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def set_forecast_qty(self, date_index, quantity, period_scale=False):
|
||||||
|
if self.packaging_id:
|
||||||
|
quantity = float(quantity) * self.packaging_id.qty
|
||||||
|
return super().set_forecast_qty(date_index, quantity, period_scale)
|
||||||
|
|
||||||
|
def set_replenish_qty(self, date_index, quantity, period_scale=False):
|
||||||
|
if self.packaging_id:
|
||||||
|
quantity = float(quantity) * self.packaging_id.qty
|
||||||
|
return super().set_replenish_qty(date_index, quantity, period_scale)
|
||||||
2
security/ir.model.access.csv
Normal file
2
security/ir.model.access.csv
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_mrp_packaging,mrp.packaging,model_mrp_packaging,base.group_user,1,1,1,1
|
||||||
|
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import test_mrp_packaging_qty
|
||||||
75
tests/test_mrp_packaging_qty.py
Normal file
75
tests/test_mrp_packaging_qty.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
from odoo.tests import TransactionCase, tagged
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
|
class TestMrpPackagingQty(TransactionCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.product = cls.env['product.product'].create({
|
||||||
|
'name': 'Test Product',
|
||||||
|
'type': 'product',
|
||||||
|
})
|
||||||
|
# Create mrp.packaging instead of product.uom/product.packaging
|
||||||
|
cls.packaging = cls.env['mrp.packaging'].create({
|
||||||
|
'name': 'Box of 10',
|
||||||
|
'product_tmpl_id': cls.product.product_tmpl_id.id,
|
||||||
|
'qty': 10.0,
|
||||||
|
'barcode': 'PACK10',
|
||||||
|
})
|
||||||
|
cls.bom = cls.env['mrp.bom'].create({
|
||||||
|
'product_tmpl_id': cls.product.product_tmpl_id.id,
|
||||||
|
'product_qty': 1.0,
|
||||||
|
'packaging_id': cls.packaging.id,
|
||||||
|
'packaging_qty': 2.0,
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_bom_packaging_calculation(self):
|
||||||
|
""" Test if BOM quantity is calculated correctly based on packaging """
|
||||||
|
# Trigger onchange manually
|
||||||
|
self.bom._onchange_packaging_qty()
|
||||||
|
# Logic: packaging_qty (2) * packaging.qty (10) = 20
|
||||||
|
self.assertEqual(self.bom.product_qty, 20.0, "BOM Quantity should be 2 * 10 = 20")
|
||||||
|
|
||||||
|
def test_mo_creation_packaging(self):
|
||||||
|
""" Test if MO created from BOM gets the packaging """
|
||||||
|
mo_form = self.env['mrp.production'].create({
|
||||||
|
'product_id': self.product.id,
|
||||||
|
'bom_id': self.bom.id,
|
||||||
|
'product_qty': 1.0, # Initial dummy value
|
||||||
|
})
|
||||||
|
# Simulate onchange
|
||||||
|
mo_form._onchange_bom_id()
|
||||||
|
self.assertEqual(mo_form.packaging_id, self.packaging, "MO should inherit packaging from BOM")
|
||||||
|
self.assertEqual(mo_form.packaging_qty, 2.0, "MO should inherit packaging qty from BOM")
|
||||||
|
|
||||||
|
# Trigger calculation
|
||||||
|
mo_form._onchange_packaging_qty()
|
||||||
|
self.assertEqual(mo_form.product_qty, 20.0, "MO product qty should be updated based on packaging")
|
||||||
|
|
||||||
|
def test_mps_packaging(self):
|
||||||
|
""" Test MPS view state with packaging """
|
||||||
|
if 'mrp.production.schedule' not in self.env:
|
||||||
|
return # Skip if mps not installed
|
||||||
|
|
||||||
|
mps = self.env['mrp.production.schedule'].create({
|
||||||
|
'product_id': self.product.id,
|
||||||
|
'warehouse_id': self.env['stock.warehouse'].search([], limit=1).id,
|
||||||
|
'packaging_id': self.packaging.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Test get_view_state scaling
|
||||||
|
# Mock some forecast data
|
||||||
|
date_range = self.env.company._get_date_range()
|
||||||
|
date_start = date_range[0][0]
|
||||||
|
# Set forecast quantity in packaging units (e.g. 5 packs)
|
||||||
|
mps.set_forecast_qty(0, 5)
|
||||||
|
|
||||||
|
# Verify stored value is 5 * 10 = 50
|
||||||
|
forecast = mps.forecast_ids.filtered(lambda f: f.date >= date_start)[:1]
|
||||||
|
self.assertEqual(forecast.forecast_qty, 50.0, "Forecast qty should be stored as 5 * 10 = 50")
|
||||||
|
|
||||||
|
# Verify view state returns 5 (50 / 10)
|
||||||
|
view_state = mps.get_production_schedule_view_state()[0]
|
||||||
|
view_forecast = view_state['forecast_ids'][0]
|
||||||
|
self.assertEqual(view_forecast['forecast_qty'], 5.0, "View state should return 5.0 packs")
|
||||||
17
views/mrp_bom_views.xml
Normal file
17
views/mrp_bom_views.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="mrp_bom_form_view_inherit_packaging" model="ir.ui.view">
|
||||||
|
<field name="name">mrp.bom.form.inherit.packaging</field>
|
||||||
|
<field name="model">mrp.bom</field>
|
||||||
|
<field name="inherit_id" ref="mrp.mrp_bom_form_view"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='product_qty']/.." position="after">
|
||||||
|
<label for="packaging_id" string="Packaging"/>
|
||||||
|
<div class="o_row">
|
||||||
|
<field name="packaging_qty"/>
|
||||||
|
<field name="packaging_id" context="{'default_product_tmpl_id': product_tmpl_id}"/>
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
28
views/mrp_mps_views.xml
Normal file
28
views/mrp_mps_views.xml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- We cannot easily inherit the MPS client action view as it is rendered via JS -->
|
||||||
|
<!-- However, we can add the field to the tree view if it exists or form view -->
|
||||||
|
|
||||||
|
<record id="mrp_mps_production_schedule_form_view_inherit_packaging" model="ir.ui.view">
|
||||||
|
<field name="name">mrp.production.schedule.form.inherit.packaging</field>
|
||||||
|
<field name="model">mrp.production.schedule</field>
|
||||||
|
<field name="inherit_id" ref="mrp_mps.mrp_mps_production_schedule_form_view"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='warehouse_id']" position="after">
|
||||||
|
<field name="packaging_id"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="mrp_mps_search_view_inherit_packaging" model="ir.ui.view">
|
||||||
|
<field name="name">mrp.production.schedule.search.inherit.packaging</field>
|
||||||
|
<field name="model">mrp.production.schedule</field>
|
||||||
|
<field name="inherit_id" ref="mrp_mps.mrp_mps_search_view"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='product_id']" position="after">
|
||||||
|
<field name="packaging_id"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
36
views/mrp_packaging_views.xml
Normal file
36
views/mrp_packaging_views.xml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="mrp_packaging_form_view" model="ir.ui.view">
|
||||||
|
<field name="name">mrp.packaging.form</field>
|
||||||
|
<field name="model">mrp.packaging</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Packaging">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="product_tmpl_id"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="qty"/>
|
||||||
|
<field name="barcode"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="mrp_packaging_tree_view" model="ir.ui.view">
|
||||||
|
<field name="name">mrp.packaging.list</field>
|
||||||
|
<field name="model">mrp.packaging</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Packaging" editable="bottom">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="product_tmpl_id"/>
|
||||||
|
<field name="qty"/>
|
||||||
|
<field name="barcode"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@ -5,9 +5,24 @@
|
|||||||
<field name="model">mrp.production</field>
|
<field name="model">mrp.production</field>
|
||||||
<field name="inherit_id" ref="mrp.mrp_production_form_view"/>
|
<field name="inherit_id" ref="mrp.mrp_production_form_view"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//field[@name='product_qty']/.." position="after">
|
<xpath expr="//label[@for='product_qty']" position="attributes">
|
||||||
<field name="packaging_id"/>
|
<attribute name="invisible">packaging_id</attribute>
|
||||||
<field name="packaging_qty"/>
|
</xpath>
|
||||||
|
<xpath expr="//div[@name='qty']" position="attributes">
|
||||||
|
<attribute name="invisible">packaging_id</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//div[@name='qty']" position="after">
|
||||||
|
<field name="packaging_id" placeholder="Packaging" invisible="packaging_id" readonly="state in ('done', 'cancel')"/>
|
||||||
|
<label for="packaging_qty" string="Quantity" invisible="not packaging_id"/>
|
||||||
|
<div class="o_row g-0 d-flex" name="qty_packaging" invisible="not packaging_id">
|
||||||
|
<div invisible="state == 'draft'" class="o_row flex-grow-1">
|
||||||
|
<field name="qty_producing_packaging" class="text-start text-truncate" readonly="state == 'cancel' or (state == 'done' and is_locked)" force_save="1"/>
|
||||||
|
/
|
||||||
|
</div>
|
||||||
|
<field name="packaging_qty" class="oe_inline text-start text-truncate" readonly="state != 'draft'" force_save="1" style="width:auto!important"/>
|
||||||
|
<field name="packaging_id" options="{'no_open': True, 'no_create': True}" readonly="state in ('done', 'cancel')" class="oe_inline" style="width:auto!important"/>
|
||||||
|
<span name="to_produce_pkg" class='fw-bold text-nowrap'>To Produce</span>
|
||||||
|
</div>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user