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:
Marc
2026-03-31 23:09:12 +02:00
parent 0ed29fe2fd
commit 8741282322
2933 changed files with 280644 additions and 264 deletions

View File

@@ -0,0 +1,23 @@
import { monetaryField, MonetaryField } from "@web/views/fields/monetary/monetary_field";
import { registry } from "@web/core/registry";
import { floatIsZero } from "@web/core/utils/numbers";
export class MonetaryFieldNoZero extends MonetaryField {
static props = {
...MonetaryField.props,
};
/** Override **/
get value() {
const originalValue = super.value;
const decimals = this.currencyDigits ? this.currencyDigits[1] : 2;
return floatIsZero(originalValue, decimals) ? false : originalValue;
}
}
export const monetaryFieldNoZero = {
...monetaryField,
component: MonetaryFieldNoZero,
};
registry.category("fields").add("monetary_no_zero", monetaryFieldNoZero);

View File

@@ -0,0 +1,27 @@
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
import { Component } from "@odoo/owl";
class OpenMatchLineWidget extends Component {
static template = "purchase.OpenMatchLineWidget";
static props = { ...standardFieldProps };
setup() {
super.setup();
this.action = useService("action");
}
async openMatchLine() {
this.action.doActionButton({
type: "object",
resId: this.props.record.resId,
name: "action_open_line",
resModel: "purchase.bill.line.match",
});
}
}
registry.category("fields").add("open_match_line_widget", {
component: OpenMatchLineWidget,
});

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<templates>
<t t-name="purchase.OpenMatchLineWidget">
<div t-out="props.record.data[props.name]" t-on-click.prevent.stop="openMatchLine"/>
</t>
</templates>

View File

@@ -0,0 +1,90 @@
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
import { FileUploader } from "@web/views/fields/file_handler";
import { WarningDialog } from "@web/core/errors/error_dialogs";
import { _t } from "@web/core/l10n/translation";
import { Component } from "@odoo/owl";
export class PurchaseFileUploader extends Component {
static template = "purchase.DocumentFileUploader";
static props = {
...standardWidgetProps,
record: { type: Object, optional: true },
list: { type: Object, optional: true },
};
static components = { FileUploader };
setup() {
this.orm = useService("orm");
this.action = useService("action");
this.dialog = useService("dialog");
this.attachmentIdsToProcess = [];
}
get resModel() {
return "purchase.order";
}
get records() {
return this.props.record ? [this.props.record] : this.props.list.records;
}
async getIds() {
if (this.props.record) {
return this.props.record.data.id;
}
return this.props.list.getResIds(true);
}
onClick(ev) {
if (this.env.config.viewType !== "list") {
return;
}
const vendorSet = new Set(this.props.list.selection.map((record) => record.data.partner_id.id));
if (vendorSet.size > 1) {
this.dialog.add(WarningDialog, {
title: _t("Validation Error"),
message: _t("You can only upload a bill for a single vendor at a time."),
});
return false;
}
}
async onFileUploaded(file) {
const att_data = {
name: file.name,
mimetype: file.type,
datas: file.data,
};
const [att_id] = await this.orm.create("ir.attachment", [att_data], {
context: { ...this.env.searchModel.context },
});
this.attachmentIdsToProcess.push(att_id);
}
async onUploadComplete() {
const resModel = this.resModel;
const ids = await this.getIds();
let action;
try {
action = await this.orm.call(
resModel,
"action_create_invoice",
[ids, this.attachmentIdsToProcess],
{ context: { ...this.env.searchModel.context } }
);
} finally {
// ensures attachments are cleared on success as well as on error
this.attachmentIdsToProcess = [];
}
this.action.doAction(action);
}
}
export const purchaseFileUploader = {
component: PurchaseFileUploader,
};
registry.category("view_widgets").add("purchase_file_uploader", purchaseFileUploader);

View File

@@ -0,0 +1,18 @@
<templates>
<t t-name="purchase.DocumentFileUploader">
<FileUploader
acceptedFileExtensions="props.acceptedFileExtensions"
fileUploadClass="'document_file_uploader'"
multiUpload="true"
onClick.bind="onClick"
onUploaded.bind="onFileUploaded"
onUploadComplete.bind="onUploadComplete">
<t t-set-slot="toggler">
<button type="button" class="btn btn-secondary" data-hotkey="shift+i">
Upload Bill
</button>
</t>
<t t-slot="default"/>
</FileUploader>
</t>
</templates>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-inherit="account.TaxTotalsField" t-inherit-mode="extension" owl="1">
<xpath expr="//tr[2]" position="after">
<tr t-if="totals.amount_total_cc">
<td colspan="2">
<span t-out="totals.amount_total_cc" style="font-size: 1.1em;"/>
</td>
</tr>
</xpath>
</t>
</templates>