[IMP] stock(_*), mrp: highlight PO/SO links on forecast report
Replenishment is great to replace MTO but misses the direct link between SO and PO (smart buttons on both SO and PO to link to the corresponding PO and SO). The info is actually there in the forecasted report but it is not accessible directly from the PO and in the case of the SO does not highlight the corresponding line of the SO. This adds a graph icon in the PO, MO and receipt form to directly access the forecasted report (it already exists for SO and delivery). The information linked to the SO/PO/MO/receipt/delivery is also sent to the forecasted report to highlight the corresponding line. Note that in the case of the SO, the information needs to go first through the qty_at_date widget (which contains the link to the forecast report). The graph icon for the PO, receipts and MO turns red when the forecasted quantity at expected date is negative. This is done using the new 'forecasted_issue' field. closes odoo/odoo#62264 Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com>
This commit is contained in:
@@ -255,6 +255,7 @@ class MrpProduction(models.Model):
|
||||
('expected', 'Expected'),
|
||||
('late', 'Late')], compute='_compute_components_availability')
|
||||
show_lot_ids = fields.Boolean('Display the serial number shortcut on the moves', compute='_compute_show_lot_ids')
|
||||
forecasted_issue = fields.Boolean(compute='_compute_forecasted_issue')
|
||||
|
||||
@api.depends('product_id', 'bom_id', 'company_id')
|
||||
def _compute_allowed_product_ids(self):
|
||||
@@ -516,6 +517,18 @@ class MrpProduction(models.Model):
|
||||
('qty_positive', 'check (product_qty > 0)', 'The quantity to produce must be positive!'),
|
||||
]
|
||||
|
||||
@api.depends('product_uom_qty', 'date_planned_start')
|
||||
def _compute_forecasted_issue(self):
|
||||
for order in self:
|
||||
warehouse = order.location_dest_id.get_warehouse()
|
||||
order.forecasted_issue = False
|
||||
if order.product_id:
|
||||
virtual_available = order.product_id.with_context(warehouse=warehouse.id, to_date=order.date_planned_start).virtual_available
|
||||
if order.state == 'draft':
|
||||
virtual_available += order.product_uom_qty
|
||||
if virtual_available < 0:
|
||||
order.forecasted_issue = True
|
||||
|
||||
@api.model
|
||||
def _search_delay_alert_date(self, operator, value):
|
||||
late_stock_moves = self.env['stock.move'].search([('delay_alert_date', operator, value)])
|
||||
@@ -777,6 +790,19 @@ class MrpProduction(models.Model):
|
||||
self.is_locked = not self.is_locked
|
||||
return True
|
||||
|
||||
def action_product_forecast_report(self):
|
||||
self.ensure_one()
|
||||
action = self.product_id.action_product_forecast_report()
|
||||
action['context'] = {
|
||||
'active_id': self.product_id.id,
|
||||
'active_model': 'product.product',
|
||||
'move_to_match_ids': self.move_finished_ids.filtered(lambda m: m.product_id == self.product_id).ids
|
||||
}
|
||||
warehouse = self.picking_type_id.warehouse_id
|
||||
if warehouse:
|
||||
action['context']['warehouse'] = warehouse.id
|
||||
return action
|
||||
|
||||
def _create_workorder(self):
|
||||
for production in self:
|
||||
if not production.bom_id:
|
||||
|
||||
@@ -151,3 +151,30 @@ class TestSaleStockReports(TestReportsCommon):
|
||||
# Checks the forecast report.
|
||||
report_values, docs, lines = self.get_report_forecast(product_template_ids=product_apple_pie.product_tmpl_id.ids)
|
||||
self.assertEqual(len(lines), 0, "Must have no line")
|
||||
|
||||
def test_report_forecast_3_report_line_corresponding_to_mo_highlighted(self):
|
||||
""" When accessing the report from a MO, checks if the correct MO is highlighted in the report
|
||||
"""
|
||||
product_banana = self.env['product.product'].create({
|
||||
'name': 'Banana',
|
||||
'type': 'product',
|
||||
})
|
||||
|
||||
# We create 2 identical MO
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = product_banana
|
||||
mo_form.product_qty = 10
|
||||
mo_1 = mo_form.save()
|
||||
mo_1.button_plan()
|
||||
mo_2 = mo_1.copy()
|
||||
mo_2.button_plan()
|
||||
|
||||
# Check for both MO if the highlight (is_matched) corresponds to the correct MO
|
||||
for mo in [mo_1, mo_2]:
|
||||
context = mo.action_product_forecast_report()['context']
|
||||
_, _, lines = self.get_report_forecast(product_template_ids=product_banana.product_tmpl_id.ids, context=context)
|
||||
for line in lines:
|
||||
if line['document_in'] == mo:
|
||||
self.assertTrue(line['is_matched'], "The corresponding MO line should be matched in the forecast report.")
|
||||
else:
|
||||
self.assertFalse(line['is_matched'], "A line of the forecast report not linked to the MO shoud not be matched.")
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
<field name="product_tracking" invisible="1"/>
|
||||
<field name="product_id" context="{'default_type': 'product'}" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
|
||||
<field name="product_tmpl_id" invisible="1"/>
|
||||
<field name="forecasted_issue" invisible="1"/>
|
||||
<field name="product_description_variants" attrs="{'invisible': [('product_description_variants', 'in', (False, ''))], 'readonly': [('state', '!=', 'draft')]}"/>
|
||||
<label for="product_qty" string="Quantity"/>
|
||||
<div class="o_row no-gutters d-flex">
|
||||
@@ -159,6 +160,8 @@
|
||||
<field name="product_uom_category_id" invisible="1"/>
|
||||
<field name="product_uom_id" options="{'no_open': True, 'no_create': True}" force_save="1" groups="uom.group_uom" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
|
||||
<span class='text-bf'>To Produce</span>
|
||||
<button type="object" name="action_product_forecast_report" icon="fa-area-chart" attrs="{'invisible': [('forecasted_issue', '=', True)]}"/>
|
||||
<button type="object" name="action_product_forecast_report" icon="fa-area-chart" attrs="{'invisible': [('forecasted_issue', '=', False)]}" class="text-danger"/>
|
||||
</div>
|
||||
<label for="lot_producing_id" attrs="{'invisible': ['|', ('state', '=', 'draft'), ('product_tracking', 'in', ('none', False))]}"/>
|
||||
<div class="o_row" attrs="{'invisible': ['|', ('state', '=', 'draft'), ('product_tracking', 'in', ('none', False))]}">
|
||||
|
||||
@@ -276,6 +276,7 @@ class PurchaseOrderLine(models.Model):
|
||||
move_dest_ids = fields.One2many('stock.move', 'created_purchase_line_id', 'Downstream Moves')
|
||||
product_description_variants = fields.Char('Custom Description')
|
||||
propagate_cancel = fields.Boolean('Propagate cancellation', default=True)
|
||||
forecasted_issue = fields.Boolean(compute='_compute_forecasted_issue')
|
||||
|
||||
def _compute_qty_received_method(self):
|
||||
super(PurchaseOrderLine, self)._compute_qty_received_method()
|
||||
@@ -316,6 +317,18 @@ class PurchaseOrderLine(models.Model):
|
||||
line._track_qty_received(total)
|
||||
line.qty_received = total
|
||||
|
||||
@api.depends('product_uom_qty', 'date_planned')
|
||||
def _compute_forecasted_issue(self):
|
||||
for line in self:
|
||||
warehouse = line.order_id.picking_type_id.warehouse_id
|
||||
line.forecasted_issue = False
|
||||
if line.product_id:
|
||||
virtual_available = line.product_id.with_context(warehouse=warehouse.id, to_date=line.date_planned).virtual_available
|
||||
if line.state == 'draft':
|
||||
virtual_available += line.product_uom_qty
|
||||
if virtual_available < 0:
|
||||
line.forecasted_issue = True
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
lines = super(PurchaseOrderLine, self).create(vals_list)
|
||||
@@ -333,6 +346,20 @@ class PurchaseOrderLine(models.Model):
|
||||
self.filtered(lambda l: l.order_id.state == 'purchase')._create_or_update_picking()
|
||||
return result
|
||||
|
||||
def action_product_forecast_report(self):
|
||||
self.ensure_one()
|
||||
action = self.product_id.action_product_forecast_report()
|
||||
action['context'] = {
|
||||
'active_id': self.product_id.id,
|
||||
'active_model': 'product.product',
|
||||
'move_to_match_ids': self.move_ids.filtered(lambda m: m.product_id == self.product_id).ids,
|
||||
'purchase_line_to_match_id': self.id,
|
||||
}
|
||||
warehouse = self.order_id.picking_type_id.warehouse_id
|
||||
if warehouse:
|
||||
action['context']['warehouse'] = warehouse.id
|
||||
return action
|
||||
|
||||
# --------------------------------------------------
|
||||
# Business methods
|
||||
# --------------------------------------------------
|
||||
|
||||
@@ -22,6 +22,7 @@ class ReplenishmentReport(models.AbstractModel):
|
||||
in_sum = sum(quantities)
|
||||
res['draft_purchase_qty'] = in_sum
|
||||
res['draft_purchase_orders'] = po_lines.mapped("order_id").sorted(key=lambda po: po.name)
|
||||
res['draft_purchase_orders_matched'] = self.env.context.get('purchase_line_to_match_id') in po_lines.ids
|
||||
res['qty']['in'] += in_sum
|
||||
return res
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<odoo>
|
||||
<template id="purchase_report_product_product_replenishment" inherit_id="stock.report_product_product_replenishment">
|
||||
<xpath expr="//tr[@name='draft_picking_in']" position="after">
|
||||
<tr t-if="docs['draft_purchase_qty']" name="draft_po_in">
|
||||
<tr t-if="docs['draft_purchase_qty']" name="draft_po_in" t-attf-class="#{docs['draft_purchase_orders_matched'] and 'o_grid_match'}">
|
||||
<td colspan="2">Requests for quotation</td>
|
||||
<td t-esc="docs['draft_purchase_qty']" t-options="{'widget': 'float', 'precision': precision}" class="text-right"/>
|
||||
<td>
|
||||
|
||||
@@ -145,3 +145,27 @@ class TestPurchaseStockReports(TestReportsCommon):
|
||||
self.assertEqual(draft_picking_qty_in, 0)
|
||||
self.assertEqual(draft_purchase_qty, 0)
|
||||
self.assertEqual(pending_qty_in, 0)
|
||||
|
||||
def test_report_forecast_3_report_line_corresponding_to_po_line_highlighted(self):
|
||||
""" When accessing the report from a PO line, checks if the correct PO line is highlighted in the report
|
||||
"""
|
||||
# We create 2 identical PO
|
||||
po_form = Form(self.env['purchase.order'])
|
||||
po_form.partner_id = self.partner
|
||||
with po_form.order_line.new() as line:
|
||||
line.product_id = self.product
|
||||
line.product_qty = 5
|
||||
po1 = po_form.save()
|
||||
po1.button_confirm()
|
||||
po2 = po1.copy()
|
||||
po2.button_confirm()
|
||||
|
||||
# Check for both PO if the highlight (is_matched) corresponds to the correct PO
|
||||
for po in [po1, po2]:
|
||||
context = po.order_line[0].action_product_forecast_report()['context']
|
||||
_, _, lines = self.get_report_forecast(product_template_ids=self.product_template.ids, context=context)
|
||||
for line in lines:
|
||||
if line['document_in'] == po:
|
||||
self.assertTrue(line['is_matched'], "The corresponding PO line should be matched in the forecast report.")
|
||||
else:
|
||||
self.assertFalse(line['is_matched'], "A line of the forecast report not linked to the PO shoud not be matched.")
|
||||
|
||||
@@ -27,6 +27,11 @@
|
||||
<xpath expr="//field[@name='order_line']/tree//field[@name='date_planned']" position="after">
|
||||
<field name="move_dest_ids" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//page/field[@name='order_line']/tree/field[@name='product_qty']" position="after">
|
||||
<field name="forecasted_issue" invisible="1"/>
|
||||
<button type="object" name="action_product_forecast_report" icon="fa-area-chart" attrs="{'invisible': ['|', ('forecasted_issue', '=', False), ('product_type', '!=', 'product')]}" class="text-danger"/>
|
||||
<button type="object" name="action_product_forecast_report" icon="fa-area-chart" attrs="{'invisible': ['|', ('forecasted_issue', '=', True), ('product_type', '!=', 'product')]}"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='date_planned_div']" position="inside">
|
||||
<button name="%(action_purchase_vendor_delay_report)d" class="oe_link" type="action" context="{'search_default_partner_id': partner_id}" attrs="{'invisible': ['|', ('state', 'in', ['purchase', 'done']), ('partner_id', '=', False)]}">
|
||||
<span attrs="{'invisible': [('on_time_rate', '<', 0)]}"><field name="on_time_rate" widget="integer" class="oe_inline"/>% On-Time Delivery</span>
|
||||
|
||||
@@ -18,6 +18,7 @@ class ReplenishmentReport(models.AbstractModel):
|
||||
out_sum = sum(quantities)
|
||||
res['draft_sale_qty'] = out_sum
|
||||
res['draft_sale_orders'] = so_lines.mapped("order_id").sorted(key=lambda so: so.name)
|
||||
res['draft_sale_orders_matched'] = self.env.context.get('sale_line_to_match_id') in so_lines.ids
|
||||
res['qty']['out'] += out_sum
|
||||
return res
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<odoo>
|
||||
<template id="sale_report_product_product_replenishment" inherit_id="stock.report_product_product_replenishment">
|
||||
<xpath expr="//tr[@name='draft_picking_out']" position="after">
|
||||
<tr t-if="docs['draft_sale_qty']" name="draft_so_out">
|
||||
<tr t-if="docs['draft_sale_qty']" name="draft_so_out" t-attf-class="#{docs['draft_sale_orders_matched'] and 'o_grid_match'}">
|
||||
<td colspan="2">Quotations</td>
|
||||
<td t-esc="-docs['draft_sale_qty']" t-options="{'widget': 'float', 'precision': precision}" class="text-right"/>
|
||||
<td>
|
||||
|
||||
@@ -80,7 +80,9 @@ var QtyAtDateWidget = Widget.extend({
|
||||
action.context = {
|
||||
active_model: 'product.product',
|
||||
active_id: this.data.product_id.data.id,
|
||||
warehouse: this.data.warehouse_id && this.data.warehouse_id.res_id
|
||||
warehouse: this.data.warehouse_id && this.data.warehouse_id.res_id,
|
||||
move_to_match_ids: this.data.move_ids.res_ids,
|
||||
sale_line_to_match_id: this.data.id,
|
||||
};
|
||||
return this.do_action(action);
|
||||
},
|
||||
|
||||
@@ -58,3 +58,27 @@ class TestSaleStockReports(TestReportsCommon):
|
||||
self.assertEqual(line_2['quantity'], 5)
|
||||
self.assertEqual(line_2['replenishment_filled'], False)
|
||||
self.assertEqual(line_2['document_out'].id, so_1.id)
|
||||
|
||||
def test_report_forecast_2_report_line_corresponding_to_so_line_highlighted(self):
|
||||
""" When accessing the report from a SO line, checks if the correct SO line is highlighted in the report
|
||||
"""
|
||||
# We create 2 identical SO
|
||||
so_form = Form(self.env['sale.order'])
|
||||
so_form.partner_id = self.partner
|
||||
with so_form.order_line.new() as line:
|
||||
line.product_id = self.product
|
||||
line.product_uom_qty = 5
|
||||
so1 = so_form.save()
|
||||
so1.action_confirm()
|
||||
so2 = so1.copy()
|
||||
so2.action_confirm()
|
||||
|
||||
# Check for both SO if the highlight (is_matched) corresponds to the correct SO
|
||||
for so in [so1, so2]:
|
||||
context = {"move_to_match_ids": so.order_line.move_ids.ids}
|
||||
_, _, lines = self.get_report_forecast(product_template_ids=self.product_template.ids, context=context)
|
||||
for line in lines:
|
||||
if line['document_out'] == so:
|
||||
self.assertTrue(line['is_matched'], "The corresponding SO line should be matched in the forecast report.")
|
||||
else:
|
||||
self.assertFalse(line['is_matched'], "A line of the forecast report not linked to the SO shoud not be matched.")
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
<field name="scheduled_date" invisible="1"/>
|
||||
<field name="forecast_expected_date" invisible="1"/>
|
||||
<field name="warehouse_id" invisible="1"/>
|
||||
<field name="move_ids" invisible="1"/>
|
||||
<field name="qty_to_deliver" invisible="1"/>
|
||||
<field name="is_mto" invisible="1"/>
|
||||
<field name="display_qty_widget" invisible="1"/>
|
||||
|
||||
@@ -417,6 +417,8 @@ class StockMove(models.Model):
|
||||
outgoing_unreserved_moves_per_warehouse[warehouse_by_location[move.location_id]] |= move
|
||||
elif move.picking_type_id.code in self._consuming_picking_types():
|
||||
move.forecast_availability = move.reserved_availability
|
||||
elif move.picking_type_id.code == 'incoming':
|
||||
move._get_forecast_availability_incoming()
|
||||
|
||||
for warehouse, moves in outgoing_unreserved_moves_per_warehouse.items():
|
||||
if not warehouse: # No prediction possible if no warehouse.
|
||||
@@ -660,8 +662,18 @@ class StockMove(models.Model):
|
||||
def action_product_forecast_report(self):
|
||||
self.ensure_one()
|
||||
action = self.product_id.action_product_forecast_report()
|
||||
warehouse = self.location_id.get_warehouse()
|
||||
action['context'] = {'warehouse': warehouse.id, } if warehouse else {}
|
||||
action['context'] = {
|
||||
'active_id': self.product_id.id,
|
||||
'active_model': 'product.product',
|
||||
'move_to_match_ids': self.ids,
|
||||
}
|
||||
if self.picking_type_id.code == 'outgoing':
|
||||
warehouse = self.location_id.get_warehouse()
|
||||
else:
|
||||
warehouse = self.location_dest_id.get_warehouse()
|
||||
|
||||
if warehouse:
|
||||
action['context']['warehouse'] = warehouse.id
|
||||
return action
|
||||
|
||||
def _do_unreserve(self):
|
||||
@@ -864,6 +876,13 @@ class StockMove(models.Model):
|
||||
else:
|
||||
return moves_todo[-1:].state or 'draft'
|
||||
|
||||
def _get_forecast_availability_incoming(self):
|
||||
self.ensure_one()
|
||||
warehouse = self.location_dest_id.get_warehouse()
|
||||
self.forecast_availability = self.product_id.with_context(warehouse=warehouse.id, to_date=self.date).virtual_available
|
||||
if self.state == 'draft':
|
||||
self.forecast_availability += self.product_uom_qty
|
||||
|
||||
@api.onchange('product_id', 'picking_type_id')
|
||||
def _onchange_product_id(self):
|
||||
product = self.product_id.with_context(lang=self._get_lang())
|
||||
|
||||
@@ -110,6 +110,11 @@ class ReplenishmentReport(models.AbstractModel):
|
||||
timezone = self._context.get('tz')
|
||||
product = product or (move_out.product_id if move_out else move_in.product_id)
|
||||
is_late = move_out.date < move_in.date if (move_out and move_in) else False
|
||||
|
||||
move_to_match_ids = self.env.context.get('move_to_match_ids') or []
|
||||
move_in_id = move_in.id if move_in else None
|
||||
move_out_id = move_out.id if move_out else None
|
||||
|
||||
return {
|
||||
'document_in': move_in._get_source_document() if move_in else False,
|
||||
'document_out': move_out._get_source_document() if move_out else False,
|
||||
@@ -126,6 +131,7 @@ class ReplenishmentReport(models.AbstractModel):
|
||||
'move_out': move_out,
|
||||
'move_in': move_in,
|
||||
'reservation': reservation,
|
||||
'is_matched': any(move_id in [move_in_id, move_out_id] for move_id in move_to_match_ids),
|
||||
}
|
||||
|
||||
def _get_report_lines(self, product_template_ids, product_variant_ids, wh_location_ids):
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
<td/>
|
||||
<td/>
|
||||
</tr>
|
||||
<tr t-foreach="docs['lines']" t-as="line">
|
||||
<tr t-foreach="docs['lines']" t-as="line" t-attf-class="#{line['is_matched'] and 'o_grid_match'}">
|
||||
<td t-attf-class="#{line['is_late'] and 'o_grid_warning'}">
|
||||
<a t-if="line['document_in']"
|
||||
t-attf-href="#" t-esc="line['document_in'].name"
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
.o_grid_warning {
|
||||
background-color: #f4cccc;
|
||||
}
|
||||
.o_grid_match {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.o_forecasted_row {
|
||||
|
||||
@@ -1041,3 +1041,53 @@ class TestReports(TestReportsCommon):
|
||||
delivery2_line = [l for l in lines if l['document_out'].id == delivery2.id][0]
|
||||
self.assertTrue(delivery2_line, 'No line for delivery 2')
|
||||
self.assertTrue(delivery2_line['replenishment_filled'])
|
||||
|
||||
def test_report_forecast_10_report_line_corresponding_to_picking_highlighted(self):
|
||||
""" When accessing the report from a stock move, checks if the correct picking is highlighted in the report
|
||||
and if the forecasted availability for incoming moves is correct
|
||||
"""
|
||||
# Creation of one delivery with date 'today'
|
||||
delivery_form = Form(self.env['stock.picking'])
|
||||
delivery_form.partner_id = self.partner
|
||||
delivery_form.picking_type_id = self.picking_type_out
|
||||
delivery_form.scheduled_date = date.today()
|
||||
with delivery_form.move_ids_without_package.new() as move:
|
||||
move.product_id = self.product
|
||||
move.product_uom_qty = 200
|
||||
delivery1 = delivery_form.save()
|
||||
delivery1.action_confirm()
|
||||
|
||||
# Creation of one receipt with date 'today + 1' and smaller qty than the delivery
|
||||
receipt_form = Form(self.env['stock.picking'])
|
||||
receipt_form.partner_id = self.partner
|
||||
receipt_form.picking_type_id = self.picking_type_in
|
||||
receipt_form.scheduled_date = date.today() + timedelta(days=1)
|
||||
with receipt_form.move_ids_without_package.new() as move:
|
||||
move.product_id = self.product
|
||||
move.product_uom_qty = 150
|
||||
receipt1 = receipt_form.save()
|
||||
receipt1.action_confirm()
|
||||
self.assertEqual(receipt1.move_lines.forecast_availability, -50.0)
|
||||
|
||||
# Creation of an identical receipt which should lead to a positive forecast availability
|
||||
receipt2 = receipt1.copy()
|
||||
receipt2.action_confirm()
|
||||
for move in receipt2.move_lines:
|
||||
move.quantity_done = 150
|
||||
receipt2.button_validate()
|
||||
self.assertEqual(receipt1.move_lines.forecast_availability, 100.0)
|
||||
|
||||
delivery2 = delivery1.copy()
|
||||
delivery2.action_confirm()
|
||||
receipt1.move_lines._compute_forecast_information()
|
||||
self.assertEqual(receipt1.move_lines.forecast_availability, -100.0)
|
||||
|
||||
# Check for both deliveries and receipts if the highlight (is_matched) corresponds to the correct picking
|
||||
for picking in [delivery1, delivery2, receipt1, receipt2]:
|
||||
context = picking.move_lines[0].action_product_forecast_report()['context']
|
||||
_, _, lines = self.get_report_forecast(product_template_ids=self.product_template.ids, context=context)
|
||||
for line in lines:
|
||||
if picking in [line['document_in'], line['document_out']]:
|
||||
self.assertTrue(line['is_matched'], "The corresponding picking should be matched in the forecast report.")
|
||||
else:
|
||||
self.assertFalse(line['is_matched'], "A line of the forecast report not linked to the picking shoud not be matched.")
|
||||
|
||||
@@ -368,7 +368,7 @@
|
||||
<field name="date_deadline" optional="hide"/>
|
||||
<field name="is_initial_demand_editable" invisible="1"/>
|
||||
<field name="is_quantity_done_editable" invisible="1"/>
|
||||
<field name="product_uom_qty" string="Demand" attrs="{'column_invisible': [('parent.immediate_transfer', '=', True)], 'readonly': ['|', ('is_initial_demand_editable', '=', False), '&', '&', ('show_operations', '=', True), ('is_locked', '=', True), ('is_initial_demand_editable', '=', False)]}"/>
|
||||
<field name="product_uom_qty" string="Demand" attrs="{'column_invisible': [('parent.immediate_transfer', '=', True)], 'readonly': ['|', ('is_initial_demand_editable', '=', False), '&', '&', ('show_operations', '=', True), ('is_locked', '=', True), ('is_initial_demand_editable', '=', False)]}" widget="forecast_widget"/>
|
||||
<field name="reserved_availability" string="Reserved" attrs="{'column_invisible': (['|','|', ('parent.state','=', 'done'), ('parent.picking_type_code', 'in', ['incoming', 'outgoing']), ('parent.immediate_transfer', '=', True)])}"/>
|
||||
<field name="product_qty" invisible="1" readonly="1"/>
|
||||
<field name="forecast_expected_date" invisible="1" />
|
||||
|
||||
Reference in New Issue
Block a user