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,32 @@
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("mail_attachment_removal_tour", {
steps: () => [
{
content: "click on send by email",
trigger: ".o_statusbar_buttons > button[name='action_quotation_send']",
run: "click"
},
{
content: "save a new layout",
trigger: ".o_technical_modal button[name='document_layout_save']",
run: "click"
},
{
content: "delete attachment",
trigger: ".o_field_widget[name='attachment_ids'] li > button .fa-times",
run: "click"
},
{
content: "send the email",
trigger: ".o_mail_send",
run: "click"
},
{
content: "confirm quotation",
trigger: "button[name='action_confirm']",
run: "click"
}
]
})

View File

@@ -0,0 +1,56 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
const openProductAttribute = (product_attribute) => [
...stepUtils.goToAppSteps("sale.sale_menu_root", "Go to the Sales App"),
{
content: 'Open configuration menu',
trigger: '.o-dropdown[data-menu-xmlid="sale.menu_sale_config"]',
run: "click",
},
{
content: 'Navigate to product attribute list view',
trigger: '.o-dropdown-item[data-menu-xmlid="sale.menu_product_attribute_action"]',
run: "click",
},
{
content: `Navigate to ${product_attribute}`,
trigger: `.o_data_cell[data-tooltip=${product_attribute}]`,
run: "click",
},
];
const deletePAV = (product_attribute_value, message) => [
{
content: 'Click delete button',
trigger: `.o_data_cell[data-tooltip=${product_attribute_value}] ~ .o_list_record_remove`,
run: "click",
},
{
content: 'Check correct message in modal',
trigger: message || '.modal-title:contains("Bye-bye, record!")',
run: "click",
},
{
content: 'Close modal',
trigger: '.btn-close',
run: "click",
}
]
// This tour relies on data created on the Python test.
registry.category("web_tour.tours").add('delete_product_attribute_value_tour', {
url: '/odoo',
steps: () => [
...openProductAttribute("PA"),
// Test error message on a used attribute value
...deletePAV("pa_value_1", ".text-prewrap:contains('pa_value_1')"),
// Test deletability of a used attribute value on archived product
...deletePAV("pa_value_2"),
// Test deletability of a removed attribute value on product
...deletePAV("pa_value_3"),
{
content: 'Check test finished',
trigger: 'a:contains("Attributes")',
}
]
});

View File

@@ -0,0 +1,98 @@
import { addSectionFromProductCatalog } from "@account/js/tours/tour_utils";
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add('sale_catalog', {
steps: () => [
{
content: "Create a new SO",
trigger: '.o_list_button_add',
run: 'click',
},
{
content: "Select the customer field",
trigger: ".o_field_res_partner_many2one input.o_input",
run: 'click',
},
{
content: "Wait for the field to be active",
trigger: ".o_field_res_partner_many2one input[aria-expanded=true]",
},
{
content: "Select a customer from the dropdown",
trigger: ".o_field_res_partner_many2one .dropdown-item:not([id$='_loading']):first",
run: 'click',
},
{
content: "Open product catalog",
trigger: 'button[name="action_add_from_catalog"]',
run: 'click',
},
{
content: "Type 'Restricted' into the search bar",
trigger: 'input.o_searchview_input',
run: "edit Restricted",
},
{
content: "Search for the product",
trigger: 'input.o_searchview_input',
run: "press Enter",
},
{
content: "Wait for catalog rendering",
trigger: '.o_kanban_record:contains("Restricted Product")',
},
{
content: "Wait for filtering",
trigger: '.o_kanban_renderer:not(:has(.o_kanban_record:contains("AAA Product")))',
},
{
content: "Add the product to the SO",
trigger: '.o_kanban_record:contains("Restricted Product") .fa-shopping-cart',
run: 'click',
},
{
content: "Wait for product to be added",
trigger: '.o_kanban_record:contains("Restricted Product"):not(:has(.fa-shopping-cart))',
},
{
content: "Input a custom quantity",
trigger: '.o_kanban_record:contains("Restricted Product") .o_input',
run: "edit 6",
},
{
content: "Increase the quantity",
trigger: '.o_kanban_record:contains("Restricted Product") .fa-plus',
run: 'click',
},
{
content: "Close the catalog",
trigger: '.o-kanban-button-back',
run: 'click',
},
]
});
registry.category("web_tour.tours").add('test_add_section_from_product_catalog_on_sale_order', {
steps: () => [
{
content: "Create a new SO",
trigger: '.o_list_button_add',
run: 'click',
},
{
content: "Select the customer field",
trigger: '.o_field_res_partner_many2one input.o_input',
run: 'click',
},
{
content: "Wait for the field to be active",
trigger: '.o_field_res_partner_many2one input[aria-expanded=true]',
},
{
content: "Select a customer from the dropdown",
trigger: '.o_field_res_partner_many2one .dropdown-item:not([id$="_loading"]):first',
run: 'click',
},
...addSectionFromProductCatalog(),
]
});

