Eliminate Python dependency: embed frontend assets in odoo-go
- Copy all OWL frontend assets (JS/CSS/XML/fonts/images) into frontend/ directory (2925 files, 43MB) — no more runtime reads from Python Odoo - Replace OdooAddonsPath config with FrontendDir pointing to local frontend/ - Rewire bundle.go, static.go, templates.go, webclient.go to read from frontend/ instead of external Python Odoo addons directory - Auto-detect frontend/ and build/ dirs relative to binary in main.go - Delete obsolete Python helper scripts (tools/*.py) The Go server is now fully self-contained: single binary + frontend/ folder. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
95
frontend/stock/static/src/fields/stock_action_field.js
Normal file
95
frontend/stock/static/src/fields/stock_action_field.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { evaluateExpr } from "@web/core/py_js/py";
|
||||
import { floatField, FloatField } from "@web/views/fields/float/float_field";
|
||||
import { monetaryField, MonetaryField } from "@web/views/fields/monetary/monetary_field";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const fieldRegistry = registry.category("fields");
|
||||
|
||||
class StockActionField extends Component {
|
||||
static props = {
|
||||
...FloatField.props,
|
||||
...MonetaryField.props,
|
||||
actionName: { type: String, optional: false },
|
||||
actionContext: { type: String, optional: true },
|
||||
disabled: { type: String, optional: true },
|
||||
};
|
||||
static components = {
|
||||
FloatField,
|
||||
MonetaryField,
|
||||
}
|
||||
static template = "stock.actionField";
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.actionService = useService("action");
|
||||
this.orm = useService("orm");
|
||||
this.fieldType = this.props.record.fields[this.props.name].type;
|
||||
}
|
||||
|
||||
extractProps () {
|
||||
const keysToRemove = ["actionName", "actionContext", "disabled"];
|
||||
return Object.fromEntries(
|
||||
Object.entries(this.props).filter(([prop]) => !keysToRemove.includes(prop))
|
||||
);
|
||||
}
|
||||
|
||||
get disabled() {
|
||||
return this.props.disabled ? evaluateExpr(this.props.disabled, this.props.record.evalContext) : false;
|
||||
}
|
||||
|
||||
_onClick(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
// Get the action name from props.options
|
||||
const actionName = this.props.actionName;
|
||||
const actionContext = evaluateExpr(this.props.actionContext, this.props.record.evalContext);
|
||||
|
||||
// const action = this.orm.call(this.props.record.resModel, actionName, this.props.record.resId);
|
||||
// Use the action service to perform the action
|
||||
this.actionService.doAction(actionName, {
|
||||
additionalContext: { ...actionContext, ...this.props.record.context },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const stockActionField = {
|
||||
...floatField,
|
||||
...monetaryField,
|
||||
component: StockActionField,
|
||||
supportedOptions: [
|
||||
Object.values(
|
||||
Object.fromEntries(
|
||||
[...floatField.supportedOptions, ...monetaryField.supportedOptions].map(
|
||||
(option) => [option.name, option]
|
||||
)
|
||||
)
|
||||
),
|
||||
{
|
||||
label: _t("Action Name"),
|
||||
name: "action_name",
|
||||
type: "string",
|
||||
},
|
||||
],
|
||||
extractProps: (...args) => {
|
||||
const [{ context, fieldType, options }] = args;
|
||||
const action_props = {
|
||||
actionName: options.action_name,
|
||||
disabled: options.disabled,
|
||||
actionContext: context,
|
||||
}
|
||||
let props = {...action_props}
|
||||
if (fieldType === "monetary") {
|
||||
props = { ...action_props, ...monetaryField.extractProps(...args) };
|
||||
} else if (fieldType === "float") {
|
||||
props = { ...action_props, ...floatField.extractProps(...args) };
|
||||
};
|
||||
return props;
|
||||
},
|
||||
};
|
||||
|
||||
fieldRegistry.add("stock_action_field", stockActionField);
|
||||
20
frontend/stock/static/src/fields/stock_action_field.xml
Normal file
20
frontend/stock/static/src/fields/stock_action_field.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
<t t-name="stock.actionField" owl="1">
|
||||
<a t-if="! disabled" href="#" t-on-click="_onClick" class="btn-link">
|
||||
<t t-call="stock.actionFieldContent"/>
|
||||
</a>
|
||||
<t t-else="">
|
||||
<t t-call="stock.actionFieldContent"/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="stock.actionFieldContent" owl="1">
|
||||
<t t-if="fieldType === 'monetary'">
|
||||
<MonetaryField t-props="extractProps()"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<FloatField t-props="extractProps()"/>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,190 @@
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field";
|
||||
import { useSelectCreate, useOpenMany2XRecord} from "@web/views/fields/relational_utils";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { Domain } from "@web/core/domain";
|
||||
|
||||
export class SMLX2ManyField extends X2ManyField {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.orm = useService("orm");
|
||||
this.dirtyQuantsData = new Map();
|
||||
const selectCreate = useSelectCreate({
|
||||
resModel: "stock.quant",
|
||||
activeActions: this.activeActions,
|
||||
onSelected: (resIds) => this.selectRecord(resIds),
|
||||
onCreateEdit: () => this.createOpenRecord(),
|
||||
});
|
||||
|
||||
this.selectCreate = (params) => {
|
||||
return selectCreate(params);
|
||||
};
|
||||
this.openQuantRecord = useOpenMany2XRecord({
|
||||
resModel: "stock.quant",
|
||||
activeActions: this.activeActions,
|
||||
onRecordSaved: (record) => this.selectRecord([record.resId]),
|
||||
fieldString: this.props.string,
|
||||
is2Many: true,
|
||||
});
|
||||
}
|
||||
|
||||
get quantListViewShowOnHandOnly(){
|
||||
return true; // To override in mrp_subcontracting
|
||||
}
|
||||
|
||||
async onAdd({ context, editable } = {}) {
|
||||
if (!this.props.record.data.show_quant) {
|
||||
return super.onAdd(...arguments);
|
||||
}
|
||||
// Compute the quant offset from move lines quantity changes that were not saved yet.
|
||||
// Hence, did not yet affect quant's quantity in DB.
|
||||
await this.updateDirtyQuantsData();
|
||||
context = {
|
||||
...context,
|
||||
single_product: true,
|
||||
list_view_ref: "stock.view_stock_quant_tree_simple",
|
||||
};
|
||||
const productName = this.props.record.data.product_id.display_name;
|
||||
const title = _t("Add line: %s", productName);
|
||||
let domain = [
|
||||
["product_id", "=", this.props.record.data.product_id.id],
|
||||
["location_id", "child_of", this.props.context.default_location_id],
|
||||
["quantity", ">", 0.0],
|
||||
];
|
||||
if (this.quantListViewShowOnHandOnly) {
|
||||
domain.push(["on_hand", "=", true]);
|
||||
}
|
||||
if (this.dirtyQuantsData.size) {
|
||||
const notFullyUsed = [];
|
||||
const fullyUsed = [];
|
||||
for (const [quantId, quantData] of this.dirtyQuantsData.entries()) {
|
||||
if (quantData.available_quantity > 0) {
|
||||
notFullyUsed.push(quantId);
|
||||
} else {
|
||||
fullyUsed.push(quantId);
|
||||
}
|
||||
}
|
||||
if (fullyUsed.length) {
|
||||
domain = Domain.and([domain, [["id", "not in", fullyUsed]]]).toList();
|
||||
}
|
||||
if (notFullyUsed.length) {
|
||||
domain = Domain.or([domain, [["id", "in", notFullyUsed]]]).toList();
|
||||
}
|
||||
}
|
||||
return this.selectCreate({ domain, context, title });
|
||||
}
|
||||
|
||||
async updateDirtyQuantsData() {
|
||||
// Since changes of move line quantities will not affect the available quantity of the quant before
|
||||
// the record has been saved, it is necessary to determine the offset of the DB quant data.
|
||||
this.dirtyQuantsData.clear();
|
||||
const dirtyQuantityMoveLines = this._move_line_ids.filter(
|
||||
(ml) => !ml.data.quant_id && ml._values.quantity - ml._changes.quantity
|
||||
);
|
||||
const dirtyQuantMoveLines = this._move_line_ids.filter(
|
||||
(ml) => ml.data.quant_id.id
|
||||
);
|
||||
const dirtyMoveLines = [...dirtyQuantityMoveLines, ...dirtyQuantMoveLines];
|
||||
if (!dirtyMoveLines.length) {
|
||||
return;
|
||||
}
|
||||
const match = await this.orm.call(
|
||||
"stock.move.line",
|
||||
"get_move_line_quant_match",
|
||||
[
|
||||
this._move_line_ids
|
||||
.filter((rec) => rec.resId)
|
||||
.map((rec) => rec.resId),
|
||||
this.props.record.resId,
|
||||
dirtyMoveLines.filter((rec) => rec.resId).map((rec) => rec.resId),
|
||||
dirtyQuantMoveLines.map((ml) => ml.data.quant_id.id),
|
||||
],
|
||||
{}
|
||||
);
|
||||
const quants = match[0];
|
||||
if (!quants.length) {
|
||||
return;
|
||||
}
|
||||
const dbMoveLinesData = new Map();
|
||||
for (const data of match[1]) {
|
||||
dbMoveLinesData.set(data[0], { quantity: data[1].quantity, quantId: data[1].quant_id });
|
||||
}
|
||||
const offsetByQuant = new Map();
|
||||
for (const ml of dirtyQuantMoveLines) {
|
||||
const quantId = ml.data.quant_id.id;
|
||||
offsetByQuant.set(quantId, (offsetByQuant.get(quantId) || 0) - ml.data.quantity);
|
||||
const dbQuantId = dbMoveLinesData.get(ml.resId)?.quantId;
|
||||
if (dbQuantId && quantId != dbQuantId) {
|
||||
offsetByQuant.set(
|
||||
dbQuantId,
|
||||
(offsetByQuant.get(dbQuantId) || 0) + dbMoveLinesData.get(ml.resId).quantity
|
||||
);
|
||||
}
|
||||
}
|
||||
const offsetByQuantity = new Map();
|
||||
for (const ml of dirtyQuantityMoveLines) {
|
||||
offsetByQuantity.set(ml.resId, ml._values.quantity - ml._changes.quantity);
|
||||
}
|
||||
for (const quant of quants) {
|
||||
const quantityOffest = quant[1].move_line_ids
|
||||
.map((ml) => offsetByQuantity.get(ml) || 0)
|
||||
.reduce((val, sum) => val + sum, 0);
|
||||
const quantOffest = offsetByQuant.get(quant[0]) || 0;
|
||||
this.dirtyQuantsData.set(quant[0], {
|
||||
available_quantity: quant[1].available_quantity + quantityOffest + quantOffest,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async selectRecord(res_ids) {
|
||||
const demand =
|
||||
this.props.record.data.product_uom_qty -
|
||||
this._move_line_ids
|
||||
.map((ml) => ml.data.quantity)
|
||||
.reduce((val, sum) => val + sum, 0);
|
||||
const params = {
|
||||
context: { default_quant_id: res_ids[0] },
|
||||
};
|
||||
if (demand <= 0) {
|
||||
params.context.default_quantity = 0;
|
||||
} else if (this.dirtyQuantsData.has(res_ids[0])) {
|
||||
params.context.default_quantity = Math.min(
|
||||
this.dirtyQuantsData.get(res_ids[0]).available_quantity,
|
||||
demand
|
||||
);
|
||||
}
|
||||
this.list.addNewRecord(params).then((record) => {
|
||||
// Make it dirty to force the save of the record. addNewRecord make
|
||||
// the new record dirty === False by default to remove them at unfocus event
|
||||
record.dirty = true;
|
||||
});
|
||||
}
|
||||
|
||||
createOpenRecord() {
|
||||
const activeElement = document.activeElement;
|
||||
this.openQuantRecord({
|
||||
context: {
|
||||
...this.props.context,
|
||||
form_view_ref: "stock.view_stock_quant_form",
|
||||
},
|
||||
immediate: true,
|
||||
onClose: () => {
|
||||
if (activeElement) {
|
||||
activeElement.focus();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
get _move_line_ids() {
|
||||
return this.props.record.data.move_line_ids.records;
|
||||
}
|
||||
}
|
||||
|
||||
export const smlX2ManyField = {
|
||||
...x2ManyField,
|
||||
component: SMLX2ManyField,
|
||||
};
|
||||
|
||||
registry.category("fields").add("sml_x2_many", smlX2ManyField);
|
||||
Reference in New Issue
Block a user