Initial commit: Mrp packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:50 +02:00
commit 50d736b3bd
739 changed files with 538193 additions and 0 deletions

View file

@ -0,0 +1,168 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { useService } 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;
export class BomOverviewComponent extends Component {
setup() {
this.orm = useService("orm");
this.context = this.props.action.context;
this.actionService = useService("action");
this.variants = [];
this.warehouses = [];
this.showVariants = false;
this.uomName = "";
this.extraColumnCount = 0;
this.unfoldedIds = new Set();
this.state = useState({
showOptions: {
uom: false,
availabilities: true,
costs: true,
operations: true,
leadTimes: true,
attachments: false,
},
currentWarehouse: null,
currentVariantId: null,
bomData: {},
precision: 2,
bomQuantity: null,
});
useSubEnv({
overviewBus: new EventBus(),
});
onWillStart(async () => {
await this.getWarehouses();
await this.initBomData();
});
}
//---- Data ----
async initBomData() {
const bomData = await this.getBomData();
this.state.bomQuantity = bomData["bom_qty"];
this.state.showOptions.uom = bomData["is_uom_applied"];
this.uomName = bomData["bom_uom_name"];
this.variants = bomData["variants"];
this.showVariants = bomData["is_variant_applied"];
if (this.showVariants) {
this.state.currentVariantId = Object.keys(this.variants)[0];
}
this.state.precision = bomData["precision"];
}
async getBomData() {
const args = [
this.activeId,
this.state.bomQuantity,
this.state.currentVariantId,
];
const context = { ...this.context};
if (this.state.currentWarehouse) {
context.warehouse = this.state.currentWarehouse.id;
}
const bomData = await this.orm.call(
"report.mrp.report_bom_structure",
"get_html",
args,
{ context }
);
this.state.bomData = bomData["lines"];
this.state.showOptions.attachments = bomData["has_attachments"];
return bomData;
}
async getWarehouses() {
const warehouses = await this.orm.call(
"report.mrp.report_bom_structure",
"get_warehouses",
);
this.warehouses = warehouses;
this.state.currentWarehouse = warehouses[0];
}
//---- Handlers ----
onChangeFolded(foldInfo) {
const { ids, isFolded } = foldInfo;
const operation = isFolded ? "delete" : "add";
ids.forEach(id => this.unfoldedIds[operation](id));
}
onChangeDisplay(displayInfo) {
this.state.showOptions[displayInfo] = !this.state.showOptions[displayInfo];
}
async onChangeBomQuantity(newQuantity) {
if (this.state.bomQuantity != newQuantity) {
this.state.bomQuantity = newQuantity;
await this.getBomData();
}
}
async onChangeVariant(variantId) {
if (this.state.currentVariantId != variantId) {
this.state.currentVariantId = variantId;
await this.getBomData();
}
}
async onChangeWarehouse(warehouseId) {
if (this.state.currentWarehouse.id != warehouseId) {
this.state.currentWarehouse = this.warehouses.find(wh => wh.id == warehouseId);
await this.getBomData();
}
}
async onClickPrint(printAll) {
return this.actionService.doAction({
type: "ir.actions.report",
report_type: "qweb-pdf",
report_name: this.getReportName(printAll),
report_file: "mrp.report_bom_structure",
});
}
//---- Getters ----
get activeId() {
return this.props.action.context.active_id;
}
// ---- Helpers ----
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 +
"&quantity=" + (this.state.bomQuantity || 1) +
"&unfolded_ids=" + JSON.stringify(Array.from(this.unfoldedIds)) +
"&warehouse_id=" + (this.state.currentWarehouse ? this.state.currentWarehouse.id : false);
if (printAll) {
reportName += "&all_variants=1";
} else if (this.showVariants && this.state.currentVariantId) {
reportName += "&variant=" + this.state.currentVariantId;
}
return reportName;
}
}
BomOverviewComponent.template = "mrp.BomOverviewComponent";
BomOverviewComponent.components = {
BomOverviewControlPanel,
BomOverviewTable,
};
registry.category("actions").add("mrp_bom_report", BomOverviewComponent);

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<div t-name="mrp.BomOverviewComponent" class="o_action" owl="1">
<BomOverviewControlPanel
bomQuantity="state.bomQuantity"
uomName="uomName"
variants="variants"
showOptions="state.showOptions"
showVariants="showVariants"
currentWarehouse="state.currentWarehouse"
warehouses="warehouses"
print.bind="onClickPrint"
changeWarehouse.bind="onChangeWarehouse"
changeVariant.bind="onChangeVariant"
changeBomQuantity.bind="onChangeBomQuantity"
changeDisplay.bind="onChangeDisplay"
precision="state.precision"
/>
<BomOverviewTable
uomName="uomName"
showOptions="state.showOptions"
currentWarehouseId="state.currentWarehouse.id"
data="state.bomData"
precision="state.precision"
changeFolded.bind="onChangeFolded"/>
</div>
</templates>