View File

@@ -0,0 +1,136 @@
import { registry } from '@web/core/registry';
import { stepUtils } from '@web_tour/tour_utils';
import comboConfiguratorTourUtils from '@sale/js/tours/combo_configurator_tour_utils';
import productConfiguratorTourUtils from '@sale/js/tours/product_configurator_tour_utils';
import tourUtils from '@sale/js/tours/tour_utils';
registry
.category('web_tour.tours')
.add('sale_combo_configurator', {
url: '/odoo',
steps: () => [
...stepUtils.goToAppSteps('sale.sale_menu_root', "Open the sales app"),
...tourUtils.createNewSalesOrder(),
...tourUtils.selectCustomer("Test Partner"),
...tourUtils.addProduct("Combo product"),
// Assert that the combo configurator has the correct data.
comboConfiguratorTourUtils.assertComboCount(2),
comboConfiguratorTourUtils.assertComboItemCount("Combo A", 2),
comboConfiguratorTourUtils.assertComboItemCount("Combo B", 2),
// Assert that price changes when the quantity is updated.
comboConfiguratorTourUtils.assertQuantity(1),
comboConfiguratorTourUtils.assertPrice('25.00'),
comboConfiguratorTourUtils.increaseQuantity(),
comboConfiguratorTourUtils.assertQuantity(2),
comboConfiguratorTourUtils.assertPrice('50.00'),
comboConfiguratorTourUtils.decreaseQuantity(),
comboConfiguratorTourUtils.assertQuantity(1),
comboConfiguratorTourUtils.assertPrice('25.00'),
comboConfiguratorTourUtils.setQuantity(3),
comboConfiguratorTourUtils.assertQuantity(3),
comboConfiguratorTourUtils.assertPrice('75.00'),
// Assert that the combo configurator can only be saved after selecting an item for each
// combo.
comboConfiguratorTourUtils.assertConfirmButtonDisabled(),
comboConfiguratorTourUtils.selectComboItem("Product A2"),
comboConfiguratorTourUtils.selectComboItem("Product B2"),
comboConfiguratorTourUtils.assertConfirmButtonEnabled(),
// Assert that the product configurator is opened when a product with configurable
// `no_variant` PTALs is selected.
comboConfiguratorTourUtils.selectComboItem("Product A1"),
productConfiguratorTourUtils.selectAttribute("Product A1", "No variant attribute", "A"),
...productConfiguratorTourUtils.saveConfigurator(),
// Assert that the extra price of a combo item is applied correctly.
comboConfiguratorTourUtils.assertPrice('90.00'),
// Assert that the extra price of a `no_variant` PTAV is applied correctly.
comboConfiguratorTourUtils.selectComboItem("Product A1"),
...productConfiguratorTourUtils.selectAndSetCustomAttribute(
"Product A1", "No variant attribute", "B", "Some custom value"
),
...productConfiguratorTourUtils.saveConfigurator(),
comboConfiguratorTourUtils.assertPrice('93.00'),
// Assert that the order's content is correct.
...comboConfiguratorTourUtils.saveConfigurator(),
tourUtils.checkSOLDescriptionContains("Combo product x 3"),
tourUtils.checkSOLDescriptionContains(
"Product A1", "No variant attribute: B: Some custom value"
),
tourUtils.checkSOLDescriptionContains("Product B2"),
{
content: "Verify the combo item quantities",
trigger: 'td[name="product_uom_qty"]:contains(3.00)',
},
{
content: "Verify the first combo item's unit price",
trigger: 'td[name="price_unit"]:contains(18.50)',
},
{
content: "Verify the second combo item's unit price",
trigger: 'td[name="price_unit"]:contains(12.50)',
},
{
content: "Verify the order's total price",
trigger: 'div.oe_subtotal_footer:contains(93.00)',
},
// Assert that the combo configurator is opened with the previous selection when the
// combo is edited.
tourUtils.editLineMatching("Combo product x 3"),
tourUtils.editConfiguration(),
comboConfiguratorTourUtils.setQuantity(2),
comboConfiguratorTourUtils.assertComboItemSelected("Product A1"),
comboConfiguratorTourUtils.assertComboItemSelected("Product B2"),
comboConfiguratorTourUtils.selectComboItem("Product A2"),
// Assert that the order's content has been updated.
...comboConfiguratorTourUtils.saveConfigurator(),
tourUtils.checkSOLDescriptionContains("Combo product x 2"),
tourUtils.checkSOLDescriptionContains("Product A2"),
tourUtils.checkSOLDescriptionContains("Product B2"),
{
content: "Verify the combo item quantities",
trigger: 'td[name="product_uom_qty"]:contains(2.00)',
},
{
content: "Verify the first combo item's unit price",
trigger: 'td[name="price_unit"]:contains(12.50)',
},
{
content: "Verify the second combo item's unit price",
trigger: 'td[name="price_unit"]:contains(12.50)',
},
{
content: "Verify the order's total price",
trigger: 'div.oe_subtotal_footer:contains(50.00)',
},
// Don't end the tour with a form in edition mode.
...stepUtils.saveForm(),
],
});
registry
.category('web_tour.tours')
.add('sale_combo_configurator_with_optional_products', {
url: '/odoo',
steps: () => [
...stepUtils.goToAppSteps('sale.sale_menu_root', "Open the sales app"),
...tourUtils.createNewSalesOrder(),
...tourUtils.selectCustomer("Test Partner"),
...tourUtils.addProduct("Combo product"),
comboConfiguratorTourUtils.selectComboItem("Product B2"),
...comboConfiguratorTourUtils.saveConfigurator(),
productConfiguratorTourUtils.addOptionalProduct("Optional product"),
{
content: "verify that we cannot reduce main product quantity",
trigger: ':not(button[name="sale_quantity_button_minus"])',
},
{
content: "verify that we cannot increase main product quantity",
trigger: ':not(button[name="sale_quantity_button_plus"])',
},
...productConfiguratorTourUtils.saveConfigurator(),
tourUtils.checkSOLDescriptionContains("Combo product"),
tourUtils.checkSOLDescriptionContains("Product B2"),
tourUtils.checkSOLDescriptionContains("Optional product"),
// Don't end the tour with a form in edition mode.
...stepUtils.saveForm(),
],
});

