新增主生产计划模块
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>
|
||||
317
mrp_mps/static/src/models/master_production_schedule_model.js
Normal file
317
mrp_mps/static/src/models/master_production_schedule_model.js
Normal file
@@ -0,0 +1,317 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { Mutex } from "@web/core/utils/concurrency";
|
||||
|
||||
const { EventBus } = owl;
|
||||
|
||||
export class MasterProductionScheduleModel extends EventBus {
|
||||
constructor(params, services) {
|
||||
super();
|
||||
this.domain = [];
|
||||
this.offset = 0;
|
||||
this.limit = false;
|
||||
this.params = params;
|
||||
this.orm = services.orm;
|
||||
this.action = services.action;
|
||||
this.dialog = services.dialog;
|
||||
this.selectedRecords = new Set();
|
||||
this.mutex = new Mutex();
|
||||
}
|
||||
|
||||
async load(domain, offset, limit) {
|
||||
if (domain !== undefined) {
|
||||
this.domain = domain;
|
||||
}
|
||||
if (offset !== undefined) {
|
||||
this.offset = offset;
|
||||
}
|
||||
if (limit !== undefined) {
|
||||
this.limit = limit;
|
||||
}
|
||||
this.data = await this.orm.call('mrp.production.schedule', 'get_mps_view_state', [this.domain, this.offset, this.limit]);
|
||||
this.notify();
|
||||
}
|
||||
|
||||
async reload(productionScheduleId) {
|
||||
return await this.orm.call(
|
||||
'mrp.production.schedule',
|
||||
'get_impacted_schedule',
|
||||
[productionScheduleId, this.domain],
|
||||
).then((productionScheduleIds) => {
|
||||
productionScheduleIds.push(productionScheduleId);
|
||||
return this.orm.call(
|
||||
'mrp.production.schedule',
|
||||
'get_production_schedule_view_state',
|
||||
[productionScheduleIds],
|
||||
);
|
||||
}).then((production_schedule_ids) => {
|
||||
for (var i = 0; i < production_schedule_ids.length; i++) {
|
||||
const index = this.data.production_schedule_ids.findIndex(ps => ps.id === production_schedule_ids[i].id);
|
||||
if (index >= 0) {
|
||||
this.data.production_schedule_ids.splice(index, 1, production_schedule_ids[i]);
|
||||
} else {
|
||||
this.data.production_schedule_ids.push(production_schedule_ids[i]);
|
||||
}
|
||||
}
|
||||
this.notify();
|
||||
});
|
||||
}
|
||||
|
||||
notify() {
|
||||
this.unselectAll();
|
||||
this.trigger('update');
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an rpc to replenish the different schedules passed as arguments.
|
||||
* If the procurementIds list is empty, it replenish all the schedules under
|
||||
* the current domain. Reload the content after the replenish in order to
|
||||
* display the new forecast cells to run.
|
||||
* @private
|
||||
* @param {Integer[]} productionScheduleIds mrp.production.schedule ids to
|
||||
* replenish.
|
||||
* @return {Promise}
|
||||
*/
|
||||
_actionReplenish(productionScheduleIds, basedOnLeadTime = false) {
|
||||
this.mutex.exec(() => {
|
||||
return this.orm.call(
|
||||
'mrp.production.schedule',
|
||||
'action_replenish',
|
||||
[productionScheduleIds, basedOnLeadTime],
|
||||
).then(() => {
|
||||
if (productionScheduleIds.length === 1) {
|
||||
this.reload(productionScheduleIds[0]);
|
||||
} else {
|
||||
this.load();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
replenishAll() {
|
||||
this.orm.search("mrp.production.schedule", this.domain).then((ids) => {
|
||||
this._actionReplenish(ids, true);
|
||||
});
|
||||
}
|
||||
|
||||
replenishSelectedRecords() {
|
||||
this._actionReplenish(Array.from(this.selectedRecords), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the forecasted quantity and reload the current schedule in order
|
||||
* to update its To Replenish quantity and its safety stock (current and
|
||||
* future period). Also update the other schedules linked by BoM in order
|
||||
* to update them depending the indirect demand.
|
||||
* @private
|
||||
* @param {Integer} productionScheduleId mrp.production.schedule Id.
|
||||
* @param {Integer} dateIndex period to save (column number)
|
||||
* @param {Float} forecastQty The new forecasted quantity
|
||||
* @return {Promise}
|
||||
*/
|
||||
_saveForecast(productionScheduleId, dateIndex, forecastQty) {
|
||||
return this.mutex.exec(() => {
|
||||
this.orm.call(
|
||||
'mrp.production.schedule',
|
||||
'set_forecast_qty',
|
||||
[productionScheduleId, dateIndex, forecastQty],
|
||||
).then(() => {
|
||||
return this.reload(productionScheduleId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the mrp.production.schedule form view in order to create the record.
|
||||
* Once the record is created get its state and render it.
|
||||
* @private
|
||||
* @return {Promise}
|
||||
*/
|
||||
_createProduct() {
|
||||
this.mutex.exec(() => {
|
||||
this.action.doAction('mrp_mps.action_mrp_mps_form_view', {
|
||||
onClose: () => this.load(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the mrp.production.schedule form view in order to edit the record.
|
||||
* Once the record is edited get its state and render it.
|
||||
* @private
|
||||
* @param {Integer} productionScheduleId mrp.production.schedule Id.
|
||||
*/
|
||||
_editProduct(productionScheduleId) {
|
||||
this.mutex.exec(() => {
|
||||
this.action.doAction({
|
||||
name: 'Edit Production Schedule',
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'mrp.production.schedule',
|
||||
views: [[false, 'form']],
|
||||
target: 'new',
|
||||
res_id: productionScheduleId,
|
||||
}, {
|
||||
onClose: () => this.reload(productionScheduleId),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink the production schedule and remove it from the DOM. Use a
|
||||
* confirmation dialog in order to avoid a mistake from the user.
|
||||
* @private
|
||||
* @param {Integer[]} productionScheduleIds mrp.production.schedule Ids.
|
||||
* @return {Promise}
|
||||
*/
|
||||
_unlinkProduct(productionScheduleIds) {
|
||||
function doIt() {
|
||||
this.mutex.exec(async () => {
|
||||
return Promise.all(productionScheduleIds.map((id) => this.orm.unlink(
|
||||
'mrp.production.schedule',
|
||||
[id]
|
||||
))).then(() => {
|
||||
for (const productionScheduleId of productionScheduleIds) {
|
||||
const index = this.data.production_schedule_ids.findIndex(ps => ps.id === productionScheduleId);
|
||||
this.data.production_schedule_ids.splice(index, 1);
|
||||
}
|
||||
this.notify();
|
||||
});
|
||||
});
|
||||
}
|
||||
const body = productionScheduleIds.length > 1
|
||||
? _t("Are you sure you want to delete these records?")
|
||||
: _t("Are you sure you want to delete this record?");
|
||||
this.dialog.add(ConfirmationDialog, {
|
||||
body: body,
|
||||
title: _t("Confirmation"),
|
||||
confirm: doIt.bind(this),
|
||||
});
|
||||
}
|
||||
|
||||
unlinkSelectedRecord() {
|
||||
return this._unlinkProduct(Array.from(this.selectedRecords));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Integer} productionScheduleId mrp.production.schedule Id
|
||||
* @param {String} action name of the action to be undertaken
|
||||
* @param {String} dateStr name of the period
|
||||
* @param {String} dateStart start date of the period
|
||||
* @param {String} dateStop end date of the period
|
||||
* @return {Promise}
|
||||
*/
|
||||
_actionOpenDetails(productionScheduleId, action, dateStr, dateStart, dateStop) {
|
||||
this.mutex.exec(() => {
|
||||
return this.orm.call(
|
||||
'mrp.production.schedule',
|
||||
action,
|
||||
[productionScheduleId, dateStr, dateStart, dateStop]
|
||||
).then((action) => {
|
||||
return this.action.doAction(action);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the quantity To Replenish and reload the current schedule in order
|
||||
* to update it's safety stock and quantity in future period. Also mark
|
||||
* the cell with a blue background in order to show that it was manually
|
||||
* updated.
|
||||
* @private
|
||||
* @param {Integer} productionScheduleId mrp.production.schedule Id.
|
||||
* @param {Integer} dateIndex period to save (column number)
|
||||
* @param {Float} replenishQty The new quantity To Replenish
|
||||
* @return {Promise}
|
||||
*/
|
||||
_saveToReplenish(productionScheduleId, dateIndex, replenishQty) {
|
||||
return this.mutex.exec(() => {
|
||||
this.orm.call(
|
||||
'mrp.production.schedule',
|
||||
'set_replenish_qty',
|
||||
[productionScheduleId, dateIndex, replenishQty],
|
||||
).then(() => {
|
||||
return this.reload(productionScheduleId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the manual change of replenishQty and load the suggested value.
|
||||
* @private
|
||||
* @param {Integer} productionScheduleId mrp.production.schedule Id.
|
||||
* @param {Integer} dateIndex period to save (column number)
|
||||
* @return {Promise}
|
||||
*/
|
||||
_removeQtyToReplenish(productionScheduleId, dateIndex) {
|
||||
return this.mutex.exec(() => {
|
||||
this.orm.call(
|
||||
'mrp.production.schedule',
|
||||
'remove_replenish_qty',
|
||||
[productionScheduleId, dateIndex]
|
||||
).then(() => {
|
||||
return this.reload(productionScheduleId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_getOriginValue(productionScheduleId, dateIndex, inputName) {
|
||||
return this.data.production_schedule_ids.find(ps => ps.id === productionScheduleId).forecast_ids[dateIndex][inputName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the company settings and hide or display the rows.
|
||||
* @private
|
||||
* @param {Object} values {field_name: field_value}
|
||||
*/
|
||||
_saveCompanySettings(values) {
|
||||
this.mutex.exec(() => {
|
||||
this.orm.write(
|
||||
'res.company',
|
||||
[this.data.company_id],
|
||||
values,
|
||||
).then(() => {
|
||||
this.load();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
mouseOverReplenish() {
|
||||
this.trigger('mouse-over');
|
||||
}
|
||||
|
||||
mouseOutReplenish() {
|
||||
this.trigger('mouse-out');
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
this.data.production_schedule_ids.map(
|
||||
({ id }) => this.selectedRecords.add(id)
|
||||
);
|
||||
}
|
||||
|
||||
unselectAll() {
|
||||
this.selectedRecords.clear();
|
||||
}
|
||||
|
||||
toggleRecordSelection(productionScheduleId) {
|
||||
if (this.selectedRecords.has(productionScheduleId)) {
|
||||
this.selectedRecords.delete(productionScheduleId);
|
||||
} else {
|
||||
this.selectedRecords.add(productionScheduleId);
|
||||
}
|
||||
this.trigger('update');
|
||||
}
|
||||
|
||||
toggleSelection() {
|
||||
if (this.selectedRecords.size === this.data.production_schedule_ids.length) {
|
||||
this.unselectAll();
|
||||
} else {
|
||||
this.selectAll();
|
||||
}
|
||||
this.trigger('update');
|
||||
}
|
||||
|
||||
}
|
||||
22
mrp_mps/static/src/search/group_menu.js
Normal file
22
mrp_mps/static/src/search/group_menu.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class GroupMenu extends Component {
|
||||
get items() {
|
||||
return this.props.items;
|
||||
}
|
||||
|
||||
_toggle_group(group) {
|
||||
const value = {};
|
||||
value[group] = !this.props.items[group];
|
||||
this.env.model._saveCompanySettings(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
GroupMenu.template = "mrp_mps.GroupMenu";
|
||||
GroupMenu.components = { Dropdown, DropdownItem };
|
||||
73
mrp_mps/static/src/search/group_menu.xml
Normal file
73
mrp_mps/static/src/search/group_menu.xml
Normal file
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mrp_mps.GroupMenu" owl="1">
|
||||
<Dropdown togglerClass="'btn btn-light'">
|
||||
<t t-set-slot="toggler">
|
||||
<i class="me-1" t-att-class="icon"/>
|
||||
<span class="dropdown-toggle o_dropdown_title">Rows</span>
|
||||
</t>
|
||||
<DropdownItem
|
||||
class="{ o_menu_item: true, selected: items.mrp_mps_show_starting_inventory }"
|
||||
parentClosingMode="'none'"
|
||||
onSelected="() => this._toggle_group('mrp_mps_show_starting_inventory')">
|
||||
Starting Inventory
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
class="{ o_menu_item: true, selected: items.mrp_mps_show_demand_forecast }"
|
||||
parentClosingMode="'none'"
|
||||
onSelected="() => this._toggle_group('mrp_mps_show_demand_forecast')">
|
||||
Demand Forecast
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
class="{ o_menu_item: true, selected: items.mrp_mps_show_actual_demand }"
|
||||
parentClosingMode="'none'"
|
||||
onSelected="() => this._toggle_group('mrp_mps_show_actual_demand')">
|
||||
Actual Demand
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
class="{ o_menu_item: true, selected: items.mrp_mps_show_actual_demand_year_minus_1 }"
|
||||
parentClosingMode="'none'"
|
||||
onSelected="() => this._toggle_group('mrp_mps_show_actual_demand_year_minus_1')">
|
||||
Actual Demand Y-1
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
class="{ o_menu_item: true, selected: items.mrp_mps_show_actual_demand_year_minus_2 }"
|
||||
parentClosingMode="'none'"
|
||||
onSelected="() => this._toggle_group('mrp_mps_show_actual_demand_year_minus_2')">
|
||||
Actual Demand Y-2
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
class="{ o_menu_item: true, selected: items.mrp_mps_show_indirect_demand }"
|
||||
parentClosingMode="'none'"
|
||||
onSelected="() => this._toggle_group('mrp_mps_show_indirect_demand')">
|
||||
Indirect Demand Forecast
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
class="{ o_menu_item: true, selected: items.mrp_mps_show_to_replenish }"
|
||||
parentClosingMode="'none'"
|
||||
onSelected="() => this._toggle_group('mrp_mps_show_to_replenish')">
|
||||
To Replenish
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
class="{ o_menu_item: true, selected: items.mrp_mps_show_actual_replenishment }"
|
||||
parentClosingMode="'none'"
|
||||
onSelected="() => this._toggle_group('mrp_mps_show_actual_replenishment')">
|
||||
Actual Replenishment
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
class="{ o_menu_item: true, selected: items.mrp_mps_show_safety_stock }"
|
||||
parentClosingMode="'none'"
|
||||
onSelected="() => this._toggle_group('mrp_mps_show_safety_stock')">
|
||||
Forecasted Stock
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
class="{ o_menu_item: true, selected: items.mrp_mps_show_available_to_promise }"
|
||||
parentClosingMode="'none'"
|
||||
onSelected="() => this._toggle_group('mrp_mps_show_available_to_promise')">
|
||||
Available to Promise
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
13
mrp_mps/static/src/search/import_records.js
Normal file
13
mrp_mps/static/src/search/import_records.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { ImportRecords } from "@base_import/import_records/import_records";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const favoriteMenuRegistry = registry.category("favoriteMenu");
|
||||
const mpsImportRecordsItem = {
|
||||
Component: ImportRecords,
|
||||
groupNumber: 4,
|
||||
isDisplayed: ({ config }) =>
|
||||
config.mpsImportRecords
|
||||
};
|
||||
favoriteMenuRegistry.add("mps-import-records-menu", mpsImportRecordsItem, { sequence: 1 });
|
||||
139
mrp_mps/static/src/search/mrp_mps_control_panel.js
Normal file
139
mrp_mps/static/src/search/mrp_mps_control_panel.js
Normal file
@@ -0,0 +1,139 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { GroupMenu } from "./group_menu";
|
||||
import { download } from "@web/core/network/download";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { ActionMenus } from "@web/search/action_menus/action_menus";
|
||||
import { ControlPanel } from "@web/search/control_panel/control_panel";
|
||||
import { ExportDataDialog } from "@web/views/view_dialogs/export_data_dialog";
|
||||
|
||||
|
||||
export class MrpMpsControlPanel extends ControlPanel {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.rpc = useService("rpc");
|
||||
this.dialogService = useService("dialog");
|
||||
}
|
||||
|
||||
get model() {
|
||||
return this.env.model;
|
||||
}
|
||||
|
||||
get groups() {
|
||||
return this.env.model.data.groups[0];
|
||||
}
|
||||
|
||||
get isRecordSelected() {
|
||||
return this.model.selectedRecords.size > 0;
|
||||
}
|
||||
|
||||
getActionMenuItems() {
|
||||
return Object.assign({}, {
|
||||
other: [{
|
||||
key: "export",
|
||||
description: this.env._t("Export"),
|
||||
callback: () => this.onExportData(),
|
||||
}, {
|
||||
key: "delete",
|
||||
description: this.env._t("Delete"),
|
||||
callback: () => this.unlinkSelectedRecord(),
|
||||
}, {
|
||||
key: "replenish",
|
||||
description: this.env._t("Replenish"),
|
||||
callback: () => this.replenishSelectedRecords(),
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the click on replenish button. It will call action_replenish with
|
||||
* all the Ids present in the view.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickReplenish(ev) {
|
||||
this.env.model.replenishAll();
|
||||
}
|
||||
|
||||
_onMouseOverReplenish(ev) {
|
||||
this.model.mouseOverReplenish()
|
||||
}
|
||||
|
||||
_onMouseOutReplenish(ev) {
|
||||
this.model.mouseOutReplenish()
|
||||
}
|
||||
|
||||
_onClickCreate(ev) {
|
||||
this.env.model._createProduct();
|
||||
}
|
||||
|
||||
replenishSelectedRecords() {
|
||||
this.env.model.replenishSelectedRecords();
|
||||
}
|
||||
|
||||
unlinkSelectedRecord() {
|
||||
this.env.model.unlinkSelectedRecord();
|
||||
}
|
||||
|
||||
async getExportedFields(model, import_compat, parentParams) {
|
||||
return await this.rpc("/web/export/get_fields", {
|
||||
...parentParams,
|
||||
model,
|
||||
import_compat,
|
||||
});
|
||||
}
|
||||
|
||||
async downloadExport(fields, import_compat, format) {
|
||||
const resIds = Array.from(this.model.selectedRecords);
|
||||
const exportedFields = fields.map((field) => ({
|
||||
name: field.name || field.id,
|
||||
label: field.label || field.string,
|
||||
store: field.store,
|
||||
type: field.field_type,
|
||||
}));
|
||||
if (import_compat) {
|
||||
exportedFields.unshift({ name: "id", label: this.env._t("External ID") });
|
||||
}
|
||||
await download({
|
||||
data: {
|
||||
data: JSON.stringify({
|
||||
import_compat,
|
||||
context: this.props.context,
|
||||
domain: this.model.domain,
|
||||
fields: exportedFields,
|
||||
ids: resIds.length > 0 && resIds,
|
||||
model: "mrp.production.schedule",
|
||||
}),
|
||||
},
|
||||
url: `/web/export/${format}`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the Export Dialog
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
onExportData() {
|
||||
const resIds = Array.from(this.model.selectedRecords);
|
||||
const dialogProps = {
|
||||
resIds,
|
||||
context: this.props.context,
|
||||
download: this.downloadExport.bind(this),
|
||||
getExportedFields: this.getExportedFields.bind(this),
|
||||
root: {
|
||||
resModel: "mrp.production.schedule",
|
||||
activeFields: [],
|
||||
},
|
||||
};
|
||||
this.dialogService.add(ExportDataDialog, dialogProps);
|
||||
}
|
||||
}
|
||||
|
||||
MrpMpsControlPanel.template = "mrp_mps.MrpMpsControlPanel";
|
||||
MrpMpsControlPanel.components = {
|
||||
...ControlPanel.components,
|
||||
ActionMenus,
|
||||
GroupMenu,
|
||||
};
|
||||
37
mrp_mps/static/src/search/mrp_mps_control_panel.xml
Normal file
37
mrp_mps/static/src/search/mrp_mps_control_panel.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="mrp_mps.MrpMpsControlPanel.Regular" t-inherit="web.ControlPanel.Regular" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//t[@t-slot='control-panel-bottom-left']" position="replace">
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
t-on-click.stop="_onClickReplenish"
|
||||
t-on-mouseover.stop="_onMouseOverReplenish"
|
||||
t-on-mouseout.stop="_onMouseOutReplenish">Replenish</button>
|
||||
<button
|
||||
type="button"
|
||||
class='btn btn-secondary'
|
||||
t-on-click.stop="_onClickCreate">Add a Product</button>
|
||||
</div>
|
||||
<ActionMenus t-if="isRecordSelected"
|
||||
getActiveIds="() => Array.from(model.selectedRecords)"
|
||||
context="{}"
|
||||
domain="model.domain"
|
||||
items="getActionMenuItems()"
|
||||
isDomainSelected="model.isDomainSelected"
|
||||
resModel="'mrp.production.schedule'"/>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-foreach='searchMenus']" position="before">
|
||||
<GroupMenu items="groups"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="mrp_mps.MrpMpsControlPanel" t-inherit="web.ControlPanel" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//t[@t-call='web.ControlPanel.Regular']" position="replace">
|
||||
<t t-call="mrp_mps.MrpMpsControlPanel.Regular"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
28
mrp_mps/static/src/search/mrp_mps_search_model.js
Normal file
28
mrp_mps/static/src/search/mrp_mps_search_model.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { SearchModel } from "@web/search/search_model";
|
||||
|
||||
export class MrpMpsSearchModel extends SearchModel {
|
||||
/**
|
||||
* When search with field bom_id, also show components of that bom
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_getFieldDomain(field, autocompleteValues) {
|
||||
let domain = super._getFieldDomain(...arguments);
|
||||
if (field.fieldName === "bom_id") {
|
||||
const additionalDomain = [
|
||||
"|",
|
||||
["product_id.bom_line_ids.bom_id", "ilike", autocompleteValues[0].value],
|
||||
"|",
|
||||
["product_id.variant_bom_ids", "ilike", autocompleteValues[0].value],
|
||||
"&",
|
||||
["product_tmpl_id.bom_ids.product_id", "=", false],
|
||||
["product_tmpl_id.bom_ids", "ilike", autocompleteValues[0].value],
|
||||
];
|
||||
domain = Domain.or([additionalDomain, domain.toList()]);
|
||||
}
|
||||
return domain;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user