19.0 vanilla
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 1.2 KiB |
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70"><defs><path id="a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/><linearGradient id="c" x1="100%" x2="0%" y1="0%" y2="100%"><stop offset="0%" stop-color="#7CC098"/><stop offset="100%" stop-color="#5F8A71"/></linearGradient><path id="d" d="M53.373 32.454c.742 0 1.206.784.824 1.4-2.009 3.235-5.668 5.4-9.848 5.4-6.318 0-11.445-4.944-11.484-11.056C32.825 22.058 38.012 17 44.349 17c4.176 0 7.831 2.16 9.842 5.389.385.62-.07 1.41-.818 1.41h-8.386l-3.19 4.328 3.19 4.327h8.386zM39.684 39.64l-15.97 15.473c-1.994 1.931-5.226 1.931-7.22 0a4.837 4.837 0 0 1 0-6.994l15.971-15.472c1.289 3.183 3.932 5.744 7.219 6.993zm-16.39 10.74c0-1.024-.857-1.854-1.914-1.854s-1.914.83-1.914 1.854.857 1.854 1.914 1.854 1.914-.83 1.914-1.854z"/><path id="e" d="M53.373 30.454c.742 0 1.206.784.824 1.4-2.009 3.235-5.668 5.4-9.848 5.4-6.318 0-11.445-4.944-11.484-11.056C32.825 20.058 38.012 15 44.349 15c4.176 0 7.831 2.16 9.842 5.389.385.62-.07 1.41-.818 1.41h-8.386l-3.19 4.328 3.19 4.327h8.386zM39.684 37.64l-15.97 15.473c-1.994 1.931-5.226 1.931-7.22 0a4.837 4.837 0 0 1 0-6.994l15.971-15.472c1.289 3.183 3.932 5.744 7.219 6.993zm-16.39 10.74c0-1.024-.857-1.854-1.914-1.854s-1.914.83-1.914 1.854.857 1.854 1.914 1.854 1.914-.83 1.914-1.854z"/></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path fill="url(#c)" d="M0 0H70V70H0z"/><path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/><path fill="#393939" d="M4 69c-2 0-4-1-4-4V36.075l15.053-15.2 18.452.218 2.734 6.93v4.524L53.62 51.98 42.667 69H4z" opacity=".324"/><path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/><use fill="#000" fill-rule="nonzero" opacity=".3" transform="matrix(-1 0 0 1 69.334 0)" xlink:href="#d"/><use fill="#FFF" fill-rule="nonzero" transform="matrix(-1 0 0 1 69.334 0)" xlink:href="#e"/></g></g></svg>
|
||||
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><path d="M50 19.023c0-.735-.737-1.23-1.398-.938l-28 12.403a1.026 1.026 0 0 0-.602.939v9.55c0 .565.448 1.023 1 1.023h25c2.21 0 4-1.83 4-4.089V19.023Z" fill="#FBB945"/><path d="M42 14.018c0-.722-.719-1.214-1.376-.942l-30 12.394a1.017 1.017 0 0 0-.624.943v14.57c0 .562.448 1.017 1 1.017h27c2.21 0 4-1.821 4-4.068V14.018Z" fill="#FC868B"/><path d="m42 21.01-21.398 9.478c-.365.162-.602.53-.602.938v9.552c0 .564.448 1.022 1 1.022h17c2.21 0 4-1.822 4-4.068V21.01Z" fill="#F86126"/><path d="M32 9.015c0-.72-.719-1.21-1.376-.94l-30 12.364a1.015 1.015 0 0 0-.624.94v19.607C0 41.546.448 42 1 42h27c2.21 0 4-1.817 4-4.058V9.015Z" fill="#1AD3BB"/><path d="M32 16.639 10.624 25.47a1.017 1.017 0 0 0-.624.943v14.57c0 .562.448 1.017 1 1.017h17c2.21 0 4-1.817 4-4.058V16.64Z" fill="#1A6F66"/></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 866 B |
BIN
odoo-bringout-oca-ocb-mrp/mrp/static/description/icon_hi.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
odoo-bringout-oca-ocb-mrp/mrp/static/img/cutting-worksheet-1.pdf
Normal file
BIN
odoo-bringout-oca-ocb-mrp/mrp/static/img/cutting-worksheet-2.pdf
Normal file
BIN
odoo-bringout-oca-ocb-mrp/mrp/static/img/cutting-worksheet-3.pdf
Normal file
BIN
odoo-bringout-oca-ocb-mrp/mrp/static/img/cutting-worksheet-4.pdf
Normal file
5
odoo-bringout-oca-ocb-mrp/mrp/static/img/routing.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="1920" height="1024" fill="none" id="svg" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="62" y="227" width="196" height="173" rx="10" ry="10" fill="grey" transform="skewY(-25.5),skewX(25.5)"/>
|
||||
<polygon points="254,272 190,250 255,220" rx="10" ry="10" stroke-linejoin="round" stroke-width="5" stroke="grey"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 325 B |
BIN
odoo-bringout-oca-ocb-mrp/mrp/static/img/routing_shopfloor.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
|
|
@ -1,13 +1,17 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { useService, useBus } from "@web/core/utils/hooks";
|
||||
import { BomOverviewControlPanel } from "../bom_overview_control_panel/mrp_bom_overview_control_panel";
|
||||
import { BomOverviewTable } from "../bom_overview_table/mrp_bom_overview_table";
|
||||
|
||||
const { Component, EventBus, onWillStart, useSubEnv, useState } = owl;
|
||||
import { Component, EventBus, onWillStart, useSubEnv, useState } from "@odoo/owl";
|
||||
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
|
||||
|
||||
export class BomOverviewComponent extends Component {
|
||||
static template = "mrp.BomOverviewComponent";
|
||||
static components = {
|
||||
BomOverviewControlPanel,
|
||||
BomOverviewTable,
|
||||
};
|
||||
static props = { ...standardActionServiceProps };
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.context = this.props.action.context;
|
||||
|
|
@ -17,16 +21,12 @@ export class BomOverviewComponent extends Component {
|
|||
this.warehouses = [];
|
||||
this.showVariants = false;
|
||||
this.uomName = "";
|
||||
this.extraColumnCount = 0;
|
||||
this.unfoldedIds = new Set();
|
||||
|
||||
this.state = useState({
|
||||
showOptions: {
|
||||
mode: this.props.action.context.mode || 'overview',
|
||||
uom: false,
|
||||
availabilities: true,
|
||||
costs: true,
|
||||
operations: true,
|
||||
leadTimes: true,
|
||||
attachments: false,
|
||||
},
|
||||
currentWarehouse: null,
|
||||
|
|
@ -34,12 +34,20 @@ export class BomOverviewComponent extends Component {
|
|||
bomData: {},
|
||||
precision: 2,
|
||||
bomQuantity: null,
|
||||
foldable: true,
|
||||
allFolded: true,
|
||||
});
|
||||
|
||||
useSubEnv({
|
||||
overviewBus: new EventBus(),
|
||||
});
|
||||
|
||||
useBus(
|
||||
this.env.overviewBus,
|
||||
"toggle-fold-all",
|
||||
() => (this.state.allFolded = !this.state.allFolded)
|
||||
);
|
||||
|
||||
onWillStart(async () => {
|
||||
await this.getWarehouses();
|
||||
await this.initBomData();
|
||||
|
|
@ -49,6 +57,13 @@ export class BomOverviewComponent extends Component {
|
|||
//---- Data ----
|
||||
|
||||
async initBomData() {
|
||||
const variantId = this.props.action.context.active_product_id;
|
||||
const resModel = this.props.action.context.active_model;
|
||||
this.state.currentVariantId = false;
|
||||
if (resModel === 'product.product' && variantId !== undefined) {
|
||||
this.state.currentVariantId = variantId;
|
||||
}
|
||||
|
||||
const bomData = await this.getBomData();
|
||||
this.state.bomQuantity = bomData["bom_qty"];
|
||||
this.state.showOptions.uom = bomData["is_uom_applied"];
|
||||
|
|
@ -56,9 +71,10 @@ export class BomOverviewComponent extends Component {
|
|||
this.variants = bomData["variants"];
|
||||
this.showVariants = bomData["is_variant_applied"];
|
||||
if (this.showVariants) {
|
||||
this.state.currentVariantId = Object.keys(this.variants)[0];
|
||||
this.state.currentVariantId ||= this.state.bomData.product_id;
|
||||
}
|
||||
this.state.precision = bomData["precision"];
|
||||
this.state.foldable = bomData["lines"]["foldable"];
|
||||
}
|
||||
|
||||
async getBomData() {
|
||||
|
|
@ -69,7 +85,7 @@ export class BomOverviewComponent extends Component {
|
|||
];
|
||||
const context = { ...this.context};
|
||||
if (this.state.currentWarehouse) {
|
||||
context.warehouse = this.state.currentWarehouse.id;
|
||||
context.warehouse_id = this.state.currentWarehouse.id;
|
||||
}
|
||||
const bomData = await this.orm.call(
|
||||
"report.mrp.report_bom_structure",
|
||||
|
|
@ -99,8 +115,8 @@ export class BomOverviewComponent extends Component {
|
|||
ids.forEach(id => this.unfoldedIds[operation](id));
|
||||
}
|
||||
|
||||
onChangeDisplay(displayInfo) {
|
||||
this.state.showOptions[displayInfo] = !this.state.showOptions[displayInfo];
|
||||
onChangeMode(mode) {
|
||||
this.state.showOptions.mode = mode;
|
||||
}
|
||||
|
||||
async onChangeBomQuantity(newQuantity) {
|
||||
|
|
@ -109,7 +125,7 @@ export class BomOverviewComponent extends Component {
|
|||
await this.getBomData();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async onChangeVariant(variantId) {
|
||||
if (this.state.currentVariantId != variantId) {
|
||||
this.state.currentVariantId = variantId;
|
||||
|
|
@ -143,10 +159,7 @@ export class BomOverviewComponent extends Component {
|
|||
|
||||
getReportName(printAll) {
|
||||
let reportName = "mrp.report_bom_structure?docids=" + this.activeId +
|
||||
"&availabilities=" + this.state.showOptions.availabilities +
|
||||
"&costs=" + this.state.showOptions.costs +
|
||||
"&operations=" + this.state.showOptions.operations +
|
||||
"&lead_times=" + this.state.showOptions.leadTimes +
|
||||
"&mode=" + this.state.showOptions.mode +
|
||||
"&quantity=" + (this.state.bomQuantity || 1) +
|
||||
"&unfolded_ids=" + JSON.stringify(Array.from(this.unfoldedIds)) +
|
||||
"&warehouse_id=" + (this.state.currentWarehouse ? this.state.currentWarehouse.id : false);
|
||||
|
|
@ -159,10 +172,4 @@ export class BomOverviewComponent extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
BomOverviewComponent.template = "mrp.BomOverviewComponent";
|
||||
BomOverviewComponent.components = {
|
||||
BomOverviewControlPanel,
|
||||
BomOverviewTable,
|
||||
};
|
||||
|
||||
registry.category("actions").add("mrp_bom_report", BomOverviewComponent);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<div t-name="mrp.BomOverviewComponent" class="o_action" owl="1">
|
||||
<div t-name="mrp.BomOverviewComponent" class="o_action o_mrp_overview">
|
||||
|
||||
<BomOverviewControlPanel
|
||||
bomQuantity="state.bomQuantity"
|
||||
uomName="uomName"
|
||||
variants="variants"
|
||||
data="state.bomData"
|
||||
showOptions="state.showOptions"
|
||||
showVariants="showVariants"
|
||||
currentWarehouse="state.currentWarehouse"
|
||||
|
|
@ -14,8 +16,10 @@
|
|||
changeWarehouse.bind="onChangeWarehouse"
|
||||
changeVariant.bind="onChangeVariant"
|
||||
changeBomQuantity.bind="onChangeBomQuantity"
|
||||
changeDisplay.bind="onChangeDisplay"
|
||||
changeMode.bind="onChangeMode"
|
||||
precision="state.precision"
|
||||
foldable="state.foldable"
|
||||
allFolded="state.allFolded"
|
||||
/>
|
||||
|
||||
<BomOverviewTable
|
||||
|
|
@ -24,6 +28,7 @@
|
|||
currentWarehouseId="state.currentWarehouse.id"
|
||||
data="state.bomData"
|
||||
precision="state.precision"
|
||||
bomQuantity="state.bomQuantity"
|
||||
changeFolded.bind="onChangeFolded"/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,27 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useBus } from "@web/core/utils/hooks";
|
||||
import { BomOverviewLine } from "../bom_overview_line/mrp_bom_overview_line";
|
||||
import { BomOverviewExtraBlock } from "../bom_overview_extra_block/mrp_bom_overview_extra_block";
|
||||
|
||||
const { Component, onWillUnmount, onWillUpdateProps, useState } = owl;
|
||||
import { Component, onWillUnmount, onWillUpdateProps, useState } from "@odoo/owl";
|
||||
|
||||
export class BomOverviewComponentsBlock extends Component {
|
||||
static template = "mrp.BomOverviewComponentsBlock";
|
||||
static components = {
|
||||
BomOverviewLine,
|
||||
BomOverviewComponentsBlock,
|
||||
BomOverviewExtraBlock,
|
||||
};
|
||||
static props = {
|
||||
unfoldAll: { type: Boolean, optional: true },
|
||||
showOptions: Object,
|
||||
currentWarehouseId: { type: Number, optional: true },
|
||||
data: Object,
|
||||
precision: Number,
|
||||
changeFolded: Function,
|
||||
};
|
||||
static defaultProps = {
|
||||
unfoldAll: false,
|
||||
};
|
||||
|
||||
setup() {
|
||||
const childFoldstate = this.childIds.reduce((prev, curr) => ({ ...prev, [curr]: !this.props.unfoldAll}), {});
|
||||
this.state = useState({
|
||||
|
|
@ -18,7 +33,7 @@ export class BomOverviewComponentsBlock extends Component {
|
|||
}
|
||||
|
||||
if (this.hasComponents) {
|
||||
useBus(this.env.overviewBus, "unfold-all", () => this._unfoldAll());
|
||||
useBus(this.env.overviewBus, "toggle-fold-all", () => this._toggleFoldAll());
|
||||
}
|
||||
|
||||
onWillUpdateProps(newProps => {
|
||||
|
|
@ -45,11 +60,12 @@ export class BomOverviewComponentsBlock extends Component {
|
|||
this.props.changeFolded({ ids: [foldId], isFolded: newState });
|
||||
}
|
||||
|
||||
_unfoldAll() {
|
||||
_toggleFoldAll() {
|
||||
const allChildIds = this.childIds;
|
||||
this.state.unfoldAll = true;
|
||||
allChildIds.forEach(id => this.state[id] = false);
|
||||
this.props.changeFolded({ ids: allChildIds, isFolded: false });
|
||||
|
||||
this.state.unfoldAll = !this.state.unfoldAll;
|
||||
allChildIds.forEach(id => this.state[id] = !this.state.unfoldAll);
|
||||
this.props.changeFolded({ ids: allChildIds, isFolded: !this.state.unfoldAll });
|
||||
}
|
||||
|
||||
//---- Getters ----
|
||||
|
|
@ -64,16 +80,12 @@ export class BomOverviewComponentsBlock extends Component {
|
|||
|
||||
get childIds() {
|
||||
return this.hasComponents ? this.data.components.map(c => this.getIdentifier(c)) : [];
|
||||
}
|
||||
}
|
||||
|
||||
get identifier() {
|
||||
return this.getIdentifier(this.data);
|
||||
}
|
||||
|
||||
get showOperations() {
|
||||
return this.props.showOptions.operations;
|
||||
}
|
||||
|
||||
//---- Utils ----
|
||||
|
||||
getHasComponents(data) {
|
||||
|
|
@ -84,21 +96,3 @@ export class BomOverviewComponentsBlock extends Component {
|
|||
return `${type ? type : data.type}_${data.index}`;
|
||||
}
|
||||
}
|
||||
|
||||
BomOverviewComponentsBlock.template = "mrp.BomOverviewComponentsBlock";
|
||||
BomOverviewComponentsBlock.components = {
|
||||
BomOverviewLine,
|
||||
BomOverviewComponentsBlock,
|
||||
BomOverviewExtraBlock,
|
||||
};
|
||||
BomOverviewComponentsBlock.props = {
|
||||
unfoldAll: { type: Boolean, optional: true },
|
||||
showOptions: Object,
|
||||
currentWarehouseId: { type: Number, optional: true },
|
||||
data: Object,
|
||||
precision: Number,
|
||||
changeFolded: Function,
|
||||
};
|
||||
BomOverviewComponentsBlock.defaultProps = {
|
||||
unfoldAll: false,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mrp.BomOverviewComponentsBlock" owl="1">
|
||||
<t t-name="mrp.BomOverviewComponentsBlock">
|
||||
<t name="components" t-if="hasComponents">
|
||||
<t t-foreach="data.components" t-as="line" t-key="line.index">
|
||||
<BomOverviewLine
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
</t>
|
||||
</t>
|
||||
</t>
|
||||
<t name="operations" t-if="showOperations && !!data.operations && data.operations.length > 0">
|
||||
<t name="operations" t-if="!!data.operations && data.operations.length > 0">
|
||||
<BomOverviewExtraBlock
|
||||
unfoldAll="state.unfoldAll"
|
||||
type="'operations'"
|
||||
|
|
|
|||
|
|
@ -1,17 +1,53 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { ControlPanel } from "@web/search/control_panel/control_panel";
|
||||
import { BomOverviewDisplayFilter } from "../bom_overview_display_filter/mrp_bom_overview_display_filter";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
|
||||
const { Component } = owl;
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { Many2XAutocomplete } from "@web/views/fields/relational_utils";
|
||||
import { Component, onMounted, useRef } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class BomOverviewControlPanel extends Component {
|
||||
static template = "mrp.BomOverviewControlPanel";
|
||||
static components = {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
ControlPanel,
|
||||
BomOverviewDisplayFilter,
|
||||
Many2XAutocomplete,
|
||||
};
|
||||
static props = {
|
||||
bomQuantity: Number,
|
||||
showOptions: Object,
|
||||
showVariants: { type: Boolean, optional: true },
|
||||
variants: { type: Object, optional: true },
|
||||
data: { type: Object, optional: true },
|
||||
uomName: { type: String, optional: true },
|
||||
currentWarehouse: Object,
|
||||
warehouses: { type: Array, optional: true },
|
||||
print: Function,
|
||||
changeWarehouse: Function,
|
||||
changeVariant: Function,
|
||||
changeBomQuantity: Function,
|
||||
changeMode: Function,
|
||||
precision: Number,
|
||||
foldable: Boolean,
|
||||
allFolded: Boolean,
|
||||
};
|
||||
static defaultProps = {
|
||||
variants: {},
|
||||
warehouses: [],
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.action = useService("action");
|
||||
this.controlPanelDisplay = {};
|
||||
// Cannot use 'control-panel-bottom-right' slot without this, as viewSwitcherEntries doesn't exist in this.env.config here.
|
||||
this.env.config.viewSwitcherEntries = [];
|
||||
if(this.props.showOptions.mode == "forecast") {
|
||||
this.quantity = useRef("quantity");
|
||||
onMounted(() => {
|
||||
this.quantity.el.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//---- Handlers ----
|
||||
|
|
@ -22,45 +58,51 @@ export class BomOverviewControlPanel extends Component {
|
|||
}
|
||||
|
||||
onKeyPress(ev) {
|
||||
if (ev.keyCode === 13 || ev.which === 13) {
|
||||
if (ev.key === "Enter") {
|
||||
ev.preventDefault();
|
||||
this.updateQuantity(ev);
|
||||
}
|
||||
}
|
||||
|
||||
clickUnfold() {
|
||||
this.env.overviewBus.trigger("unfold-all");
|
||||
clickTogglefold() {
|
||||
this.env.overviewBus.trigger("toggle-fold-all");
|
||||
}
|
||||
|
||||
getDomain() {
|
||||
const keys = Object.keys(this.props.variants);
|
||||
return [['id', 'in', keys]];
|
||||
}
|
||||
|
||||
async manufactureFromBoM() {
|
||||
const action = {
|
||||
res_model: "mrp.production",
|
||||
name: "Manufacture Orders",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "form"]],
|
||||
target: "current",
|
||||
context: {
|
||||
default_bom_id: this.props.data.bom_id,
|
||||
bom_overview_picking_type_id: this.props.currentWarehouse.manu_type_id[0],
|
||||
bom_overview_product_qty: this.props.bomQuantity,
|
||||
},
|
||||
};
|
||||
return this.action.doAction(action);
|
||||
}
|
||||
|
||||
get foldButtonText() {
|
||||
return this.props.allFolded ? _t("Unfold") : _t("Fold");
|
||||
}
|
||||
|
||||
get precision() {
|
||||
return this.props.precision;
|
||||
}
|
||||
}
|
||||
|
||||
BomOverviewControlPanel.template = "mrp.BomOverviewControlPanel";
|
||||
BomOverviewControlPanel.components = {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
ControlPanel,
|
||||
BomOverviewDisplayFilter,
|
||||
};
|
||||
BomOverviewControlPanel.props = {
|
||||
bomQuantity: Number,
|
||||
showOptions: Object,
|
||||
showVariants: { type: Boolean, optional: true },
|
||||
variants: { type: Object, optional: true },
|
||||
showUom: { type: Boolean, optional: true },
|
||||
uomName: { type: String, optional: true },
|
||||
currentWarehouse: Object,
|
||||
warehouses: { type: Array, optional: true },
|
||||
print: Function,
|
||||
changeWarehouse: Function,
|
||||
changeVariant: Function,
|
||||
changeBomQuantity: Function,
|
||||
changeDisplay: Function,
|
||||
precision: Number,
|
||||
};
|
||||
BomOverviewControlPanel.defaultProps = {
|
||||
variants: {},
|
||||
warehouses: [],
|
||||
};
|
||||
get warehousesItems() {
|
||||
return this.props.warehouses.map(wh => ({
|
||||
id: wh.id,
|
||||
label: wh.name,
|
||||
class: { selected: wh.name === this.props.currentWarehouse.name },
|
||||
onSelected: () => this.props.changeWarehouse(wh.id)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,62 +1,70 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mrp.BomOverviewControlPanel" owl="1">
|
||||
<t t-name="mrp.BomOverviewControlPanel">
|
||||
<ControlPanel display="controlPanelDisplay">
|
||||
<t t-set-slot="control-panel-bottom-left-buttons">
|
||||
<div class="o_cp_buttons">
|
||||
<div class="o_list_buttons o_mrp_bom_report_buttons">
|
||||
<button t-on-click="() => this.props.print()" type="button" class="btn btn-primary">Print</button>
|
||||
<t t-if="props.showVariants">
|
||||
<button t-on-click="() => this.props.print(true)" type="button" class="btn btn-primary ms-1">Print All Variants</button>
|
||||
</t>
|
||||
<button t-on-click="clickUnfold" type="button" class="btn btn-primary ms-1">Unfold</button>
|
||||
<t t-set-slot="control-panel-create-button">
|
||||
<button t-if="props.showOptions.mode == 'forecast'" t-on-click="manufactureFromBoM" type="button" class="btn btn-primary">Manufacture</button>
|
||||
</t>
|
||||
<t t-set-slot="control-panel-always-buttons">
|
||||
<button t-on-click="() => this.props.print()" type="button" class="btn btn-secondary">Print</button>
|
||||
<!-- <t t-if="props.showVariants"> commented, waiting for ui update
|
||||
<button t-on-click="() => this.props.print(true)" type="button" class="btn btn-secondary text-nowrap">Print All Variants</button>
|
||||
</t>
|
||||
</t> -->
|
||||
<button t-if="props.foldable" t-on-click="clickTogglefold" type="button" class="btn btn-secondary" t-esc="foldButtonText"/>
|
||||
</t>
|
||||
<t t-set-slot="layout-actions">
|
||||
<div t-if="props.showVariants" class="input-group align-items-center">
|
||||
<div class="col-4 col-md-auto pe-2 fw-bold">Variant</div>
|
||||
<div class="col">
|
||||
<Many2XAutocomplete
|
||||
value="props.data.name"
|
||||
getDomain.bind="getDomain"
|
||||
resModel="'product.product'"
|
||||
fieldString="props.data.name"
|
||||
activeActions="{}"
|
||||
update.bind="(ev) => this.props.changeVariant(ev[0]?.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-set-slot="control-panel-top-right">
|
||||
<form class="row gx-0">
|
||||
<div class="col-lg-8 row">
|
||||
<div class="col-lg-11 row gx-1 mb-2">
|
||||
<label for="bom_quantity" class="col-xl-2 col-lg-3 col-sm-4">Quantity:</label>
|
||||
<div t-attf-class="{{ props.showOptions.uom ? 'col-xl-7 col-lg-6 col-sm-5' : 'col-xl-10 col-lg-9 col-sm-8' }}">
|
||||
<input id="bom_quantity" type="number" step="any" t-on-change="ev => this.updateQuantity(ev)" t-on-keypress="ev => this.onKeyPress(ev)" t-att-value="props.bomQuantity" min="1" class="o_input"/>
|
||||
</div>
|
||||
<div t-if="props.showOptions.uom" class="col-xl-3 col-lg-3 col-sm-3">
|
||||
<span t-esc="props.uomName"/>
|
||||
</div>
|
||||
<div class="d-flex gap-1">
|
||||
<t t-if="props.showOptions.mode == 'forecast'">
|
||||
<div>
|
||||
<form class="d-flex flex-grow-1 gap-3 flex-column flex-md-row">
|
||||
<label class="visually-hidden" for="bom_quantity"/>
|
||||
<div t-attf-class="input-group align-items-center">
|
||||
<div class="col-4 col-md-auto px-2 fw-bold">Quantity</div>
|
||||
<input id="bom_quantity" type="number" step="any" t-on-change="ev => this.updateQuantity(ev)" t-on-keypress="ev => this.onKeyPress(ev)" t-att-value="props.bomQuantity" min="1" size="7" class="o_input form-control rounded-0" t-ref="quantity"/>
|
||||
<div t-if="props.showOptions.uom" t-out="props.uomName" class="d-flex align-items-center text-muted small lh-sm"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div t-if="props.showVariants" class="col-lg-11 row gx-1 mb-1">
|
||||
<label class="col-xl-2 col-lg-3 col-sm-4">Variant:</label>
|
||||
<div class="col-xl-10 col-lg-9 col-sm-8">
|
||||
<select class="o_input" t-on-change="(ev) => this.props.changeVariant(ev.target.value)">
|
||||
<option t-foreach="props.variants" t-as="variant" t-att-value="variant" t-key="variant">
|
||||
<t t-esc="props.variants[variant]"/>
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-set-slot="control-panel-bottom-right">
|
||||
<div class="o_search_options">
|
||||
<t t-if="props.warehouses.length > 1">
|
||||
<Dropdown class="'btn-group'" togglerClass="'btn btn-secondary'">
|
||||
<t t-set-slot="toggler">
|
||||
<span class="fa fa-home"/> Warehouse: <span t-esc="props.currentWarehouse.name"/>
|
||||
</t>
|
||||
<t t-foreach="props.warehouses" t-as="wh" t-key="wh.id">
|
||||
<DropdownItem onSelected="() => this.props.changeWarehouse(wh.id)" t-esc="wh.name"/>
|
||||
</t>
|
||||
<t t-set-slot="control-panel-navigation-additional">
|
||||
<t t-if="props.showOptions.mode == 'forecast'">
|
||||
<t t-if="props.warehouses.length > 1" class="btn-group flex-grow-1 flex-md-grow-0">
|
||||
<Dropdown items="warehousesItems">
|
||||
<button class="btn btn-secondary o-dropdown-caret">
|
||||
<span class="fa fa-home"/> Warehouse: <t t-out="props.currentWarehouse.name"/>
|
||||
</button>
|
||||
</Dropdown>
|
||||
</t>
|
||||
<BomOverviewDisplayFilter
|
||||
showOptions="props.showOptions"
|
||||
changeDisplay.bind="props.changeDisplay"/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="props.showOptions.mode == 'overview'">
|
||||
<div class="o_row" t-on-click="() => this.props.changeMode('forecast')" >
|
||||
<i class="btn fa fa-toggle-off fa-lg pe-1"/>
|
||||
<span>Forecast</span>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="o_row" t-on-click="() => this.props.changeMode('overview')" >
|
||||
<i class="btn fa fa-toggle-on fa-lg pe-1"/>
|
||||
<span>Forecast</span>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</ControlPanel>
|
||||
</t>
|
||||
|
|
|
|||
|
|
@ -1,46 +1,33 @@
|
|||
/** @odoo-module **/
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
|
||||
const { Component } = owl;
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class BomOverviewDisplayFilter extends Component {
|
||||
static template = "mrp.BomOverviewDisplayFilter";
|
||||
static components = {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
};
|
||||
static props = {
|
||||
showOptions: {
|
||||
type: Object,
|
||||
},
|
||||
changeDisplay: Function,
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.displayOptions = {
|
||||
availabilities: this.env._t('Availabilities'),
|
||||
leadTimes: this.env._t('Lead Times'),
|
||||
costs: this.env._t('Costs'),
|
||||
operations: this.env._t('Operations'),
|
||||
};
|
||||
this.displayOptions = {};
|
||||
}
|
||||
|
||||
//---- Getters ----
|
||||
|
||||
get displayableOptions() {
|
||||
return Object.keys(this.displayOptions);
|
||||
}
|
||||
|
||||
get currentDisplayedNames() {
|
||||
return this.displayableOptions.filter(key => this.props.showOptions[key]).map(key => this.displayOptions[key]).join(", ");
|
||||
return Object.keys(this.displayOptions).map(optionKey => ({
|
||||
id: optionKey,
|
||||
label: this.displayOptions[optionKey],
|
||||
onSelected: () => this.props.changeDisplay(optionKey),
|
||||
class: { o_menu_item: true, selected: this.props.showOptions[optionKey] },
|
||||
closingMode: "none",
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
BomOverviewDisplayFilter.template = "mrp.BomOverviewDisplayFilter";
|
||||
BomOverviewDisplayFilter.components = {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
}
|
||||
BomOverviewDisplayFilter.props = {
|
||||
showOptions: {
|
||||
type: Object,
|
||||
shape: {
|
||||
availabilities: Boolean,
|
||||
costs: Boolean,
|
||||
operations: Boolean,
|
||||
leadTimes: Boolean,
|
||||
uom: Boolean,
|
||||
attachments: Boolean,
|
||||
},
|
||||
},
|
||||
changeDisplay: Function,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mrp.BomOverviewDisplayFilter" owl="1">
|
||||
<Dropdown class="'btn-group'" togglerClass="'btn btn-secondary'">
|
||||
<t t-set-slot="toggler">
|
||||
<span class="fa fa-filter"/>
|
||||
Display: <span t-esc="currentDisplayedNames"/>
|
||||
</t>
|
||||
<t t-foreach="displayableOptions" t-as="optionKey" t-key="optionKey">
|
||||
<DropdownItem parentClosingMode="'none'" class="{ o_menu_item: true, selected: props.showOptions[optionKey] }" onSelected="() => this.props.changeDisplay(optionKey)" t-esc="displayOptions[optionKey]"/>
|
||||
</t>
|
||||
</Dropdown>
|
||||
<t t-name="mrp.BomOverviewDisplayFilter">
|
||||
<div class="btn-group flex-grow-1 flex-md-grow-0'">
|
||||
<Dropdown items="displayableOptions">
|
||||
<button class="btn btn-secondary o-dropdown-caret">
|
||||
<span class="fa fa-filter"/>
|
||||
Display
|
||||
</button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,26 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useBus } from "@web/core/utils/hooks";
|
||||
import { BomOverviewLine } from "../bom_overview_line/mrp_bom_overview_line";
|
||||
import { BomOverviewSpecialLine } from "../bom_overview_special_line/mrp_bom_overview_special_line";
|
||||
|
||||
const { Component, onWillUnmount, onWillUpdateProps, useState } = owl;
|
||||
import { Component, onWillUnmount, onWillUpdateProps, useState } from "@odoo/owl";
|
||||
|
||||
export class BomOverviewExtraBlock extends Component {
|
||||
static template = "mrp.BomOverviewExtraBlock";
|
||||
static components = {
|
||||
BomOverviewLine,
|
||||
BomOverviewSpecialLine,
|
||||
};
|
||||
static props = {
|
||||
unfoldAll: { type: Boolean, optional: true },
|
||||
type: {
|
||||
type: String,
|
||||
validate: (t) => ["operations", "byproducts"].includes(t),
|
||||
},
|
||||
showOptions: Object,
|
||||
data: Object,
|
||||
precision: Number,
|
||||
changeFolded: Function,
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.state = useState({
|
||||
isFolded: !this.props.unfoldAll,
|
||||
|
|
@ -15,7 +29,7 @@ export class BomOverviewExtraBlock extends Component {
|
|||
this.props.changeFolded({ ids: [this.identifier], isFolded: false });
|
||||
}
|
||||
|
||||
useBus(this.env.overviewBus, "unfold-all", () => this._unfold());
|
||||
useBus(this.env.overviewBus, "toggle-fold-all", () => this._toggleFoldAll());
|
||||
|
||||
onWillUpdateProps(newProps => {
|
||||
if (this.props.data.product_id != newProps.data.product_id) {
|
||||
|
|
@ -37,9 +51,9 @@ export class BomOverviewExtraBlock extends Component {
|
|||
this.props.changeFolded({ ids: [this.identifier], isFolded: newState });
|
||||
}
|
||||
|
||||
_unfold() {
|
||||
this.state.isFolded = false;
|
||||
this.props.changeFolded({ ids: [this.identifier], isFolded: false })
|
||||
_toggleFoldAll() {
|
||||
this.state.isFolded = !this.state.isFolded;
|
||||
this.props.changeFolded({ ids: [this.identifier], isFolded: this.state.isFolded });
|
||||
}
|
||||
|
||||
//---- Getters ----
|
||||
|
|
@ -48,25 +62,3 @@ export class BomOverviewExtraBlock extends Component {
|
|||
return `${this.props.type}_${this.props.data.index}`;
|
||||
}
|
||||
}
|
||||
|
||||
BomOverviewExtraBlock.template = "mrp.BomOverviewExtraBlock";
|
||||
BomOverviewExtraBlock.components = {
|
||||
BomOverviewLine,
|
||||
BomOverviewSpecialLine,
|
||||
};
|
||||
BomOverviewExtraBlock.props = {
|
||||
unfoldAll: { type: Boolean, optional: true },
|
||||
type: {
|
||||
type: String,
|
||||
validate: t => ["operations", "byproducts"].includes(t),
|
||||
},
|
||||
showOptions: Object,
|
||||
data: Object,
|
||||
precision: Number,
|
||||
changeFolded: Function,
|
||||
};
|
||||
BomOverviewExtraBlock.defaultProps = {
|
||||
showAvailabilities: false,
|
||||
showCosts: false,
|
||||
extraColumnCount: 0,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mrp.BomOverviewExtraBlock" owl="1">
|
||||
<t t-name="mrp.BomOverviewExtraBlock">
|
||||
<BomOverviewSpecialLine
|
||||
type="props.type"
|
||||
isFolded="state.isFolded"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,30 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { formatFloat, formatFloatTime, formatMonetary } from "@web/views/fields/formatters";
|
||||
|
||||
const { Component } = owl;
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class BomOverviewLine extends Component {
|
||||
static template = "mrp.BomOverviewLine";
|
||||
static props = {
|
||||
isFolded: { type: Boolean, optional: true },
|
||||
showOptions: {
|
||||
type: Object,
|
||||
shape: {
|
||||
mode: String,
|
||||
uom: Boolean,
|
||||
attachments: Boolean,
|
||||
},
|
||||
},
|
||||
currentWarehouseId: { type: Number, optional: true },
|
||||
data: Object,
|
||||
precision: Number,
|
||||
toggleFolded: { type: Function, optional: true },
|
||||
};
|
||||
static defaultProps = {
|
||||
isFolded: true,
|
||||
toggleFolded: () => {},
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.actionService = useService("action");
|
||||
this.ormService = useService("orm");
|
||||
|
|
@ -46,20 +65,27 @@ export class BomOverviewLine extends Component {
|
|||
active_id: this.data.link_id,
|
||||
};
|
||||
if (this.props.currentWarehouseId) {
|
||||
action.context["warehouse"] = this.props.currentWarehouseId;
|
||||
action.context["warehouse_id"] = this.props.currentWarehouseId;
|
||||
}
|
||||
return this.actionService.doAction(action);
|
||||
}
|
||||
|
||||
async goToAttachment() {
|
||||
return this.actionService.doAction({
|
||||
name: this.env._t("Attachments"),
|
||||
name: _t("Attachments"),
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "mrp.document",
|
||||
domain: [["id", "in", this.data.attachment_ids]],
|
||||
res_model: "product.document",
|
||||
domain: ['&', ["attached_on_mrp", "=", "bom"], '|',
|
||||
'&',["res_model", "=", "product.product"],["res_id", "in", [this.data.product_id]],
|
||||
'&',["res_model", "=", "product.template"],["res_id", "in", [this.data.product_template_id]]],
|
||||
views: [[false, "kanban"], [false, "list"], [false, "form"]],
|
||||
view_mode: "kanban,list,form",
|
||||
target: "current",
|
||||
context:{
|
||||
'bom_id': true,
|
||||
'default_res_id': this.data.product_id,
|
||||
'default_res_model': "product.product"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -81,8 +107,12 @@ export class BomOverviewLine extends Component {
|
|||
return this.data.components && this.data.components.length > 0;
|
||||
}
|
||||
|
||||
get hasOperations() {
|
||||
return this.data.operations && this.data.operations.length > 0;
|
||||
}
|
||||
|
||||
get hasQuantity() {
|
||||
return this.data.hasOwnProperty('quantity_available') && this.data.quantity_available !== false;
|
||||
return this.data.is_storable && this.data.hasOwnProperty('quantity_available') && this.data.quantity_available !== false;
|
||||
}
|
||||
|
||||
get hasLeadTime() {
|
||||
|
|
@ -90,23 +120,15 @@ export class BomOverviewLine extends Component {
|
|||
}
|
||||
|
||||
get hasFoldButton() {
|
||||
return this.data.level > 0 && this.hasComponents;
|
||||
return this.data.level > 0 && (this.hasComponents || this.hasOperations);
|
||||
}
|
||||
|
||||
get marginMultiplicator() {
|
||||
return this.data.level - (this.hasFoldButton ? 1 : 0);
|
||||
}
|
||||
|
||||
get showAvailabilities() {
|
||||
return this.props.showOptions.availabilities;
|
||||
}
|
||||
|
||||
get showCosts() {
|
||||
return this.props.showOptions.costs;
|
||||
}
|
||||
|
||||
get showLeadTimes() {
|
||||
return this.props.showOptions.leadTimes;
|
||||
get forecastMode() {
|
||||
return this.props.showOptions.mode == "forecast";
|
||||
}
|
||||
|
||||
get showUom() {
|
||||
|
|
@ -146,28 +168,11 @@ export class BomOverviewLine extends Component {
|
|||
return "action_product_tmpl_forecast_report";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BomOverviewLine.template = "mrp.BomOverviewLine";
|
||||
BomOverviewLine.props = {
|
||||
isFolded: { type: Boolean, optional: true },
|
||||
showOptions: {
|
||||
type: Object,
|
||||
shape: {
|
||||
availabilities: Boolean,
|
||||
costs: Boolean,
|
||||
operations: Boolean,
|
||||
leadTimes: Boolean,
|
||||
uom: Boolean,
|
||||
attachments: Boolean,
|
||||
},
|
||||
},
|
||||
currentWarehouseId: { type: Number, optional: true },
|
||||
data: Object,
|
||||
precision: Number,
|
||||
toggleFolded: { type: Function, optional: true },
|
||||
};
|
||||
BomOverviewLine.defaultProps = {
|
||||
isFolded: true,
|
||||
toggleFolded: () => {},
|
||||
};
|
||||
get statusBackgroundClass() {
|
||||
if(this.data.index == "0") {
|
||||
return "text-bg-info";
|
||||
}
|
||||
return "text-bg-danger";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<tr t-name="mrp.BomOverviewLine" owl="1">
|
||||
<tr t-name="mrp.BomOverviewLine" t-on-click="() => this.props.toggleFolded(identifier)">
|
||||
<td name="td_mrp_bom">
|
||||
<div t-attf-style="margin-left: {{ marginMultiplicator * 20 }}px">
|
||||
<t t-if="data.level > 0 && hasComponents">
|
||||
<button t-on-click="() => this.props.toggleFolded(identifier)" class="o_mrp_bom_unfoldable btn btn-light p-0" t-attf-aria-label="{{ props.isFolded ? 'Unfold' : 'Fold' }}" t-attf-title="{{ props.isFolded ? 'Unfold' : 'Fold' }}" style="margin-right: 1px">
|
||||
<div class="text-truncate" t-attf-style="margin-left: {{ marginMultiplicator * 20 }}px">
|
||||
<t t-if="hasFoldButton">
|
||||
<t t-set="title_fold">Fold</t>
|
||||
<t t-set="title_unfold">Unfold</t>
|
||||
<span class="o_mrp_bom_unfoldable btn btn-light p-0" t-att-aria-label="props.isFolded ? title_unfold : title_fold" t-att-title="props.isFolded ? title_unfold : title_fold" style="margin-right: 1px">
|
||||
<i t-attf-class="fa fa-fw fa-caret-{{ props.isFolded ? 'right' : 'down' }}" role="img"/>
|
||||
</button>
|
||||
</span>
|
||||
</t>
|
||||
<div t-attf-class="d-inline-block">
|
||||
<a href="#" t-on-click.prevent="() => this.goToAction(data.link_id, data.link_model)" t-esc="data.name"/>
|
||||
</div>
|
||||
<a href="#" t-on-click.prevent="() => this.goToAction(data.link_id, data.link_model)" t-esc="data.name"/>
|
||||
<t t-if="data.phantom_bom">
|
||||
<div class="fa fa-dropbox" title="This is a BoM of type Kit!" role="img" aria-label="This is a BoM of type Kit!"/>
|
||||
</t>
|
||||
|
|
@ -22,36 +22,38 @@
|
|||
<t t-else="" t-esc="formatFloat(data.quantity, {'digits': [false, precision]})"/>
|
||||
</td>
|
||||
<td t-if="showUom" class="text-start" t-esc="data.uom_name"/>
|
||||
<td t-if="showAvailabilities" class="text-end">
|
||||
<t t-if="data.hasOwnProperty('producible_qty')" t-esc="data.producible_qty"/>
|
||||
</td>
|
||||
<td t-if="showAvailabilities" class="text-end">
|
||||
<td t-if="forecastMode" class="text-end">
|
||||
<t t-if="hasQuantity">
|
||||
<t t-esc="formatFloat(data.quantity_available, {'digits': [false, precision]})"/> /
|
||||
<t t-esc="formatFloat(data.quantity_on_hand, {'digits': [false, precision]})"/>
|
||||
</t>
|
||||
</td>
|
||||
<td t-if="showAvailabilities" class="text-center">
|
||||
<td t-if="forecastMode" class="text-center">
|
||||
<div t-if="data.hasOwnProperty('status')" class="o_field_badge">
|
||||
<span t-attf-class="badge rounded-pill o_mrp_overview_badge {{statusBackgroundClass}}" t-out="data.status"/>
|
||||
</div>
|
||||
</td>
|
||||
<td t-if="forecastMode" class="text-center">
|
||||
<t t-if="data.hasOwnProperty('availability_state')">
|
||||
<span t-attf-class="{{availabilityColorClass}}" t-esc="data.availability_display"/>
|
||||
<a href="#" role="button" t-on-click.prevent="goToForecast" title="Forecast Report" t-attf-class="fa fa-fw fa-area-chart o_mrp_bom_forecast ms-1 {{availabilityColorClass}}"/>
|
||||
<t t-if="['bom', 'component'].includes(data.type)">
|
||||
<a href="#" role="button" t-on-click.prevent="goToForecast" title="Forecast Report" t-attf-class="fa fa-fw fa-area-chart o_mrp_bom_forecast ms-1 {{availabilityColorClass}}"/>
|
||||
</t>
|
||||
</t>
|
||||
</td>
|
||||
<td t-if="showLeadTimes" class="text-end">
|
||||
<td t-if="forecastMode" class="text-end">
|
||||
<span t-if="hasLeadTime"><t t-esc="data.lead_time"/> Days</span>
|
||||
</td>
|
||||
<td>
|
||||
<td t-if="forecastMode">
|
||||
<div t-if="data.route_name">
|
||||
<span t-attf-class="{{ data.route_alert ? 'text-danger' : '' }}"><t t-esc="data.route_name"/>: </span>
|
||||
<a href="#" t-on-click.prevent="() => this.goToRoute(data.route_type)" t-esc="data.route_detail"/>
|
||||
</div>
|
||||
</td>
|
||||
<td t-if="showCosts" t-attf-class="text-end {{ data.type == 'component' ? 'opacity-50' : '' }}" t-esc="formatMonetary(data.bom_cost)"/>
|
||||
<td t-if="showCosts" class="text-end">
|
||||
<span t-if="data.hasOwnProperty('prod_cost')" t-esc="formatMonetary(data.prod_cost)"/>
|
||||
</td>
|
||||
<td t-else=""/>
|
||||
<td t-attf-class="text-end {{ data.type == 'component' ? 'opacity-50' : '' }}" t-esc="formatMonetary(data.bom_cost)"/>
|
||||
<td t-if="showAttachments" class="text-center">
|
||||
<span t-if="!!data.attachment_ids && data.attachment_ids.length > 0">
|
||||
<span t-if="data.has_attachments">
|
||||
<a href="#" role="button" t-on-click.prevent="goToAttachment" class="fa fa-fw o_button_icon fa-files-o"/>
|
||||
</span>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,28 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { formatFloat, formatFloatTime, formatMonetary } from "@web/views/fields/formatters";
|
||||
|
||||
const { Component } = owl;
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class BomOverviewSpecialLine extends Component {
|
||||
static template = "mrp.BomOverviewSpecialLine";
|
||||
static props = {
|
||||
type: String,
|
||||
isFolded: { type: Boolean, optional: true },
|
||||
showOptions: {
|
||||
type: Object,
|
||||
shape: {
|
||||
mode: String,
|
||||
uom: Boolean,
|
||||
attachments: Boolean,
|
||||
},
|
||||
},
|
||||
data: Object,
|
||||
precision: Number,
|
||||
toggleFolded: { type: Function, optional: true },
|
||||
};
|
||||
static defaultProps = {
|
||||
isFolded: true,
|
||||
toggleFolded: () => {},
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.formatFloat = formatFloat;
|
||||
this.formatFloatTime = formatFloatTime;
|
||||
|
|
@ -25,16 +43,8 @@ export class BomOverviewSpecialLine extends Component {
|
|||
return ["operations", "byproducts"].includes(this.props.type);
|
||||
}
|
||||
|
||||
get showAvailabilities() {
|
||||
return this.props.showOptions.availabilities;
|
||||
}
|
||||
|
||||
get showCosts() {
|
||||
return this.props.showOptions.costs;
|
||||
}
|
||||
|
||||
get showLeadTimes() {
|
||||
return this.props.showOptions.leadTimes;
|
||||
get forecastMode() {
|
||||
return this.props.showOptions.mode == "forecast";
|
||||
}
|
||||
|
||||
get showUom() {
|
||||
|
|
@ -42,30 +52,6 @@ export class BomOverviewSpecialLine extends Component {
|
|||
}
|
||||
|
||||
get showAttachments() {
|
||||
return this.props.showOptions.attachments;
|
||||
return this.data.has_attachments;
|
||||
}
|
||||
}
|
||||
|
||||
BomOverviewSpecialLine.template = "mrp.BomOverviewSpecialLine";
|
||||
BomOverviewSpecialLine.props = {
|
||||
type: String,
|
||||
isFolded: { type: Boolean, optional: true },
|
||||
showOptions: {
|
||||
type: Object,
|
||||
shape: {
|
||||
availabilities: Boolean,
|
||||
costs: Boolean,
|
||||
operations: Boolean,
|
||||
leadTimes: Boolean,
|
||||
uom: Boolean,
|
||||
attachments: Boolean,
|
||||
},
|
||||
},
|
||||
data: Object,
|
||||
precision: Number,
|
||||
toggleFolded: { type: Function, optional: true },
|
||||
};
|
||||
BomOverviewSpecialLine.defaultProps = {
|
||||
isFolded: true,
|
||||
toggleFolded: () => {},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<tr t-name="mrp.BomOverviewSpecialLine" owl="1">
|
||||
<tr t-name="mrp.BomOverviewSpecialLine" t-on-click="props.toggleFolded" >
|
||||
<td name="td_mrp_bom">
|
||||
<span t-attf-style="margin-left: {{ data.level * 20 }}px"/>
|
||||
<button t-if="hasFoldButton" t-on-click="props.toggleFolded" t-attf-class="o_mrp_bom_{{ props.isFolded ? 'unfoldable' : 'foldable' }} btn btn-light ps-0 py-0" t-attf-aria-label="{{ props.isFolded ? 'Unfold' : 'Fold' }}" t-attf-title="{{ props.isFolded ? 'Unfold' : 'Fold' }}">
|
||||
<t t-set="title_fold">Fold</t>
|
||||
<t t-set="title_unfold">Unfold</t>
|
||||
<span t-if="hasFoldButton" t-attf-class="o_mrp_bom_{{ props.isFolded ? 'unfoldable' : 'foldable' }} btn btn-light ps-0 py-0" t-att-aria-label="props.isFolded ? title_unfold : title_fold" t-att-title="props.isFolded ? title_unfold : title_fold">
|
||||
<i t-attf-class="fa fa-fw fa-caret-{{ props.isFolded ? 'right' : 'down' }}" role="img"/>
|
||||
<t t-if="props.type == 'operations'">Operations</t>
|
||||
<t t-elif="props.type == 'byproducts'">By-Products</t>
|
||||
</button>
|
||||
</span>
|
||||
</td>
|
||||
<td name="quantity" class="text-end">
|
||||
<span t-if="props.type == 'operations'" t-esc="formatFloatTime(data.operations_time)"/>
|
||||
|
|
@ -17,16 +19,16 @@
|
|||
<td name="uom" t-if="showUom" class="text-start">
|
||||
<span t-if="props.type == 'operations'">Minutes</span>
|
||||
</td>
|
||||
<td t-if="showAvailabilities"/>
|
||||
<td t-if="showAvailabilities"/>
|
||||
<td t-if="showAvailabilities"/>
|
||||
<td t-if="showLeadTimes"/>
|
||||
<td/>
|
||||
<td name="bom_cost" t-if="showCosts" class="text-end">
|
||||
<td t-if="forecastMode"/>
|
||||
<td t-if="forecastMode"/>
|
||||
<td t-if="forecastMode"/>
|
||||
<td t-if="forecastMode"/>
|
||||
<td t-if="forecastMode"/>
|
||||
<td t-else=""/>
|
||||
<td name="bom_cost" class="text-end">
|
||||
<span t-if="props.type == 'operations'" t-esc="formatMonetary(data.operations_cost)"/>
|
||||
<span t-elif="props.type == 'byproducts'" t-esc="formatMonetary(data.byproducts_cost)"/>
|
||||
</td>
|
||||
<td name="prod_cost" t-if="showCosts" class="text-end"/>
|
||||
<td t-if="showAttachments"/>
|
||||
</tr>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,32 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { formatMonetary, formatFloat } from "@web/views/fields/formatters";
|
||||
import { formatFloat, formatMonetary } from "@web/views/fields/formatters";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { BomOverviewLine } from "../bom_overview_line/mrp_bom_overview_line";
|
||||
import { BomOverviewComponentsBlock } from "../bom_overview_components_block/mrp_bom_overview_components_block";
|
||||
|
||||
const { Component } = owl;
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class BomOverviewTable extends Component {
|
||||
static template = "mrp.BomOverviewTable";
|
||||
static components = {
|
||||
BomOverviewLine,
|
||||
BomOverviewComponentsBlock,
|
||||
};
|
||||
static props = {
|
||||
showOptions: {
|
||||
type: Object,
|
||||
shape: {
|
||||
mode: String,
|
||||
uom: Boolean,
|
||||
attachments: Boolean,
|
||||
},
|
||||
},
|
||||
uomName: { type: String, optional: true },
|
||||
currentWarehouseId: { type: Number, optional: true },
|
||||
data: Object,
|
||||
precision: Number,
|
||||
bomQuantity: Number,
|
||||
changeFolded: Function,
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.actionService = useService("action");
|
||||
this.formatFloat = formatFloat;
|
||||
|
|
@ -39,20 +58,12 @@ export class BomOverviewTable extends Component {
|
|||
return this.props.precision;
|
||||
}
|
||||
|
||||
get showAvailabilities() {
|
||||
return this.props.showOptions.availabilities;
|
||||
get forecastMode() {
|
||||
return this.props.showOptions.mode == "forecast";
|
||||
}
|
||||
|
||||
get showCosts() {
|
||||
return this.props.showOptions.costs;
|
||||
}
|
||||
|
||||
get showOperations() {
|
||||
return this.props.showOptions.operations;
|
||||
}
|
||||
|
||||
get showLeadTimes() {
|
||||
return this.props.showOptions.leadTimes;
|
||||
get showUnitCosts() {
|
||||
return this.props.bomQuantity > 1;
|
||||
}
|
||||
|
||||
get showUom() {
|
||||
|
|
@ -63,27 +74,3 @@ export class BomOverviewTable extends Component {
|
|||
return this.props.showOptions.attachments;
|
||||
}
|
||||
}
|
||||
|
||||
BomOverviewTable.template = "mrp.BomOverviewTable";
|
||||
BomOverviewTable.components = {
|
||||
BomOverviewLine,
|
||||
BomOverviewComponentsBlock,
|
||||
};
|
||||
BomOverviewTable.props = {
|
||||
showOptions: {
|
||||
type: Object,
|
||||
shape: {
|
||||
availabilities: Boolean,
|
||||
costs: Boolean,
|
||||
operations: Boolean,
|
||||
leadTimes: Boolean,
|
||||
uom: Boolean,
|
||||
attachments: Boolean,
|
||||
},
|
||||
},
|
||||
uomName: { type: String, optional: true },
|
||||
currentWarehouseId: { type: Number, optional: true },
|
||||
data: Object,
|
||||
precision: Number,
|
||||
changeFolded: Function,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<div t-name="mrp.BomOverviewTable" class="o_content" owl="1">
|
||||
<div t-name="mrp.BomOverviewTable" class="o_content">
|
||||
<div class="o_mrp_bom_report_page py-3 py-lg-5 px-0 overflow-auto border-bottom bg-view">
|
||||
<div t-if="!!data.components || !!data.lines || !!data.operations" class="container-fluid">
|
||||
<div class="d-flex mb-5">
|
||||
|
|
@ -10,31 +10,19 @@
|
|||
<hr t-if="data.bom_code"/>
|
||||
<h6 t-if="data.bom_code">Reference: <t t-esc="data.bom_code"/></h6>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<h3><t t-esc="formatFloat(data.quantity_available, {'digits': [false, precision]})"/> <t t-if="showUom" t-esc="props.uomName"/></h3>
|
||||
<span>Free to Use</span>
|
||||
</div>
|
||||
<div t-if="data.hasOwnProperty('earliest_capacity')" class="ps-5 text-center">
|
||||
<h3><t t-esc="formatFloat(data.earliest_capacity)"/> <t t-if="showUom" t-esc="props.uomName"/></h3>
|
||||
<span t-esc="data.earliest_date"/>
|
||||
</div>
|
||||
<div t-if="data.hasOwnProperty('leftover_capacity')" class="ps-5 text-center">
|
||||
<h3><t t-esc="formatFloat(data.leftover_capacity)"/> <t t-if="showUom" t-esc="props.uomName"/></h3>
|
||||
<span t-esc="data.leftover_date"/>
|
||||
</div>
|
||||
</div>
|
||||
<table class="o_mrp_bom_expandable table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th name="th_mrp_bom_h">Product</th>
|
||||
<th t-attf-class="{{ showUom ? 'text-center' : 'text-end' }}" t-attf-colspan="{{ showUom ? 2 : 1 }}">Quantity</th>
|
||||
<th t-if="showAvailabilities" class="text-end">Ready to Produce</th>
|
||||
<th t-if="showAvailabilities" class="text-end" title="Availabilities on products.">Free to Use / On Hand</th>
|
||||
<th t-if="showAvailabilities" class="text-center" title="Reception time estimation.">Availability</th>
|
||||
<th t-if="showLeadTimes" class="text-end" title="Resupply lead time.">Lead Time</th>
|
||||
<th>Route</th>
|
||||
<th t-if="showCosts" class="text-end" title="This is the cost based on the BoM of the product. It is computed by summing the costs of the components and operations needed to build the product.">BoM Cost</th>
|
||||
<th t-if="showCosts" class="text-end" title="This is the cost defined on the product.">Product Cost</th>
|
||||
<th t-if="forecastMode" class="text-end" title="Availabilities on products.">Free to Use / On Hand</th>
|
||||
<th t-if="forecastMode" class="text-center">Status</th>
|
||||
<th t-if="forecastMode" class="text-center" title="Reception time estimation.">Availability</th>
|
||||
<th t-if="forecastMode" class="text-end" title="Resupply lead time.">Lead Time</th>
|
||||
<th t-if="forecastMode">Route</th>
|
||||
<th t-else=""/>
|
||||
<th class="text-end" title="This is the cost based on the BoM of the product. It is computed by summing the costs of the components and operations needed to build the product.">Cost</th>
|
||||
<th t-if="showAttachments" class="text-center" title="Files attached to the product.">Attachments</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -53,34 +41,32 @@
|
|||
precision="props.precision"
|
||||
changeFolded.bind="props.changeFolded"/>
|
||||
</tbody>
|
||||
<tfoot t-if="showCosts">
|
||||
<tfoot t-if="showUnitCosts">
|
||||
<tr>
|
||||
<td name="td_mrp_bom_f" class="text-end">
|
||||
<span t-if="!!data.byproducts && data.byproducts.length > 0" t-esc="data.name"/>
|
||||
</td>
|
||||
<td class="text-end"><strong>Unit Cost</strong></td>
|
||||
<td t-if="showUom"/>
|
||||
<td t-if="showAvailabilities"/>
|
||||
<td t-if="showAvailabilities"/>
|
||||
<td t-if="showAvailabilities"/>
|
||||
<td t-if="showLeadTimes"/>
|
||||
<td/>
|
||||
<td class="text-end" t-esc="formatMonetary(data.bom_cost / data.quantity)"/>
|
||||
<td class="text-end" t-esc="formatMonetary(data.prod_cost / data.quantity)"/>
|
||||
<td t-if="showUom"><t t-esc="data.uom_name"/></td>
|
||||
<td t-if="forecastMode"/>
|
||||
<td t-if="forecastMode"/>
|
||||
<td t-if="forecastMode"/>
|
||||
<td t-if="forecastMode"/>
|
||||
<td class="text-end"><strong>Unit Cost</strong></td>
|
||||
<td class="text-end"><t t-esc="formatMonetary(data.bom_cost / data.quantity)"/></td>
|
||||
<td t-if="showAttachments"/>
|
||||
</tr>
|
||||
<t t-if="data.byproducts && data.byproducts.length > 0" t-foreach="data.byproducts" t-as="byproduct" t-key="byproduct.id">
|
||||
<tr>
|
||||
<td name="td_mrp_bom_b" class="text-end" t-esc="byproduct.name"/>
|
||||
<td class="text-end"><strong>Unit Cost</strong></td>
|
||||
<td t-if="showUom"/>
|
||||
<td t-if="showAvailabilities"/>
|
||||
<td t-if="showAvailabilities"/>
|
||||
<td t-if="showAvailabilities"/>
|
||||
<td t-if="showLeadTimes"/>
|
||||
<td/>
|
||||
<td class="text-end" t-esc="formatMonetary(byproduct.bom_cost / byproduct.quantity)"/>
|
||||
<td class="text-end" t-esc="formatMonetary(byproduct.prod_cost / byproduct.quantity)"/>
|
||||
<td t-if="showUom"><t t-esc="data.uom_name"/></td>
|
||||
<td t-if="forecastMode"/>
|
||||
<td t-if="forecastMode"/>
|
||||
<td t-if="forecastMode"/>
|
||||
<td t-if="forecastMode"/>
|
||||
<td class="text-end"><strong>Unit Cost</strong></td>
|
||||
<td class="text-end"><t t-esc="formatMonetary(byproduct.bom_cost / byproduct.quantity)"/></td>
|
||||
<td t-if="showAttachments"/>
|
||||
</tr>
|
||||
</t>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,206 @@
|
|||
import { Component, EventBus, onWillStart, useSubEnv, useState } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useBus, useService } from "@web/core/utils/hooks";
|
||||
import { Layout } from "@web/search/layout";
|
||||
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
|
||||
import { MoOverviewLine } from "../mo_overview_line/mrp_mo_overview_line";
|
||||
import { MoOverviewDisplayFilter } from "../mo_overview_display_filter/mrp_mo_overview_display_filter";
|
||||
import { MoOverviewComponentsBlock } from "../mo_overview_components_block/mrp_mo_overview_components_block";
|
||||
import { formatMonetary } from "@web/views/fields/formatters";
|
||||
|
||||
export class MoOverview extends Component {
|
||||
static components = {
|
||||
Layout,
|
||||
MoOverviewLine,
|
||||
MoOverviewDisplayFilter,
|
||||
MoOverviewComponentsBlock,
|
||||
};
|
||||
static props = { ...standardActionServiceProps };
|
||||
|
||||
static template = "mrp.MoOverview";
|
||||
|
||||
setup() {
|
||||
this.actionService = useService("action");
|
||||
this.ormService = useService("orm");
|
||||
this.unfoldedIds = new Set();
|
||||
this.context = {};
|
||||
|
||||
this.state = useState({
|
||||
data: {},
|
||||
showOptions: this.getDefaultConfig(),
|
||||
});
|
||||
|
||||
useSubEnv({ overviewBus: new EventBus() });
|
||||
|
||||
onWillStart(async () => {
|
||||
await this.getManufacturingData();
|
||||
});
|
||||
useBus(this.env.overviewBus, "update-folded", (ev) => this.onChangeFolded(ev.detail));
|
||||
useBus(this.env.overviewBus, "reload", () => this.getManufacturingData());
|
||||
}
|
||||
|
||||
async getManufacturingData() {
|
||||
const reportValues = await this.ormService.call(
|
||||
"report.mrp.report_mo_overview",
|
||||
"get_report_values",
|
||||
[this.activeId],
|
||||
);
|
||||
this.state.data = reportValues.data;
|
||||
if (this.isProductionStarted) {
|
||||
this.state.showOptions.bomCosts = false;
|
||||
} else {
|
||||
this.state.showOptions.realCosts = false;
|
||||
}
|
||||
if (this.isProductionDone) {
|
||||
// Hide Availabilities / Receipts / Status / MO Cost columns when the MO is done.
|
||||
this.state.showOptions.availabilities = false;
|
||||
this.state.showOptions.receipts = false;
|
||||
this.state.showOptions.replenishments = false;
|
||||
this.state.showOptions.unitCosts = true;
|
||||
this.state.showOptions.moCosts = false;
|
||||
}
|
||||
this.state.showOptions.uom = reportValues.context.show_uom;
|
||||
this.context = reportValues.context;
|
||||
// Main MO's operations & byproducts are always unfolded by default.
|
||||
if (reportValues.data?.operations?.summary?.index) {
|
||||
this.unfoldedIds.add(reportValues.data.operations.summary.index);
|
||||
}
|
||||
if (reportValues.data?.byproducts?.summary?.index) {
|
||||
this.unfoldedIds.add(reportValues.data.byproducts.summary.index);
|
||||
}
|
||||
}
|
||||
|
||||
//---- Handlers ----
|
||||
|
||||
onChangeDisplay(displayInfo) {
|
||||
this.state.showOptions[displayInfo] = !this.state.showOptions[displayInfo];
|
||||
}
|
||||
|
||||
onChangeFolded(foldInfo) {
|
||||
const { indexes, isFolded } = foldInfo;
|
||||
const operation = isFolded ? "delete" : "add";
|
||||
indexes.forEach(index => this.unfoldedIds[operation](index));
|
||||
}
|
||||
|
||||
async onPrint() {
|
||||
return this.actionService.doAction({
|
||||
type: "ir.actions.report",
|
||||
report_type: "qweb-pdf",
|
||||
report_name: this.reportName,
|
||||
report_file: "mrp.report_mo_overview",
|
||||
});
|
||||
}
|
||||
|
||||
onUnfold() {
|
||||
this.env.overviewBus.trigger("unfold-all")
|
||||
}
|
||||
|
||||
//---- Helpers ----
|
||||
|
||||
getDefaultConfig() {
|
||||
return {
|
||||
uom: false,
|
||||
replenishments: true,
|
||||
availabilities: true,
|
||||
receipts: true,
|
||||
unitCosts: false,
|
||||
moCosts: true,
|
||||
bomCosts: true,
|
||||
realCosts: true,
|
||||
};
|
||||
}
|
||||
|
||||
getColorClass(decorator) {
|
||||
return decorator ? `text-${decorator}` : "";
|
||||
}
|
||||
|
||||
formatCost(cost) {
|
||||
return formatMonetary(cost, { currencyId: this.state.data.summary.currency_id });
|
||||
}
|
||||
|
||||
//---- Getters ----
|
||||
|
||||
get activeId() {
|
||||
return this.props.action.context.active_id;
|
||||
}
|
||||
|
||||
get showUom() {
|
||||
return this.state.showOptions.uom;
|
||||
}
|
||||
|
||||
get showReplenishments() {
|
||||
return this.state.showOptions.replenishments;
|
||||
}
|
||||
|
||||
get showAvailabilities() {
|
||||
return this.state.showOptions.availabilities;
|
||||
}
|
||||
|
||||
get showReceipts() {
|
||||
return this.state.showOptions.receipts;
|
||||
}
|
||||
|
||||
get showUnitCosts() {
|
||||
return this.state.showOptions.unitCosts;
|
||||
}
|
||||
|
||||
get showMoCosts() {
|
||||
return this.state.showOptions.moCosts;
|
||||
}
|
||||
|
||||
get showBomCosts() {
|
||||
return this.state.showOptions.bomCosts;
|
||||
}
|
||||
|
||||
get showRealCosts() {
|
||||
return this.state.showOptions.realCosts;
|
||||
}
|
||||
|
||||
get hasBom() {
|
||||
return this.state.data?.summary?.has_bom;
|
||||
}
|
||||
|
||||
get isProductionStarted() {
|
||||
return !["draft", "confirmed"].includes(this.state.data?.summary?.state);
|
||||
}
|
||||
|
||||
get isProductionDraft() {
|
||||
return this.state.data?.summary?.state === "draft";
|
||||
}
|
||||
|
||||
get isProductionDone() {
|
||||
return this.state.data?.summary?.state === "done";
|
||||
}
|
||||
|
||||
get hasOperations() {
|
||||
return this.state.data?.operations?.details?.length > 0;
|
||||
}
|
||||
|
||||
get hasBreakdown() {
|
||||
return this.state.data?.cost_breakdown?.length > 0;
|
||||
}
|
||||
|
||||
get totalColspan() {
|
||||
let colspan = 2; // Name & Quantity
|
||||
if (this.showReplenishments) colspan++;
|
||||
if (this.showAvailabilities) colspan += 2; // Free to use / On Hand & Reserved
|
||||
if (this.showUom) colspan++;
|
||||
if (this.showReceipts) colspan++;
|
||||
if (this.showUnitCosts) colspan++;
|
||||
return colspan;
|
||||
}
|
||||
|
||||
get reportName() {
|
||||
return `mrp.report_mo_overview?docids=${this.activeId}`
|
||||
+ `&replenishments=${+this.state.showOptions.replenishments}`
|
||||
+ `&availabilities=${+this.state.showOptions.availabilities}`
|
||||
+ `&receipts=${+this.state.showOptions.receipts}`
|
||||
+ `&unitCosts=${+this.state.showOptions.unitCosts}`
|
||||
+ `&moCosts=${+this.state.showOptions.moCosts}`
|
||||
+ `&bomCosts=${+this.state.showOptions.bomCosts}`
|
||||
+ `&realCosts=${+this.state.showOptions.realCosts}`
|
||||
+ `&unfoldedIds=${JSON.stringify(Array.from(this.unfoldedIds))}`;
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("actions").add("mrp_mo_overview", MoOverview);
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<div t-name="mrp.MoOverview" class="o_action">
|
||||
<Layout display="{ controlPanel: {} }">
|
||||
<t t-set-slot="layout-actions">
|
||||
<MoOverviewDisplayFilter showOptions="state.showOptions" limited="isProductionDone" changeDisplay.bind="onChangeDisplay" isProductionDraft="isProductionDraft"/>
|
||||
</t>
|
||||
|
||||
<t t-set-slot="control-panel-create-button">
|
||||
<div class="d-flex gap-1">
|
||||
<button class="btn btn-primary" t-on-click="onPrint">Print</button>
|
||||
<button class="btn btn-primary" t-on-click="onUnfold">Unfold</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<div class="overflow-auto border-bottom bg-view container-fluid">
|
||||
<table name="overview" class="table">
|
||||
<thead class="o_mrp_mo_overview_thead">
|
||||
<tr>
|
||||
<th class="text-start"/>
|
||||
<th class="text-center" t-if="showReplenishments">Status</th>
|
||||
<th t-attf-class="{{ showUom ? 'text-center' : 'text-end' }}" t-attf-colspan="{{ showUom ? 2 : 1 }}">Quantity</th>
|
||||
<th class="text-end" t-if="showAvailabilities">Free to use / On Hand</th>
|
||||
<th class="text-end" t-if="showAvailabilities">Reserved</th>
|
||||
<th class="text-end" t-if="showReceipts">Receipt</th>
|
||||
<th class="text-end" t-if="showUnitCosts">Unit Cost</th>
|
||||
<th class="text-end" t-if="showMoCosts" title="Cost based on related replenishments. Otherwise cost from product form">MO Cost</th>
|
||||
<th class="text-end" t-if="showBomCosts" title="Cost based on the BoM">BoM Cost</th>
|
||||
<th class="text-end" t-if="showRealCosts and realInDraft" title="Cost based on cost projection">Real Cost</th>
|
||||
<th class="text-end" t-elif="showRealCosts" title="Cost as it is currently accumulated">Real Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<MoOverviewLine data="state.data.summary" showOptions="state.showOptions"/>
|
||||
<MoOverviewComponentsBlock
|
||||
components="state.data.components"
|
||||
operations="state.data.operations"
|
||||
byproducts="state.data.byproducts"
|
||||
showOptions="state.showOptions"/>
|
||||
</tbody>
|
||||
<tfoot t-if="showMoCosts or showRealCosts">
|
||||
<tr name="unitCost">
|
||||
<td class="text-end" t-att-colspan="totalColspan">Unit Cost</td>
|
||||
<td t-attf-class="text-end" t-if="showMoCosts" t-out="formatCost(state.data.extras.unit_mo_cost)"/>
|
||||
<td class="text-end" t-if="showBomCosts" t-out="formatCost(state.data.extras.unit_bom_cost)"/>
|
||||
<td t-attf-class="text-end" t-if="showRealCosts" t-out="formatCost(state.data.extras.unit_real_cost)"/>
|
||||
</tr>
|
||||
<t t-if="isProductionDone">
|
||||
<tr>
|
||||
<td class="text-end" t-att-colspan="totalColspan">Total Cost of Components</td>
|
||||
<td t-attf-class="text-end" t-if="showMoCosts" t-out="formatCost(state.data.extras.total_mo_cost_components)"/>
|
||||
<td class="text-end" t-if="showBomCosts" t-out="formatCost(state.data.extras.total_bom_cost_components)"/>
|
||||
<td t-attf-class="text-end {{ getColorClass(state.data.extras.total_real_cost_components_decorator) }}" t-if="showRealCosts" t-out="formatCost(state.data.extras.total_real_cost_components)"/>
|
||||
</tr>
|
||||
<tr t-if="state.data.summary.quantity != 1">
|
||||
<td class="text-end" t-att-colspan="totalColspan">
|
||||
Cost of Components per unit
|
||||
<t t-if="showUom">(in <t t-out='state.data.summary.uom_name'/>)</t>
|
||||
</td>
|
||||
<td t-attf-class="text-end" t-if="showMoCosts" t-out="formatCost(state.data.extras.unit_mo_cost_components)"/>
|
||||
<td class="text-end" t-if="showBomCosts" t-out="formatCost(state.data.extras.unit_bom_cost_components)"/>
|
||||
<td t-attf-class="text-end {{ getColorClass(state.data.extras.total_real_cost_components_decorator) }}" t-if="showRealCosts" t-out="formatCost(state.data.extras.unit_real_cost_components)"/>
|
||||
</tr>
|
||||
<tr t-if="hasOperations">
|
||||
<td class="text-end" t-att-colspan="totalColspan">Total Cost of Operations</td>
|
||||
<td t-attf-class="text-end {{ getColorClass(state.data.extras.total_mo_cost_operations_decorator) }}" t-if="showMoCosts" t-out="formatCost(state.data.extras.total_mo_cost_operations)"/>
|
||||
<td class="text-end" t-if="showBomCosts" t-out="formatCost(state.data.extras.total_bom_cost_operations)"/>
|
||||
<td t-attf-class="text-end" t-if="showRealCosts" t-out="formatCost(state.data.extras.total_real_cost_operations)"/>
|
||||
</tr>
|
||||
<tr t-if="hasOperations and state.data.summary.quantity != 1">
|
||||
<td class="text-end" t-att-colspan="totalColspan">
|
||||
Cost of Operations per unit
|
||||
<t t-if="showUom">(in <t t-out='state.data.summary.uom_name'/>)</t>
|
||||
</td>
|
||||
<td t-attf-class="text-end {{ getColorClass(state.data.extras.total_mo_cost_operations_decorator) }}" t-if="showMoCosts" t-out="formatCost(state.data.extras.unit_mo_cost_operations)"/>
|
||||
<td class="text-end" t-if="showBomCosts" t-out="formatCost(state.data.extras.unit_bom_cost_operations)"/>
|
||||
<td t-attf-class="text-end" t-if="showRealCosts" t-out="formatCost(state.data.extras.unit_real_cost_operations)"/>
|
||||
</tr>
|
||||
<tr t-if="hasBreakdown and hasOperations">
|
||||
<td class="text-end" t-att-colspan="totalColspan">Total Cost of Production</td>
|
||||
<td t-attf-class="text-end" t-if="showMoCosts" t-out="formatCost(state.data.extras.total_mo_cost)"/>
|
||||
<td class="text-end" t-if="showBomCosts" t-out="formatCost(state.data.extras.total_bom_cost)"/>
|
||||
<td t-attf-class="text-end" t-if="showRealCosts" t-out="formatCost(state.data.extras.total_real_cost)"/>
|
||||
</tr>
|
||||
</t>
|
||||
</tfoot>
|
||||
</table>
|
||||
<t t-if="hasBreakdown">
|
||||
<hr/>
|
||||
<h2 class="pt-3">Cost Breakdown of Products</h2>
|
||||
<table name="breakdown" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-start">Product</th>
|
||||
<th class="text-end">Avg Cost of Components per Unit</th>
|
||||
<th t-if="hasOperations" class="text-end">Avg Cost of Operations per Unit</th>
|
||||
<th class="text-end">Avg Total Cost per Unit</th>
|
||||
<th t-if="showUom" class="text-end">Unit of Measure</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="state.data.cost_breakdown" t-as="line" t-key="line.index">
|
||||
<td class="text-start" t-out="line.name"/>
|
||||
<td class="text-end" t-out="formatCost(line.unit_avg_cost_component)"/>
|
||||
<td t-if="hasOperations" class="text-end" t-out="formatCost(line.unit_avg_cost_operation)"/>
|
||||
<td class="text-end" t-out="formatCost(line.unit_avg_total_cost)"/>
|
||||
<td t-if="showUom" class="text-end" t-out="line.uom_name"/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { MoOverviewOperationsBlock } from "../mo_overview_operations_block/mrp_mo_overview_operations_block";
|
||||
import { MoOverviewLine } from "../mo_overview_line/mrp_mo_overview_line";
|
||||
|
||||
export class MoOverviewByproductsBlock extends MoOverviewOperationsBlock {
|
||||
static components = {
|
||||
MoOverviewLine,
|
||||
};
|
||||
static props = {
|
||||
// Keep all props except "operations"
|
||||
...(({ operations, ...props }) => props)(MoOverviewOperationsBlock.props),
|
||||
byproducts: Array,
|
||||
};
|
||||
|
||||
static template = "mrp.MoOverviewByproductsBlock";
|
||||
|
||||
//---- Getters ----
|
||||
|
||||
get hasByproducts() {
|
||||
return this.props?.byproducts?.length > 0;
|
||||
}
|
||||
|
||||
get level() {
|
||||
return this.hasByproducts ? this.props.byproducts[0].level - 1 : 0;
|
||||
}
|
||||
}
|
||||
MoOverviewByproductsBlock.props.summary.shape = {
|
||||
...MoOverviewByproductsBlock.props.summary.shape,
|
||||
product_cost: { type: Number, optional: true },
|
||||
};
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mrp.MoOverviewByproductsBlock">
|
||||
<t t-if="hasByproducts">
|
||||
<tr>
|
||||
<td class="text-start">
|
||||
<span t-attf-style="margin-left: {{ level * 20 }}px"/>
|
||||
<button t-on-click="toggleFolded" class="btn btn-light ps-0 py-0" t-attf-aria-label="{{ state.isFolded ? 'Unfold' : 'Fold' }}" t-attf-title="{{ state.isFolded ? 'Unfold' : 'Fold' }}">
|
||||
<i t-attf-class="fa fa-fw fa-caret-{{ state.isFolded ? 'right' : 'down' }}" role="img"/>
|
||||
By-Products
|
||||
</button>
|
||||
</td>
|
||||
<td class="text-center" t-if="props.showOptions.replenishments"/>
|
||||
<td class="text-end"/>
|
||||
<td class="text-start" t-if="props.showOptions.uom"/>
|
||||
<td class="text-end" t-if="props.showOptions.availabilities"/>
|
||||
<td class="text-end" t-if="props.showOptions.availabilities"/>
|
||||
<td class="text-end" t-if="props.showOptions.receipts"/>
|
||||
<td class="text-end" t-if="props.showOptions.unitCosts"/>
|
||||
<td t-attf-class="text-end {{ getColorClass(props.summary.mo_cost_decorator) }}" t-if="props.showOptions.moCosts" t-out="formatMonetary(props.summary.mo_cost)"/>
|
||||
<td class="text-end" t-if="props.showOptions.bomCosts" t-out="formatMonetary(props.summary.bom_cost)"/>
|
||||
<td t-attf-class="text-end {{ getColorClass(props.summary.real_cost_decorator) }}" t-if="props.showOptions.realCosts" t-out="formatMonetary(props.summary.real_cost)"/>
|
||||
</tr>
|
||||
<t t-if="!state.isFolded" t-foreach="props.byproducts" t-as="byproduct" t-key="byproduct.index">
|
||||
<MoOverviewLine data="byproduct" showOptions="props.showOptions"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
import { Component, onWillUpdateProps, useState } from "@odoo/owl";
|
||||
import { useBus } from "@web/core/utils/hooks";
|
||||
import { MoOverviewLine } from "../mo_overview_line/mrp_mo_overview_line";
|
||||
import { MoOverviewOperationsBlock } from "../mo_overview_operations_block/mrp_mo_overview_operations_block";
|
||||
import { MoOverviewByproductsBlock } from "../mo_overview_byproducts_block/mrp_mo_overview_byproducts_block";
|
||||
import { SHOW_OPTIONS } from "../mo_overview_display_filter/mrp_mo_overview_display_filter";
|
||||
|
||||
export class MoOverviewComponentsBlock extends Component {
|
||||
static components = {
|
||||
MoOverviewLine,
|
||||
MoOverviewOperationsBlock,
|
||||
MoOverviewByproductsBlock,
|
||||
MoOverviewComponentsBlock,
|
||||
};
|
||||
static props = {
|
||||
unfoldAll: { type: Boolean, optional: true },
|
||||
components: { type: Array, optional: true },
|
||||
operations: {
|
||||
type: Object,
|
||||
shape: {
|
||||
summary: Object,
|
||||
details: Array,
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
byproducts: {
|
||||
type: Object,
|
||||
shape: {
|
||||
summary: Object,
|
||||
details: Array,
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
showOptions: SHOW_OPTIONS,
|
||||
};
|
||||
static defaultProps = {
|
||||
unfoldAll: false,
|
||||
};
|
||||
|
||||
static template = "mrp.MoOverviewComponentsBlock";
|
||||
|
||||
setup() {
|
||||
this.state = useState({
|
||||
fold: this.getIndexStates(this.props),
|
||||
unfoldAll: this.props.unfoldAll || false,
|
||||
});
|
||||
|
||||
if (this.props.unfoldAll) {
|
||||
this.env.overviewBus.trigger("update-folded", { indexes: Object.keys(this.state.fold), isFolded: false });
|
||||
}
|
||||
|
||||
useBus(this.env.overviewBus, "unfold-all", () => this.unfoldAll());
|
||||
|
||||
onWillUpdateProps(newProps => {
|
||||
// Update the fold indexes so it matches the newly added lines.
|
||||
this.state.fold = { ...this.getIndexStates(newProps), ...this.state.fold };
|
||||
});
|
||||
}
|
||||
|
||||
//---- Handlers ----
|
||||
|
||||
onToggleFolded(foldIndex) {
|
||||
this.state.unfoldAll = false;
|
||||
const newState = !this.state.fold[foldIndex];
|
||||
if (newState) {
|
||||
// If a line is folded, its children lines must be folded as well
|
||||
Object.keys(this.state.fold).filter(key => key.startsWith(foldIndex)).forEach(index => {
|
||||
this.state.fold[index] = newState;
|
||||
});
|
||||
}
|
||||
this.state.fold[foldIndex] = newState;
|
||||
this.env.overviewBus.trigger("update-folded", { indexes: [foldIndex], isFolded: newState });
|
||||
}
|
||||
|
||||
unfoldAll() {
|
||||
this.state.unfoldAll = true;
|
||||
const foldIndexes = Object.keys(this.state.fold);
|
||||
foldIndexes.forEach(index => this.state.fold[index] = false);
|
||||
this.env.overviewBus.trigger("update-folded", { indexes: foldIndexes, isFolded: false });
|
||||
}
|
||||
|
||||
//---- Helpers ----
|
||||
|
||||
getIndexStates(props) {
|
||||
const indexStates = {};
|
||||
(props?.components ?? []).forEach(component => {
|
||||
indexStates[component?.summary.index] = !props.unfoldAll;
|
||||
(component?.replenishments ?? []).forEach(replenishment => {
|
||||
indexStates[replenishment?.summary.index] = !props.unfoldAll;
|
||||
});
|
||||
});
|
||||
return indexStates;
|
||||
}
|
||||
|
||||
hasReplenishments(component) {
|
||||
return component?.replenishments?.length > 0;
|
||||
}
|
||||
|
||||
hasReplenishmentsBlock(component) {
|
||||
return this.hasReplenishments(component) && !this.state.fold[component?.summary.index];
|
||||
}
|
||||
|
||||
hasComponents(replenishment) {
|
||||
return replenishment?.components?.length > 0 || replenishment?.operations?.details?.length > 0;
|
||||
}
|
||||
|
||||
hasComponentsBlock(replenishment) {
|
||||
return this.hasComponents(replenishment) && !this.state.fold[replenishment?.summary.index];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mrp.MoOverviewComponentsBlock">
|
||||
<t t-foreach="props.components" t-as="component" t-key="component.summary.index">
|
||||
<MoOverviewLine
|
||||
data="component.summary"
|
||||
hasFoldButton="hasReplenishments(component)"
|
||||
isFolded="state.fold[component.summary.index]"
|
||||
showOptions="props.showOptions"
|
||||
toggleFolded.bind="onToggleFolded"/>
|
||||
<!-- Display Replenishment block -->
|
||||
<t t-if="hasReplenishmentsBlock(component)" t-foreach="component.replenishments" t-as="replenishment" t-key="replenishment.summary.index">
|
||||
<MoOverviewLine
|
||||
data="replenishment.summary"
|
||||
hasFoldButton="hasComponents(replenishment)"
|
||||
isFolded="state.fold[replenishment.summary.index]"
|
||||
showOptions="props.showOptions"
|
||||
toggleFolded.bind="onToggleFolded"/>
|
||||
<t t-if="hasComponentsBlock(replenishment)">
|
||||
<MoOverviewComponentsBlock
|
||||
unfoldAll="state.unfoldAll"
|
||||
components="replenishment.components"
|
||||
operations="replenishment.operations"
|
||||
byproducts="replenishment.byproducts"
|
||||
showOptions="props.showOptions"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
<MoOverviewOperationsBlock
|
||||
unfoldAll="state.unfoldAll"
|
||||
summary="props.operations.summary"
|
||||
operations="props.operations.details"
|
||||
showOptions="props.showOptions"/>
|
||||
<MoOverviewByproductsBlock
|
||||
unfoldAll="state.unfoldAll"
|
||||
summary="props.byproducts.summary"
|
||||
byproducts="props.byproducts.details"
|
||||
showOptions="props.showOptions"/>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { BomOverviewDisplayFilter } from "../bom_overview_display_filter/mrp_bom_overview_display_filter";
|
||||
|
||||
export const SHOW_OPTIONS = {
|
||||
type: Object,
|
||||
shape: {
|
||||
uom: Boolean,
|
||||
replenishments: Boolean,
|
||||
availabilities: Boolean,
|
||||
receipts: Boolean,
|
||||
unitCosts: Boolean,
|
||||
moCosts: Boolean,
|
||||
bomCosts: Boolean,
|
||||
realCosts: Boolean,
|
||||
},
|
||||
};
|
||||
|
||||
export class MoOverviewDisplayFilter extends BomOverviewDisplayFilter {
|
||||
static props = {
|
||||
showOptions: SHOW_OPTIONS,
|
||||
changeDisplay: Function,
|
||||
limited: { type: Boolean, optional: true },
|
||||
isProductionDraft: { type: Boolean, optional: true},
|
||||
};
|
||||
static defaultProps = {
|
||||
limited: false,
|
||||
isProductionDraft: false,
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.displayOptions = {
|
||||
unitCosts: _t("Unit Costs"),
|
||||
moCosts: _t("MO Costs"),
|
||||
bomCosts: _t("BoM Costs"),
|
||||
};
|
||||
if (!this.props.limited) {
|
||||
this.displayOptions = {
|
||||
...this.displayOptions,
|
||||
replenishments: _t("Replenishments"),
|
||||
availabilities: _t("Availabilities"),
|
||||
receipts: _t("Receipts"),
|
||||
};
|
||||
}
|
||||
if (!this.props.isProductionDraft) {
|
||||
this.displayOptions = {
|
||||
...this.displayOptions,
|
||||
realCosts: _t("Real Costs"),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
const PRODUCTION_DECORATORS = {
|
||||
draft: "secondary",
|
||||
confirmed: "info",
|
||||
progress: "warning",
|
||||
done: "success",
|
||||
to_close: "success",
|
||||
cancel: "danger",
|
||||
};
|
||||
|
||||
const PURCHASE_DECORATORS = {
|
||||
draft: "info",
|
||||
sent: "info",
|
||||
['to approve']: "info",
|
||||
purchase: "info",
|
||||
cancel: "secondary",
|
||||
};
|
||||
|
||||
const PICKING_DECORATORS = {
|
||||
draft: "secondary",
|
||||
waiting: "warning",
|
||||
confirmed: "warning",
|
||||
assigned: "info",
|
||||
done: "success",
|
||||
cancel: "danger",
|
||||
};
|
||||
|
||||
const OPERATION_DECORATORS = {
|
||||
blocked: "warning",
|
||||
ready: "muted",
|
||||
progress: "info",
|
||||
done: "success",
|
||||
cancel: "danger",
|
||||
};
|
||||
|
||||
const PRODUCT_DECORATORS = {
|
||||
to_order: "danger",
|
||||
};
|
||||
|
||||
export function getStateDecorator(model, state) {
|
||||
let decorators = null;
|
||||
switch (model) {
|
||||
case "mrp.production":
|
||||
decorators = PRODUCTION_DECORATORS;
|
||||
break;
|
||||
case "mrp.workorder":
|
||||
decorators = OPERATION_DECORATORS;
|
||||
break;
|
||||
case "stock.picking":
|
||||
decorators = PICKING_DECORATORS;
|
||||
break;
|
||||
case "purchase.order":
|
||||
decorators = PURCHASE_DECORATORS;
|
||||
break;
|
||||
case "product.product":
|
||||
decorators = PRODUCT_DECORATORS;
|
||||
break;
|
||||
}
|
||||
return decorators ? `text-bg-${decorators[state]}` : "";
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { Component } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { formatFloat, formatFloatTime, formatMonetary } from "@web/views/fields/formatters";
|
||||
import { getStateDecorator } from "./mo_overview_colors";
|
||||
import { SHOW_OPTIONS } from "../mo_overview_display_filter/mrp_mo_overview_display_filter";
|
||||
|
||||
export class MoOverviewLine extends Component {
|
||||
static props = {
|
||||
data: {
|
||||
type: Object,
|
||||
shape: {
|
||||
level: Number,
|
||||
index: { type: String, optional: true },
|
||||
id: { type: Number, optional: true },
|
||||
model: { type: String, optional: true },
|
||||
name: String,
|
||||
product_model: { type: String, optional: true },
|
||||
product: { type: String, optional: true },
|
||||
product_id: { type: Number, optional: true },
|
||||
state: { type: String, optional: true },
|
||||
formatted_state: { type: String, optional: true },
|
||||
has_bom: { type: Boolean, optional: true },
|
||||
quantity: Number,
|
||||
replenish_quantity: { type: Number, optional: true },
|
||||
uom: { type: String, optional: true },
|
||||
uom_name: { type: String, optional: true },
|
||||
uom_precision: { type: Number, optional: true },
|
||||
quantity_free: { type: [Number, Boolean], optional: true },
|
||||
quantity_on_hand: { type: [Number, Boolean], optional: true },
|
||||
quantity_reserved: { type: Number, optional: true },
|
||||
receipt: {
|
||||
type: Object,
|
||||
shape: {
|
||||
display: String,
|
||||
type: String,
|
||||
decorator: [String, Boolean],
|
||||
date: [String, Boolean],
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
unit_cost: { type: Number, optional: true },
|
||||
mo_cost: { type: [Number, Boolean], optional: true },
|
||||
mo_cost_decorator: { type: [String, Boolean], optional: true },
|
||||
bom_cost: { type: [Number, Boolean], optional: true },
|
||||
real_cost: { type: [Number, Boolean], optional: true },
|
||||
real_cost_decorator: { type: [String, Boolean], optional: true },
|
||||
currency_id: Number,
|
||||
currency: { type: String, optional: true },
|
||||
production_id: { type: Number, optional: true },
|
||||
},
|
||||
},
|
||||
showOptions: SHOW_OPTIONS,
|
||||
hasFoldButton: { type: Boolean, optional: true },
|
||||
isFolded: { type: Boolean, optional: true },
|
||||
toggleFolded: { type: Function, optional: true },
|
||||
};
|
||||
|
||||
static template = "mrp.MoOverviewLine";
|
||||
|
||||
setup() {
|
||||
this.actionService = useService("action");
|
||||
this.ormService = useService("orm");
|
||||
this.formatFloat = (val) => formatFloat(val, { digits: [false, this.data.uom_precision || undefined] });
|
||||
this.formatFloatTime = formatFloatTime;
|
||||
this.formatMonetary = (val) => formatMonetary(val, { currencyId: this.data.currency_id });
|
||||
}
|
||||
|
||||
//---- Handlers ----
|
||||
|
||||
async openForm() {
|
||||
const model = this.data.level === 0 ? this.data.product_model : this.data.model;
|
||||
const id = this.data.level === 0 ? this.data.product_id : this.data.id;
|
||||
return this.actionService.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: model,
|
||||
res_id: id,
|
||||
views: [[false, "form"]],
|
||||
target: "current",
|
||||
context: {
|
||||
active_id: id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async openForecast() {
|
||||
const action = await this.ormService.call(
|
||||
this.data.product_model,
|
||||
this.forecastAction,
|
||||
[[this.data.product_id]],
|
||||
);
|
||||
action.context = {
|
||||
active_model: this.data.product_model,
|
||||
active_id: this.data.product_id,
|
||||
};
|
||||
return this.actionService.doAction(action);
|
||||
}
|
||||
|
||||
async openReplenish() {
|
||||
return this.actionService.doAction("stock.action_product_replenish", {
|
||||
additionalContext: { default_product_id: this.data.product_id, default_quantity: this.data.replenish_quantity || this.data.quantity },
|
||||
onClose: (closeInfo) => {
|
||||
if (closeInfo?.done) {
|
||||
// Trigger the reload only if a replenishment was done.
|
||||
this.env.overviewBus.trigger("reload");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async openWorkorder() {
|
||||
return this.actionService.doAction({
|
||||
name: this.data.name,
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "mrp.workorder",
|
||||
views: [[false, "list"]],
|
||||
context: {
|
||||
search_default_ready: true,
|
||||
search_default_waiting: true,
|
||||
search_default_progress: true,
|
||||
search_default_blocked: true,
|
||||
search_default_name: this.data.name,
|
||||
search_default_production_id: this.data.production_id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
//---- Helpers ----
|
||||
|
||||
getColorClass(decorator) {
|
||||
return decorator ? `text-${decorator}` : "";
|
||||
}
|
||||
|
||||
hasQuantity(keyName) {
|
||||
return this.data.hasOwnProperty(keyName) && this.data[keyName] !== false;
|
||||
}
|
||||
|
||||
//---- Getters ----
|
||||
|
||||
get data() {
|
||||
return this.props.data;
|
||||
}
|
||||
|
||||
get stateDecorator() {
|
||||
return getStateDecorator(this.data.model, this.data.state);
|
||||
}
|
||||
|
||||
get formattedQuantity() {
|
||||
if (this.data.model === "mrp.workorder") {
|
||||
return this.formatFloatTime(this.data.quantity);
|
||||
}
|
||||
return this.formatFloat(this.data.quantity);
|
||||
}
|
||||
|
||||
get hasFoldButton() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get marginMultiplicator() {
|
||||
return this.data.level - (this.props.hasFoldButton ? 1 : 0);
|
||||
}
|
||||
|
||||
get foldButtonTitle() {
|
||||
return this.props.isFolded ? _t("Unfold") : _t("Fold");
|
||||
}
|
||||
|
||||
get forecastAction() {
|
||||
switch (this.data.product_model) {
|
||||
case "product.product":
|
||||
return "action_product_forecast_report";
|
||||
case "product.template":
|
||||
return "action_product_tmpl_forecast_report";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
.o_mrp_overview_badge {
|
||||
font-size: $badge-font-size * 1.1;
|
||||
}
|
||||
|
||||
.o_mrp_overview_badge.text-bg-muted {
|
||||
@include o-print-color(map-get($o-grays, 300), background-color, bg-opacity);
|
||||
@include o-print-color(color-contrast(map-get($o-grays, 300)), color, text-opacity);
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<tr t-name="mrp.MoOverviewLine" t-on-click="() => this.props.hasFoldButton? this.props.toggleFolded(data.index) : false">
|
||||
<td class="text-start">
|
||||
<div t-attf-style="margin-left: {{ marginMultiplicator * 20 }}px" class="d-inline-block">
|
||||
<span t-if="props.hasFoldButton" class="btn btn-light p-0" t-attf-aria-label="{{ foldButtonTitle }}" t-attf-title="{{ foldButtonTitle }}" style="margin-right: 1px">
|
||||
<i t-attf-class="fa fa-fw fa-caret-{{ props.isFolded ? 'right' : 'down' }}" role="img"/>
|
||||
</span>
|
||||
<a t-if="data.id and data.model === 'mrp.workorder'" href="#" t-on-click.prevent="openWorkorder" t-out="data.name"/>
|
||||
<a t-elif="data.id and data.model" href="#" t-on-click.prevent="openForm" t-out="data.name"/>
|
||||
<t t-else="" t-out="data.name"/>
|
||||
<button t-if="data.model === 'to_order'" class="ms-2 py-0 btn btn-secondary" t-on-click="openReplenish">Replenish</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center" t-if="props.showOptions.replenishments">
|
||||
<div class="o_field_badge">
|
||||
<span t-attf-class="badge rounded-pill o_mrp_overview_badge {{ stateDecorator }}" t-out="data.formatted_state"/>
|
||||
</div>
|
||||
</td>
|
||||
<td t-attf-class="text-end" t-out="formattedQuantity"/>
|
||||
<td t-if="props.showOptions.uom" class="text-start" t-out="data.uom_name"/>
|
||||
<td class="text-end" t-if="props.showOptions.availabilities">
|
||||
<t t-if="hasQuantity('quantity_on_hand')">
|
||||
<t t-out="formatFloat(data.quantity_free)"/> /
|
||||
<t t-out="formatFloat(data.quantity_on_hand)"/>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-end" t-if="props.showOptions.availabilities">
|
||||
<t t-if="hasQuantity('quantity_reserved')" t-out="formatFloat(data.quantity_reserved)"/>
|
||||
</td>
|
||||
<td class="text-end" t-if="props.showOptions.receipts">
|
||||
<t t-if="data.receipt">
|
||||
<span t-attf-class="{{ getColorClass(data.receipt.decorator) }}" t-out="data.receipt.display"/>
|
||||
<a href="#" role="button" t-on-click.prevent="openForecast" title="Forecast Report" t-attf-class="fa fa-fw fa-area-chart ms-1 {{getColorClass(data.receipt.decorator)}}"/>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-end" t-if="props.showOptions.unitCosts" t-out="formatMonetary(data.unit_cost)"/>
|
||||
<td t-attf-class="text-end {{ getColorClass(data.mo_cost_decorator) }}" t-if="props.showOptions.moCosts" t-out="formatMonetary(data.mo_cost)"/>
|
||||
<td class="text-end" t-if="props.showOptions.bomCosts" t-out="formatMonetary(data.bom_cost)"/>
|
||||
<td t-attf-class="text-end {{ getColorClass(data.real_cost_decorator) }}" t-if="props.showOptions.realCosts" t-out="formatMonetary(data.real_cost)"/>
|
||||
</tr>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
import { Component, useState } from "@odoo/owl";
|
||||
import { useBus } from "@web/core/utils/hooks";
|
||||
import { formatFloat, formatFloatTime, formatMonetary } from "@web/views/fields/formatters";
|
||||
import { MoOverviewLine } from "../mo_overview_line/mrp_mo_overview_line";
|
||||
import { SHOW_OPTIONS } from "../mo_overview_display_filter/mrp_mo_overview_display_filter";
|
||||
|
||||
export class MoOverviewOperationsBlock extends Component {
|
||||
static template = "mrp.MoOverviewOperationsBlock";
|
||||
static components = {
|
||||
MoOverviewLine,
|
||||
};
|
||||
static props = {
|
||||
unfoldAll: { type: Boolean, optional: true },
|
||||
operations: Array,
|
||||
summary: {
|
||||
type: Object,
|
||||
shape: {
|
||||
index: String,
|
||||
quantity: { type: Number, optional: true },
|
||||
quantity_decorator: { type: [String, Boolean], optional: true },
|
||||
mo_cost: { type: Number, optional: true },
|
||||
mo_cost_decorator: { type: [String, Boolean], optional: true },
|
||||
bom_cost: { type: [Number, Boolean], optional: true },
|
||||
real_cost: { type: Number, optional: true },
|
||||
real_cost_decorator: { type: [String, Boolean], optional: true },
|
||||
uom_name: { type: String, optional: true },
|
||||
currency_id: { type: Number, optional: true },
|
||||
currency: { type: String, optional: true },
|
||||
done: { type: Boolean, optional: true },
|
||||
},
|
||||
},
|
||||
showOptions: SHOW_OPTIONS,
|
||||
};
|
||||
static defaultProps = {
|
||||
unfoldAll: false,
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.formatFloatTime = formatFloatTime;
|
||||
this.state = useState({
|
||||
// Unfold the main MO's operations by default
|
||||
isFolded: this.level > 0 && !this.props.unfoldAll,
|
||||
});
|
||||
if (this.props.unfoldAll) {
|
||||
this.env.overviewBus.trigger("update-folded", { indexes: [this.index], isFolded: false });
|
||||
}
|
||||
|
||||
useBus(this.env.overviewBus, "unfold-all", () => this.unfold());
|
||||
}
|
||||
|
||||
//---- Handlers ----
|
||||
|
||||
toggleFolded() {
|
||||
this.state.isFolded = !this.state.isFolded;
|
||||
this.env.overviewBus.trigger("update-folded", { indexes: [this.index], isFolded: this.state.isFolded });
|
||||
}
|
||||
|
||||
unfold() {
|
||||
this.state.isFolded = false;
|
||||
this.env.overviewBus.trigger("update-folded", { indexes: [this.index], isFolded: false });
|
||||
}
|
||||
|
||||
//---- Helpers ----
|
||||
|
||||
formatMonetary(val) {
|
||||
return formatMonetary(val, { currencyId: this.props.summary.currency_id });
|
||||
}
|
||||
|
||||
getColorClass(decorator) {
|
||||
return decorator ? `text-${decorator}` : "";
|
||||
}
|
||||
|
||||
//---- Getters ----
|
||||
|
||||
get hasOperations() {
|
||||
return this.props?.operations?.length > 0;
|
||||
}
|
||||
|
||||
get level() {
|
||||
return this.hasOperations ? this.props.operations[0].level - 1 : 0;
|
||||
}
|
||||
|
||||
get index() {
|
||||
return this.props.summary.index;
|
||||
}
|
||||
|
||||
get totalQuantity() {
|
||||
// Float for Hours when displaying done productions, FloatTime for Minutes otherwise.
|
||||
return this.props.summary?.done ?
|
||||
formatFloat(this.props.summary.quantity, { digits: [false, this.props.operations[0].uom_precision || undefined] }) :
|
||||
formatFloatTime(this.props.summary.quantity)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mrp.MoOverviewOperationsBlock">
|
||||
<t t-if="hasOperations">
|
||||
<tr t-on-click="toggleFolded">
|
||||
<td class="text-start">
|
||||
<span t-attf-style="margin-left: {{ level * 20 }}px"/>
|
||||
<span class="btn btn-light ps-0 py-0" t-attf-aria-label="{{ state.isFolded ? 'Unfold' : 'Fold' }}" t-attf-title="{{ state.isFolded ? 'Unfold' : 'Fold' }}">
|
||||
<i t-attf-class="fa fa-fw fa-caret-{{ state.isFolded ? 'right' : 'down' }}" role="img"/>
|
||||
Operations
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center" t-if="props.showOptions.replenishments"/>
|
||||
<td t-attf-class="text-end" t-out="totalQuantity"/>
|
||||
<td class="text-start" t-if="props.showOptions.uom" t-out="props.summary.uom_name"/>
|
||||
<td class="text-end" t-if="props.showOptions.availabilities"/>
|
||||
<td class="text-end" t-if="props.showOptions.availabilities"/>
|
||||
<td class="text-end" t-if="props.showOptions.receipts"/>
|
||||
<td class="text-end" t-if="props.showOptions.unitCosts"/>
|
||||
<td t-attf-class="text-end {{ getColorClass(props.summary.mo_cost_decorator) }}" t-if="props.showOptions.moCosts" t-out="formatMonetary(props.summary.mo_cost)"/>
|
||||
<td class="text-end" t-if="props.showOptions.bomCosts" t-out="formatMonetary(props.summary.bom_cost)"/>
|
||||
<td t-attf-class="text-end {{ getColorClass(props.summary.real_cost_decorator) }}" t-if="props.showOptions.realCosts" t-out="formatMonetary(props.summary.real_cost)"/>
|
||||
</tr>
|
||||
<t t-if="!state.isFolded" t-foreach="props.operations" t-as="operation" t-key="operation.index">
|
||||
<MoOverviewLine data="operation" showOptions="props.showOptions"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { ProductCatalogKanbanController } from "@product/product_catalog/kanban_controller";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
patch(ProductCatalogKanbanController.prototype, {
|
||||
setOrderStateInfo() {
|
||||
if (["mrp.bom", "mrp.production"].includes(this.orderResModel)) {
|
||||
return {};
|
||||
}
|
||||
return super.setOrderStateInfo();
|
||||
},
|
||||
|
||||
_defineButtonContent() {
|
||||
if (this.orderResModel === "mrp.bom") {
|
||||
this.buttonString = _t("Back to BoM");
|
||||
} else if (this.orderResModel === "mrp.production") {
|
||||
this.buttonString = _t("Back to Production");
|
||||
} else {
|
||||
super._defineButtonContent();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import { Component, useState } from "@odoo/owl";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
|
||||
export class MOListViewDropdown extends Component {
|
||||
static template = "mrp.MOViewListDropdown";
|
||||
static components = {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
};
|
||||
static props = { ...standardWidgetProps };
|
||||
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.action = useService("action");
|
||||
this.workorderState = useState({
|
||||
state: this.props.record.data.state,
|
||||
});
|
||||
this.colorIcons = {
|
||||
"blocked": "bg-warning",
|
||||
"ready": "bg-muted",
|
||||
"progress": "bg-info",
|
||||
"cancel": "bg-danger",
|
||||
"done": "bg-success",
|
||||
};
|
||||
}
|
||||
|
||||
async reload(){
|
||||
await this.env.model.root.load();
|
||||
this.env.model.notify();
|
||||
}
|
||||
|
||||
get statusColor() {
|
||||
const state = this.workorderState.state;
|
||||
return this.colorIcons[state] || "";
|
||||
}
|
||||
|
||||
async setState(state) {
|
||||
let selectedWorkorders = this.props.record.model.root.selection;
|
||||
if (!selectedWorkorders || selectedWorkorders.length == 0) {
|
||||
selectedWorkorders = [this.props.record];
|
||||
}
|
||||
let ids = selectedWorkorders.filter((wo) => !([state, 'done'].includes(wo.data.state) || wo.data.production_state == 'done')).map((wo) => wo.resId)
|
||||
if (ids && ids.length > 0) {
|
||||
await this.callOrm("set_state", [state], ids);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async callOrm(functionName, args, ids = undefined) {
|
||||
if (!ids){
|
||||
ids = this.props.record.model.root.selection?.map((element) => element.evalContext.id);
|
||||
}
|
||||
// if no records selected, take the current clicked one
|
||||
if (!ids || ids.length == 0) {
|
||||
ids = [this.props.record.resId];
|
||||
}
|
||||
if (args !== undefined) {
|
||||
await this.orm.call("mrp.workorder", functionName, [ids, ...args]);
|
||||
} else {
|
||||
await this.orm.call("mrp.workorder", functionName, [ids]);
|
||||
}
|
||||
await this.reload();
|
||||
}
|
||||
}
|
||||
|
||||
export const moListViewDropdown = {
|
||||
listViewWidth: 20,
|
||||
component: MOListViewDropdown,
|
||||
};
|
||||
|
||||
registry.category("view_widgets").add("mo_view_list_dropdown", moListViewDropdown);
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
.o_widget_mo_view_list_dropdown {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 100%;
|
||||
.o_status {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
td.o_data_cell:has(> div.o_widget_mo_view_list_dropdown) {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mrp.MOViewListDropdown" style="btn" t-if="this.props.record.data.state !== 'done' and !['draft', 'done', 'cancel'].includes(this.props.record.data.production_state)">
|
||||
<Dropdown>
|
||||
<button t-attf-class="btn btn-link d-flex p-0 pb-1 {{!this.props.record.resId ? 'invisible': ''}}">
|
||||
<div class="d-flex align-items-center">
|
||||
<span t-attf-class="o_status {{statusColor}}"/>
|
||||
</div>
|
||||
</button>
|
||||
<t t-set-slot="content">
|
||||
<DropdownItem
|
||||
class="`d-flex align-items-center`"
|
||||
onSelected="() => this.setState('blocked')">
|
||||
<span t-attf-class="fa fa-lg fa-exclamation-circle text-warning me-2"/>
|
||||
<span>Blocked</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
class="`d-flex align-items-center`"
|
||||
onSelected="() => this.setState('ready')">
|
||||
<span t-attf-class="fa fa-lg fa-circle text-muted me-2"/>
|
||||
<span>To Do</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
class="`d-flex align-items-center`"
|
||||
onSelected="() => this.setState('progress')">
|
||||
<span t-attf-class="fa fa-lg fa-play-circle text-info me-2"/>
|
||||
<span>In Progress</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
class="`d-flex align-items-center`"
|
||||
onSelected="() => this.setState('cancel')">
|
||||
<span t-attf-class="fa fa-lg fa-times-circle text-danger me-2"/>
|
||||
<span>Cancelled</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
class="`d-flex align-items-center`"
|
||||
onSelected="() => this.setState('done')">
|
||||
<span t-attf-class="fa fa-lg fa-check-circle text-success me-2"/>
|
||||
<span>Done</span>
|
||||
</DropdownItem>
|
||||
</t>
|
||||
</Dropdown>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
BIN
odoo-bringout-oca-ocb-mrp/mrp/static/src/img/milk-mrp-tablet.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
|
@ -1,12 +1,10 @@
|
|||
/** @odoo-module **/
|
||||
import { ForecastedButtons } from "@stock/stock_forecasted/forecasted_buttons";
|
||||
import { patch } from '@web/core/utils/patch';
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { onWillStart } from "@odoo/owl";
|
||||
|
||||
const { onWillStart } = owl;
|
||||
|
||||
patch(ForecastedButtons.prototype, 'mrp.ForecastedButtons',{
|
||||
patch(ForecastedButtons.prototype, {
|
||||
setup() {
|
||||
this._super.apply();
|
||||
super.setup();
|
||||
onWillStart(async () =>{
|
||||
const fields = this.resModel === "product.template" ? ['bom_ids'] : ['bom_ids', 'variant_bom_ids'];
|
||||
const res = (await this.orm.call(this.resModel, 'read', [this.productId], { fields }))[0];
|
||||
|
|
@ -18,6 +16,9 @@ patch(ForecastedButtons.prototype, 'mrp.ForecastedButtons',{
|
|||
return this.actionService.doAction("mrp.action_report_mrp_bom", {
|
||||
additionalContext: {
|
||||
active_id: this.bomId,
|
||||
active_product_id: this.productId,
|
||||
active_model: this.resModel,
|
||||
mode: "forecast",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mrp.ForecastedButtons" owl="1" t-inherit="stock.ForecastedButtons" t-inherit-mode="extension">
|
||||
<t t-name="mrp.ForecastedButtons" t-inherit="stock.ForecastedButtons" t-inherit-mode="extension">
|
||||
<xpath expr="//button[@title='Replenish']" position="after">
|
||||
<button t-if="bomId" t-name="mrp_replenish_report_buttons"
|
||||
class="btn btn-primary o_bom_overview_report"
|
||||
class="btn btn-secondary o_bom_overview_report"
|
||||
type="button" title="Manufacturing Forecast"
|
||||
t-on-click="_onClickBom">
|
||||
Manufacturing Forecast
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import { ForecastedDetails } from '@stock/stock_forecasted/forecasted_details';
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(ForecastedDetails.prototype, {
|
||||
|
||||
canReserveOperation(line){
|
||||
return super.canReserveOperation(line) || line.move_out?.raw_material_production_id;
|
||||
}
|
||||
});
|
||||
|
|
@ -1,35 +1,29 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates id="template">
|
||||
<t t-name="mrp.ForecastedDetails" owl="1" t-inherit="stock.ForecastedDetails" t-inherit-mode="extension">
|
||||
<t t-name="mrp.ForecastedDetails" t-inherit="stock.ForecastedDetails" t-inherit-mode="extension">
|
||||
<xpath expr="//tr[@name='draft_picking_in']" position="after">
|
||||
<tr t-if="props.docs.draft_production_qty.in" name="draft_mo_in">
|
||||
<td colspan="2">Production of Draft MO</td>
|
||||
<td t-esc="_formatFloat(props.docs.draft_production_qty.in)" class="text-end"/>
|
||||
<tr t-if="currentProduct.draft_production_qty.in" name="draft_mo_in" t-attf-class="#{multipleProducts and 'collapse show' or ''} collapseGroup_#{line.product.id}">
|
||||
<td/>
|
||||
<td>Production of Draft MO</td>
|
||||
<td class="text-end text-success">
|
||||
<t t-out="_formatFloat(currentProduct.draft_production_qty.in)"/> <t t-out="line.uom_id.display_name"/>
|
||||
</td>
|
||||
</tr>
|
||||
</xpath>
|
||||
<xpath expr="//tr[@name='draft_picking_out']" position="after">
|
||||
<tr t-if="props.docs.draft_production_qty.out" name="draft_mo_out">
|
||||
<td colspan="2">Component of Draft MO</td>
|
||||
<td t-esc="_formatFloat(-props.docs.draft_production_qty.out)" class="text-end"/>
|
||||
<tr t-if="currentProduct.draft_production_qty.out" name="draft_mo_out" t-attf-class="#{multipleProducts and 'collapse show' or ''} collapseGroup_#{line.product.id}">
|
||||
<td/>
|
||||
<td>Component of Draft MO</td>
|
||||
<td class="text-end">
|
||||
<span class="text-danger">
|
||||
<t t-out="_formatFloat(-currentProduct.draft_production_qty.out)"/> <t t-out="line.uom_id.display_name"/>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='unreserve_link']" position="after">
|
||||
<button t-if="line.move_out and line.move_out.raw_material_production_id and line.move_out.raw_material_production_id.unreserve_visible"
|
||||
class="btn btn-sm btn-primary o_report_replenish_unreserve"
|
||||
t-on-click="() => this._unreserve('mrp.production', line.move_out.raw_material_production_id.id)">
|
||||
Unreserve
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='reserve_link']" position="after">
|
||||
<button t-if="line.move_out and line.move_out.raw_material_production_id and line.move_out.raw_material_production_id.reserve_visible"
|
||||
class="btn btn-sm btn-primary o_report_replenish_reserve"
|
||||
t-on-click="() => this._reserve('mrp.production', line.move_out.raw_material_production_id.id)">
|
||||
Reserve
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='change_priority_link']" position="after">
|
||||
<button t-if="line.move_out and line.move_out.raw_material_production_id"
|
||||
t-attf-class="o_priority o_priority_star o_report_replenish_change_priority fa fa-star#{line.move_out.raw_material_production_id.priority=='1' ? ' one' : '-o zero'}"
|
||||
t-attf-class="o_priority o_priority_star fa fa-star#{line.move_out.raw_material_production_id.priority=='1' ? ' one' : '-o zero'}"
|
||||
t-on-click="() => this._onClickChangePriority('mrp.production', line.move_out.raw_material_production_id)"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
import { ProductCatalogOrderLine } from "@product/product_catalog/order_line/order_line";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(ProductCatalogOrderLine.prototype, {
|
||||
get showPrice() {
|
||||
return super.showPrice && this.env.orderResModel !== "mrp.production";
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
.o_mrp_overview {
|
||||
.o_control_panel_breadcrumbs {
|
||||
max-width: 30%;
|
||||
}
|
||||
.o_control_panel_actions {
|
||||
justify-content: start!important;
|
||||
max-width: 100%;
|
||||
}
|
||||
.o_control_panel_navigation {
|
||||
max-width: 25%;
|
||||
}
|
||||
.input-group {
|
||||
max-width: 250px;
|
||||
}
|
||||
input.o_input.o-autocomplete--input {
|
||||
max-width: 150px;
|
||||
}
|
||||
input.o_input {
|
||||
max-width: 75px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
@mixin gantt-decoration-color($color) {
|
||||
background-image: linear-gradient($color, $color);
|
||||
background-color: lighten($color, 10%);
|
||||
&:before {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
.o_mrp_workorder_gantt .o_gantt_view .o_gantt_row_container .o_gantt_row .o_gantt_cell .o_gantt_pill_wrapper {
|
||||
div.o_gantt_pill.decoration-success {
|
||||
@include gantt-decoration-color($success);
|
||||
}
|
||||
div.o_gantt_pill.decoration-warning {
|
||||
@include gantt-decoration-color(map-get($grays, '500'));
|
||||
}
|
||||
div.o_gantt_pill.decoration-danger {
|
||||
@include gantt-decoration-color($danger);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.o_mrp_mo_overview_thead {
|
||||
background-color: #{$o-webclient-background-color};
|
||||
}
|
||||
153
odoo-bringout-oca-ocb-mrp/mrp/static/src/scss/mrp_routing.scss
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
div.o_mrp_routing_form div.o_field_integer.o_small_integer {
|
||||
&, input {
|
||||
max-width: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.o_mrp_routing_image {
|
||||
@extend %o-nocontent-init-image;
|
||||
|
||||
overflow: hidden;
|
||||
min-width: 650px;
|
||||
margin-top: 50px;
|
||||
|
||||
background: url('/mrp/static/img/routing.svg') no-repeat top left;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
margin-left: 22rem;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
div.separator {
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.o_annotation {
|
||||
color: gray;
|
||||
}
|
||||
.o_annotation:before {
|
||||
content: "";
|
||||
@include o-position-absolute($top: 50%);
|
||||
border: $border-width * 2 solid gray;
|
||||
width: 2rem;
|
||||
}
|
||||
.o_annotation_end:before {
|
||||
right: 105%;
|
||||
border-top-left-radius: $border-radius-lg;
|
||||
border-bottom: none;
|
||||
border-right: none;
|
||||
}
|
||||
.o_annotation_start:before {
|
||||
left: 105%;
|
||||
border-top-right-radius: $border-radius-lg;
|
||||
border-bottom: none;
|
||||
border-left: none;
|
||||
}
|
||||
.o_annotation_1 {
|
||||
top: 0;
|
||||
&:before {
|
||||
height: 7rem;
|
||||
}
|
||||
}
|
||||
.o_annotation_2 {
|
||||
&:before {
|
||||
height: 4rem;
|
||||
}
|
||||
}
|
||||
.o_annotation_3 {
|
||||
&:before {
|
||||
height: 4rem;
|
||||
}
|
||||
}
|
||||
.o_annotation_4 {
|
||||
&:before {
|
||||
height: 4rem;
|
||||
}
|
||||
}
|
||||
.o_annotation_5 {
|
||||
&:before {
|
||||
height: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.o_text {
|
||||
color: gray;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
.o_mrp_routing_image.bill_of_material {
|
||||
min-height: 300px;
|
||||
img {
|
||||
left: 180px;
|
||||
top: 60px;
|
||||
}
|
||||
// div.separator {
|
||||
// left: 490px;
|
||||
// top: 40px;
|
||||
// height: 250px;
|
||||
// width: 2px;
|
||||
// }
|
||||
.o_annotation_1 {
|
||||
left: 323px;
|
||||
}
|
||||
.o_annotation_2 {
|
||||
top: 27px;
|
||||
left: 360px;
|
||||
}
|
||||
.o_annotation_3 {
|
||||
top: 40px;
|
||||
left: 133px;
|
||||
}
|
||||
.o_annotation_4 {
|
||||
top: 70px;
|
||||
left: 90px;
|
||||
}
|
||||
.o_annotation_5 {
|
||||
top: 97px;
|
||||
left: 58px;
|
||||
}
|
||||
.o_text {
|
||||
left: 510px;
|
||||
top: 80px;
|
||||
}
|
||||
}
|
||||
.o_mrp_routing_image.settings {
|
||||
min-height: 600px;
|
||||
img {
|
||||
left: 179px;
|
||||
top: 61px;
|
||||
}
|
||||
div.separator {
|
||||
left: 125px;
|
||||
top: 300px;
|
||||
height: 2px;
|
||||
width: 350px;
|
||||
}
|
||||
.o_annotation_1 {
|
||||
left: 325px;
|
||||
}
|
||||
.o_annotation_2 {
|
||||
top: 29px;
|
||||
left: 360px;
|
||||
}
|
||||
.o_annotation_3 {
|
||||
top: 35px;
|
||||
left: 120px;
|
||||
}
|
||||
.o_annotation_4 {
|
||||
top: 65px;
|
||||
left: 70px;
|
||||
}
|
||||
.o_annotation_5 {
|
||||
top: 95px;
|
||||
left: 45px;
|
||||
}
|
||||
.o_text {
|
||||
left: 15%;
|
||||
top: 55%;
|
||||
right: 25%;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +1,36 @@
|
|||
.o_kanban_dashboard{
|
||||
&.o_mrp_workorder_kanban {
|
||||
.o_kanban_record {
|
||||
flex-basis: 40%;
|
||||
@include media-breakpoint-down(lg) {
|
||||
flex-basis: 100%
|
||||
}
|
||||
}
|
||||
.o_kanban_group .o_kanban_record_top {
|
||||
flex-direction: column;
|
||||
}
|
||||
.o_kanban_record_top {
|
||||
justify-content: space-between;
|
||||
.o_kanban_workorder_title {
|
||||
flex-basis: 40%;
|
||||
}
|
||||
.o_kanban_workorder_date {
|
||||
}
|
||||
.o_kanban_workorder_status {
|
||||
flex-basis: 20%;
|
||||
}
|
||||
.o_mrp_workorder_kanban {
|
||||
.o_kanban_record {
|
||||
flex-basis: 40%;
|
||||
@include media-breakpoint-down(lg) {
|
||||
flex-basis: 100%
|
||||
}
|
||||
}
|
||||
&.o_workcenter_kanban {
|
||||
--KanbanRecord-width: 400px;
|
||||
.o_kanban_group .o_kanban_record_top {
|
||||
flex-direction: column;
|
||||
}
|
||||
.o_kanban_record_top {
|
||||
justify-content: space-between;
|
||||
.o_kanban_workorder_title {
|
||||
flex-basis: 40%;
|
||||
}
|
||||
.o_kanban_workorder_status {
|
||||
flex-basis: 20%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.o_workcenter_kanban {
|
||||
--KanbanRecord-width: 400px;
|
||||
.o_kanban_renderer {
|
||||
--KanbanRecord-width: 480px;
|
||||
|
||||
@include media-only(screen) {
|
||||
--KanbanGroup-width: 480px;
|
||||
}
|
||||
@include media-only(print) {
|
||||
--KanbanGroup-width: 400px;
|
||||
}
|
||||
}
|
||||
.o_dashboard_graph {
|
||||
margin: 0px -8px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { CharField } from "@web/views/fields/char/char_field";
|
||||
|
||||
const { useState } = owl;
|
||||
|
||||
export class SlidesViewer extends CharField {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.notification = useService("notification");
|
||||
this.page = 1;
|
||||
this.state = useState({
|
||||
isValid: true,
|
||||
});
|
||||
}
|
||||
|
||||
get fileName() {
|
||||
return this.state.fileName || this.props.value || "";
|
||||
}
|
||||
|
||||
_get_slide_page() {
|
||||
return this.props.record.data[this.props.name+'_page'] ? this.props.record.data[this.props.name+'_page'] : this.page;
|
||||
}
|
||||
|
||||
get url() {
|
||||
var src = false;
|
||||
if (this.props.value) {
|
||||
// check given google slide url is valid or not
|
||||
var googleRegExp = /(^https:\/\/docs.google.com).*(\/d\/e\/|\/d\/)([A-Za-z0-9-_]+)/;
|
||||
var google = this.props.value.match(googleRegExp);
|
||||
if (google && google[3]) {
|
||||
src =
|
||||
"https://docs.google.com/presentation" +
|
||||
google[2] +
|
||||
google[3] +
|
||||
"/preview?slide=" +
|
||||
this._get_slide_page();
|
||||
}
|
||||
}
|
||||
return src || this.props.value;
|
||||
}
|
||||
|
||||
onLoadFailed() {
|
||||
this.state.isValid = false;
|
||||
this.notification.add(this.env._t("Could not display the selected spreadsheet"), {
|
||||
type: "danger",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
SlidesViewer.template = "mrp.SlidesViewer";
|
||||
SlidesViewer.displayName = _lt("Google Slides Viewer");
|
||||
|
||||
registry.category("fields").add("embed_viewer", SlidesViewer);
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mrp.SlidesViewer" t-inherit="web.CharField">
|
||||
<xpath expr="//t[@t-else='']" position="after">
|
||||
<t t-if="url">
|
||||
<iframe class="o_embed_iframe w-100"
|
||||
alt="Slides viewer"
|
||||
t-att-src="url"
|
||||
t-att-name="props.name"
|
||||
t-on-error="onLoadFailed"
|
||||
/>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -1,40 +1,11 @@
|
|||
/** @odoo-module **/
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { ProductDocumentKanbanController } from "@product/js/product_document_kanban/product_document_kanban_controller";
|
||||
|
||||
import { KanbanController } from "@web/views/kanban/kanban_controller";
|
||||
import { useBus, useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { useRef } = owl;
|
||||
|
||||
export class MrpDocumentsKanbanController extends KanbanController {
|
||||
patch(ProductDocumentKanbanController.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.uploadFileInputRef = useRef("uploadFileInput");
|
||||
this.fileUploadService = useService("file_upload");
|
||||
useBus(
|
||||
this.fileUploadService.bus,
|
||||
"FILE_UPLOAD_LOADED",
|
||||
async () => {
|
||||
await this.model.root.load();
|
||||
this.model.notify();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async onFileInputChange(ev) {
|
||||
if (!ev.target.files.length) {
|
||||
return;
|
||||
super.setup(...arguments);
|
||||
if (this.props.context.attached_on_bom) {
|
||||
this.formData.attached_on_bom = this.props.context.bom_id;
|
||||
}
|
||||
await this.fileUploadService.upload(
|
||||
"/mrp/upload_attachment",
|
||||
ev.target.files,
|
||||
{
|
||||
buildFormData: (formData) => {
|
||||
formData.append("res_model", this.props.context.default_res_model);
|
||||
formData.append("res_id", this.props.context.default_res_id);
|
||||
},
|
||||
},
|
||||
);
|
||||
// Reset the file input's value so that the same file may be uploaded twice.
|
||||
ev.target.value = "";
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="mrp.MrpDocumentsKanbanView.Buttons" t-inherit="web.KanbanView.Buttons" t-inherit-mode="primary" owl="1">
|
||||
<div role="toolbar" position="inside">
|
||||
<input type="file" multiple="true" t-ref="uploadFileInput" class="o_input_file o_hidden" t-on-change.stop="onFileInputChange"/>
|
||||
<button type="button" t-attf-class="btn btn-primary o_mrp_documents_kanban_upload"
|
||||
t-on-click.stop.prevent="() => this.uploadFileInputRef.el.click()">
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { CANCEL_GLOBAL_CLICK, KanbanRecord } from "@web/views/kanban/kanban_record";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class MrpDocumentsKanbanRecord extends KanbanRecord {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.messaging = useService("messaging");
|
||||
}
|
||||
/**
|
||||
* @override
|
||||
*
|
||||
* Override to open the preview upon clicking the image, if compatible.
|
||||
*/
|
||||
onGlobalClick(ev) {
|
||||
if (ev.target.closest(CANCEL_GLOBAL_CLICK) && !ev.target.classList.contains("o_mrp_download")) {
|
||||
return;
|
||||
}
|
||||
if (ev.target.classList.contains("o_mrp_download")) {
|
||||
window.location = `/web/content/mrp.document/${this.props.record.resId}/datas?download=true`;
|
||||
return;
|
||||
} else if (ev.target.closest(".o_kanban_previewer")) {
|
||||
this.messaging.get().then((messaging) => {
|
||||
const attachmentList = messaging.models["AttachmentList"].insert({
|
||||
selectedAttachment: messaging.models["Attachment"].insert({
|
||||
id: this.props.record.data.ir_attachment_id[0],
|
||||
filename: this.props.record.data.name,
|
||||
name: this.props.record.data.name,
|
||||
mimetype: this.props.record.data.mimetype,
|
||||
}),
|
||||
});
|
||||
this.dialog = messaging.models["Dialog"].insert({
|
||||
attachmentListOwnerAsAttachmentView: attachmentList,
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
return super.onGlobalClick(...arguments);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { KanbanRenderer } from "@web/views/kanban/kanban_renderer";
|
||||
import { MrpDocumentsKanbanRecord } from "@mrp/views/mrp_documents_kanban/mrp_documents_kanban_record";
|
||||
import { FileUploadProgressContainer } from "@web/core/file_upload/file_upload_progress_container";
|
||||
import { FileUploadProgressKanbanRecord } from "@web/core/file_upload/file_upload_progress_record";
|
||||
|
||||
export class MrpDocumentsKanbanRenderer extends KanbanRenderer {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.fileUploadService = useService("file_upload");
|
||||
}
|
||||
}
|
||||
|
||||
MrpDocumentsKanbanRenderer.components = {
|
||||
...KanbanRenderer.components,
|
||||
FileUploadProgressContainer,
|
||||
FileUploadProgressKanbanRecord,
|
||||
KanbanRecord: MrpDocumentsKanbanRecord,
|
||||
};
|
||||
MrpDocumentsKanbanRenderer.template = "mrp.MrpDocumentsKanbanRenderer";
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="mrp.MrpDocumentsKanbanRenderer" owl="1" t-inherit-mode="primary" t-inherit="web.KanbanRenderer">
|
||||
<!-- Before the first t-foreach -->
|
||||
<xpath expr="//t[@t-key='groupOrRecord.key']" position="before">
|
||||
<FileUploadProgressContainer fileUploads="fileUploadService.uploads" Component="constructor.components.FileUploadProgressKanbanRecord"/>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { kanbanView } from "@web/views/kanban/kanban_view";
|
||||
import { MrpDocumentsKanbanController } from "@mrp/views/mrp_documents_kanban/mrp_documents_kanban_controller";
|
||||
import { MrpDocumentsKanbanRenderer } from "@mrp/views/mrp_documents_kanban/mrp_documents_kanban_renderer";
|
||||
|
||||
export const mrpDocumentsKanbanView = {
|
||||
...kanbanView,
|
||||
Controller: MrpDocumentsKanbanController,
|
||||
Renderer: MrpDocumentsKanbanRenderer,
|
||||
buttonTemplate: "mrp.MrpDocumentsKanbanView.Buttons",
|
||||
};
|
||||
|
||||
registry.category("views").add("mrp_documents_kanban", mrpDocumentsKanbanView);
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { FloatField } from '@web/views/fields/float/float_field';
|
||||
|
||||
const { useRef, useEffect } = owl;
|
||||
|
||||
export class MrpConsumed extends FloatField {
|
||||
setup() {
|
||||
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({ manual_consumption: true });
|
||||
}
|
||||
}
|
||||
|
||||
registry.category('fields').add('mrp_consumed', MrpConsumed);
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { X2ManyField } from "@web/views/fields/x2many/x2many_field";
|
||||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
|
||||
export class MrpProductionComponentsListRenderer extends ListRenderer {
|
||||
getCellClass(column, record) {
|
||||
let classNames = super.getCellClass(...arguments);
|
||||
if (column.name == "quantity_done" && !record.data.manual_consumption) {
|
||||
classNames += ' o_non_manual_consumption';
|
||||
}
|
||||
return classNames;
|
||||
}
|
||||
}
|
||||
|
||||
export class MrpProductionComponentsX2ManyField extends X2ManyField {}
|
||||
MrpProductionComponentsX2ManyField.components = { ...X2ManyField.components, ListRenderer: MrpProductionComponentsListRenderer };
|
||||
|
||||
MrpProductionComponentsX2ManyField.additionalClasses = ['o_field_many2many'];
|
||||
registry.category("fields").add("mrp_production_components_x2many", MrpProductionComponentsX2ManyField);
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
td.o_non_manual_consumption {
|
||||
background-color: rgba($gray-200, .5) !important;
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { RemainingDaysField } from "@web/views/fields/remaining_days/remaining_days_field";
|
||||
|
||||
export class MrpRemainingDaysUnformattedField extends RemainingDaysField {
|
||||
static template = "mrp.MrpRemainingDaysUnformattedField"
|
||||
}
|
||||
|
||||
export const mrpRemainingDaysUnformattedField = {
|
||||
component: MrpRemainingDaysUnformattedField,
|
||||
displayName: _t("Remaining Days"),
|
||||
supportedTypes: ["date", "datetime"],
|
||||
};
|
||||
|
||||
registry.category("fields").add("mrp_remaining_days_unformatted", mrpRemainingDaysUnformattedField);
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mrp.MrpRemainingDaysUnformattedField" t-inherit="web.RemainingDaysField">
|
||||
<xpath expr="//div" position="attributes">
|
||||
<attribute name="t-att-class"></attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { FloatField } from "@web/views/fields/float/float_field";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { FloatField, floatField } from "@web/views/fields/float/float_field";
|
||||
import { formatFloat } from "@web/views/fields/formatters";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useRef, onPatched, onMounted, useState } from "@odoo/owl";
|
||||
|
||||
/**
|
||||
* This widget is used to display alongside the total quantity to consume of a production order,
|
||||
|
|
@ -12,8 +11,8 @@ import { formatFloat } from "@web/views/fields/formatters";
|
|||
* The widget will be '3.000 / 5.000'.
|
||||
*/
|
||||
|
||||
const { useRef, onPatched, onMounted, useState } = owl;
|
||||
export class MrpShouldConsumeOwl extends FloatField {
|
||||
static template = "mrp.ShouldConsume";
|
||||
setup() {
|
||||
super.setup();
|
||||
this.fields = this.props.record.fields;
|
||||
|
|
@ -41,9 +40,12 @@ export class MrpShouldConsumeOwl extends FloatField {
|
|||
...this.nodeOptions,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MrpShouldConsumeOwl.template = "mrp.ShouldConsume";
|
||||
MrpShouldConsumeOwl.displayName = "MRP Should Consume";
|
||||
export const mrpShouldConsumeOwl = {
|
||||
...floatField,
|
||||
component: MrpShouldConsumeOwl,
|
||||
displayName: "MRP Should Consume",
|
||||
};
|
||||
|
||||
registry.category("fields").add("mrp_should_consume", MrpShouldConsumeOwl);
|
||||
registry.category("fields").add("mrp_should_consume", mrpShouldConsumeOwl);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mrp.ShouldConsume" owl="1">
|
||||
<t t-name="mrp.ShouldConsume">
|
||||
<t t-if="displayShouldConsume">
|
||||
<span t-attf-class="o_should_consume ps-1 {{!props.readonly ? 'o_row mx-0' : ''}}">
|
||||
<span><t t-esc="shouldConsumeQty"/> / </span>
|
||||
<span t-attf-class="o_should_consume">
|
||||
<span t-if="props.readonly"><t t-esc="shouldConsumeQty"/> / </span>
|
||||
<t t-call="web.FloatField"/>
|
||||
</span>
|
||||
</t>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
/** @odoo-module */
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { PopoverComponent, PopoverWidgetField } from '@stock/widgets/popover_widget';
|
||||
import {
|
||||
PopoverComponent,
|
||||
PopoverWidgetField,
|
||||
popoverWidgetField,
|
||||
} from "@stock/widgets/popover_widget";
|
||||
|
||||
/**
|
||||
* Link to a Char field representing a JSON:
|
||||
|
|
@ -30,10 +33,13 @@ class WorkOrderPopover extends PopoverComponent {
|
|||
}
|
||||
};
|
||||
|
||||
class WorkOrderPopoverField extends PopoverWidgetField {};
|
||||
class WorkOrderPopoverField extends PopoverWidgetField {
|
||||
static components = {
|
||||
Popover: WorkOrderPopover,
|
||||
};
|
||||
}
|
||||
|
||||
WorkOrderPopoverField.components = {
|
||||
Popover: WorkOrderPopover
|
||||
};
|
||||
|
||||
registry.category("fields").add("mrp_workorder_popover", WorkOrderPopoverField);
|
||||
registry.category("fields").add("mrp_workorder_popover", {
|
||||
...popoverWidgetField,
|
||||
component: WorkOrderPopoverField,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
|
||||
<div t-name="mrp.workorderPopover" owl="1">
|
||||
<div t-name="mrp.workorderPopover">
|
||||
<h6>Scheduling Information</h6>
|
||||
<t t-foreach="props.infos" t-as="info" t-key="info_index">
|
||||
<i t-attf-class="fa fa-arrow-right me-2 #{ info.color }"></i><t t-esc="info.msg"/><br/>
|
||||
<i t-attf-class="oi oi-arrow-right me-2 #{ info.color }"></i><t t-esc="info.msg"/><br/>
|
||||
</t>
|
||||
<button t-if="props.replan" t-on-click="onReplanClick" class="btn btn-sm btn-primary m-1 float-end action_replan_button">Replan</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { parseFloatTime } from "@web/views/fields/parsers";
|
||||
import { useInputField } from "@web/views/fields/input_field_hook";
|
||||
|
||||
const { Component, useState, onWillUpdateProps, onWillStart, onWillDestroy } = owl;
|
||||
import { useRecordObserver } from "@web/model/relational_model/utils";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
import { Component, useState, onWillUpdateProps, onWillStart, onWillDestroy } from "@odoo/owl";
|
||||
|
||||
function formatMinutes(value) {
|
||||
if (value === false) {
|
||||
|
|
@ -23,53 +22,39 @@ function formatMinutes(value) {
|
|||
}
|
||||
|
||||
export class MrpTimer extends Component {
|
||||
static template = "mrp.MrpTimer";
|
||||
static props = {
|
||||
value: { type: Number },
|
||||
ongoing: { type: Boolean, optional: true },
|
||||
};
|
||||
static defaultProps = { ongoing: false };
|
||||
|
||||
setup() {
|
||||
this.orm = useService('orm');
|
||||
this.state = useState({
|
||||
// duration is expected to be given in minutes
|
||||
duration:
|
||||
this.props.value !== undefined ? this.props.value : this.props.record.data.duration,
|
||||
duration: this.props.value,
|
||||
});
|
||||
useInputField({
|
||||
getValue: () => this.durationFormatted,
|
||||
refName: "numpadDecimal",
|
||||
parse: (v) => parseFloatTime(v),
|
||||
});
|
||||
|
||||
this.ongoing =
|
||||
this.props.ongoing !== undefined
|
||||
? this.props.ongoing
|
||||
: this.props.record.data.is_user_working;
|
||||
|
||||
onWillStart(async () => {
|
||||
if(this.props.ongoing === undefined && !this.props.record.model.useSampleModel && this.props.record.data.state == "progress") {
|
||||
const additionalDuration = await this.orm.call('mrp.workorder', 'get_working_duration', [this.props.record.resId]);
|
||||
this.state.duration += additionalDuration;
|
||||
}
|
||||
this.lastDateTime = Date.now();
|
||||
this.ongoing = this.props.ongoing;
|
||||
onWillStart(() => {
|
||||
if (this.ongoing) {
|
||||
this._runTimer();
|
||||
this._runSleepTimer();
|
||||
}
|
||||
});
|
||||
onWillUpdateProps((nextProps) => {
|
||||
const newOngoing =
|
||||
"ongoing" in nextProps
|
||||
? nextProps.ongoing
|
||||
: "record" in nextProps && nextProps.record.data.is_user_working;
|
||||
const rerun = !this.ongoing && newOngoing;
|
||||
this.ongoing = newOngoing;
|
||||
const rerun = !this.ongoing && nextProps.ongoing;
|
||||
this.ongoing = nextProps.ongoing;
|
||||
if (rerun) {
|
||||
this.state.duration = nextProps.value;
|
||||
this._runTimer();
|
||||
this._runSleepTimer();
|
||||
}
|
||||
});
|
||||
onWillDestroy(() => clearTimeout(this.timer));
|
||||
}
|
||||
|
||||
get durationFormatted() {
|
||||
if(this.props.value!=this.state.duration && this.props.record && this.props.record.isDirty){
|
||||
if (typeof this.props.setDirty==='function')this.props.setDirty(false);
|
||||
this.state.duration=this.props.value
|
||||
}
|
||||
return formatMinutes(this.state.duration);
|
||||
}
|
||||
|
||||
|
|
@ -81,10 +66,64 @@ export class MrpTimer extends Component {
|
|||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
//updates the time when the computer wakes from sleep mode
|
||||
_runSleepTimer() {
|
||||
this.timer = setTimeout(async () => {
|
||||
const diff = Date.now() - this.lastDateTime - 10000;
|
||||
if (diff > 1000) {
|
||||
this.state.duration += diff / (1000 * 60);
|
||||
}
|
||||
this.lastDateTime = Date.now();
|
||||
this._runSleepTimer();
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
MrpTimer.supportedTypes = ["float"];
|
||||
MrpTimer.template = "mrp.MrpTimer";
|
||||
class MrpTimerField extends Component {
|
||||
static template = "mrp.MrpTimerField";
|
||||
static components = { MrpTimer };
|
||||
static props = standardFieldProps;
|
||||
|
||||
registry.category("fields").add("mrp_timer", MrpTimer);
|
||||
registry.category("formatters").add("mrp_timer", formatMinutes);
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
useInputField({
|
||||
getValue: () => this.durationFormatted,
|
||||
refName: "numpadDecimal",
|
||||
parse: (v) => parseFloatTime(v),
|
||||
});
|
||||
|
||||
useRecordObserver(async (record) => {
|
||||
if (!this.props.record.model.useSampleModel && record.data.state === "progress") {
|
||||
this.duration = await this.orm.call(
|
||||
"mrp.workorder",
|
||||
"get_duration",
|
||||
[this.props.record.resId]
|
||||
);
|
||||
} else {
|
||||
this.duration = record.data[this.props.name];
|
||||
}
|
||||
})
|
||||
|
||||
onWillDestroy(() => clearTimeout(this.timer));
|
||||
}
|
||||
|
||||
get durationFormatted() {
|
||||
if (this.props.record.data[this.props.name] != this.duration && this.props.record.dirty) {
|
||||
this.duration = this.props.record.data[this.props.name];
|
||||
}
|
||||
return formatMinutes(this.duration);
|
||||
}
|
||||
|
||||
get ongoing() {
|
||||
return this.props.record.data.is_user_working;
|
||||
}
|
||||
}
|
||||
|
||||
export const mrpTimerField = {
|
||||
component: MrpTimerField,
|
||||
supportedTypes: ["float"],
|
||||
};
|
||||
|
||||
registry.category("fields").add("mrp_timer", mrpTimerField);
|
||||
registry.category("formatters").add("mrp_timer", formatMinutes);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="mrp.MrpTimer" owl="1">
|
||||
<span t-if="props.readonly" t-esc="durationFormatted"/>
|
||||
<t t-name="mrp.MrpTimer">
|
||||
<span t-esc="durationFormatted"/>
|
||||
</t>
|
||||
|
||||
<t t-name="mrp.MrpTimerField">
|
||||
<MrpTimer t-if="props.readonly" value="duration" ongoing="ongoing"/>
|
||||
<input t-else="" t-att-id="props.id" t-ref="numpadDecimal" t-att-placeholder="props.placeholder" inputmode="numeric" class="o_input" />
|
||||
</t>
|
||||
</templates>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { cookie } from "@web/core/browser/cookie";
|
||||
import { getColor, hexToRGBA, darkenColor } from "@web/core/colors/colors";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { JournalDashboardGraphField } from "@web/views/fields/journal_dashboard_graph/journal_dashboard_graph_field";
|
||||
|
||||
export class WorkcenterDashboardGraphField extends JournalDashboardGraphField{
|
||||
getBarChartConfig() {
|
||||
const labels = this.data[0].labels;
|
||||
const color19 = getColor(1, cookie.get("color_scheme"), "odoo");
|
||||
const color13 = getColor(2, cookie.get("color_scheme"), "odoo");
|
||||
const color10 = getColor(3, cookie.get("color_scheme"), "odoo");
|
||||
const loadBarColor = this.data[0].is_sample_data ? hexToRGBA(color19, 0.1) : color19;
|
||||
const excessBarColor = this.data[0].is_sample_data ? hexToRGBA(color13, 0.1) : color13;
|
||||
const maxLoadLineColor = this.data[0].is_sample_data ? hexToRGBA(color10, 0.1) : hexToRGBA(color10, 0.5);
|
||||
const darkColorOnHover = this.data[0].is_sample_data ? loadBarColor : darkenColor(loadBarColor, 0.1);
|
||||
return {
|
||||
type: 'scatter',
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
type: 'line',
|
||||
borderColor: maxLoadLineColor,
|
||||
// normally the line stops in the middle of the first and last columns, which is ugly
|
||||
// to make it go through all the graph, the single point has been configured as a line
|
||||
// in the middle of the graph and extended. The real line is not shown.
|
||||
data: [,,this.data[0].values[1]],
|
||||
showLine: false,
|
||||
pointStyle: 'line',
|
||||
pointRadius: 500,
|
||||
pointBorderWidth: 2,
|
||||
// this is so that hovering on the 'line' does not change its appearance
|
||||
pointHoverRadius: 500,
|
||||
pointHoverBorderWidth: 2,
|
||||
},
|
||||
{
|
||||
type: 'bar',
|
||||
backgroundColor: loadBarColor,
|
||||
data: this.data[0].values[0],
|
||||
label: "Total Load",
|
||||
borderWidth: 0,
|
||||
stack: 'mainStack',
|
||||
hoverBackgroundColor: function(ctx, options) {
|
||||
return (ctx.parsed._stacks.y[2] !== 0) ? excessBarColor : darkColorOnHover;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'bar',
|
||||
backgroundColor: excessBarColor,
|
||||
data: this.data[0].values[2],
|
||||
label: "Excess Load",
|
||||
borderWidth: 0,
|
||||
stack: 'mainStack',
|
||||
}
|
||||
],
|
||||
labels,
|
||||
},
|
||||
options: {
|
||||
layout: {
|
||||
autoPadding: false,
|
||||
},
|
||||
interaction: {
|
||||
includeInvisible: true,
|
||||
axis: 'x',
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
enabled: !this.data[0].is_sample_data,
|
||||
intersect: true,
|
||||
mode: 'nearest',
|
||||
position: 'nearest',
|
||||
caretSize: 0,
|
||||
filter: function(ctx) {
|
||||
if (ctx.dataset.type !== 'line') {
|
||||
return ctx;
|
||||
}
|
||||
},
|
||||
callbacks: {
|
||||
label: function(ctx) {
|
||||
if (ctx.datasetIndex === 1) {
|
||||
const totalLoadValue = Object.values(ctx.parsed._stacks.y._visualValues).reduce((accu, curr) => {return accu + curr}, 0);
|
||||
return _t(ctx.dataset.label + ": " + totalLoadValue + " hours");
|
||||
}
|
||||
return _t(ctx.dataset.label + ": " + ctx.parsed.y + " hours");
|
||||
},
|
||||
title: function(ctx) {
|
||||
return "";
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
display: false,
|
||||
suggestedMax: this.data[0].values[1] * 1.5,
|
||||
stacked: true,
|
||||
},
|
||||
x: {
|
||||
type: 'category',
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
border: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
maintainAspectRatio: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const workcenterDashboardGraphField = {
|
||||
component: WorkcenterDashboardGraphField,
|
||||
supportedTypes: ["text"],
|
||||
extractProps: ({ attrs }) => ({
|
||||
graphType: attrs.graph_type,
|
||||
}),
|
||||
};
|
||||
|
||||
registry.category("fields").add("workcenter_dashboard_graph", workcenterDashboardGraphField);
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<div t-name="MrpDocumentsKanbanView.buttons">
|
||||
<button type="button" t-attf-class="btn btn-primary o_mrp_documents_kanban_upload">
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
</templates>
|
||||
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ProductDocument extends models.ServerModel {
|
||||
_name = "product.document";
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { mailModels } from "@mail/../tests/mail_test_helpers";
|
||||
import { fields } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ResFake extends mailModels.ResFake {
|
||||
duration = fields.Float({ string: "duration" });
|
||||
|
||||
_views = {
|
||||
form: /* xml */ `
|
||||
<form>
|
||||
<field name="duration" widget="mrp_timer" readonly="1"/>
|
||||
</form>`,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
openView,
|
||||
registerArchs,
|
||||
start,
|
||||
startServer
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { defineMrpModels } from "@mrp/../tests/mrp_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { inputFiles } from "@web/../tests/utils";
|
||||
import { asyncStep, getService, patchWithCleanup, waitForSteps } from "@web/../tests/web_test_helpers";
|
||||
import { fileUploadService } from "@web/core/file_upload/file_upload_service";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMrpModels();
|
||||
|
||||
const newArchs = {
|
||||
"product.document,false,kanban": `<kanban js_class="product_documents_kanban" create="false"><templates><t t-name="card">
|
||||
<field name="name"/>
|
||||
</t></templates></kanban>`,
|
||||
};
|
||||
|
||||
test("MRP documents kanban basic rendering", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const irAttachment = pyEnv["ir.attachment"].create({
|
||||
mimetype: "image/png",
|
||||
name: "test.png",
|
||||
});
|
||||
pyEnv["product.document"].create([
|
||||
{ name: "test1", ir_attachment_id: irAttachment, mimetype: "image/png" },
|
||||
{ name: "test2" },
|
||||
{ name: "test3" },
|
||||
]);
|
||||
registerArchs(newArchs);
|
||||
await start();
|
||||
await openView({ res_model: "product.document", views: [[false, "kanban"]] });
|
||||
await contains("button[name='product_upload_document']");
|
||||
await contains(".o_kanban_renderer .o_kanban_record:not(.o_kanban_ghost)", { count: 3 });
|
||||
// check control panel buttons
|
||||
await contains(".o_control_panel_main_buttons .btn-primary", { text: "Upload" });
|
||||
});
|
||||
|
||||
test("mrp: upload multiple files", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const irAttachment = pyEnv["ir.attachment"].create({
|
||||
mimetype: "image/png",
|
||||
name: "test.png",
|
||||
});
|
||||
const text1 = new File(["hello, world"], "text1.txt", { type: "text/plain" });
|
||||
const text2 = new File(["hello, world"], "text2.txt", { type: "text/plain" });
|
||||
const text3 = new File(["hello, world"], "text3.txt", { type: "text/plain" });
|
||||
pyEnv["product.document"].create([
|
||||
{ name: "test1", ir_attachment_id: irAttachment, mimetype: "image/png" },
|
||||
{ name: "test2" },
|
||||
{ name: "test3" },
|
||||
]);
|
||||
|
||||
registerArchs(newArchs);
|
||||
await start();
|
||||
await openView({ res_model: "product.document", views: [[false, "kanban"]] });
|
||||
|
||||
getService("file_upload").bus.addEventListener("FILE_UPLOAD_ADDED", () => asyncStep("xhrSend"));
|
||||
await inputFiles(".o_control_panel_main_buttons .o_input_file", [text1]);
|
||||
await waitForSteps(["xhrSend"]);
|
||||
await inputFiles(".o_control_panel_main_buttons .o_input_file", [text2, text3]);
|
||||
await waitForSteps(["xhrSend"]);
|
||||
});
|
||||
|
||||
test("mrp: click on image opens attachment viewer", async () => {
|
||||
const newArchs = {
|
||||
"product.document,false,kanban": `
|
||||
<kanban js_class="product_documents_kanban" create="false">
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<div class="o_kanban_previewer" t-if="record.ir_attachment_id.raw_value">
|
||||
<field name="ir_attachment_id" invisible="1"/>
|
||||
<img t-attf-src="/web/image/#{record.ir_attachment_id.raw_value}" width="100" height="100" alt="Document" class="o_attachment_image"/>
|
||||
</div>
|
||||
<field name="name"/>
|
||||
<field name="mimetype"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
};
|
||||
const pyEnv = await startServer();
|
||||
const irAttachment = pyEnv["ir.attachment"].create({
|
||||
mimetype: "image/png",
|
||||
name: "test.png",
|
||||
});
|
||||
pyEnv["product.document"].create([
|
||||
{ name: "test1", ir_attachment_id: irAttachment, mimetype: "image/png" },
|
||||
{ name: "test2" },
|
||||
{ name: "test3" },
|
||||
]);
|
||||
|
||||
registerArchs(newArchs);
|
||||
await start();
|
||||
await openView({ res_model: "product.document", views: [[false, "kanban"]] });
|
||||
|
||||
await click(".o_kanban_previewer");
|
||||
await contains(".o-FileViewer");
|
||||
await click(".o-FileViewer-headerButton .fa-times");
|
||||
await contains(".o-FileViewer", { count: 0 });
|
||||
});
|
||||
|
||||
test("mrp: upload progress bars", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const irAttachment = pyEnv["ir.attachment"].create({
|
||||
mimetype: "image/png",
|
||||
name: "test.png",
|
||||
});
|
||||
const text1 = new File(["hello, world"], "text1.txt", { type: "text/plain" });
|
||||
pyEnv["product.document"].create([
|
||||
{ name: "test1", ir_attachment_id: irAttachment, mimetype: "image/png" },
|
||||
{ name: "test2" },
|
||||
{ name: "test3" },
|
||||
]);
|
||||
|
||||
registerArchs(newArchs);
|
||||
await start();
|
||||
await openView({ res_model: "product.document", views: [[false, "kanban"]] });
|
||||
|
||||
let xhr;
|
||||
patchWithCleanup(fileUploadService, {
|
||||
createXhr() {
|
||||
xhr = super.createXhr(...arguments);
|
||||
xhr.send = () => {};
|
||||
return xhr;
|
||||
},
|
||||
});
|
||||
|
||||
await inputFiles(".o_control_panel_main_buttons .o_input_file", [text1]);
|
||||
|
||||
const progressEvent = new Event("progress", { bubbles: true });
|
||||
progressEvent.loaded = 250000000;
|
||||
progressEvent.total = 500000000;
|
||||
progressEvent.lengthComputable = true;
|
||||
xhr.upload.dispatchEvent(progressEvent);
|
||||
await contains(".o_file_upload_progress_text_left", { text: "Uploading... (50%)" });
|
||||
|
||||
progressEvent.loaded = 350000000;
|
||||
xhr.upload.dispatchEvent(progressEvent);
|
||||
await contains(".o_file_upload_progress_text_right", { text: "(350/500MB)" });
|
||||
});
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import testUtils from 'web.test_utils';
|
||||
import { registry } from "@web/core/registry";
|
||||
import {
|
||||
click,
|
||||
getFixture,
|
||||
nextTick,
|
||||
} from '@web/../tests/helpers/utils';
|
||||
import { setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import {
|
||||
start,
|
||||
startServer,
|
||||
} from '@mail/../tests/helpers/test_utils';
|
||||
|
||||
import { fileUploadService } from "@web/core/file_upload/file_upload_service";
|
||||
|
||||
import { addModelNamesToFetch } from '@bus/../tests/helpers/model_definitions_helpers';
|
||||
addModelNamesToFetch([
|
||||
'mrp.document',
|
||||
]);
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let target;
|
||||
let pyEnv;
|
||||
|
||||
QUnit.module('Views', {}, function () {
|
||||
|
||||
QUnit.module('MrpDocumentsKanbanView', {
|
||||
beforeEach: async function () {
|
||||
serviceRegistry.add("file_upload", fileUploadService);
|
||||
this.ORIGINAL_CREATE_XHR = fileUploadService.createXhr;
|
||||
this.patchDocumentXHR = (mockedXHRs, customSend) => {
|
||||
fileUploadService.createXhr = () => {
|
||||
const xhr = new window.EventTarget();
|
||||
Object.assign(xhr, {
|
||||
upload: new window.EventTarget(),
|
||||
open() {},
|
||||
send(data) { customSend && customSend(data); },
|
||||
});
|
||||
mockedXHRs.push(xhr);
|
||||
return xhr;
|
||||
};
|
||||
};
|
||||
pyEnv = await startServer();
|
||||
const irAttachment = pyEnv['ir.attachment'].create({
|
||||
mimetype: 'image/png',
|
||||
name: 'test.png',
|
||||
})
|
||||
pyEnv['mrp.document'].create([
|
||||
{name: 'test1', priority: 2, ir_attachment_id: irAttachment},
|
||||
{name: 'test2', priority: 1},
|
||||
{name: 'test3', priority: 3},
|
||||
]);
|
||||
target = getFixture();
|
||||
setupViewRegistries();
|
||||
},
|
||||
afterEach() {
|
||||
fileUploadService.createXhr = this.ORIGINAL_CREATE_XHR;
|
||||
},
|
||||
}, function () {
|
||||
QUnit.test('MRP documents kanban basic rendering', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const views = {
|
||||
'mrp.document,false,kanban':
|
||||
`<kanban js_class="mrp_documents_kanban" create="false"><templates><t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="name"/>
|
||||
</div>
|
||||
</t></templates></kanban>`
|
||||
};
|
||||
const { openView } = await start({
|
||||
serverData: { views },
|
||||
});
|
||||
await openView({
|
||||
res_model: 'mrp.document',
|
||||
views: [[false, 'kanban']],
|
||||
});
|
||||
|
||||
assert.ok(target.querySelector('.o_mrp_documents_kanban_upload'),
|
||||
"should have upload button in kanban buttons");
|
||||
assert.containsN(target, '.o_kanban_renderer .o_kanban_record:not(.o_kanban_ghost)', 3,
|
||||
"should have 3 records in the renderer");
|
||||
// check control panel buttons
|
||||
assert.containsN(target, '.o_cp_buttons .btn-primary', 1,
|
||||
"should have only 1 primary button i.e. Upload button");
|
||||
assert.equal(target.querySelector(".o_cp_buttons .btn-primary").innerText.trim().toUpperCase(), 'UPLOAD',
|
||||
"should have a primary 'Upload' button");
|
||||
});
|
||||
|
||||
QUnit.test('mrp: upload multiple files', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const file1 = await testUtils.file.createFile({
|
||||
name: 'text1.txt',
|
||||
content: 'hello, world',
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
const file2 = await testUtils.file.createFile({
|
||||
name: 'text2.txt',
|
||||
content: 'hello, world',
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
const file3 = await testUtils.file.createFile({
|
||||
name: 'text3.txt',
|
||||
content: 'hello, world',
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
|
||||
const mockedXHRs = [];
|
||||
this.patchDocumentXHR(mockedXHRs, data => assert.step('xhrSend'));
|
||||
|
||||
const views = {
|
||||
'mrp.document,false,kanban':
|
||||
`<kanban js_class="mrp_documents_kanban" create="false"><templates><t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="name"/>
|
||||
</div>
|
||||
</t></templates></kanban>`
|
||||
};
|
||||
const { openView } = await start({
|
||||
serverData: { views },
|
||||
});
|
||||
await openView({
|
||||
res_model: 'mrp.document',
|
||||
views: [[false, 'kanban']],
|
||||
});
|
||||
|
||||
const fileInput = target.querySelector(".o_input_file");
|
||||
|
||||
let dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file1);
|
||||
fileInput.files = dataTransfer.files;
|
||||
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
assert.verifySteps(['xhrSend']);
|
||||
|
||||
dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file2);
|
||||
dataTransfer.items.add(file3);
|
||||
fileInput.files = dataTransfer.files;
|
||||
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
assert.verifySteps(['xhrSend']);
|
||||
});
|
||||
|
||||
QUnit.test('mrp: upload progress bars', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const file1 = await testUtils.file.createFile({
|
||||
name: 'text1.txt',
|
||||
content: 'hello, world',
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
|
||||
const mockedXHRs = [];
|
||||
this.patchDocumentXHR(mockedXHRs, data => assert.step('xhrSend'));
|
||||
|
||||
const views = {
|
||||
'mrp.document,false,kanban':
|
||||
`<kanban js_class="mrp_documents_kanban" create="false"><templates><t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="name"/>
|
||||
</div>
|
||||
</t></templates></kanban>`
|
||||
};
|
||||
const { openView } = await start({
|
||||
serverData: { views },
|
||||
});
|
||||
await openView({
|
||||
res_model: 'mrp.document',
|
||||
views: [[false, 'kanban']],
|
||||
});
|
||||
|
||||
const fileInput = target.querySelector(".o_input_file");
|
||||
|
||||
let dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file1);
|
||||
fileInput.files = dataTransfer.files;
|
||||
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
assert.verifySteps(['xhrSend']);
|
||||
|
||||
const progressEvent = new Event('progress', { bubbles: true });
|
||||
progressEvent.loaded = 250000000;
|
||||
progressEvent.total = 500000000;
|
||||
progressEvent.lengthComputable = true;
|
||||
mockedXHRs[0].upload.dispatchEvent(progressEvent);
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_file_upload_progress_text_left').innerText,
|
||||
"Uploading... (50%)",
|
||||
"the current upload progress should be at 50%"
|
||||
);
|
||||
|
||||
progressEvent.loaded = 350000000;
|
||||
mockedXHRs[0].upload.dispatchEvent(progressEvent);
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
target.querySelector('.o_file_upload_progress_text_right').innerText,
|
||||
"(350/500MB)",
|
||||
"the current upload progress should be at (350/500Mb)"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("mrp: click on image opens attachment viewer", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const views = {
|
||||
'mrp.document,false,kanban':
|
||||
`<kanban js_class="mrp_documents_kanban" create="false"><templates><t t-name="kanban-box">
|
||||
<div class="o_kanban_image" t-if="record.ir_attachment_id.raw_value">
|
||||
<div class="o_kanban_previewer">
|
||||
<field name="ir_attachment_id" invisible="1"/>
|
||||
<img t-attf-src="/web/image/#{record.ir_attachment_id.raw_value}" width="100" height="100" alt="Document" class="o_attachment_image"/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<field name="name"/>
|
||||
</div>
|
||||
</t></templates></kanban>`
|
||||
};
|
||||
const { openView } = await start({
|
||||
serverData: { views },
|
||||
});
|
||||
await openView({
|
||||
res_model: 'mrp.document',
|
||||
views: [[false, 'kanban']],
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_kanban_previewer");
|
||||
await click(target.querySelector(".o_kanban_previewer"));
|
||||
await nextTick();
|
||||
|
||||
assert.containsOnce(target, '.o_AttachmentViewer',
|
||||
"should have a document preview");
|
||||
assert.containsOnce(target, '.o_AttachmentViewer_headerItemButtonClose',
|
||||
"should have a close button");
|
||||
|
||||
await click(target, '.o_AttachmentViewer_headerItemButtonClose');
|
||||
assert.containsNone(target, '.o_AttachmentViewer',
|
||||
"should not have a document preview");
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { mailModels } from "@mail/../tests/mail_test_helpers";
|
||||
import { defineModels } from "@web/../tests/web_test_helpers";
|
||||
import { ProductDocument } from "@mrp/../tests/mock_server/mock_models/product_document";
|
||||
import { ResFake } from "@mrp/../tests/mock_server/mock_models/res_fake";
|
||||
|
||||
export function defineMrpModels() {
|
||||
return defineModels(mrpModels);
|
||||
}
|
||||
|
||||
export const mrpModels = {
|
||||
...mailModels,
|
||||
ProductDocument,
|
||||
ResFake,
|
||||
};
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Mrp", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
foo: {
|
||||
fields: {
|
||||
duration: { string: "Duration", type: "float" },
|
||||
},
|
||||
records: [{ id: 1, duration: 150.5 }],
|
||||
},
|
||||
},
|
||||
};
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("MrpTimer");
|
||||
|
||||
QUnit.test("ensure the rendering is based on minutes and seconds", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
resId: 1,
|
||||
arch: '<form><field name="duration" widget="mrp_timer" readonly="1"/></form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_mrp_timer").textContent,
|
||||
"150:30",
|
||||
"should not contain hours and be correctly set base on minutes seconds"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { openFormView, start, startServer } from "@mail/../tests/mail_test_helpers";
|
||||
import { defineMrpModels } from "@mrp/../tests/mrp_test_helpers";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineMrpModels();
|
||||
|
||||
test("ensure the rendering is based on minutes and seconds", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const fakeId = pyEnv["res.fake"].create({ duration: 150.5 });
|
||||
await start();
|
||||
await openFormView("res.fake", fakeId);
|
||||
expect(".o_field_mrp_timer").toHaveText("150:30");
|
||||
});
|
||||
|
|
@ -1,70 +1,34 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import tour from 'web_tour.tour';
|
||||
|
||||
|
||||
tour.register('test_mrp_manual_consumption', {test: true}, [
|
||||
{
|
||||
trigger: 'div[name=move_raw_ids] td[name="quantity_done"]:last:contains("5.00")',
|
||||
run: () => {},
|
||||
},
|
||||
{
|
||||
trigger: 'div[name=move_raw_ids] td[name="quantity_done"]:last',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
trigger: 'div[name="quantity_done"] input',
|
||||
run: 'text 6.0'
|
||||
},
|
||||
{
|
||||
content: "Click Pager",
|
||||
trigger: ".o_pager_value:first()",
|
||||
},
|
||||
{
|
||||
trigger: "input[id='qty_producing']",
|
||||
run: 'text 8.0',
|
||||
},
|
||||
{
|
||||
content: "Click Pager",
|
||||
trigger: ".o_pager_value:first()",
|
||||
},
|
||||
{
|
||||
trigger: 'div[name=move_raw_ids] td[name="quantity_done"]:last:contains("6.00")',
|
||||
run: () => {},
|
||||
},
|
||||
{
|
||||
trigger: 'button[name=button_mark_done]',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
trigger: 'button[name=action_confirm]',
|
||||
extra_trigger: '.o_technical_modal',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
trigger: 'button[name=action_backorder]',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
trigger: "input[id='qty_producing']",
|
||||
run: 'text 2.0',
|
||||
},
|
||||
{
|
||||
content: "Click Pager",
|
||||
trigger: ".o_pager_value:first()",
|
||||
},
|
||||
{
|
||||
trigger: 'div[name=move_raw_ids] td[name="quantity_done"]:last:contains("2.00")',
|
||||
run: () => {},
|
||||
},
|
||||
{
|
||||
trigger: 'button[name=button_mark_done]',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
trigger: 'button[name=action_confirm]',
|
||||
extra_trigger: '.o_technical_modal',
|
||||
run: 'click',
|
||||
},
|
||||
...tour.stepUtils.saveForm(),
|
||||
]);
|
||||
import { registry } from "@web/core/registry";
|
||||
import { stepUtils } from '@web_tour/tour_utils';
|
||||
registry.category("web_tour.tours").add('test_mrp_manual_consumption_02', {
|
||||
steps: () => [
|
||||
{
|
||||
trigger: 'div[name=move_raw_ids] td[name="quantity"]:last:contains("0.00")',
|
||||
},
|
||||
{
|
||||
trigger: 'div[name=move_raw_ids] td[name="quantity"]:last',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
trigger: 'div[name="quantity"] input',
|
||||
run: "edit 16.0 && click body",
|
||||
},
|
||||
{
|
||||
content: "Click Pager",
|
||||
trigger: ".o_pager_value:first()",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: "input[id='qty_producing_0']",
|
||||
run: "edit 8.0 && click body",
|
||||
},
|
||||
{
|
||||
content: "Click Pager",
|
||||
trigger: ".o_pager_value:first()",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: 'div[name=move_raw_ids] td[name="quantity"]:last:contains("16.00")',
|
||||
},
|
||||
...stepUtils.saveForm(),
|
||||
]});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
|
||||
registry.category("web_tour.tours").add("test_manufacture_from_bom", {
|
||||
steps: () => [
|
||||
{
|
||||
trigger: '[name="product_tmpl_id"]',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: '.o_stat_text:contains("BoM Overview")',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: '.fa-toggle-off',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: 'button.btn-primary:contains("Manufacture")',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: 'button[aria-checked="true"]:contains("Draft")',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
|
||||
registry.category("web_tour.tours").add('test_mrp_bom_product_catalog', {
|
||||
steps: () => [
|
||||
{
|
||||
trigger: 'button[name=action_add_from_catalog]',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: '.o_kanban_record:nth-child(1)',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: '.o_product_added',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: 'button:contains("Back to BoM")',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: 'div.o_field_one2many:contains("Component")',
|
||||
},
|
||||
]});
|
||||
|
||||
registry.category("web_tour.tours").add('test_mrp_production_product_catalog', {
|
||||
steps: () => [
|
||||
{
|
||||
trigger: 'button[name=action_add_from_catalog_raw]',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: '.o_kanban_record:nth-child(1)',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: '.o_product_added',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: 'button:contains("Back to Production")',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: 'div.o_field_widget:contains("WH/MO/")',
|
||||
},
|
||||
]});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/** @odoo-module **/
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
registry.category("web_tour.tours").add("mrp_bom_report_tour", {
|
||||
steps: () => [
|
||||
{
|
||||
content: "Check the current displayed variant",
|
||||
trigger: ".o_mrp_bom_report_page h2 a:contains('[alpaca] Product Test Sync (L)')",
|
||||
run: () => {},
|
||||
},
|
||||
{
|
||||
content: "Open dropdown menu",
|
||||
trigger: ".o-autocomplete--input",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Select the other variant",
|
||||
trigger: ".o-autocomplete--dropdown-menu.show li.o-autocomplete--dropdown-item:eq(1)",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Ensure the second variant is displayed",
|
||||
trigger: ".o_mrp_bom_report_page h2 a:contains('[zebra] Product Test Sync (S)')",
|
||||
run: () => {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
|
||||
registry.category("web_tour.tours").add('test_manufacturing_and_byproduct_sm_to_sml_synchronization', {
|
||||
steps: () => [
|
||||
{
|
||||
trigger: ".btn-primary[name=action_confirm]",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_data_row:has([name=quantity]:contains(5.00)) > td:contains(product2)",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: "button:contains('Details')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: "h4:contains('Components')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".modal .o_list_number:contains(5)",
|
||||
},
|
||||
{
|
||||
content: "Click Save",
|
||||
trigger: ".modal .modal-footer .o_form_button_save",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_data_row:has([name=quantity]:contains(5.00)) > td:contains(product2)",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_field_widget[name=quantity] input",
|
||||
run: 'edit 21',
|
||||
},
|
||||
{
|
||||
trigger: "button:contains('Details')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: "h4:contains('Components')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".modal .modal-body .o_data_row > td:contains('WH/Stock')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".modal .modal-body .o_field_widget[name=quantity] input",
|
||||
run: 'edit 25',
|
||||
},
|
||||
{
|
||||
content: "Click Save",
|
||||
trigger: ".modal .modal-footer .o_form_button_save",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_data_row:has([name=product_uom_qty]:contains(5.00)) > td:contains(25)",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_field_widget[name=quantity] input",
|
||||
run: 'edit 7',
|
||||
},
|
||||
{
|
||||
trigger: "button:contains('Details')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".modal .o_data_row > td:contains('7')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Click Save",
|
||||
trigger: ".modal .modal-footer .o_form_button_save",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".nav-link[name=finished_products]",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_data_row:has([name=quantity]:contains(2.00)) > td:contains(product2)",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".fa-list",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: "h4:contains('Move Byproduct')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".modal .modal-body .o_data_row > td:contains('WH/Stock')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".modal .modal-body .o_field_widget[name=quantity] input",
|
||||
run: 'edit 2',
|
||||
},
|
||||
{
|
||||
content: "Click Save",
|
||||
trigger: ".modal .modal-footer .o_form_button_save",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_data_row:has([name=quantity]:contains(2.00)) > td[name=product_id]:contains(product2)",
|
||||
run: "dblclick",
|
||||
},
|
||||
{
|
||||
trigger: ".o_field_widget[name=quantity] input",
|
||||
run: 'edit 5',
|
||||
},
|
||||
{
|
||||
trigger: ".fa-list",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: "h4:contains('Move Byproduct')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".modal .modal-body .o_data_row > td:contains('WH/Stock')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".modal .modal-body .o_field_widget[name=quantity] input",
|
||||
run: 'edit 7',
|
||||
},
|
||||
{
|
||||
content: "Click Save",
|
||||
trigger: ".modal .modal-footer .o_form_button_save",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_data_row:has([name=product_uom_qty]:contains(2.00)) > td:contains(10)",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_field_widget[name=quantity] input",
|
||||
run: 'edit 7',
|
||||
},
|
||||
{
|
||||
trigger: ".fa-list",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_list_footer .o_list_number > span:contains('7')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Click Save",
|
||||
trigger: ".modal .modal-footer .o_form_button_save",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "wait for save completion",
|
||||
trigger: ".o_form_readonly, .o_form_saved",
|
||||
},
|
||||
]
|
||||
});
|
||||