新增主生产计划模块
This commit is contained in:
205
mrp_mps/static/src/components/line.js
Normal file
205
mrp_mps/static/src/components/line.js
Normal file
@@ -0,0 +1,205 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { CheckBox } from "@web/core/checkbox/checkbox";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import fieldUtils from 'web.field_utils';
|
||||
|
||||
const { Component, useRef, onPatched, onWillStart } = owl;
|
||||
|
||||
export default class MpsLineComponent extends Component {
|
||||
|
||||
setup() {
|
||||
this.actionService = useService("action");
|
||||
this.dialogService = useService("dialog");
|
||||
this.orm = useService("orm");
|
||||
this.field_utils = fieldUtils;
|
||||
this.model = this.env.model;
|
||||
this.forecastRow = useRef("forecastRow");
|
||||
this.replenishRow = useRef("replenishRow");
|
||||
|
||||
onPatched(() => {
|
||||
// after a replenishment, switch to next column if possible
|
||||
const previousEl = this.replenishRow.el.getElementsByClassName('o_mrp_mps_hover')[0];
|
||||
if (previousEl) {
|
||||
previousEl.classList.remove('o_mrp_mps_hover');
|
||||
const el = this.replenishRow.el.getElementsByClassName('o_mrp_mps_forced_replenish')[0];
|
||||
if (el) {
|
||||
el.classList.add('o_mrp_mps_hover');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onWillStart(async () => {
|
||||
this.model.on('mouse-over', this, () => this._onMouseOverReplenish());
|
||||
this.model.on('mouse-out', this, () => this._onMouseOutReplenish());
|
||||
});
|
||||
}
|
||||
|
||||
get productionSchedule() {
|
||||
return this.props.data;
|
||||
}
|
||||
|
||||
get groups() {
|
||||
return this.props.groups;
|
||||
}
|
||||
|
||||
get formatFloat() {
|
||||
return this.field_utils.format.float;
|
||||
}
|
||||
|
||||
get isSelected() {
|
||||
return this.model.selectedRecords.has(this.productionSchedule.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the click on replenish button. It will call action_replenish with
|
||||
* all the Ids present in the view.
|
||||
* @private
|
||||
* @param {Integer} id mrp.production.schedule Id.
|
||||
*/
|
||||
_onClickReplenish(id) {
|
||||
this.model._actionReplenish([id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the click on product name. It will open the product form view
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickRecordLink(ev) {
|
||||
this.actionService.doAction({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: ev.currentTarget.dataset.model,
|
||||
res_id: Number(ev.currentTarget.dataset.resId),
|
||||
views: [[false, 'form']],
|
||||
target: 'current',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the click on `min..max` or 'targeted stock' Event. It will open
|
||||
* a form view in order to edit a production schedule and update the
|
||||
* template on save.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
* @param {Integer} id mrp.production.schedule Id.
|
||||
*/
|
||||
_onClickEdit(ev, id) {
|
||||
this.model._editProduct(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the click on unlink button. A dialog ask for a confirmation and
|
||||
* it will unlink the product.
|
||||
* @private
|
||||
* @param {Object} id mrp.production.schedule Id.
|
||||
*/
|
||||
_onClickUnlink(id) {
|
||||
this.model._unlinkProduct([id]);
|
||||
}
|
||||
|
||||
_onClickOpenDetails(ev) {
|
||||
const dateStart = ev.target.dataset.date_start;
|
||||
const dateStop = ev.target.dataset.date_stop;
|
||||
const dateStr = this.model.data.dates[ev.target.dataset.date_index];
|
||||
const action = ev.target.dataset.action;
|
||||
const productionScheduleId = Number(ev.target.closest('.o_mps_content').dataset.id);
|
||||
this.model._actionOpenDetails(productionScheduleId, action, dateStr, dateStart, dateStop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the change on a forecast cell.
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
* @param {Object} productionScheduleId mrp.production.schedule Id.
|
||||
*/
|
||||
_onChangeForecast(ev, productionScheduleId) {
|
||||
const dateIndex = parseInt(ev.target.dataset.date_index);
|
||||
const forecastQty = ev.target.value;
|
||||
if (forecastQty === "" || isNaN(forecastQty)) {
|
||||
ev.target.value = this.model._getOriginValue(productionScheduleId, dateIndex, 'forecast_qty');
|
||||
} else {
|
||||
this.model._saveForecast(productionScheduleId, dateIndex, forecastQty).then(() => {
|
||||
const inputSelector = 'input[data-date_index="' + (dateIndex + 1) + '"]';
|
||||
const nextInput = this.forecastRow.el.querySelector(inputSelector);
|
||||
if (nextInput) {
|
||||
nextInput.select();
|
||||
}
|
||||
}, () => {
|
||||
ev.target.value = this.model._getOriginValue(productionScheduleId, dateIndex, 'forecast_qty');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the quantity To Replenish change on a forecast cell.
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
* @param {Object} productionScheduleId mrp.production.schedule Id.
|
||||
*/
|
||||
_onChangeToReplenish(ev, productionScheduleId) {
|
||||
const dateIndex = parseInt(ev.target.dataset.date_index);
|
||||
const replenishQty = ev.target.value;
|
||||
if (replenishQty === "" || isNaN(replenishQty)) {
|
||||
ev.target.value = this.model._getOriginValue(productionScheduleId, dateIndex, 'replenish_qty');
|
||||
} else {
|
||||
this.model._saveToReplenish(productionScheduleId, dateIndex, replenishQty).then(() => {
|
||||
const inputSelector = 'input[data-date_index="' + (dateIndex + 1) + '"]';
|
||||
const nextInput = this.replenishRow.el.querySelector(inputSelector);
|
||||
if (nextInput) {
|
||||
nextInput.select();
|
||||
}
|
||||
}, () => {
|
||||
ev.target.value = this.model._getOriginValue(productionScheduleId, dateIndex, 'replenish_qty');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _onClickForecastReport() {
|
||||
const action = await this.orm.call(
|
||||
"product.product",
|
||||
"action_product_forecast_report",
|
||||
[[this.productionSchedule.id]],
|
||||
);
|
||||
action.context = {
|
||||
active_model: "product.product",
|
||||
active_id: this.productionSchedule.product_id[0],
|
||||
warehouse_id: this.productionSchedule.warehouse_id && this.productionSchedule.warehouse_id[0],
|
||||
};
|
||||
return this.actionService.doAction(action);
|
||||
}
|
||||
|
||||
_onClickAutomaticMode(ev, productionScheduleId) {
|
||||
const dateIndex = parseInt(ev.target.dataset.date_index);
|
||||
this.model._removeQtyToReplenish(productionScheduleId, dateIndex);
|
||||
}
|
||||
|
||||
_onFocusInput(ev) {
|
||||
ev.target.select();
|
||||
}
|
||||
|
||||
_onMouseOverReplenish(ev) {
|
||||
const el = this.replenishRow.el.getElementsByClassName('o_mrp_mps_forced_replenish')[0];
|
||||
if (el) {
|
||||
el.classList.add('o_mrp_mps_hover');
|
||||
}
|
||||
}
|
||||
|
||||
_onMouseOutReplenish(ev) {
|
||||
const el = this.replenishRow.el.getElementsByClassName('o_mrp_mps_hover')[0];
|
||||
if (el) {
|
||||
el.classList.remove('o_mrp_mps_hover');
|
||||
}
|
||||
}
|
||||
|
||||
toggleSelection(ev, productionScheduleId) {
|
||||
this.model.toggleRecordSelection(productionScheduleId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MpsLineComponent.template = 'mrp_mps.MpsLineComponent';
|
||||
MpsLineComponent.components = {
|
||||
CheckBox,
|
||||
};
|
||||
76
mrp_mps/static/src/components/line.scss
Normal file
76
mrp_mps/static/src/components/line.scss
Normal file
@@ -0,0 +1,76 @@
|
||||
.o_mrp_mps {
|
||||
|
||||
button {
|
||||
&.o_mrp_mps_procurement {
|
||||
background-color: $gray-200;
|
||||
border: none;
|
||||
}
|
||||
&.o_no_padding {
|
||||
padding: 0 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
input:not(.form-check-input) {
|
||||
border-style: groove;
|
||||
height: 100%;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
thead th {
|
||||
@include o-position-sticky($top: 0px, $left: 0px);
|
||||
z-index: 10;
|
||||
border-top: none;
|
||||
&:first-child {
|
||||
z-index: 11;
|
||||
height: 1em;
|
||||
}
|
||||
}
|
||||
tbody th {
|
||||
@include o-position-sticky($left: 0px);
|
||||
&:first-child,&:nth-child(2) {
|
||||
z-index: 1;
|
||||
background-color: $gray-200;
|
||||
}
|
||||
}
|
||||
th {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.o_mps_inline {
|
||||
display: inline-flex;
|
||||
max-width: 38%;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
line-height: 1em;
|
||||
min-height: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.btn {
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
padding: 0em 0em;
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.o_input {
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
.o_mrp_mps_hover {
|
||||
background-color: #d6d8d9;
|
||||
|
||||
&.alert-success {
|
||||
background-color: darken(#dff0d8, 10%);
|
||||
}
|
||||
|
||||
&.alert-warning {
|
||||
background-color: darken(#fcf8e3, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
184
mrp_mps/static/src/components/line.xml
Normal file
184
mrp_mps/static/src/components/line.xml
Normal file
@@ -0,0 +1,184 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mrp_mps.MpsLineComponent" owl="1">
|
||||
<tbody class="o_mps_content" t-att-data-id="productionSchedule.id" t-att-data-warehouse_id="'warehouse_id' in productionSchedule and productionSchedule.warehouse_id[0]">
|
||||
<tr class="bg-light">
|
||||
<th>
|
||||
<CheckBox value='isSelected' onChange.bind="(ev) => this.toggleSelection(ev, productionSchedule.id)"/>
|
||||
</th>
|
||||
<th scope="col">
|
||||
<a href="#" class="o_mrp_mps_record_url" t-att-data-res-id="productionSchedule.product_id[0]" t-att-data-model="'product.product'" t-on-click.prevent="_onClickRecordLink"><t t-esc="productionSchedule.product_id[1]"/></a>
|
||||
<span t-if="'product_uom_id' in productionSchedule" class="text-muted"> by <t t-esc="productionSchedule.product_uom_id[1]"/></span>
|
||||
<span t-if="'warehouse_id' in productionSchedule"> - <t t-esc="productionSchedule.warehouse_id[1]"/></span>
|
||||
<span> </span>
|
||||
<a href="#" role="button" title="Forecast Report" t-attf-class="fa fa-fw fa-area-chart" t-on-click.prevent="_onClickForecastReport"/>
|
||||
</th>
|
||||
<th/>
|
||||
<t t-foreach="productionSchedule.forecast_ids" t-as="forecast" t-key="forecast_index">
|
||||
<th class="text-end pe-4">
|
||||
<span t-attf-class="{{! groups.mrp_mps_show_starting_inventory and 'o_hidden' or 'text-end'}}" t-esc="formatFloat(forecast.starting_inventory_qty, false, {'digits': [false, productionSchedule.precision_digits]})" data-bs-toggle="tooltip" data-bs-placement="bottom" title="The forecasted quantity in stock at the beginning of the period."/>
|
||||
</th>
|
||||
</t>
|
||||
</tr>
|
||||
<tr name="demand_forecast_year_minus_2" t-attf-class="{{! groups.mrp_mps_show_actual_demand_year_minus_2 and 'o_hidden' or ''}}">
|
||||
<th/>
|
||||
<th scope="row">
|
||||
<span>Actual Demand Y-2</span>
|
||||
</th>
|
||||
<th/>
|
||||
<t t-foreach="productionSchedule.forecast_ids" t-as="forecast" t-key="forecast_index">
|
||||
<th class="text-end pe-4">
|
||||
<span t-esc="formatFloat(forecast.outgoing_qty_year_minus_2, false, {'digits': [false, productionSchedule.precision_digits]})"/>
|
||||
</th>
|
||||
</t>
|
||||
</tr>
|
||||
<tr name="demand_forecast_year_minus_1" t-attf-class="{{! groups.mrp_mps_show_actual_demand_year_minus_1 and 'o_hidden' or ''}}">
|
||||
<th/>
|
||||
<th scope="row">
|
||||
<span>Actual Demand Y-1</span>
|
||||
</th>
|
||||
<th></th>
|
||||
<t t-foreach="productionSchedule.forecast_ids" t-as="forecast" t-key="forecast_index">
|
||||
<th class="text-end pe-4">
|
||||
<span t-esc="formatFloat(forecast.outgoing_qty_year_minus_1, false, {'digits': [false, productionSchedule.precision_digits]})"/>
|
||||
</th>
|
||||
</t>
|
||||
</tr>
|
||||
<tr t-ref="forecastRow" name="demand_forecast" t-attf-class="{{! (groups.mrp_mps_show_demand_forecast or groups.mrp_mps_show_actual_demand) and 'o_hidden' or ''}}">
|
||||
<th/>
|
||||
<th scope="row">
|
||||
- <span t-attf-class="{{! groups.mrp_mps_show_actual_demand and 'o_hidden' or ''}}" data-bs-toggle="tooltip" data-bs-placement="bottom" title="The confirmed demand, based on the confirmed sales orders.">Actual</span>
|
||||
<span t-attf-class="{{! (groups.mrp_mps_show_actual_demand and groups.mrp_mps_show_demand_forecast) and 'o_hidden' or ''}}"> / </span>
|
||||
<span t-attf-class="{{! groups.mrp_mps_show_demand_forecast and 'o_hidden' or ''}}" data-bs-toggle="tooltip" data-bs-placement="bottom" title="The forecasted demand. This value has to be entered manually.">Forecasted Demand</span>
|
||||
</th>
|
||||
<th></th>
|
||||
<t t-foreach="productionSchedule.forecast_ids" t-as="forecast" t-key="forecast_index">
|
||||
<th class="text-end pe-4">
|
||||
<a href="#"
|
||||
name="actual_demand"
|
||||
t-on-click="_onClickOpenDetails"
|
||||
data-action="action_open_actual_demand_details"
|
||||
t-att-data-date_index="forecast_index"
|
||||
t-att-data-date_start="forecast.date_start"
|
||||
t-att-data-date_stop="forecast.date_stop"
|
||||
t-attf-class="{{! groups.mrp_mps_show_actual_demand and 'o_hidden' or 'o_mrp_mps_open_details'}}">
|
||||
<t t-esc="formatFloat(forecast.outgoing_qty, false, {'digits': [false, productionSchedule.precision_digits]})"/>
|
||||
</a>
|
||||
<span t-attf-class="{{! (groups.mrp_mps_show_actual_demand and groups.mrp_mps_show_demand_forecast) and 'o_hidden' or ''}}"> / </span>
|
||||
<input type="text"
|
||||
t-att-data-date_index="forecast_index"
|
||||
t-attf-class="text-end form-control o_mrp_mps_input_forcast_qty {{! groups.mrp_mps_show_demand_forecast and 'o_hidden' or groups.mrp_mps_show_actual_demand and 'o_mps_inline' or ''}}"
|
||||
t-att-value="formatFloat(forecast.forecast_qty, false, {'digits': [false, productionSchedule.precision_digits]})"
|
||||
t-on-change.stop="(ev) => this._onChangeForecast(ev, productionSchedule.id)"
|
||||
t-on-focus.prevent="_onFocusInput"/>
|
||||
</th>
|
||||
</t>
|
||||
</tr>
|
||||
<tr name="indirect_demand" t-attf-class="{{(! groups.mrp_mps_show_indirect_demand or ! productionSchedule.has_indirect_demand) and 'o_hidden' or ''}}">
|
||||
<th/>
|
||||
<th scope="row" data-bs-toggle="tooltip" data-bs-placement="bottom" title="The forecasted demand to fulfill the needs in components of the Manufacturing Orders.">
|
||||
- Indirect Demand Forecast
|
||||
</th>
|
||||
<th/>
|
||||
<t t-foreach="productionSchedule.forecast_ids" t-as="forecast" t-key="forecast_index">
|
||||
<th t-attf-class="text-end pe-4 {{forecast.indirect_demand_qty == 0 and 'text-muted' or ''}}">
|
||||
<t t-esc="formatFloat(forecast.indirect_demand_qty, false, {'digits': [false, productionSchedule.precision_digits]})"/>
|
||||
</th>
|
||||
</t>
|
||||
</tr>
|
||||
<tr t-ref="replenishRow" name="to_replenish" t-attf-class="{{! (groups.mrp_mps_show_to_replenish or groups.mrp_mps_show_actual_replenishment) and 'o_hidden' or ''}}">
|
||||
<th/>
|
||||
<th scope="row">
|
||||
+ <span t-attf-class="{{! groups.mrp_mps_show_actual_replenishment and 'o_hidden' or ''}}" data-bs-toggle="tooltip" data-bs-placement="bottom" title="The quantity being replenished, based on the Requests for Quotation and the Manufacturing Orders.">Actual</span>
|
||||
<span t-attf-class="{{! (groups.mrp_mps_show_actual_replenishment and groups.mrp_mps_show_to_replenish) and 'o_hidden' or ''}}"> / </span>
|
||||
<span t-attf-class="{{! groups.mrp_mps_show_to_replenish and 'o_hidden' or ''}}" data-bs-toggle="tooltip" data-bs-placement="bottom" title="The quantity to replenish through Purchase Orders or Manufacturing Orders.">Suggested Replenishment </span>
|
||||
<button type="button" title="Replenish" t-attf-class="{{! groups.mrp_mps_show_to_replenish and 'o_hidden' or 'btn btn-secondary o_no_padding o_mrp_mps_procurement'}}"
|
||||
t-on-click.stop="() => this._onClickReplenish(productionSchedule.id)"
|
||||
t-on-mouseover.stop="_onMouseOverReplenish"
|
||||
t-on-mouseout.stop="_onMouseOutReplenish">
|
||||
Replenish
|
||||
</button>
|
||||
</th>
|
||||
<th class="text-end">
|
||||
<button type="button" t-attf-class="{{! groups.mrp_mps_show_to_replenish and 'o_hidden' or 'btn btn-link o_no_padding o_mrp_mps_edit'}}" t-on-click.stop="(ev) => this._onClickEdit(ev, productionSchedule.id)">
|
||||
<t t-esc="productionSchedule.min_to_replenish_qty"/> ≤…≤ <t t-esc="productionSchedule.max_to_replenish_qty"/>
|
||||
</button>
|
||||
</th>
|
||||
<t t-foreach="productionSchedule.forecast_ids" t-as="forecast" t-key="forecast_index">
|
||||
<th t-attf-class="o_forecast_stock text-end pe-4 {{
|
||||
forecast.to_replenish and 'o_mrp_mps_to_replenish' or ''
|
||||
}} {{
|
||||
forecast.forced_replenish and 'o_mrp_mps_forced_replenish' or ''
|
||||
}}">
|
||||
<a href="#"
|
||||
name="actual_replenishment"
|
||||
t-on-click.prevent="_onClickOpenDetails"
|
||||
data-action="action_open_actual_replenishment_details"
|
||||
t-att-data-date_index="forecast_index"
|
||||
t-att-data-date_start="forecast.date_start"
|
||||
t-att-data-date_stop="forecast.date_stop"
|
||||
t-attf-class="o_mrp_mps_open_details {{
|
||||
! groups.mrp_mps_show_actual_replenishment and 'o_hidden'
|
||||
}} {{
|
||||
forecast.to_replenish and 'o_mrp_mps_to_replenish' or ''
|
||||
}} {{
|
||||
forecast.forced_replenish and 'o_mrp_mps_forced_replenish' or ''
|
||||
}}">
|
||||
<t t-esc="formatFloat(forecast.incoming_qty, false, {'digits': [false, productionSchedule.precision_digits]})"/>
|
||||
</a>
|
||||
<span t-attf-class="{{! (groups.mrp_mps_show_actual_replenishment and groups.mrp_mps_show_to_replenish) and 'o_hidden' or ''}}"> / </span>
|
||||
<div t-attf-class="input-group {{! groups.mrp_mps_show_to_replenish and 'o_hidden' or groups.mrp_mps_show_actual_replenishment and 'o_mps_inline' or ''}}">
|
||||
<button type="button"
|
||||
t-if="forecast.replenish_qty_updated"
|
||||
t-on-click.stop="(ev) => this._onClickAutomaticMode(ev, productionSchedule.id)"
|
||||
t-att-data-date_index="forecast_index"
|
||||
class="btn btn-link input-group-addon
|
||||
o_mrp_mps_automatic_mode fa fa-times
|
||||
o_no_padding"/>
|
||||
<input type="text"
|
||||
t-att-data-date_index="forecast_index"
|
||||
t-attf-class="form-control text-end
|
||||
o_mrp_mps_input_replenish_qty
|
||||
{{forecast.state == 'launched' and 'alert-dark' or
|
||||
forecast.state == 'to_relaunch' and 'alert-warning' or
|
||||
forecast.state == 'to_correct' and 'alert-danger' or
|
||||
forecast.to_replenish and 'alert-success' or
|
||||
forecast.replenish_qty_updated and 'alert-info' or ''
|
||||
}} {{
|
||||
forecast.to_replenish and 'o_mrp_mps_to_replenish' or ''
|
||||
}} {{
|
||||
forecast.forced_replenish and 'o_mrp_mps_forced_replenish' or ''
|
||||
}}"
|
||||
t-att-value="formatFloat(forecast.replenish_qty, false, {'digits': [false, productionSchedule.precision_digits]})"
|
||||
t-on-change.stop="(ev) => this._onChangeToReplenish(ev, productionSchedule.id)"
|
||||
t-on-focus="_onFocusInput"/>
|
||||
</div>
|
||||
</th>
|
||||
</t>
|
||||
</tr>
|
||||
<tr name="safety_stock" t-attf-class="{{! (groups.mrp_mps_show_safety_stock or groups.mrp_mps_show_available_to_promise) and 'o_hidden' or ''}}">
|
||||
<th/>
|
||||
<th scope="row">
|
||||
= <span t-attf-class="{{! groups.mrp_mps_show_available_to_promise and 'o_hidden' or ''}}" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Quantity predicted to be available for sale at the end of the period (= to replenish - actual demand).">ATP</span>
|
||||
<span t-attf-class="{{! (groups.mrp_mps_show_safety_stock and groups.mrp_mps_show_available_to_promise) and 'o_hidden' or ''}}"> / </span>
|
||||
<span t-attf-class="{{! groups.mrp_mps_show_safety_stock and 'o_hidden' or ''}}" data-bs-toggle="tooltip" data-bs-placement="bottom" title="The forecasted quantity in stock at the end of the period.">Forecasted Stock</span>
|
||||
</th>
|
||||
<th class="text-end">
|
||||
<button type="button" t-attf-class="{{! groups.mrp_mps_show_safety_stock and 'o_hidden' or 'btn btn-link text-muted o_no_padding o_mrp_mps_edit'}}" t-on-click.stop="(ev) => this._onClickEdit(ev, productionSchedule.id)">
|
||||
<span class="fa fa-bullseye text-muted fa-fw" role="img" aria-label="Forecasted" title="Forecasted"/>
|
||||
<t t-esc="productionSchedule.forecast_target_qty or 0.0"/>
|
||||
</button>
|
||||
</th>
|
||||
<t t-foreach="productionSchedule.forecast_ids" t-as="forecast" t-key="forecast_index">
|
||||
<th class="text-end pe-4">
|
||||
<span t-attf-class="{{! groups.mrp_mps_show_available_to_promise and 'o_hidden' or ''}}" t-esc="formatFloat(forecast.starting_inventory_qty + forecast.replenish_qty - forecast.outgoing_qty, false, {'digits': [false, productionSchedule.precision_digits]})"/>
|
||||
<span t-attf-class="{{! (groups.mrp_mps_show_safety_stock and groups.mrp_mps_show_available_to_promise) and 'o_hidden' or ''}}"> / </span>
|
||||
<span t-attf-class="{{forecast.safety_stock_qty < 0 and 'text-danger' or ''}} {{! groups.mrp_mps_show_safety_stock and 'o_hidden' or ''}}" t-esc="formatFloat(forecast.safety_stock_qty, false, {'digits': [false, productionSchedule.precision_digits]})"/>
|
||||
</th>
|
||||
</t>
|
||||
</tr>
|
||||
</tbody>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
133
mrp_mps/static/src/components/main.js
Normal file
133
mrp_mps/static/src/components/main.js
Normal file
@@ -0,0 +1,133 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { MrpMpsControlPanel } from '../search/mrp_mps_control_panel';
|
||||
import { MrpMpsSearchModel } from '../search/mrp_mps_search_model';
|
||||
import MpsLineComponent from '@mrp_mps/components/line';
|
||||
import { MasterProductionScheduleModel } from '@mrp_mps/models/master_production_schedule_model';
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useBus, useService } from "@web/core/utils/hooks";
|
||||
import { CheckBox } from "@web/core/checkbox/checkbox";
|
||||
import { getDefaultConfig } from "@web/views/view";
|
||||
import { usePager } from "@web/search/pager_hook";
|
||||
import { CallbackRecorder, useSetupAction } from "@web/webclient/actions/action_hook";
|
||||
|
||||
const { Component, onWillStart, useSubEnv, useChildSubEnv } = owl;
|
||||
|
||||
class MainComponent extends Component {
|
||||
//--------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
//--------------------------------------------------------------------------
|
||||
setup() {
|
||||
this.action = useService("action");
|
||||
this.dialog = useService("dialog");
|
||||
this.orm = useService("orm");
|
||||
this.viewService = useService("view");
|
||||
|
||||
const { orm, action, dialog } = this;
|
||||
this.model = new MasterProductionScheduleModel(this.props, { orm, action, dialog });
|
||||
|
||||
useSubEnv({
|
||||
manufacturingPeriods: [],
|
||||
model: this.model,
|
||||
__getContext__: new CallbackRecorder(),
|
||||
__getOrderBy__: new CallbackRecorder(),
|
||||
config: {
|
||||
...getDefaultConfig(),
|
||||
offset: 0,
|
||||
limit: 20,
|
||||
mpsImportRecords: true,
|
||||
},
|
||||
});
|
||||
|
||||
useSetupAction({
|
||||
getContext: () => {
|
||||
return this.props.action.context;
|
||||
},
|
||||
});
|
||||
|
||||
this.SearchModel = new MrpMpsSearchModel(this.env, {
|
||||
user: useService("user"),
|
||||
orm: this.orm,
|
||||
view: useService("view"),
|
||||
});
|
||||
useChildSubEnv({
|
||||
searchModel: this.SearchModel,
|
||||
});
|
||||
|
||||
useBus(this.SearchModel, "update", () => {
|
||||
this.env.config.offset = 0;
|
||||
this.env.config.limit = 20;
|
||||
this.model.load(this.SearchModel.domain, this.env.config.offset, this.env.config.limit);
|
||||
});
|
||||
|
||||
onWillStart(async () => {
|
||||
this.env.config.setDisplayName(this.env._t("Master Production Schedule"));
|
||||
const views = await this.viewService.loadViews(
|
||||
{
|
||||
resModel: "mrp.production.schedule",
|
||||
context: this.props.action.context,
|
||||
views: [[false, "search"]],
|
||||
}
|
||||
);
|
||||
await this.SearchModel.load({
|
||||
resModel: "mrp.production.schedule",
|
||||
context: this.props.action.context,
|
||||
orderBy: "id",
|
||||
searchMenuTypes: ['filter', 'favorite'],
|
||||
searchViewArch: views.views.search.arch,
|
||||
searchViewId: views.views.search.id,
|
||||
searchViewFields: views.fields,
|
||||
loadIrFilters: true
|
||||
});
|
||||
this.model.on('update', this, () => this.render(true));
|
||||
const domain = this.props.action.domain || this.SearchModel.domain;
|
||||
await this.model.load(domain, this.env.config.offset, this.env.config.limit);
|
||||
});
|
||||
|
||||
usePager(() => {
|
||||
return {
|
||||
offset: this.env.config.offset,
|
||||
limit: this.env.config.limit,
|
||||
total: this.model.data.count,
|
||||
onUpdate: async ({ offset, limit }) => {
|
||||
this.env.config.offset = offset;
|
||||
this.env.config.limit = limit;
|
||||
this.model.load(this.SearchModel.domain, offset, limit);
|
||||
this.render(true);
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
get lines() {
|
||||
return this.model.data.production_schedule_ids;
|
||||
}
|
||||
|
||||
get manufacturingPeriods() {
|
||||
return this.model.data.dates;
|
||||
}
|
||||
|
||||
get groups() {
|
||||
return this.model.data.groups[0];
|
||||
}
|
||||
|
||||
get isSelected() {
|
||||
return this.model.selectedRecords.size === this.lines.length;
|
||||
}
|
||||
|
||||
toggleSelection() {
|
||||
this.model.toggleSelection();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MainComponent.template = 'mrp_mps.mrp_mps';
|
||||
MainComponent.components = {
|
||||
MrpMpsControlPanel,
|
||||
MpsLineComponent,
|
||||
CheckBox,
|
||||
};
|
||||
|
||||
registry.category("actions").add("mrp_mps_client_action", MainComponent);
|
||||
|
||||
export default MainComponent;
|
||||
7
mrp_mps/static/src/components/main.scss
Normal file
7
mrp_mps/static/src/components/main.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.o_mrp_mps {
|
||||
.o_mps_period {
|
||||
th {
|
||||
background-color: $gray-200;
|
||||
}
|
||||
}
|
||||
}
|
||||
49
mrp_mps/static/src/components/main.xml
Normal file
49
mrp_mps/static/src/components/main.xml
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<div t-name="mrp_mps.mrp_mps" class="main o_action" owl="1">
|
||||
<MrpMpsControlPanel/>
|
||||
<div class="o_mrp_mps o_content bg-view">
|
||||
<t t-if="lines.length">
|
||||
<div class="text-nowrap mr0 ml0">
|
||||
<table class="table o_mps_product_table">
|
||||
<thead class="table-light">
|
||||
<tr class="o_mps_period">
|
||||
<th>
|
||||
<CheckBox value='isSelected' onChange.bind="toggleSelection"/>
|
||||
</th>
|
||||
<th/>
|
||||
<th/>
|
||||
<th class="text-end pe-4" scope="col" t-foreach="manufacturingPeriods" t-as="period" t-key="period">
|
||||
<div><t t-esc="period"/></div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<t t-foreach="lines" t-as="productionSchedule" t-key="productionSchedule.id">
|
||||
<MpsLineComponent data="productionSchedule" groups="groups"/>
|
||||
</t>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-call="mrp_mps_nocontent_helper"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<t t-name="mrp_mps_nocontent_helper" owl="1">
|
||||
<div class="o_view_nocontent">
|
||||
<div class="o_nocontent_help">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No product yet. Add one to start scheduling.
|
||||
</p><p>
|
||||
The master schedule translates your sales and demand forecasts into a production and purchase planning for each component.
|
||||
It ensures everything gets scheduled on time, based on constraints such as: safety stock, production capacity, lead times.
|
||||
It's the perfect tool to support your S&OP meetings.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
Reference in New Issue
Block a user