View file

@ -0,0 +1,104 @@
/** @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;
export class BomOverviewComponentsBlock extends Component {
setup() {
const childFoldstate = this.childIds.reduce((prev, curr) => ({ ...prev, [curr]: !this.props.unfoldAll}), {});
this.state = useState({
...childFoldstate,
unfoldAll: this.props.unfoldAll || false,
});
if (this.props.unfoldAll) {
this.props.changeFolded({ ids: this.childIds, isFolded: false });
}
if (this.hasComponents) {
useBus(this.env.overviewBus, "unfold-all", () => this._unfoldAll());
}
onWillUpdateProps(newProps => {
if (this.data.product_id != newProps.data.product_id) {
this.childIds.forEach(id => delete this.state[id]);
const newChildIds = this.getHasComponents(newProps.data) ? newProps.data.components.map(c => this.getIdentifier(c)) : [];
newChildIds.forEach(id => this.state[id] = true);
this.state.unfoldAll = false;
}
});
onWillUnmount(() => {
if (this.hasComponents) {
this.props.changeFolded({ ids: this.childIds, isFolded: true });
}
});
}
//---- Handlers ----
onToggleFolded(foldId) {
const newState = !this.state[foldId];
this.state[foldId] = newState;
this.state.unfoldAll = false;
this.props.changeFolded({ ids: [foldId], isFolded: newState });
}
_unfoldAll() {
const allChildIds = this.childIds;
this.state.unfoldAll = true;
allChildIds.forEach(id => this.state[id] = false);
this.props.changeFolded({ ids: allChildIds, isFolded: false });
}
//---- Getters ----
get data() {
return this.props.data;
}
get hasComponents() {
return this.getHasComponents(this.data);
}
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) {
return data.components && data.components.length > 0;
}
getIdentifier(data, type=null) {
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,
};

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mrp.BomOverviewComponentsBlock" owl="1">
<t name="components" t-if="hasComponents">
<t t-foreach="data.components" t-as="line" t-key="line.index">
<BomOverviewLine
isFolded="state[getIdentifier(line)]"
showOptions="props.showOptions"
data="line"
precision="props.precision"
toggleFolded.bind="onToggleFolded"/>
<t t-if="!state[getIdentifier(line)] &amp;&amp; hasComponents">
<BomOverviewComponentsBlock
unfoldAll="state.unfoldAll"
showOptions="props.showOptions"
currentWarehouseId="props.currentWarehouseId"
data="line"
precision="props.precision"
changeFolded.bind="props.changeFolded"/>
</t>
</t>
</t>
<t name="operations" t-if="showOperations &amp;&amp; !!data.operations &amp;&amp; data.operations.length > 0">
<BomOverviewExtraBlock
unfoldAll="state.unfoldAll"
type="'operations'"
showOptions="props.showOptions"
data="data"
precision="props.precision"
changeFolded.bind="props.changeFolded"/>
</t>
<t name="byproducts" t-if="!!data.byproducts &amp;&amp; data.byproducts.length > 0">
<BomOverviewExtraBlock
unfoldAll="state.unfoldAll"
type="'byproducts'"
showOptions="props.showOptions"
data="data"
precision="props.precision"
changeFolded.bind="props.changeFolded"/>
</t>
</t>
</templates>

View file

@ -0,0 +1,66 @@
/** @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;
export class BomOverviewControlPanel extends Component {
setup() {
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 = [];
}
//---- Handlers ----
updateQuantity(ev) {
const newVal = isNaN(ev.target.value) ? 1 : parseFloat(parseFloat(ev.target.value).toFixed(this.precision));
this.props.changeBomQuantity(newVal);
}
onKeyPress(ev) {
if (ev.keyCode === 13 || ev.which === 13) {
ev.preventDefault();
this.updateQuantity(ev);
}
}
clickUnfold() {
this.env.overviewBus.trigger("unfold-all");
}
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: [],
};

View file

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mrp.BomOverviewControlPanel" owl="1">
<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>
</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>
<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>
<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>
</Dropdown>
</t>
<BomOverviewDisplayFilter
showOptions="props.showOptions"
changeDisplay.bind="props.changeDisplay"/>
</div>
</t>
</ControlPanel>
</t>
</templates>

View file

@ -0,0 +1,46 @@
/** @odoo-module **/
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
const { Component } = owl;
export class BomOverviewDisplayFilter extends Component {
setup() {
this.displayOptions = {
availabilities: this.env._t('Availabilities'),
leadTimes: this.env._t('Lead Times'),
costs: this.env._t('Costs'),
operations: this.env._t('Operations'),
};
}
//---- 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(", ");
}
}
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,
};

