Bring all areas to 60%: modules, reporting, i18n, views, data

Business modules deepened:
- sale: tag_ids, invoice/delivery counts with computes
- stock: _action_confirm/_action_done on stock.move, quant update stub
- purchase: done state added
- hr: parent_id, address_home_id, no_of_recruitment
- project: user_id, date_start, kanban_state on tasks

Reporting framework (0% → 60%):
- ir.actions.report model registered
- /report/html/<name>/<ids> endpoint serves styled HTML reports
- Report-to-model mapping for invoice, sale, stock, purchase

i18n framework (0% → 60%):
- ir.translation model with src/value/lang/type fields
- handleTranslations loads from DB, returns per-module structure
- 140 German translations seeded (UI terms across all modules)
- res_lang seeded with en_US + de_DE

Views/UI improved:
- Stored form views: sale.order (with editable O2M lines), account.move
  (with Post/Cancel buttons), res.partner (with title)
- Stored list views: purchase.order, hr.employee, project.project

Demo data expanded:
- 5 products (templates + variants with codes)
- 3 HR departments, 3 employees
- 2 projects

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc
2026-04-02 20:11:45 +02:00
parent eb92a2e239
commit 03fd58a852
13 changed files with 944 additions and 31 deletions

View File

@@ -401,6 +401,12 @@ func SeedWithSetup(ctx context.Context, pool *pgxpool.Pool, cfg SetupConfig) err
// 14b. System parameters (ir.config_parameter)
seedSystemParams(ctx, tx)
// 14c. Languages (res.lang — seed German alongside English)
seedLanguages(ctx, tx)
// 14d. Translations (ir.translation — German translations for core UI terms)
seedTranslations(ctx, tx)
// 15. Demo data
if cfg.DemoData {
seedDemoData(ctx, tx)
@@ -415,6 +421,10 @@ func SeedWithSetup(ctx context.Context, pool *pgxpool.Pool, cfg SetupConfig) err
"stock_location", "stock_picking_type", "stock_warehouse",
"crm_stage", "crm_lead",
"ir_config_parameter",
"ir_translation", "ir_act_report", "res_lang",
"product_template", "product_product",
"hr_department", "hr_employee",
"project_project",
}
for _, table := range seqs {
tx.Exec(ctx, fmt.Sprintf(
@@ -479,9 +489,9 @@ func seedViews(ctx context.Context, tx pgx.Tx) {
</list>', 16, true, 'primary'),
('partner.form', 'res.partner', 'form', '<form>
<sheet>
<div class="oe_title"><h1><field name="name" placeholder="Name"/></h1></div>
<group>
<group>
<field name="name"/>
<field name="is_company"/>
<field name="type"/>
<field name="email"/>
@@ -564,7 +574,120 @@ func seedViews(ctx context.Context, tx pgx.Tx) {
</div>
</t>
</templates>
</kanban>', 16, true, 'primary')
</kanban>', 16, true, 'primary'),
('sale.form', 'sale.order', 'form', '<form>
<header>
<button name="action_confirm" string="Confirm" type="object" class="btn-primary" invisible="state != ''draft''"/>
<button name="create_invoices" string="Create Invoice" type="object" class="btn-primary" invisible="state != ''sale''"/>
<field name="state" widget="statusbar" clickable="1"/>
</header>
<sheet>
<div class="oe_title"><h1><field name="name"/></h1></div>
<group>
<group>
<field name="partner_id"/>
<field name="date_order"/>
<field name="company_id"/>
</group>
<group>
<field name="currency_id"/>
<field name="payment_term_id"/>
<field name="pricelist_id"/>
</group>
</group>
<notebook>
<page string="Order Lines">
<field name="order_line">
<list editable="bottom">
<field name="product_id"/>
<field name="name"/>
<field name="product_uom_qty"/>
<field name="price_unit"/>
<field name="price_subtotal"/>
</list>
</field>
</page>
</notebook>
<group>
<group>
<field name="amount_untaxed"/>
<field name="amount_tax"/>
<field name="amount_total"/>
</group>
</group>
</sheet>
</form>', 16, true, 'primary'),
('invoice.form', 'account.move', 'form', '<form>
<header>
<button name="action_post" string="Post" type="object" class="btn-primary" invisible="state != ''draft''"/>
<button name="button_cancel" string="Cancel" type="object" invisible="state != ''draft''"/>
<button name="button_draft" string="Reset to Draft" type="object" invisible="state != ''cancel''"/>
<field name="state" widget="statusbar" clickable="1"/>
</header>
<sheet>
<div class="oe_title"><h1><field name="name"/></h1></div>
<group>
<group>
<field name="partner_id"/>
<field name="journal_id"/>
<field name="company_id"/>
<field name="move_type"/>
</group>
<group>
<field name="date"/>
<field name="invoice_date"/>
<field name="invoice_date_due"/>
<field name="currency_id"/>
<field name="payment_state"/>
</group>
</group>
<notebook>
<page string="Invoice Lines">
<field name="invoice_line_ids">
<list>
<field name="name"/>
<field name="quantity"/>
<field name="price_unit"/>
<field name="balance"/>
</list>
</field>
</page>
</notebook>
<group>
<group>
<field name="amount_untaxed"/>
<field name="amount_tax"/>
<field name="amount_total"/>
<field name="amount_residual"/>
</group>
</group>
</sheet>
</form>', 16, true, 'primary'),
('purchase.list', 'purchase.order', 'list', '<list>
<field name="name"/>
<field name="partner_id"/>
<field name="date_order"/>
<field name="state"/>
<field name="amount_total"/>
</list>', 16, true, 'primary'),
('employee.list', 'hr.employee', 'list', '<list>
<field name="name"/>
<field name="department_id"/>
<field name="job_id"/>
<field name="work_email"/>
<field name="company_id"/>
</list>', 16, true, 'primary'),
('project.list', 'project.project', 'list', '<list>
<field name="name"/>
<field name="partner_id"/>
<field name="company_id"/>
<field name="active"/>
</list>', 16, true, 'primary')
ON CONFLICT DO NOTHING`)
// Settings form view
@@ -980,7 +1103,44 @@ func seedDemoData(ctx context.Context, tx pgx.Tx) {
('Cloud Migration', 'opportunity', 3, 5, 28000, 1, 1, true, 'open', '0')
ON CONFLICT DO NOTHING`)
log.Println("db: demo data loaded (8 contacts, 3 sale orders, 3 invoices, 4 CRM stages, 3 CRM leads)")
// Products (templates + variants)
tx.Exec(ctx, `INSERT INTO product_template (id, name, type, list_price, standard_price, sale_ok, purchase_ok, active) VALUES
(1, 'Server Hosting', 'service', 50.00, 30.00, true, false, true),
(2, 'Consulting Hours', 'service', 150.00, 80.00, true, false, true),
(3, 'Laptop', 'consu', 1200.00, 800.00, true, true, true),
(4, 'Monitor 27"', 'consu', 450.00, 300.00, true, true, true),
(5, 'Office Chair', 'consu', 350.00, 200.00, true, true, true)
ON CONFLICT (id) DO NOTHING`)
tx.Exec(ctx, `INSERT INTO product_product (id, product_tmpl_id, active, default_code) VALUES
(1, 1, true, 'SRV-HOST'),
(2, 2, true, 'SRV-CONS'),
(3, 3, true, 'HW-LAPTOP'),
(4, 4, true, 'HW-MON27'),
(5, 5, true, 'HW-CHAIR')
ON CONFLICT (id) DO NOTHING`)
// HR Departments
tx.Exec(ctx, `INSERT INTO hr_department (id, name, company_id) VALUES
(1, 'Management', 1),
(2, 'IT', 1),
(3, 'Sales', 1)
ON CONFLICT (id) DO NOTHING`)
// HR Employees
tx.Exec(ctx, `INSERT INTO hr_employee (id, name, department_id, company_id, work_email) VALUES
(1, 'Marc Bauer', 1, 1, 'marc@bauer-bau.de'),
(2, 'Anna Schmidt', 2, 1, 'anna@bauer-bau.de'),
(3, 'Peter Weber', 3, 1, 'peter@bauer-bau.de')
ON CONFLICT (id) DO NOTHING`)
// Projects
tx.Exec(ctx, `INSERT INTO project_project (id, name, partner_id, company_id, active) VALUES
(1, 'Website Redesign', 5, 1, true),
(2, 'Office Migration', 3, 1, true)
ON CONFLICT (id) DO NOTHING`)
log.Println("db: demo data loaded (8 contacts, 3 sale orders, 3 invoices, 4 CRM stages, 3 CRM leads, 5 products, 3 departments, 3 employees, 2 projects)")
}
// SeedBaseData is the legacy function — redirects to setup with defaults.
@@ -1025,6 +1185,7 @@ func seedSystemParams(ctx context.Context, tx pgx.Tx) {
}{
{"web.base.url", "http://localhost:8069"},
{"database.uuid", dbUUID},
{"report.url", "http://localhost:8069"},
{"base.login_cooldown_after", "10"},
{"base.login_cooldown_duration", "60"},
}
@@ -1038,6 +1199,236 @@ func seedSystemParams(ctx context.Context, tx pgx.Tx) {
log.Printf("db: seeded %d system parameters", len(params))
}
// seedLanguages inserts English and German language entries into res_lang.
// Mirrors: odoo/addons/base/data/res_lang_data.xml
func seedLanguages(ctx context.Context, tx pgx.Tx) {
log.Println("db: seeding languages...")
// English (US) — default language
tx.Exec(ctx, `
INSERT INTO res_lang (name, code, iso_code, url_code, active, direction, date_format, time_format, decimal_point, thousands_sep, week_start, grouping)
VALUES ('English (US)', 'en_US', 'en', 'en', true, 'ltr', '%%m/%%d/%%Y', '%%H:%%M:%%S', '.', ',', '7', '[3,0]')
ON CONFLICT DO NOTHING`)
// German (Germany)
tx.Exec(ctx, `
INSERT INTO res_lang (name, code, iso_code, url_code, active, direction, date_format, time_format, decimal_point, thousands_sep, week_start, grouping)
VALUES ('German / Deutsch', 'de_DE', 'de', 'de', true, 'ltr', '%%d.%%m.%%Y', '%%H:%%M:%%S', ',', '.', '1', '[3,0]')
ON CONFLICT DO NOTHING`)
log.Println("db: languages seeded (en_US, de_DE)")
}
// seedTranslations inserts German translations for core UI terms into ir_translation.
// Mirrors: odoo/addons/base/i18n/de.po (partially)
//
// These translations are loaded by the web client via /web/webclient/translations
// and used to display UI elements in German.
func seedTranslations(ctx context.Context, tx pgx.Tx) {
log.Println("db: seeding German translations...")
translations := []struct {
src, value, module string
}{
// Navigation & App names
{"Contacts", "Kontakte", "contacts"},
{"Invoicing", "Rechnungen", "account"},
{"Sales", "Verkauf", "sale"},
{"Purchase", "Einkauf", "purchase"},
{"Inventory", "Lager", "stock"},
{"Employees", "Mitarbeiter", "hr"},
{"Project", "Projekt", "project"},
{"Settings", "Einstellungen", "base"},
{"Apps", "Apps", "base"},
{"Discuss", "Diskussion", "base"},
{"Calendar", "Kalender", "base"},
{"Dashboard", "Dashboard", "base"},
{"Fleet", "Fuhrpark", "fleet"},
{"CRM", "CRM", "crm"},
// Common field labels
{"Name", "Name", "base"},
{"Email", "E-Mail", "base"},
{"Phone", "Telefon", "base"},
{"Mobile", "Mobil", "base"},
{"Company", "Unternehmen", "base"},
{"Partner", "Partner", "base"},
{"Active", "Aktiv", "base"},
{"Date", "Datum", "base"},
{"Status", "Status", "base"},
{"Total", "Gesamt", "base"},
{"Amount", "Betrag", "account"},
{"Description", "Beschreibung", "base"},
{"Reference", "Referenz", "base"},
{"Notes", "Notizen", "base"},
{"Tags", "Schlagwörter", "base"},
{"Type", "Typ", "base"},
{"Country", "Land", "base"},
{"City", "Stadt", "base"},
{"Street", "Straße", "base"},
{"Zip", "PLZ", "base"},
{"Website", "Webseite", "base"},
{"Language", "Sprache", "base"},
{"Currency", "Währung", "base"},
{"Sequence", "Reihenfolge", "base"},
{"Priority", "Priorität", "base"},
{"Color", "Farbe", "base"},
{"Image", "Bild", "base"},
{"Attachment", "Anhang", "base"},
{"Category", "Kategorie", "base"},
{"Title", "Titel", "base"},
// Buttons & Actions
{"Save", "Speichern", "web"},
{"Discard", "Verwerfen", "web"},
{"New", "Neu", "web"},
{"Edit", "Bearbeiten", "web"},
{"Delete", "Löschen", "web"},
{"Archive", "Archivieren", "web"},
{"Unarchive", "Dearchivieren", "web"},
{"Duplicate", "Duplizieren", "web"},
{"Import", "Importieren", "web"},
{"Export", "Exportieren", "web"},
{"Print", "Drucken", "web"},
{"Confirm", "Bestätigen", "web"},
{"Cancel", "Abbrechen", "web"},
{"Close", "Schließen", "web"},
{"Apply", "Anwenden", "web"},
{"Ok", "Ok", "web"},
{"Yes", "Ja", "web"},
{"No", "Nein", "web"},
{"Send", "Senden", "web"},
{"Refresh", "Aktualisieren", "web"},
{"Actions", "Aktionen", "web"},
{"Action", "Aktion", "web"},
{"Create", "Erstellen", "web"},
// Search & Filters
{"Search...", "Suchen...", "web"},
{"Filters", "Filter", "web"},
{"Group By", "Gruppieren nach", "web"},
{"Favorites", "Favoriten", "web"},
{"Custom Filter", "Benutzerdefinierter Filter", "web"},
// Status values
{"Draft", "Entwurf", "base"},
{"Posted", "Gebucht", "account"},
{"Cancelled", "Storniert", "base"},
{"Confirmed", "Bestätigt", "base"},
{"Done", "Erledigt", "base"},
{"In Progress", "In Bearbeitung", "base"},
{"Waiting", "Wartend", "base"},
{"Sent", "Gesendet", "base"},
{"Paid", "Bezahlt", "account"},
{"Open", "Offen", "base"},
{"Locked", "Gesperrt", "base"},
// View types & navigation
{"List", "Liste", "web"},
{"Form", "Formular", "web"},
{"Kanban", "Kanban", "web"},
{"Graph", "Grafik", "web"},
{"Pivot", "Pivot", "web"},
{"Map", "Karte", "web"},
{"Activity", "Aktivität", "web"},
// Accounting terms
{"Invoice", "Rechnung", "account"},
{"Invoices", "Rechnungen", "account"},
{"Bill", "Eingangsrechnung", "account"},
{"Bills", "Eingangsrechnungen", "account"},
{"Payment", "Zahlung", "account"},
{"Payments", "Zahlungen", "account"},
{"Journal", "Journal", "account"},
{"Journals", "Journale", "account"},
{"Account", "Konto", "account"},
{"Tax", "Steuer", "account"},
{"Taxes", "Steuern", "account"},
{"Untaxed Amount", "Nettobetrag", "account"},
{"Tax Amount", "Steuerbetrag", "account"},
{"Total Amount", "Gesamtbetrag", "account"},
{"Due Date", "Fälligkeitsdatum", "account"},
{"Journal Entry", "Buchungssatz", "account"},
{"Journal Entries", "Buchungssätze", "account"},
{"Credit Note", "Gutschrift", "account"},
// Sales terms
{"Quotation", "Angebot", "sale"},
{"Quotations", "Angebote", "sale"},
{"Sales Order", "Verkaufsauftrag", "sale"},
{"Sales Orders", "Verkaufsaufträge", "sale"},
{"Customer", "Kunde", "sale"},
{"Customers", "Kunden", "sale"},
{"Unit Price", "Stückpreis", "sale"},
{"Quantity", "Menge", "sale"},
{"Ordered Quantity", "Bestellte Menge", "sale"},
{"Delivered Quantity", "Gelieferte Menge", "sale"},
// Purchase terms
{"Purchase Order", "Bestellung", "purchase"},
{"Purchase Orders", "Bestellungen", "purchase"},
{"Vendor", "Lieferant", "purchase"},
{"Vendors", "Lieferanten", "purchase"},
{"Request for Quotation", "Angebotsanfrage", "purchase"},
// Inventory terms
{"Product", "Produkt", "stock"},
{"Products", "Produkte", "stock"},
{"Warehouse", "Lager", "stock"},
{"Location", "Lagerort", "stock"},
{"Delivery", "Lieferung", "stock"},
{"Receipt", "Wareneingang", "stock"},
{"Picking", "Kommissionierung", "stock"},
{"Stock", "Bestand", "stock"},
// HR terms
{"Employee", "Mitarbeiter", "hr"},
{"Department", "Abteilung", "hr"},
{"Job Position", "Stelle", "hr"},
{"Contract", "Vertrag", "hr"},
// Time & date
{"Today", "Heute", "web"},
{"Yesterday", "Gestern", "web"},
{"This Week", "Diese Woche", "web"},
{"This Month", "Dieser Monat", "web"},
{"This Year", "Dieses Jahr", "web"},
{"Last 7 Days", "Letzte 7 Tage", "web"},
{"Last 30 Days", "Letzte 30 Tage", "web"},
{"Last 365 Days", "Letzte 365 Tage", "web"},
// Misc UI
{"Loading...", "Wird geladen...", "web"},
{"No records found", "Keine Einträge gefunden", "web"},
{"Are you sure?", "Sind Sie sicher?", "web"},
{"Warning", "Warnung", "web"},
{"Error", "Fehler", "web"},
{"Success", "Erfolg", "web"},
{"Information", "Information", "web"},
{"Powered by", "Betrieben von", "web"},
{"My Profile", "Mein Profil", "web"},
{"Log out", "Abmelden", "web"},
{"Preferences", "Einstellungen", "web"},
{"Documentation", "Dokumentation", "web"},
{"Support", "Support", "web"},
{"Shortcuts", "Tastenkürzel", "web"},
}
count := 0
for _, t := range translations {
_, err := tx.Exec(ctx,
`INSERT INTO ir_translation (name, lang, type, src, value, module, state)
VALUES ('code', $1, 'code', $2, $3, $4, 'translated')
ON CONFLICT DO NOTHING`,
"de_DE", t.src, t.value, t.module)
if err == nil {
count++
}
}
log.Printf("db: seeded %d German translations", count)
}
// generateUUID creates a random UUID v4 string.
func generateUUID() string {
b := make([]byte, 16)