View File

@@ -0,0 +1,37 @@
import { registry } from '@web/core/registry';
import { stepUtils } from '@web_tour/tour_utils';
import comboConfiguratorTourUtils from '@sale/js/tours/combo_configurator_tour_utils';
import productConfiguratorTourUtils from '@sale/js/tours/product_configurator_tour_utils';
import tourUtils from '@sale/js/tours/tour_utils';
registry
.category('web_tour.tours')
.add('sale_combo_configurator_preconfigure_unconfigurable_ptals', {
url: '/odoo',
steps: () => [
...stepUtils.goToAppSteps('sale.sale_menu_root', "Open the sales app"),
...tourUtils.createNewSalesOrder(),
...tourUtils.selectCustomer("Test Partner"),
...tourUtils.addProduct("Combo product"),
{
content: "Verify that unconfigurable ptals are preconfigured",
trigger: `${comboConfiguratorTourUtils.comboItemSelector("Test product")}:contains("Attribute A: A")`,
},
{
content: "Verify that configurable ptals aren't preconfigured",
trigger: `${comboConfiguratorTourUtils.comboItemSelector("Test product")}:not(:contains("Attribute B: B"))`,
},
comboConfiguratorTourUtils.selectComboItem("Test product"),
productConfiguratorTourUtils.selectAttribute(
"Test product", "Attribute B", "B", 'multi'
),
...productConfiguratorTourUtils.saveConfigurator(),
{
content: "Verify that configurable ptals are now configured",
trigger: `${comboConfiguratorTourUtils.comboItemSelector("Test product")}:contains("Attribute B: B")`,
},
...comboConfiguratorTourUtils.saveConfigurator(),
// Don't end the tour with a form in edition mode.
...stepUtils.saveForm(),
],
});

View File