View file

@ -0,0 +1,16 @@
<?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>
</templates>

View file

@ -0,0 +1,72 @@
/** @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;
export class BomOverviewExtraBlock extends Component {
setup() {
this.state = useState({
isFolded: !this.props.unfoldAll,
});
if (this.props.unfoldAll) {
this.props.changeFolded({ ids: [this.identifier], isFolded: false });
}
useBus(this.env.overviewBus, "unfold-all", () => this._unfold());
onWillUpdateProps(newProps => {
if (this.props.data.product_id != newProps.data.product_id) {
this.state.isFolded = true;
}
});
onWillUnmount(() => {
// Need to notify main component that the block was folded so it doesn't appear on the PDF.
this.props.changeFolded({ ids: [this.identifier], isFolded: true });
});
}
//---- Handlers ----
onToggleFolded() {
const newState = !this.state.isFolded;
this.state.isFolded = newState;
this.props.changeFolded({ ids: [this.identifier], isFolded: newState });
}
_unfold() {
this.state.isFolded = false;
this.props.changeFolded({ ids: [this.identifier], isFolded: false })
}
//---- Getters ----
get identifier() {
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,
};

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mrp.BomOverviewExtraBlock" owl="1">
<BomOverviewSpecialLine
type="props.type"
isFolded="state.isFolded"
showOptions="props.showOptions"
data="props.data"
precision="props.precision"
toggleFolded.bind="onToggleFolded"/>
<t t-if="!state.isFolded" t-foreach="props.type == 'operations' ? props.data.operations : props.data.byproducts" t-as="extra_data" t-key="extra_data.index">
<BomOverviewLine
showOptions="props.showOptions"
data="extra_data"
precision="props.precision"
/>
</t>
</t>
</templates>

View file

@ -0,0 +1,173 @@
/** @odoo-module **/
import { useService } from "@web/core/utils/hooks";
import { formatFloat, formatFloatTime, formatMonetary } from "@web/views/fields/formatters";
const { Component } = owl;
export class BomOverviewLine extends Component {
setup() {
this.actionService = useService("action");
this.ormService = useService("orm");
this.formatFloat = formatFloat;
this.formatFloatTime = formatFloatTime;
this.formatMonetary = (val) => formatMonetary(val, { currencyId: this.data.currency_id });
}
//---- Handlers ----
async goToRoute(routeType) {
if (routeType == "manufacture") {
return this.goToAction(this.data.bom_id, "mrp.bom");
}
}
async goToAction(id, model) {
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 goToForecast() {
const action = await this.ormService.call(
this.data.link_model,
this.forecastAction,
[[this.data.link_id]],
);
action.context = {
active_model: this.data.link_model,
active_id: this.data.link_id,
};
if (this.props.currentWarehouseId) {
action.context["warehouse"] = this.props.currentWarehouseId;
}
return this.actionService.doAction(action);
}
async goToAttachment() {
return this.actionService.doAction({
name: this.env._t("Attachments"),
type: "ir.actions.act_window",
res_model: "mrp.document",
domain: [["id", "in", this.data.attachment_ids]],
views: [[false, "kanban"], [false, "list"], [false, "form"]],
view_mode: "kanban,list,form",
target: "current",
});
}
//---- Getters ----
get data() {
return this.props.data;
}
get precision() {
return this.props.precision;
}
get identifier() {
return `${this.data.type}_${this.data.index}`;
}
get hasComponents() {
return this.data.components && this.data.components.length > 0;
}
get hasQuantity() {
return this.data.hasOwnProperty('quantity_available') && this.data.quantity_available !== false;
}
get hasLeadTime() {
return this.data.hasOwnProperty('lead_time') && this.data.lead_time !== false;
}
get hasFoldButton() {
return this.data.level > 0 && this.hasComponents;
}
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 showUom() {
return this.props.showOptions.uom;
}
get showAttachments() {
return this.props.showOptions.attachments;
}
get availabilityColorClass() {
// For first line, another rule applies : green if doable now, red otherwise.
if (this.data.hasOwnProperty('components_available')) {
if (this.data.components_available && this.data.availability_state != 'unavailable') {
return "text-success";
} else {
return "text-danger";
}
}
switch (this.data.availability_state) {
case "available":
return "text-success";
case "expected":
return "text-warning";
case "unavailable":
return "text-danger";
default:
return "";
}
}
get forecastAction() {
switch (this.data.link_model) {
case "product.product":
return "action_product_forecast_report";
case "product.template":
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: () => {},
};

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<tr t-name="mrp.BomOverviewLine" owl="1">
<td name="td_mrp_bom">
<div t-attf-style="margin-left: {{ marginMultiplicator * 20 }}px">
<t t-if="data.level > 0 &amp;&amp; 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">
<i t-attf-class="fa fa-fw fa-caret-{{ props.isFolded ? 'right' : 'down' }}" role="img"/>
</button>
</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>
<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>
</div>
</td>
<td class="text-end">
<t t-if="data.type == 'operation'" t-esc="formatFloatTime(data.quantity)"/>
<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">
<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">
<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>
</td>
<td t-if="showLeadTimes" class="text-end">
<span t-if="hasLeadTime"><t t-esc="data.lead_time"/> Days</span>
</td>
<td>
<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-if="showAttachments" class="text-center">
<span t-if="!!data.attachment_ids &amp;&amp; data.attachment_ids.length > 0">
<a href="#" role="button" t-on-click.prevent="goToAttachment" class="fa fa-fw o_button_icon fa-files-o"/>
</span>
</td>
</tr>
</templates>

View file

@ -0,0 +1,71 @@
/** @odoo-module **/
import { formatFloat, formatFloatTime, formatMonetary } from "@web/views/fields/formatters";
const { Component } = owl;
export class BomOverviewSpecialLine extends Component {
setup() {
this.formatFloat = formatFloat;
this.formatFloatTime = formatFloatTime;
this.formatMonetary = (val) => formatMonetary(val, { currencyId: this.data.currency_id });
}
//---- Getters ----
get data() {
return this.props.data;
}
get precision() {
return this.props.precision;
}
get hasFoldButton() {
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 showUom() {
return this.props.showOptions.uom;
}
get showAttachments() {
return this.props.showOptions.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: () => {},
};

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<tr t-name="mrp.BomOverviewSpecialLine" owl="1">
<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' }}">
<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>
</td>
<td name="quantity" class="text-end">
<span t-if="props.type == 'operations'" t-esc="formatFloatTime(data.operations_time)"/>
<span t-elif="props.type == 'byproducts'" t-esc="formatFloat(data.byproducts_total, {'digits': [false, precision]})"/>
</td>
<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">
<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>
</templates>

View file

@ -0,0 +1,89 @@
/** @odoo-module **/
import { formatMonetary, formatFloat } 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;
export class BomOverviewTable extends Component {
setup() {
this.actionService = useService("action");
this.formatFloat = formatFloat;
this.formatMonetary = (val) => formatMonetary(val, { currencyId: this.data.currency_id });
}
//---- Handlers ----
async goToProduct() {
return this.actionService.doAction({
type: "ir.actions.act_window",
res_model: this.data.link_model,
res_id: this.data.link_id,
views: [[false, "form"]],
target: "current",
context: {
active_id: this.data.link_id,
},
});
}
//---- Getters ----
get data() {
return this.props.data;
}
get precision() {
return this.props.precision;
}
get showAvailabilities() {
return this.props.showOptions.availabilities;
}
get showCosts() {
return this.props.showOptions.costs;
}
get showOperations() {
return this.props.showOptions.operations;
}
get showLeadTimes() {
return this.props.showOptions.leadTimes;
}
get showUom() {
return this.props.showOptions.uom;
}
get showAttachments() {
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,
};

View file

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<div t-name="mrp.BomOverviewTable" class="o_content" owl="1">
<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">
<div class="me-auto">
<h2><a href="#" t-on-click.prevent="goToProduct" t-esc="data.name"/></h2>
<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="showAttachments" class="text-center" title="Files attached to the product.">Attachments</th>
</tr>
</thead>
<tbody>
<BomOverviewLine
showOptions="props.showOptions"
currentWarehouseId="props.currentWarehouseId"
data="data"
precision="props.precision"
/>
<BomOverviewComponentsBlock
showOptions="props.showOptions"
currentWarehouseId="props.currentWarehouseId"
data="data"
precision="props.precision"
changeFolded.bind="props.changeFolded"/>
</tbody>
<tfoot t-if="showCosts">
<tr>
<td name="td_mrp_bom_f" class="text-end">
<span t-if="!!data.byproducts &amp;&amp; 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="showAttachments"/>
</tr>
<t t-if="data.byproducts &amp;&amp; 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="showAttachments"/>
</tr>
</t>
</tfoot>
</table>
</div>
<div t-else="" class="d-flex align-items-center justify-content-center h-50">
<h4 class="text-muted">No data available.</h4>
</div>
</div>
</div>
</templates>

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,24 @@
/** @odoo-module **/
import { ForecastedButtons } from "@stock/stock_forecasted/forecasted_buttons";
import { patch } from '@web/core/utils/patch';
const { onWillStart } = owl;
patch(ForecastedButtons.prototype, 'mrp.ForecastedButtons',{
setup() {
this._super.apply();
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];
this.bomId = res.variant_bom_ids ? res.variant_bom_ids[0] || res.bom_ids[0] : res.bom_ids[0];
});
},
async _onClickBom(){
return this.actionService.doAction("mrp.action_report_mrp_bom", {
additionalContext: {
active_id: this.bomId,
},
});
}
});

View file

@ -0,0 +1,13 @@
<?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">
<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"
type="button" title="Manufacturing Forecast"
t-on-click="_onClickBom">
Manufacturing Forecast
</button>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,36 @@
<?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">
<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>
</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>
</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-on-click="() => this._onClickChangePriority('mrp.production', line.move_out.raw_material_production_id)"/>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,3 @@
.o_kanban_previewer:hover {
cursor: zoom-in;
}

View file

@ -0,0 +1,3 @@
.o_field_widget.o_embed_url_viewer iframe {
aspect-ratio: 3/2;
}

View file

@ -0,0 +1,19 @@
@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);
}
}

