[REF] stock: counted qty, forecast, json stock widgets to owl

This pr follow the framework refactoring and translate the widgets
of stock module to wowl.

CountedQuantityWidgetField needs to add an upper listener on input since
the function onChange and onInput of `useInputField` are not overidable.

Json fields had their template in the json send from the server. With
wowl, the template needs to be declare before the setup. So a new wiget
is create for each template and those widgets extends the
a common component that put the json content in the props.

co-author svs-odoo <svs@odoo.com>

X-original-commit: 350a7a0d6f4816ee75676b80a9a5b9769bf80fe6
Part-of: odoo/odoo#101305
This commit is contained in:
Arnold Moyaux
2022-10-04 17:01:21 +02:00
parent 84f6010fc1
commit 989c6df74d
12 changed files with 224 additions and 197 deletions
+4 -5
View File
@@ -519,13 +519,12 @@ class Picking(models.Model):
picking_no_alert.json_popover = False
for picking in (self - picking_no_alert):
picking.json_popover = json.dumps({
'popoverTemplate': 'stock.PopoverStockRescheduling',
'delay_alert_date': format_datetime(self.env, picking.delay_alert_date, dt_format=False),
'late_elements': [{
'id': late_move.id,
'name': late_move.display_name,
'model': late_move._name,
} for late_move in picking.move_ids.filtered(lambda m: m.delay_alert_date).move_orig_ids._delay_alert_get_documents()
'id': late_move.id,
'name': late_move.display_name,
'model': late_move._name,
} for late_move in picking.move_ids.filtered(lambda m: m.delay_alert_date).move_orig_ids._delay_alert_get_documents()
]
})
@@ -1,36 +0,0 @@
/** @odoo-module alias=stock.counted_quantity_widget **/
import BasicFields from 'web.basic_fields';
import fieldRegistry from 'web.field_registry';
const CountedQuantityWidgetField = BasicFields.FieldFloat.extend({
supportedFieldTypes: ['float'],
_renderReadonly: function () {
if (this.recordData.inventory_quantity_set) {
this.el.textContent = this._formatValue(this.recordData.inventory_quantity);
} else {
this.el.textContent = "";
}
},
_onChange: function () {
if (!this.recordData.inventory_quantity_set) {
this.recordData.inventory_quantity_set = true;
}
this._super.apply(this);
},
_isSameValue: function(value) {
// We want to trigger the update of the view when inserting 0
if (value == 0) {
return false;
}
return this._super(...arguments);
}
});
fieldRegistry.add('counted_quantity_widget', CountedQuantityWidgetField);
export default CountedQuantityWidgetField;
@@ -1,90 +0,0 @@
odoo.define('stock.forecast_widget', function (require) {
'use strict';
const AbstractField = require('web.AbstractField');
const fieldRegistry = require('web.field_registry');
const field_utils = require('web.field_utils');
const utils = require('web.utils');
const core = require('web.core');
const QWeb = core.qweb;
const ForecastWidgetField = AbstractField.extend({
supportedFieldTypes: ['float'],
_render: function () {
var data = Object.assign({}, this.record.data, {
forecast_availability_str: field_utils.format.float(
this.record.data.forecast_availability,
this.record.fields.forecast_availability,
this.nodeOptions
),
reserved_availability_str: field_utils.format.float(
this.record.data.reserved_availability,
this.record.fields.reserved_availability,
this.nodeOptions
),
forecast_expected_date_str: field_utils.format.date(
this.record.data.forecast_expected_date,
this.record.fields.forecast_expected_date
),
});
if (data.forecast_expected_date && data.date_deadline) {
data.forecast_is_late = data.forecast_expected_date > data.date_deadline;
}
data.will_be_fulfilled = utils.round_decimals(data.forecast_availability, this.record.fields.forecast_availability.digits[1]) >= utils.round_decimals(data.product_qty, this.record.fields.forecast_availability.digits[1]);
this.$el.html(QWeb.render('stock.forecastWidget', data));
this.$('.o_forecast_report_button').on('click', this._onOpenReport.bind(this));
},
isSet: function () {
return true;
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Opens the Forecast Report for the `stock.move` product.
*
* @param {MouseEvent} ev
*/
_onOpenReport: function (ev) {
ev.preventDefault();
ev.stopPropagation();
if (!this.recordData.id) {
return;
}
this._rpc({
model: 'stock.move',
method: 'action_product_forecast_report',
args: [this.recordData.id],
}).then(action => {
action.context = Object.assign(action.context || {}, {
active_model: 'product.product',
active_id: this.recordData.product_id.res_id,
});
this.do_action(action);
});
},
});
const JsonWidget = AbstractField.extend({
supportedFieldTypes: ['char'],
_render: function () {
var value = JSON.parse(this.value);
if (!value || !value.template) {
this.$el.html('');
return;
}
$(QWeb.render(value.template, value)).appendTo(this.$el);
},
});
fieldRegistry.add('forecast_widget', ForecastWidgetField);
fieldRegistry.add('json_widget', JsonWidget);
return ForecastWidgetField;
});
@@ -0,0 +1,44 @@
/** @odoo-module **/
import { FloatField } from "@web/views/fields/float/float_field";
import { registry } from "@web/core/registry";
const { useEffect, useRef } = owl;
export class CountedQuantityWidgetField extends FloatField {
setup() {
// Need to adapt useInputField to overide onInput and onChange
super.setup();
const inputRef = useRef("numpadDecimal");
useEffect(
(inputEl) => {
if (inputEl) {
inputEl.addEventListener("input", this.onInput.bind(this));
return () => {
inputEl.removeEventListener("input", this.onInput.bind(this));
};
}
},
() => [inputRef.el]
);
}
onInput(ev) {
this.props.setDirty(true);
return this.props.record.update({ inventory_quantity_set: true });
}
get formattedValue() {
if (
this.props.readonly &&
!this.props.value & !this.props.record.data.inventory_quantity_set
) {
return "";
}
return super.formattedValue;
}
}
registry.category("fields").add("counted_quantity_widget", CountedQuantityWidgetField);
@@ -0,0 +1,52 @@
/** @odoo-module */
import { FloatField } from "@web/views/fields/float/float_field";
import { formatDate } from "@web/core/l10n/dates";
import { formatFloat } from "@web/views/fields/formatters";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
export class ForecastWidgetField extends FloatField {
setup() {
const { data, fields } = this.props.record;
this.actionService = useService("action");
this.orm = useService("orm");
this.resId = data.id;
this.reservedAvailability = formatFloat(data.reserved_availability, {
...fields.reserved_availability,
...this.nodeOptions,
});
this.forecastExpectedDate = formatDate(
data.forecast_expected_date,
fields.forecast_expected_date
);
if (data.forecast_expected_date && data.date_deadline) {
this.forecastIsLate = data.forecast_expected_date > data.date_deadline;
}
this.willBeFulfilled = data.forecast_availability >= data.product_qty;
this.state = data.state;
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Opens the Forecast Report for the `stock.move` product.
*/
async _openReport(ev) {
ev.preventDefault();
ev.stopPropagation();
if (!this.resId) {
return;
}
const action = await this.orm.call("stock.move", "action_product_forecast_report", [
this.resId,
]);
this.actionService.doAction(action);
}
}
ForecastWidgetField.template = "stock.ForecastWidget";
registry.category("fields").add("forecast_widget", ForecastWidgetField);
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<t t-name="stock.ForecastWidget" owl="1">
<span t-if="['draft', 'partially_available', 'assigned', 'cancel', 'done'].includes(state)"
t-out="reservedAvailability"/>
<span t-elif="!forecastExpectedDate and willBeFulfilled" class="text-success">
Available
</span>
<span t-elif="forecastExpectedDate and willBeFulfilled"
t-att-class="forecastIsLate ? 'text-danger' : 'text-warning'">
Exp <t t-out="forecastExpectedDate"/>
</span>
<span t-else="" class="text-danger">Not Available</span>
<button t-if="props.record.data.product_type == 'product'" title="Forecasted Report"
t-on-click="_openReport" t-att="resId ? {} : {'disabled': ''}"
class="o_forecast_report_button btn btn-link o_icon_button ms-2">
<i class="fa fa-fw fa-area-chart"
t-att-class="state != 'draft' and (!willBeFulfilled or forecastIsLate) ? 'text-danger' : ''"/>
</button>
</t>
</templates>
@@ -0,0 +1,53 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import { _lt } from "@web/core/l10n/translation";
import { useService } from "@web/core/utils/hooks";
const { Component, onWillStart } = owl;
export class JsonPpopOver extends Component {
setup() {
this.props = { ...this.props, ...JSON.parse(this.props.value) };
}
}
JsonPpopOver.displayName = _lt("Json Popup");
JsonPpopOver.supportedTypes = ["char"];
export class PopOverLeadDays extends JsonPpopOver {
setup() {
super.setup();
const user = useService("user");
onWillStart(async () => {
this.displayUOM = await user.hasGroup("uom.group_uom");
});
}
get qtyForecast() {
return this._formatQty("qty_forecast");
}
get qtyToOrder() {
return this._formatQty("qty_to_order");
}
get productMaxQty() {
return this._formatQty("product_max_qty");
}
get productMinQty() {
return this._formatQty("product_min_qty");
}
_formatQty(field) {
return this.displayUOM
? `${this.props[field]} ${this.props.product_uom_name}`
: this.props[field];
}
}
PopOverLeadDays.template = "stock.leadDaysPopOver";
export class ReplenishmentHistoryWidget extends JsonPpopOver {}
ReplenishmentHistoryWidget.template = "stock.replenishmentHistory";
registry.category("fields").add("lead_days_widget", PopOverLeadDays);
registry.category("fields").add("replenishment_history_widget", ReplenishmentHistoryWidget);
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<div t-name="stock.leadDaysPopOver" owl="1">
<h2>Lead Times</h2>
<hr/>
<p>
The forecasted stock on the <b t-esc="props.lead_days_date"/>
is <t t-if="props.qty_to_order &lt;= 0"><b><t t-esc="qtyForecast"/></b></t><t t-else="">
below the inventory <b>minimum of <t t-esc="productMinQty"/>
: <t t-esc="qtyToOrder"/> should be replenished</b> to reach the maximum of
<t t-esc="productMaxQty"/>.</t>
</p>
<table t-if="props.lead_days_description" class="table table-borderless table-sm">
<tbody>
<tr>
<td>Today</td>
<td class="text-end" t-out="props.today"/>
</tr>
<tr t-foreach="props.lead_days_description" t-key="descr" t-as="descr">
<td t-out="descr[0]"/>
<td class="text-end" t-out="descr[1]"/>
</tr>
<tr class="table-info">
<td>Forecasted Date</td>
<td class="text-end text-nowrap">
= <t t-out="props.lead_days_date"/>
</td>
</tr>
</tbody>
</table>
</div>
<div t-name="stock.replenishmentHistory" owl="1">
<h2>Sales History</h2>
<hr/>
<table t-if="props.replenishment_history" class="table table-borderless">
<tbody>
<t t-foreach="props.replenishment_history" t-key="line" t-as="line">
<tr>
<td><t t-esc="line.name"/></td>
<td class="text-end text-nowrap" t-esc="line.quantity + ' ' + line.uom_name"/>
</tr>
</t>
</tbody>
</table>
</div>
</templates>
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<t t-name="stock.forecastWidget">
<span t-if="['draft', 'partially_available', 'assigned', 'cancel', 'done'].includes(state)" t-esc="reserved_availability_str"/>
<span t-elif="!forecast_expected_date_str and will_be_fulfilled" class="text-success">Available</span>
<span t-elif="forecast_expected_date_str and will_be_fulfilled" t-att-class="forecast_is_late ? 'text-danger' : 'text-warning'">Exp <t t-esc="forecast_expected_date_str"/></span>
<span t-else="" class="text-danger">Not Available</span>
<button t-if="product_type == 'product'" t-att="id ? {} : {'disabled': ''}" class="o_forecast_report_button btn btn-link o_icon_button ms-2" title="Forecasted Report">
<i t-attf-class="fa fa-fw fa-area-chart {{ state != 'draft' and (!will_be_fulfilled or forecast_is_late) ? 'text-danger' : '' }}"/>
</button>
</t>
</templates>
@@ -1,41 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<div t-name="stock.leadDaysPopOver">
<h2>Lead Times</h2>
<hr/>
<p>
The forecasted stock on the <b><t t-esc="lead_days_date"/></b>
is <t t-if="qty_to_order &lt;= 0"><b><t t-esc="qty_forecast"/> <t t-esc="product_uom_name"/></b></t><t t-else="">
below the inventory <b>minimum of <t t-esc="product_min_qty"/> <t t-esc="product_uom_name"/>
: <t t-esc="qty_to_order"/> <t t-esc="product_uom_name"/> should be replenished</b> to reach the maximum of
<t t-esc="product_max_qty"/> <t t-esc="product_uom_name"/>.</t>
</p>
<table t-if="lead_days_description" class="table table-borderless table-sm">
<tbody>
<tr>
<td>
Today
</td>
<td class="text-end">
<t t-out="today"/>
</td>
</tr>
<tr t-foreach="lead_days_description" t-as="descr">
<td><t t-out="descr[0]"/></td>
<td class="text-end"><t t-out="descr[1]"/></td>
</tr>
<tr class="table-info">
<td>
Forecasted Date
</td>
<td class="text-end text-nowrap">
= <t t-out="lead_days_date"/>
</td>
</tr>
</tbody>
</table>
</div>
<t t-name="StockOrderpoint.Buttons">
<span>
<button type="button" class="btn d-none btn-primary o_button_order">
@@ -46,19 +10,4 @@
</button>
</span>
</t>
<div t-name="stock.replenishmentHistory">
<h2>Sales History</h2>
<hr/>
<table t-if="replenishment_history" class="table table-borderless">
<tbody>
<t t-foreach="replenishment_history" t-as="line">
<tr>
<td><t t-esc="line.name"/></td>
<td class="text-end text-nowrap" t-esc="line.quantity + ' ' + line.uom_name"/>
</tr>
</t>
</tbody>
</table>
</div>
</templates>
@@ -42,7 +42,6 @@ class StockReplenishmentInfo(models.TransientModel):
dummy, lead_days_description = orderpoint.rule_ids._get_lead_days(
orderpoint.product_id, **orderpoints_values)
replenishment_report.json_lead_days = dumps({
'template': 'stock.leadDaysPopOver',
'lead_days_date': format_date(self.env, replenishment_report.orderpoint_id.lead_days_date),
'lead_days_description': lead_days_description,
'today': format_date(self.env, fields.Date.today()),
@@ -9,10 +9,10 @@
<field name="qty_to_order" invisible="1"/>
<div class="row ml8">
<group class="col-6">
<field nolabel="1" name="json_lead_days" widget="json_widget"/>
<field nolabel="1" name="json_lead_days" widget="lead_days_widget"/>
</group>
<group class="col-6">
<field nolabel="1" name="json_replenishment_history" widget="json_widget"/>
<field nolabel="1" name="json_replenishment_history" widget="replenishment_history_widget"/>
</group>
</div>
<notebook>