@@ -0,0 +1,36 @@
import { registry } from '@web/core/registry';
import { stepUtils } from '@web_tour/tour_utils';
import comboConfiguratorTourUtils from '@sale/js/tours/combo_configurator_tour_utils';
import productConfiguratorTourUtils from '@sale/js/tours/product_configurator_tour_utils';
import tourUtils from '@sale/js/tours/tour_utils';
registry
.category('web_tour.tours')
.add('sale_combo_configurator_preselect_single_unconfigurable_items', {
url: '/odoo',
steps: () => [
...stepUtils.goToAppSteps('sale.sale_menu_root', "Open the sales app"),
...tourUtils.createNewSalesOrder(),
...tourUtils.selectCustomer("Test Partner"),
...tourUtils.addProduct("Combo product"),
// Assert that only single unconfigurable items are preselected.
comboConfiguratorTourUtils.assertPreselectedComboItemCount(2),
comboConfiguratorTourUtils.assertComboItemPreselected("Product A"),
comboConfiguratorTourUtils.assertComboItemPreselected("Product C"),
comboConfiguratorTourUtils.assertConfirmButtonDisabled(),
// Configure the remaining combos.
comboConfiguratorTourUtils.selectComboItem("Product B"),
productConfiguratorTourUtils.selectAttribute("Product B", "Attribute B", "B", 'multi'),
...productConfiguratorTourUtils.saveConfigurator(),
comboConfiguratorTourUtils.selectComboItem("Product D"),
productConfiguratorTourUtils.setCustomAttribute(
"Product D", "Attribute D", "Test D"
),
...productConfiguratorTourUtils.saveConfigurator(),
comboConfiguratorTourUtils.selectComboItem("Product E1"),
comboConfiguratorTourUtils.assertConfirmButtonEnabled(),
...comboConfiguratorTourUtils.saveConfigurator(),
// Don't end the tour with a form in edition mode.
...stepUtils.saveForm(),
],
});

View File

@@ -0,0 +1,14 @@
import { registry } from '@web/core/registry';
import { stepUtils } from '@web_tour/tour_utils';
import productConfiguratorTourUtils from '@sale/js/tours/product_configurator_tour_utils';
import tourUtils from '@sale/js/tours/tour_utils';
registry.category('web_tour.tours').add('sale_order_keep_uom_on_variant_wizard_quantity_change', {
steps: () => [
tourUtils.editLineMatching("Sofa"),
tourUtils.editConfiguration(),
productConfiguratorTourUtils.increaseProductQuantity("Sofa"),
...productConfiguratorTourUtils.saveConfigurator(),
...stepUtils.saveForm(),
],
});

View File

@@ -0,0 +1,99 @@
import { registry } from "@web/core/registry";
import { redirect } from "@web/core/utils/urls";
// This tour relies on data created on the Python test.
registry.category("web_tour.tours").add('sale_signature', {
url: '/my/quotes',
steps: () => [
{
content: "open the test SO",
trigger: 'a:text(test SO)',
run: "click",
expectUnloadPage: true,
},
{
content: "click sign",
trigger: 'a:contains("Sign")',
run: "click",
},
{
content: "clear the signature name",
trigger: '.modal .o_web_sign_name_and_signature input',
run: "clear",
},
{
content: "check submit is disabled when name is empty",
trigger: '.modal .o_portal_sign_submit:disabled',
},
{
content: "reset signature name",
trigger: '.modal .o_web_sign_name_and_signature input',
run: "fill Joel Willis",
},
{
content: "check submit is enabled",
trigger: '.o_portal_sign_submit:enabled',
},
{
trigger: ".modal .o_web_sign_name_and_signature input:value(Joel Willis)"
},
{
trigger: ".modal canvas.o_web_sign_signature",
run: "canvasNotEmpty",
},
{
content: "click select style",
trigger: '.modal .o_web_sign_auto_select_style button',
run: "click",
},
{
content: "click style 4",
trigger: ".o-dropdown-item:eq(3)",
run: "click",
},
{
content: "click submit",
trigger: '.modal .o_portal_sign_submit:enabled',
run: "click",
expectUnloadPage: true,
},
{
content: "check it's confirmed",
trigger: '#quote_content:contains("Thank You")',
run: "click",
}, {
trigger: '#quote_content',
run: function () {
redirect("/odoo");
}, // Avoid race condition at the end of the tour by returning to the home page.
expectUnloadPage: true,
},
{
trigger: 'nav',
}
]});
registry.category("web_tour.tours").add("sale_signature_without_name", {
steps: () => [
{
content: "Wait for interactions to load",
trigger: `body[is-ready=true], :iframe body[is-ready=true]`,
},
{
content: "Sign & Pay",
trigger:
".o_portal_sale_sidebar .btn-primary, :iframe .o_portal_sale_sidebar .btn-primary",
run: "click",
},
{
content: "click submit",
trigger: ".o_portal_sign_submit:enabled, :iframe .o_portal_sign_submit:enabled",
run: "click",
},
{
content: "check error because no name",
trigger:
'.o_portal_sign_error_msg:contains("Signature is missing."), :iframe .o_portal_sign_error_msg:contains("Signature is missing.")',
},
],
});