View file

@ -0,0 +1,27 @@
.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_workcenter_kanban {
--KanbanRecord-width: 400px;
}
}

View file

@ -0,0 +1,57 @@
/** @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);

View file

@ -0,0 +1,17 @@
<?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>

View file

@ -0,0 +1,40 @@
/** @odoo-module **/
import { KanbanController } from "@web/views/kanban/kanban_controller";
import { useBus, useService } from "@web/core/utils/hooks";
const { useRef } = owl;
export class MrpDocumentsKanbanController extends KanbanController {
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;
}
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 = "";
}
}

View file

@ -0,0 +1,12 @@
<?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>

View file

@ -0,0 +1,41 @@
/** @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);
}
}

View file

@ -0,0 +1,22 @@
/** @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";

View file

@ -0,0 +1,9 @@
<?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>

View file

@ -0,0 +1,16 @@
/** @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);

View file

@ -0,0 +1,32 @@
/** @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);

View file

@ -0,0 +1,21 @@
/** @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);

View file

@ -0,0 +1,3 @@
td.o_non_manual_consumption {
background-color: rgba($gray-200, .5) !important;
}

View file

@ -0,0 +1,49 @@
/** @odoo-module **/
import { FloatField } from "@web/views/fields/float/float_field";
import { registry } from "@web/core/registry";
import { formatFloat } from "@web/views/fields/formatters";
/**
* This widget is used to display alongside the total quantity to consume of a production order,
* the exact quantity that the worker should consume depending on the BoM. Ex:
* 2 components to make 1 finished product.
* The production order is created to make 5 finished product and the quantity producing is set to 3.
* The widget will be '3.000 / 5.000'.
*/
const { useRef, onPatched, onMounted, useState } = owl;
export class MrpShouldConsumeOwl extends FloatField {
setup() {
super.setup();
this.fields = this.props.record.fields;
this.record = useState(this.props.record);
this.displayShouldConsume = !["done", "draft", "cancel"].includes(this.record.data.state);
this.inputSpanRef = useRef("numpadDecimal");
onMounted(this._renderPrefix);
onPatched(this._renderPrefix);
}
_renderPrefix() {
if (this.displayShouldConsume && this.inputSpanRef.el) {
this.inputSpanRef.el.classList.add(
"o_quick_editable",
"o_field_widget",
"o_field_number",
"o_field_float"
);
}
}
get shouldConsumeQty() {
return formatFloat(this.record.data.should_consume_qty, {
...this.fields.should_consume_qty,
...this.nodeOptions,
});
}
}
MrpShouldConsumeOwl.template = "mrp.ShouldConsume";
MrpShouldConsumeOwl.displayName = "MRP Should Consume";
registry.category("fields").add("mrp_should_consume", MrpShouldConsumeOwl);

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mrp.ShouldConsume" owl="1">
<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>
<t t-call="web.FloatField"/>
</span>
</t>
<t t-else="">
<t t-call="web.FloatField"/>
</t>
</t>
</templates>

