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:
@@ -251,7 +251,17 @@ func initAccountAsset() {
|
||||
periodMonths = 1
|
||||
}
|
||||
|
||||
// Use prorata_date or acquisition_date as start, fallback to now
|
||||
startDate := time.Now()
|
||||
var prorataDate, acquisitionDate *time.Time
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT prorata_date, acquisition_date FROM account_asset WHERE id = $1`, assetID,
|
||||
).Scan(&prorataDate, &acquisitionDate)
|
||||
if prorataDate != nil {
|
||||
startDate = *prorataDate
|
||||
} else if acquisitionDate != nil {
|
||||
startDate = *acquisitionDate
|
||||
}
|
||||
|
||||
switch method {
|
||||
case "linear":
|
||||
@@ -460,6 +470,156 @@ func initAccountAsset() {
|
||||
}, nil
|
||||
})
|
||||
|
||||
// action_create_deferred_entries: generate recognition entries for deferred
|
||||
// revenue (sale) or deferred expense assets.
|
||||
// Mirrors: odoo/addons/account_asset/models/account_asset.py _generate_deferred_entries()
|
||||
//
|
||||
// Unlike depreciation (which expenses an asset), deferred entries recognise
|
||||
// income or expense over time. Monthly amount = original_value / method_number.
|
||||
// Debit: deferred account (asset/liability), Credit: income/expense account.
|
||||
m.RegisterMethod("action_create_deferred_entries", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
env := rs.Env()
|
||||
assetID := rs.IDs()[0]
|
||||
|
||||
var name, assetType, state string
|
||||
var journalID, companyID, assetAccountID, expenseAccountID int64
|
||||
var currencyID *int64
|
||||
var originalValue float64
|
||||
var methodNumber int
|
||||
err := env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(name, ''), COALESCE(asset_type, 'purchase'),
|
||||
COALESCE(journal_id, 0), COALESCE(company_id, 0),
|
||||
COALESCE(account_asset_id, 0), COALESCE(account_depreciation_expense_id, 0),
|
||||
currency_id, COALESCE(state, 'draft'),
|
||||
COALESCE(original_value::float8, 0), COALESCE(method_number, 1)
|
||||
FROM account_asset WHERE id = $1`, assetID,
|
||||
).Scan(&name, &assetType, &journalID, &companyID,
|
||||
&assetAccountID, &expenseAccountID, ¤cyID, &state,
|
||||
&originalValue, &methodNumber)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("account: read asset %d: %w", assetID, err)
|
||||
}
|
||||
|
||||
if assetType != "sale" && assetType != "expense" {
|
||||
return nil, fmt.Errorf("account: deferred entries only apply to deferred revenue (sale) or deferred expense assets, got %q", assetType)
|
||||
}
|
||||
if state != "open" {
|
||||
return nil, fmt.Errorf("account: can only create deferred entries for running assets")
|
||||
}
|
||||
if journalID == 0 || assetAccountID == 0 || expenseAccountID == 0 {
|
||||
return nil, fmt.Errorf("account: asset %d is missing journal or account configuration", assetID)
|
||||
}
|
||||
if methodNumber <= 0 {
|
||||
methodNumber = 1
|
||||
}
|
||||
|
||||
monthlyAmount := math.Round(originalValue/float64(methodNumber)*100) / 100
|
||||
|
||||
// How many entries already exist?
|
||||
var existingCount int
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COUNT(*) FROM account_move WHERE asset_id = $1`, assetID,
|
||||
).Scan(&existingCount)
|
||||
if existingCount >= methodNumber {
|
||||
return nil, fmt.Errorf("account: all deferred entries already created (%d/%d)", existingCount, methodNumber)
|
||||
}
|
||||
|
||||
// Resolve currency
|
||||
var curID int64
|
||||
if currencyID != nil {
|
||||
curID = *currencyID
|
||||
} else {
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(currency_id, 0) FROM res_company WHERE id = $1`, companyID,
|
||||
).Scan(&curID)
|
||||
}
|
||||
|
||||
// Determine start date
|
||||
startDate := time.Now()
|
||||
var acqDate *time.Time
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT acquisition_date FROM account_asset WHERE id = $1`, assetID,
|
||||
).Scan(&acqDate)
|
||||
if acqDate != nil {
|
||||
startDate = *acqDate
|
||||
}
|
||||
|
||||
entryDate := startDate.AddDate(0, existingCount+1, 0).Format("2006-01-02")
|
||||
period := existingCount + 1
|
||||
|
||||
// Last entry absorbs rounding remainder
|
||||
amount := monthlyAmount
|
||||
if period == methodNumber {
|
||||
var alreadyRecognised float64
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(SUM(ABS(l.balance)::float8), 0)
|
||||
FROM account_move m
|
||||
JOIN account_move_line l ON l.move_id = m.id
|
||||
WHERE m.asset_id = $1
|
||||
AND l.account_id = $2`, assetID, expenseAccountID,
|
||||
).Scan(&alreadyRecognised)
|
||||
amount = math.Round((originalValue-alreadyRecognised)*100) / 100
|
||||
}
|
||||
|
||||
// Create the recognition journal entry
|
||||
moveRS := env.Model("account.move")
|
||||
move, err := moveRS.Create(orm.Values{
|
||||
"move_type": "entry",
|
||||
"ref": fmt.Sprintf("Deferred recognition: %s (%d/%d)", name, period, methodNumber),
|
||||
"date": entryDate,
|
||||
"journal_id": journalID,
|
||||
"company_id": companyID,
|
||||
"currency_id": curID,
|
||||
"asset_id": assetID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("account: create deferred entry: %w", err)
|
||||
}
|
||||
|
||||
lineRS := env.Model("account.move.line")
|
||||
|
||||
// Debit: deferred account (asset account — the balance sheet deferral)
|
||||
if _, err := lineRS.Create(orm.Values{
|
||||
"move_id": move.ID(),
|
||||
"account_id": assetAccountID,
|
||||
"name": fmt.Sprintf("Deferred recognition: %s", name),
|
||||
"debit": amount,
|
||||
"credit": 0.0,
|
||||
"balance": amount,
|
||||
"company_id": companyID,
|
||||
"journal_id": journalID,
|
||||
"currency_id": curID,
|
||||
"display_type": "product",
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("account: create deferred debit line: %w", err)
|
||||
}
|
||||
|
||||
// Credit: income/expense account
|
||||
if _, err := lineRS.Create(orm.Values{
|
||||
"move_id": move.ID(),
|
||||
"account_id": expenseAccountID,
|
||||
"name": fmt.Sprintf("Deferred recognition: %s", name),
|
||||
"debit": 0.0,
|
||||
"credit": amount,
|
||||
"balance": -amount,
|
||||
"company_id": companyID,
|
||||
"journal_id": journalID,
|
||||
"currency_id": curID,
|
||||
"display_type": "product",
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("account: create deferred credit line: %w", err)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "account.move",
|
||||
"res_id": move.ID(),
|
||||
"view_mode": "form",
|
||||
"views": [][]interface{}{{nil, "form"}},
|
||||
"target": "current",
|
||||
}, nil
|
||||
})
|
||||
|
||||
// -- DefaultGet --
|
||||
m.DefaultGet = func(env *orm.Environment, fields []string) orm.Values {
|
||||
vals := orm.Values{
|
||||
|
||||
Reference in New Issue
Block a user