Bring odoo-go to ~70%: read_group, record rules, admin, sessions
Phase 1: read_group/web_read_group with SQL GROUP BY, aggregates (sum/avg/min/max/count/array_agg/sum_currency), date granularity, M2O groupby resolution to [id, display_name]. Phase 2: Record rules with domain_force parsing (Python literal parser), global AND + group OR merging. Domain operators: child_of, parent_of, any, not any compiled to SQL hierarchy/EXISTS queries. Phase 3: Button dispatch via /web/dataset/call_button, method return values interpreted as actions. Payment register wizard (account.payment.register) for sale→invoice→pay flow. Phase 4: ir.filters, ir.default, product fields expanded, SO line product_id onchange, ir_model+ir_model_fields DB seeding. Phase 5: CSV export (/web/export/csv), attachment upload/download via ir.attachment, fields_get with aggregator hints. Admin/System: Session persistence (PostgreSQL-backed), ir.config_parameter with get_param/set_param, ir.cron, ir.logging, res.lang, res.config.settings with company-related fields, Settings form view. Technical menu with Views/Actions/Parameters/Security/Logging sub-menus. User change_password, preferences. Password never exposed in UI/API. Bugfixes: false→nil for varchar/int fields, int32 in toInt64, call_button route with trailing slash, create_invoices returns action, search view always included, get_formview_action, name_create, ir.http stub. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,21 @@ func handleGetViews(env *orm.Environment, model string, params CallKWParams) (in
|
||||
}
|
||||
}
|
||||
|
||||
// Always include search view (client expects it)
|
||||
if _, hasSearch := views["search"]; !hasSearch {
|
||||
arch := loadViewArch(env, model, "search")
|
||||
if arch == "" {
|
||||
arch = generateDefaultView(model, "search")
|
||||
}
|
||||
views["search"] = map[string]interface{}{
|
||||
"arch": arch,
|
||||
"type": "search",
|
||||
"model": model,
|
||||
"view_id": 0,
|
||||
"field_parent": false,
|
||||
}
|
||||
}
|
||||
|
||||
// Build models dict with field metadata
|
||||
models := map[string]interface{}{
|
||||
model: map[string]interface{}{
|
||||
@@ -133,6 +148,7 @@ func generateDefaultListView(m *orm.Model) string {
|
||||
if added[f.Name] || f.Name == "id" || !f.IsStored() ||
|
||||
f.Name == "create_uid" || f.Name == "write_uid" ||
|
||||
f.Name == "create_date" || f.Name == "write_date" ||
|
||||
f.Name == "password" || f.Name == "password_crypt" ||
|
||||
f.Type == orm.TypeBinary || f.Type == orm.TypeText || f.Type == orm.TypeHTML {
|
||||
continue
|
||||
}
|
||||
@@ -147,13 +163,47 @@ func generateDefaultFormView(m *orm.Model) string {
|
||||
skip := map[string]bool{
|
||||
"id": true, "create_uid": true, "write_uid": true,
|
||||
"create_date": true, "write_date": true,
|
||||
"password": true, "password_crypt": true,
|
||||
}
|
||||
|
||||
// Header with state widget if state field exists
|
||||
// Header with action buttons and state widget
|
||||
// Mirrors: odoo form views with <header><button .../><field name="state" widget="statusbar"/></header>
|
||||
var header string
|
||||
if f := m.GetField("state"); f != nil && f.Type == orm.TypeSelection {
|
||||
header = ` <header>
|
||||
<field name="state" widget="statusbar"/>
|
||||
var buttons []string
|
||||
// Generate buttons from registered methods that look like actions
|
||||
if m.Methods != nil {
|
||||
actionMethods := []struct{ method, label, stateFilter string }{
|
||||
{"action_confirm", "Confirm", "draft"},
|
||||
{"action_post", "Post", "draft"},
|
||||
{"action_done", "Done", "confirmed"},
|
||||
{"action_cancel", "Cancel", ""},
|
||||
{"button_cancel", "Cancel", ""},
|
||||
{"button_draft", "Reset to Draft", "cancel"},
|
||||
{"action_send", "Send", "posted"},
|
||||
{"create_invoices", "Create Invoice", "sale"},
|
||||
}
|
||||
for _, am := range actionMethods {
|
||||
if _, ok := m.Methods[am.method]; ok {
|
||||
attrs := ""
|
||||
if am.stateFilter != "" {
|
||||
attrs = fmt.Sprintf(` invisible="state != '%s'"`, am.stateFilter)
|
||||
}
|
||||
btnClass := "btn-secondary"
|
||||
if am.method == "action_confirm" || am.method == "action_post" {
|
||||
btnClass = "btn-primary"
|
||||
}
|
||||
buttons = append(buttons, fmt.Sprintf(
|
||||
` <button name="%s" string="%s" type="object" class="%s"%s/>`,
|
||||
am.method, am.label, btnClass, attrs))
|
||||
}
|
||||
}
|
||||
}
|
||||
header = " <header>\n"
|
||||
for _, btn := range buttons {
|
||||
header += btn + "\n"
|
||||
}
|
||||
header += ` <field name="state" widget="statusbar" clickable="1"/>
|
||||
</header>
|
||||
`
|
||||
}
|
||||
@@ -306,11 +356,44 @@ func generateDefaultKanbanView(m *orm.Model) string {
|
||||
if f := m.GetField("name"); f == nil {
|
||||
nameField = "id"
|
||||
}
|
||||
|
||||
// Build a richer card with available fields
|
||||
var cardFields []string
|
||||
|
||||
// Title
|
||||
cardFields = append(cardFields, fmt.Sprintf(` <field name="%s" class="fw-bold fs-5"/>`, nameField))
|
||||
|
||||
// Partner/customer
|
||||
if f := m.GetField("partner_id"); f != nil {
|
||||
cardFields = append(cardFields, ` <field name="partner_id"/>`)
|
||||
}
|
||||
|
||||
// Revenue/amount
|
||||
for _, amtField := range []string{"expected_revenue", "amount_total", "amount_untaxed"} {
|
||||
if f := m.GetField(amtField); f != nil {
|
||||
cardFields = append(cardFields, fmt.Sprintf(` <field name="%s"/>`, amtField))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Date
|
||||
for _, dateField := range []string{"date_order", "date", "date_deadline"} {
|
||||
if f := m.GetField(dateField); f != nil {
|
||||
cardFields = append(cardFields, fmt.Sprintf(` <field name="%s"/>`, dateField))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// User/assignee
|
||||
if f := m.GetField("user_id"); f != nil {
|
||||
cardFields = append(cardFields, ` <field name="user_id" widget="many2one_avatar_user"/>`)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="%s"/>
|
||||
%s
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`, nameField)
|
||||
</kanban>`, strings.Join(cardFields, "\n"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user