View file

@ -0,0 +1,39 @@
/** @odoo-module */
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
import { PopoverComponent, PopoverWidgetField } from '@stock/widgets/popover_widget';
/**
* Link to a Char field representing a JSON:
* {
* 'replan': <REPLAN_BOOL>, // Show the replan btn
* 'color': '<COLOR_CLASS>', // Color Class of the icon (d-none to hide)
* 'infos': [
* {'msg' : '<MESSAGE>', 'color' : '<COLOR_CLASS>'},
* {'msg' : '<MESSAGE>', 'color' : '<COLOR_CLASS>'},
* ... ]
* }
*/
class WorkOrderPopover extends PopoverComponent {
setup(){
this.orm = useService("orm");
}
async onReplanClick() {
await this.orm.call(
'mrp.workorder',
'action_replan',
[this.props.record.resId]
);
await this.props.record.model.load();
}
};
class WorkOrderPopoverField extends PopoverWidgetField {};
WorkOrderPopoverField.components = {
Popover: WorkOrderPopover
};
registry.category("fields").add("mrp_workorder_popover", WorkOrderPopoverField);

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<div t-name="mrp.workorderPopover" owl="1">
<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/>
</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>
</templates>

View file

@ -0,0 +1,90 @@
/** @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;
function formatMinutes(value) {
if (value === false) {
return "";
}
const isNegative = value < 0;
if (isNegative) {
value = Math.abs(value);
}
let min = Math.floor(value);
let sec = Math.round((value % 1) * 60);
sec = `${sec}`.padStart(2, "0");
min = `${min}`.padStart(2, "0");
return `${isNegative ? "-" : ""}${min}:${sec}`;
}
export class MrpTimer extends Component {
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,
});
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;
}
if (this.ongoing) {
this._runTimer();
}
});
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;
if (rerun) {
this.state.duration = nextProps.value;
this._runTimer();
}
});
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);
}
_runTimer() {
this.timer = setTimeout(() => {
if (this.ongoing) {
this.state.duration += 1 / 60;
this._runTimer();
}
}, 1000);
}
}
MrpTimer.supportedTypes = ["float"];
MrpTimer.template = "mrp.MrpTimer";
registry.category("fields").add("mrp_timer", MrpTimer);
registry.category("formatters").add("mrp_timer", formatMinutes);

View file

@ -0,0 +1,7 @@
<?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"/>
<input t-else="" t-att-id="props.id" t-ref="numpadDecimal" t-att-placeholder="props.placeholder" inputmode="numeric" class="o_input" />
</t>
</templates>

View file

@ -0,0 +1,10 @@
<?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>