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:
@@ -330,6 +330,8 @@ func SeedWithSetup(ctx context.Context, pool *pgxpool.Pool, cfg SetupConfig) err
|
||||
VALUES (1, 1, true, 1, 1) ON CONFLICT (id) DO NOTHING`)
|
||||
})
|
||||
|
||||
safeExec(ctx, tx, "base_groups", func() { seedBaseGroups(ctx, tx) })
|
||||
safeExec(ctx, tx, "acl_rules", func() { seedACLRules(ctx, tx) })
|
||||
safeExec(ctx, tx, "system_params", func() { seedSystemParams(ctx, tx) })
|
||||
safeExec(ctx, tx, "languages", func() { seedLanguages(ctx, tx) })
|
||||
safeExec(ctx, tx, "translations", func() { seedTranslations(ctx, tx) })
|
||||
@@ -1676,3 +1678,136 @@ func generateUUID() string {
|
||||
b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
||||
}
|
||||
|
||||
// seedBaseGroups creates the base security groups and their XML IDs.
|
||||
// Mirrors: odoo/addons/base/security/base_groups.xml
|
||||
func seedBaseGroups(ctx context.Context, tx pgx.Tx) {
|
||||
log.Println("db: seeding base security groups...")
|
||||
|
||||
type groupDef struct {
|
||||
id int64
|
||||
name string
|
||||
xmlID string
|
||||
}
|
||||
groups := []groupDef{
|
||||
{1, "Internal User", "group_user"},
|
||||
{2, "Settings", "group_system"},
|
||||
{3, "Access Rights", "group_erp_manager"},
|
||||
{4, "Allow Export", "group_allow_export"},
|
||||
{5, "Portal", "group_portal"},
|
||||
{6, "Public", "group_public"},
|
||||
}
|
||||
|
||||
for _, g := range groups {
|
||||
tx.Exec(ctx, `INSERT INTO res_groups (id, name)
|
||||
VALUES ($1, $2) ON CONFLICT (id) DO NOTHING`, g.id, g.name)
|
||||
tx.Exec(ctx, `INSERT INTO ir_model_data (module, name, model, res_id)
|
||||
VALUES ('base', $1, 'res.groups', $2) ON CONFLICT DO NOTHING`, g.xmlID, g.id)
|
||||
}
|
||||
|
||||
// Add admin user (uid=1) to all groups
|
||||
for _, g := range groups {
|
||||
tx.Exec(ctx, `INSERT INTO res_groups_res_users_rel (res_groups_id, res_users_id)
|
||||
VALUES ($1, 1) ON CONFLICT DO NOTHING`, g.id)
|
||||
}
|
||||
}
|
||||
|
||||
// seedACLRules creates access control entries for ALL registered models.
|
||||
// Categorizes models into security tiers and assigns appropriate permissions.
|
||||
// Mirrors: odoo/addons/base/security/ir.model.access.csv + per-module CSVs
|
||||
func seedACLRules(ctx context.Context, tx pgx.Tx) {
|
||||
log.Println("db: seeding ACL rules for all models...")
|
||||
|
||||
// Resolve group IDs
|
||||
var groupSystem, groupUser int64
|
||||
err := tx.QueryRow(ctx,
|
||||
`SELECT g.id FROM res_groups g
|
||||
JOIN ir_model_data imd ON imd.res_id = g.id AND imd.model = 'res.groups'
|
||||
WHERE imd.module = 'base' AND imd.name = 'group_system'`).Scan(&groupSystem)
|
||||
if err != nil {
|
||||
log.Printf("db: cannot find group_system, skipping ACL seeding: %v", err)
|
||||
return
|
||||
}
|
||||
err = tx.QueryRow(ctx,
|
||||
`SELECT g.id FROM res_groups g
|
||||
JOIN ir_model_data imd ON imd.res_id = g.id AND imd.model = 'res.groups'
|
||||
WHERE imd.module = 'base' AND imd.name = 'group_user'`).Scan(&groupUser)
|
||||
if err != nil {
|
||||
log.Printf("db: cannot find group_user, skipping ACL seeding: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// ── Security Tiers ──────────────────────────────────────────────
|
||||
// Tier 1: System-only — only group_system gets full access
|
||||
systemOnly := map[string]bool{
|
||||
"ir.cron": true, "ir.rule": true, "ir.model.access": true,
|
||||
}
|
||||
|
||||
// Tier 2: Admin-only — group_user=read, group_system=full
|
||||
adminOnly := map[string]bool{
|
||||
"ir.model": true, "ir.model.fields": true, "ir.model.data": true,
|
||||
"ir.module.category": true, "ir.actions.server": true, "ir.sequence": true,
|
||||
"ir.logging": true, "ir.config_parameter": true, "ir.default": true,
|
||||
"ir.translation": true, "ir.actions.report": true, "report.paperformat": true,
|
||||
"res.config.settings": true,
|
||||
}
|
||||
|
||||
// Tier 3: Read-only for users — group_user=read, group_system=full
|
||||
readOnly := map[string]bool{
|
||||
"res.currency": true, "res.currency.rate": true,
|
||||
"res.country": true, "res.country.state": true, "res.country.group": true,
|
||||
"res.lang": true, "uom.category": true, "uom.uom": true,
|
||||
"product.category": true, "product.removal": true,
|
||||
"account.account.tag": true, "account.group": true,
|
||||
"account.tax.group": true, "account.tax.repartition.line": true,
|
||||
}
|
||||
|
||||
// Everything else → Tier 4: Standard user (group_user=full, group_system=full)
|
||||
|
||||
// Helper to insert an ACL rule
|
||||
insertACL := func(modelID int64, modelName string, groupID int64, suffix string, read, write, create, unlink bool) {
|
||||
aclName := "access_" + strings.ReplaceAll(modelName, ".", "_") + "_" + suffix
|
||||
tx.Exec(ctx, `
|
||||
INSERT INTO ir_model_access (name, model_id, group_id, perm_read, perm_write, perm_create, perm_unlink, active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, true)
|
||||
ON CONFLICT DO NOTHING`,
|
||||
aclName, modelID, groupID, read, write, create, unlink)
|
||||
}
|
||||
|
||||
// Iterate ALL registered models
|
||||
allModels := orm.Registry.Models()
|
||||
seeded := 0
|
||||
for _, m := range allModels {
|
||||
modelName := m.Name()
|
||||
if m.IsAbstract() {
|
||||
continue // Abstract models have no table → no ACL needed
|
||||
}
|
||||
|
||||
// Look up ir_model ID
|
||||
var modelID int64
|
||||
err := tx.QueryRow(ctx,
|
||||
"SELECT id FROM ir_model WHERE model = $1", modelName).Scan(&modelID)
|
||||
if err != nil {
|
||||
continue // Not yet in ir_model — will be seeded on next restart
|
||||
}
|
||||
|
||||
if systemOnly[modelName] {
|
||||
// Tier 1: only group_system full access
|
||||
insertACL(modelID, modelName, groupSystem, "system", true, true, true, true)
|
||||
} else if adminOnly[modelName] {
|
||||
// Tier 2: group_user=read, group_system=full
|
||||
insertACL(modelID, modelName, groupUser, "user_read", true, false, false, false)
|
||||
insertACL(modelID, modelName, groupSystem, "system", true, true, true, true)
|
||||
} else if readOnly[modelName] {
|
||||
// Tier 3: group_user=read, group_system=full
|
||||
insertACL(modelID, modelName, groupUser, "user_read", true, false, false, false)
|
||||
insertACL(modelID, modelName, groupSystem, "system", true, true, true, true)
|
||||
} else {
|
||||
// Tier 4: group_user=full, group_system=full
|
||||
insertACL(modelID, modelName, groupUser, "user", true, true, true, true)
|
||||
insertACL(modelID, modelName, groupSystem, "system", true, true, true, true)
|
||||
}
|
||||
seeded++
|
||||
}
|
||||
log.Printf("db: seeded ACL rules for %d models", seeded)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user