mirror of
https://github.com/bringout/oca-ocb-mrp.git
synced 2026-04-21 01:52:04 +02:00
Initial commit: Mrp packages
This commit is contained in:
commit
50d736b3bd
739 changed files with 538193 additions and 0 deletions
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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)] && 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 && !!data.operations && 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 && 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>
|
||||
|
|
@ -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: [],
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
@ -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: () => {},
|
||||
};
|
||||
|
|
@ -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 && 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 && 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>
|
||||
|
|
@ -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: () => {},
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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 && 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 && 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>
|
||||
BIN
odoo-bringout-oca-ocb-mrp/mrp/static/src/img/mrp-tablet.png
Normal file
BIN
odoo-bringout-oca-ocb-mrp/mrp/static/src/img/mrp-tablet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.o_kanban_previewer:hover {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.o_field_widget.o_embed_url_viewer iframe {
|
||||
aspect-ratio: 3/2;
|
||||
}
|
||||
19
odoo-bringout-oca-ocb-mrp/mrp/static/src/scss/mrp_gantt.scss
Normal file
19
odoo-bringout-oca-ocb-mrp/mrp/static/src/scss/mrp_gantt.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 = "";
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
td.o_non_manual_consumption {
|
||||
background-color: rgba($gray-200, .5) !important;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
90
odoo-bringout-oca-ocb-mrp/mrp/static/src/widgets/timer.js
Normal file
90
odoo-bringout-oca-ocb-mrp/mrp/static/src/widgets/timer.js
Normal 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);
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue