feat: Portal, Email Inbound, Discuss + module improvements
- Portal: /my/* routes, signup, password reset, portal user support - Email Inbound: IMAP polling (go-imap/v2), thread matching - Discuss: mail.channel, long-polling bus, DM, unread count - Cron: ir.cron runner (goroutine scheduler) - Bank Import, CSV/Excel Import - Automation (ir.actions.server) - Fetchmail service - HR Payroll model - Various fixes across account, sale, stock, purchase, crm, hr, project Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -195,6 +195,12 @@ func generateDefaultView(modelName, viewType string) string {
|
||||
return generateDefaultPivotView(m)
|
||||
case "graph":
|
||||
return generateDefaultGraphView(m)
|
||||
case "calendar":
|
||||
return generateDefaultCalendarView(m)
|
||||
case "activity":
|
||||
return generateDefaultActivityView(m)
|
||||
case "dashboard":
|
||||
return generateDefaultDashboardView(m)
|
||||
default:
|
||||
return fmt.Sprintf("<%s><field name=\"id\"/></%s>", viewType, viewType)
|
||||
}
|
||||
@@ -530,6 +536,161 @@ func generateDefaultGraphView(m *orm.Model) string {
|
||||
return fmt.Sprintf("<graph>\n %s\n</graph>", strings.Join(fields, "\n "))
|
||||
}
|
||||
|
||||
// generateDefaultCalendarView creates a calendar view with auto-detected date fields.
|
||||
// The OWL CalendarArchParser requires date_start; date_stop and color are optional.
|
||||
// Mirrors: odoo/addons/web/static/src/views/calendar/calendar_arch_parser.js
|
||||
func generateDefaultCalendarView(m *orm.Model) string {
|
||||
// Auto-detect date_start field (priority order)
|
||||
dateStart := ""
|
||||
for _, candidate := range []string{"start", "date_start", "date_from", "date_order", "date_begin", "date"} {
|
||||
if f := m.GetField(candidate); f != nil && (f.Type == orm.TypeDatetime || f.Type == orm.TypeDate) {
|
||||
dateStart = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
if dateStart == "" {
|
||||
// Fallback: find any datetime/date field
|
||||
for _, name := range sortedFieldNames(m) {
|
||||
f := m.GetField(name)
|
||||
if f != nil && (f.Type == orm.TypeDatetime || f.Type == orm.TypeDate) && f.Name != "create_date" && f.Name != "write_date" {
|
||||
dateStart = name
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if dateStart == "" {
|
||||
// No date field found — return minimal arch that won't crash
|
||||
return `<calendar date_start="create_date"><field name="display_name"/></calendar>`
|
||||
}
|
||||
|
||||
// Auto-detect date_stop field
|
||||
dateStop := ""
|
||||
for _, candidate := range []string{"stop", "date_stop", "date_to", "date_end"} {
|
||||
if f := m.GetField(candidate); f != nil && (f.Type == orm.TypeDatetime || f.Type == orm.TypeDate) {
|
||||
dateStop = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-detect color field (M2O fields make good color discriminators)
|
||||
colorField := ""
|
||||
for _, candidate := range []string{"color", "user_id", "partner_id", "stage_id"} {
|
||||
if f := m.GetField(candidate); f != nil {
|
||||
colorField = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-detect all_day field
|
||||
allDay := ""
|
||||
for _, candidate := range []string{"allday", "all_day"} {
|
||||
if f := m.GetField(candidate); f != nil && f.Type == orm.TypeBoolean {
|
||||
allDay = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Build attributes
|
||||
attrs := fmt.Sprintf(`date_start="%s"`, dateStart)
|
||||
if dateStop != "" {
|
||||
attrs += fmt.Sprintf(` date_stop="%s"`, dateStop)
|
||||
}
|
||||
if colorField != "" {
|
||||
attrs += fmt.Sprintf(` color="%s"`, colorField)
|
||||
}
|
||||
if allDay != "" {
|
||||
attrs += fmt.Sprintf(` all_day="%s"`, allDay)
|
||||
}
|
||||
|
||||
// Pick display fields for the calendar card
|
||||
var fields []string
|
||||
nameField := "display_name"
|
||||
if f := m.GetField("name"); f != nil {
|
||||
nameField = "name"
|
||||
}
|
||||
fields = append(fields, fmt.Sprintf(` <field name="%s"/>`, nameField))
|
||||
|
||||
if f := m.GetField("partner_id"); f != nil {
|
||||
fields = append(fields, ` <field name="partner_id" avatar_field="avatar_128"/>`)
|
||||
}
|
||||
if f := m.GetField("user_id"); f != nil && colorField != "user_id" {
|
||||
fields = append(fields, ` <field name="user_id"/>`)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("<calendar %s mode=\"month\">\n%s\n</calendar>",
|
||||
attrs, strings.Join(fields, "\n"))
|
||||
}
|
||||
|
||||
// generateDefaultActivityView creates a minimal activity view.
|
||||
// Mirrors: odoo/addons/mail/static/src/views/web_activity/activity_arch_parser.js
|
||||
func generateDefaultActivityView(m *orm.Model) string {
|
||||
nameField := "display_name"
|
||||
if f := m.GetField("name"); f != nil {
|
||||
nameField = "name"
|
||||
}
|
||||
return fmt.Sprintf(`<activity string="Activities">
|
||||
<templates>
|
||||
<div t-name="activity-box">
|
||||
<field name="%s"/>
|
||||
</div>
|
||||
</templates>
|
||||
</activity>`, nameField)
|
||||
}
|
||||
|
||||
// generateDefaultDashboardView creates a dashboard view with aggregate widgets.
|
||||
// Mirrors: odoo/addons/board/static/src/board_view.js
|
||||
func generateDefaultDashboardView(m *orm.Model) string {
|
||||
var widgets []string
|
||||
|
||||
// Add aggregate widgets for numeric fields
|
||||
for _, name := range sortedFieldNames(m) {
|
||||
f := m.GetField(name)
|
||||
if f == nil {
|
||||
continue
|
||||
}
|
||||
if (f.Type == orm.TypeFloat || f.Type == orm.TypeInteger || f.Type == orm.TypeMonetary) &&
|
||||
f.IsStored() && f.Name != "id" && f.Name != "sequence" &&
|
||||
f.Name != "create_uid" && f.Name != "write_uid" && f.Name != "company_id" {
|
||||
widgets = append(widgets, fmt.Sprintf(
|
||||
` <aggregate name="%s" field="%s" string="%s"/>`,
|
||||
f.Name, f.Name, f.String))
|
||||
if len(widgets) >= 6 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a graph for the first groupable dimension
|
||||
var graphField string
|
||||
for _, name := range sortedFieldNames(m) {
|
||||
f := m.GetField(name)
|
||||
if f != nil && f.IsStored() && (f.Type == orm.TypeMany2one || f.Type == orm.TypeSelection) {
|
||||
graphField = name
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
buf.WriteString("<dashboard>\n")
|
||||
if len(widgets) > 0 {
|
||||
buf.WriteString(" <group>\n")
|
||||
for _, w := range widgets {
|
||||
buf.WriteString(w + "\n")
|
||||
}
|
||||
buf.WriteString(" </group>\n")
|
||||
}
|
||||
if graphField != "" {
|
||||
buf.WriteString(fmt.Sprintf(` <view type="graph">
|
||||
<graph type="bar">
|
||||
<field name="%s"/>
|
||||
</graph>
|
||||
</view>
|
||||
`, graphField))
|
||||
}
|
||||
buf.WriteString("</dashboard>")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// sortedFieldNames returns field names in alphabetical order for deterministic output.
|
||||
func sortedFieldNames(m *orm.Model) []string {
|
||||
fields := m.Fields()
|
||||
|
||||
Reference in New Issue
Block a user