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:
79
frontend/purchase/static/tests/tours/purchase_catalog.js
Normal file
79
frontend/purchase/static/tests/tours/purchase_catalog.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import { addSectionFromProductCatalog } from "@account/js/tours/tour_utils";
|
||||
import { productCatalog, purchaseForm } from "./tour_helper";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
registry.category("web_tour.tours").add("test_add_section_from_product_catalog_on_purchase_order", {
|
||||
steps: () => [
|
||||
...purchaseForm.createNewPO(),
|
||||
...purchaseForm.selectVendor("Test Vendor"),
|
||||
...addSectionFromProductCatalog(),
|
||||
],
|
||||
});
|
||||
|
||||
registry.category("web_tour.tours").add("test_catalog_vendor_uom", {
|
||||
steps: () => [
|
||||
// Open the PO for the vendor selling product as "Units".
|
||||
{ trigger: "td[data-tooltip='PO/TEST/00002']", run: "click" },
|
||||
...purchaseForm.displayOptionalField("discount"),
|
||||
...purchaseForm.openCatalog(),
|
||||
{
|
||||
content: "Check 'No section' is selected in the catalog",
|
||||
trigger: ".o_search_panel_sections .o_selected_section:contains('No Section') span.o_section_name",
|
||||
run: "click",
|
||||
},
|
||||
...productCatalog.checkProductPrice("Crab Juice", "$ 2.50"),
|
||||
// Add 6 units and check the price is correctly updated.
|
||||
...productCatalog.addProduct("Crab Juice"),
|
||||
...productCatalog.checkProductUoM("Crab Juice", "Units"),
|
||||
...productCatalog.addProduct("Crab Juice"),
|
||||
...productCatalog.addProduct("Crab Juice"),
|
||||
...productCatalog.addProduct("Crab Juice"),
|
||||
...productCatalog.addProduct("Crab Juice"),
|
||||
...productCatalog.waitForQuantity("Crab Juice", 5),
|
||||
...productCatalog.checkProductUoM("Crab Juice", "Units"),
|
||||
...productCatalog.checkProductPrice("Crab Juice", "$ 2.50"),
|
||||
...productCatalog.addProduct("Crab Juice"),
|
||||
...productCatalog.checkProductPrice("Crab Juice", "$ 2.45"),
|
||||
// Add 6 units more and check the price is updated again.
|
||||
...productCatalog.addProduct("Crab Juice"),
|
||||
...productCatalog.addProduct("Crab Juice"),
|
||||
...productCatalog.addProduct("Crab Juice"),
|
||||
...productCatalog.addProduct("Crab Juice"),
|
||||
...productCatalog.addProduct("Crab Juice"),
|
||||
...productCatalog.waitForQuantity("Crab Juice", 11),
|
||||
...productCatalog.checkProductUoM("Crab Juice", "Units"),
|
||||
...productCatalog.checkProductPrice("Crab Juice", "$ 2.45"),
|
||||
...productCatalog.addProduct("Crab Juice"),
|
||||
...productCatalog.checkProductPrice("Crab Juice", "$ 2.20"),
|
||||
// Go back in the PO form view and check PO line price and qty is correct.
|
||||
...productCatalog.goBackToOrder(),
|
||||
...purchaseForm.checkLineValues(0, {
|
||||
product: "Crab Juice",
|
||||
discount: "10.20",
|
||||
quantity: "12.00",
|
||||
unit: "Units",
|
||||
unitPrice: "2.45",
|
||||
totalPrice: "$ 26.40",
|
||||
}),
|
||||
|
||||
// Open the PO for the vendor selling product as liter.
|
||||
{ trigger: "a[href='/odoo/purchase']", run: "click" },
|
||||
{ trigger: "td[data-tooltip='PO/TEST/00001']", run: "click" },
|
||||
...purchaseForm.openCatalog(),
|
||||
...productCatalog.checkProductPrice("Crab Juice", "$ 1.55"),
|
||||
...productCatalog.addProduct("Crab Juice"),
|
||||
...productCatalog.waitForQuantity("Crab Juice", 1),
|
||||
...productCatalog.checkProductUoM("Crab Juice", "L"),
|
||||
...productCatalog.checkProductPrice("Crab Juice", "$ 1.55"),
|
||||
// Go back in the PO form view and check PO line price and qty is correct.
|
||||
...productCatalog.goBackToOrder(),
|
||||
...purchaseForm.checkLineValues(0, {
|
||||
product: "Crab Juice",
|
||||
quantity: "1.00",
|
||||
discount: "22.50",
|
||||
unit: "L",
|
||||
unitPrice: "2.00",
|
||||
totalPrice: "$ 1.55",
|
||||
}),
|
||||
],
|
||||
});
|
||||
76
frontend/purchase/static/tests/tours/purchase_flow_tour.js
Normal file
76
frontend/purchase/static/tests/tours/purchase_flow_tour.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import { inputFiles } from "@web/../tests/utils";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
registry.category("web_tour.tours").add("test_basic_purchase_flow_with_minimal_access_rights", {
|
||||
steps: () => [
|
||||
{
|
||||
trigger: ".o_menuitem[href='/odoo/purchase']",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Check that at least one RFQ is present in the view",
|
||||
trigger: ".o_purchase_dashboard_list_view .o_data_row",
|
||||
},
|
||||
{
|
||||
trigger: ".o_list_button_add",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_input[id=partner_id_0]",
|
||||
run: "edit partner_a",
|
||||
},
|
||||
{
|
||||
trigger: ".dropdown-item:contains(partner_a)",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_field_x2many_list_row_add > a",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_data_row .o_input",
|
||||
run: "edit Test Product",
|
||||
},
|
||||
{
|
||||
trigger: ".dropdown-item:contains('Test Product')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Wait for the tax to be set by the onchange",
|
||||
trigger: ".o_field_many2many_tags[name=tax_ids] .o_tag",
|
||||
},
|
||||
{
|
||||
trigger: ".o_data_cell[name=price_unit]",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_data_cell[name=price_unit] .o_input",
|
||||
run: "edit 3",
|
||||
},
|
||||
{
|
||||
trigger: "button[name=button_confirm]",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_statusbar_status .o_arrow_button_current:contains(Purchase Order)",
|
||||
},
|
||||
{
|
||||
content: "Upload the vendor bill",
|
||||
trigger: ".o_widget_purchase_file_uploader",
|
||||
run: async () => {
|
||||
const testFile = new File(["Vendor, Bill"], "my_vendor_bill.png", {
|
||||
type: "image/*",
|
||||
});
|
||||
await inputFiles(".o_widget_purchase_file_uploader input", [testFile]);
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "Check that we are in the invoice form view",
|
||||
trigger: ".o_statusbar_status:contains(Posted) .o_arrow_button_current:contains(Draft)",
|
||||
},
|
||||
{
|
||||
content: "Check that the invoice is linked to the sale order",
|
||||
trigger: "button[name=action_view_source_purchase_orders]",
|
||||
},
|
||||
],
|
||||
});
|
||||
160
frontend/purchase/static/tests/tours/tour_helper.js
Normal file
160
frontend/purchase/static/tests/tours/tour_helper.js
Normal file
@@ -0,0 +1,160 @@
|
||||
export const purchaseForm = {
|
||||
checkLineValues(index, values) {
|
||||
const fieldAndLabelDict = {
|
||||
product: { fieldName: "product_id", label: "product" },
|
||||
quantity: { fieldName: "product_qty", label: "quantity" },
|
||||
unit: { fieldName: "product_uom_id", label: "unit of measure" },
|
||||
unitPrice: { fieldName: "price_unit", label: "unit price" },
|
||||
discount: { fieldName: "discount", label: "discount" },
|
||||
totalPrice: { fieldName: "price_subtotal", label: "subtotal price" },
|
||||
};
|
||||
const trigger = `.o_form_renderer .o_list_view.o_field_x2many tbody tr.o_data_row:eq(${index})`;
|
||||
const run = function ({ anchor }) {
|
||||
const getFieldValue = (fieldName) => {
|
||||
let selector = `td[name="${fieldName}"]`;
|
||||
if (fieldName === "product_id") {
|
||||
// Special case for the product field because it can be replace by another field
|
||||
selector += ",td[name='product_template_id']";
|
||||
}
|
||||
const fieldEl = anchor.querySelector(selector);
|
||||
return fieldEl ? fieldEl.innerText.replace(/\s/g, " ") : false;
|
||||
};
|
||||
for (const key in values) {
|
||||
if (!Object.keys(fieldAndLabelDict).includes(key)) {
|
||||
throw new Error(
|
||||
`'checkPurchaseOrderLineValues' is called with unsupported key: ${key}`
|
||||
);
|
||||
}
|
||||
const value = values[key];
|
||||
const { fieldName, label } = fieldAndLabelDict[key];
|
||||
const lineValue = getFieldValue(fieldName);
|
||||
if (!lineValue) {
|
||||
throw new Error(
|
||||
`Purchase order line at index ${index} expected ${value} as ${label} but got nothing`
|
||||
);
|
||||
} else if (lineValue !== value) {
|
||||
throw new Error(
|
||||
`Purchase order line at index ${index} expected ${value} as ${label} but got ${lineValue} instead`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
return [{ trigger, run }];
|
||||
},
|
||||
|
||||
displayOptionalField(fieldName) {
|
||||
return [
|
||||
{
|
||||
trigger:
|
||||
".o_form_renderer .o_list_view.o_field_x2many .o_optional_columns_dropdown button",
|
||||
run: "click",
|
||||
},
|
||||
{ trigger: `input[name="${fieldName}"]:not(:checked)`, run: "click" },
|
||||
{ trigger: `th[data-name="${fieldName}"]` },
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* Clicks on the "Catalog" button below the purchase order lines.
|
||||
*/
|
||||
openCatalog() {
|
||||
return [
|
||||
{
|
||||
content: "Go to product catalog",
|
||||
trigger: ".o_field_x2many_list_row_add > button[name='action_add_from_catalog']",
|
||||
run: "click",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the vendor on a purchase order (must already on PO).
|
||||
* @param {string} vendorName An existing partner.
|
||||
*/
|
||||
selectVendor(vendorName) {
|
||||
return [
|
||||
{
|
||||
content: "Fill Vendor Field on PO",
|
||||
trigger: ".o_field_res_partner_many2one[name='partner_id'] input",
|
||||
run: `edit ${vendorName}`,
|
||||
},
|
||||
{
|
||||
content: "Select vendor from many to one",
|
||||
isActive: ["auto"],
|
||||
trigger: `.ui-menu-item > a:contains(${vendorName})`,
|
||||
run: "click",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the WH on a purchase order (must already on PO).
|
||||
* @param {string} warehouseName An existing warehouse.
|
||||
*/
|
||||
selectWarehouse(warehouseName) {
|
||||
return [
|
||||
{
|
||||
content: "Fill Warehouse Field on PO",
|
||||
trigger: ".o_field_many2one[name='picking_type_id'] input",
|
||||
run: `edit ${warehouseName}`,
|
||||
},
|
||||
{
|
||||
content: "Select BaseWarehouse as PO WH",
|
||||
isActive: ["auto"],
|
||||
trigger: `.ui-menu-item > a:contains(${warehouseName})`,
|
||||
run: "click",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
createNewPO() {
|
||||
const content = "Create a New PO";
|
||||
const trigger = ".o_list_button_add, .o_form_button_create";
|
||||
return [{ content, trigger, run: "click" }];
|
||||
},
|
||||
};
|
||||
|
||||
export const productCatalog = {
|
||||
addProduct(productName) {
|
||||
const trigger = `.o_kanban_record:contains("${productName}") button:has(.fa-plus,.fa-shopping-cart)`;
|
||||
return [{ trigger, run: "click" }];
|
||||
},
|
||||
|
||||
/** Remove a product from the PO by clicking the "trash" button */
|
||||
removeProduct(productName) {
|
||||
const trigger = `.o_kanban_record:contains("${productName}") button:has(.fa-trash)`;
|
||||
return [{ trigger, run: "click" }];
|
||||
},
|
||||
|
||||
checkProductPrice(productName, price) {
|
||||
const trigger = `.o_kanban_record:contains("${productName}") .o_product_catalog_price:contains("${price}")`;
|
||||
const content = `Check that the kanban record card for product "${productName}" has a price of ${price}`;
|
||||
return [{ content, trigger }];
|
||||
},
|
||||
|
||||
checkProductUoM(productName, uom) {
|
||||
const trigger = `.o_kanban_record:contains("${productName}") .o_product_catalog_quantity:contains("${uom}")`;
|
||||
const content = `Check that the kanban record card for product "${productName}" uses ${uom} as the UoM`;
|
||||
return [{ content, trigger }];
|
||||
},
|
||||
|
||||
waitForQuantity(productName, quantity) {
|
||||
const trigger = `.o_kanban_record:contains("${productName}") input[type=number]:value("${quantity}")`;
|
||||
return [{ trigger }];
|
||||
},
|
||||
|
||||
selectSearchPanelCategory(categoryName) {
|
||||
const content = `Select the category ${categoryName}`;
|
||||
const trigger = `.o_search_panel_label_title:contains("${categoryName}")`;
|
||||
return [{ content, trigger, run: "click" }];
|
||||
},
|
||||
|
||||
/**
|
||||
* Clicks on the "Back to Order" button from the Catalog view
|
||||
*/
|
||||
goBackToOrder() {
|
||||
const content = "Go back to the Order";
|
||||
const trigger = "button.o-kanban-button-back";
|
||||
return [{ content, trigger, run: "click" }];
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user