Expand Sale/Purchase/Project + 102 ORM tests — +4633 LOC
Sale (1177→2321 LOC): - Quotation templates (apply to order, option lines) - Sales reports (by month, product, customer, salesperson, category) - Advance payment wizard (delivered/percentage/fixed modes) - SO cancel wizard, discount wizard - action_quotation_sent, action_lock/unlock, preview_quotation - Line computes: invoice_status, price_reduce, untaxed_amount - Partner extension: sale_order_total Purchase (478→1424 LOC): - Purchase reports (by month, category, bill status, receipt analysis) - Receipt creation from PO (action_create_picking) - 3-way matching: action_view_picking, action_view_invoice - button_approve, button_done, action_rfq_send - Line computes: price_subtotal/total with tax, product onchange - Partner extension: purchase_order_count/total Project (218→1161 LOC): - Project updates (status tracking: on_track/at_risk/off_track) - Milestones (deadline, reached tracking, task count) - Timesheet integration (account.analytic.line extension) - Timesheet reports (by project, employee, task, week) - Task recurrence model - Task: planned/effective/remaining hours, progress, subtask hours - Project: allocated/remaining hours, profitability actions ORM Tests (102 tests, 0→1257 LOC): - domain_test.go: 32 tests (compile, operators, AND/OR/NOT, null) - field_test.go: 15 tests (IsCopyable, SQLType, IsRelational, IsStored) - model_test.go: 21 tests (NewModel, AddFields, RegisterMethod, ExtendModel) - domain_parse_test.go: 21 tests (parse Python domain strings) - sanitize_test.go: 13 tests (false→nil, type conversions) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,4 +6,12 @@ func Init() {
|
||||
initProjectMilestone()
|
||||
initProjectProject()
|
||||
initProjectTask()
|
||||
initProjectUpdate()
|
||||
initProjectTimesheetExtension()
|
||||
initTimesheetReport()
|
||||
initProjectProjectExtension()
|
||||
initProjectTaskExtension()
|
||||
initProjectMilestoneExtension()
|
||||
initProjectTaskRecurrence()
|
||||
initProjectSharingWizard()
|
||||
}
|
||||
|
||||
651
addons/project/models/project_extend.go
Normal file
651
addons/project/models/project_extend.go
Normal file
@@ -0,0 +1,651 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"odoo-go/pkg/orm"
|
||||
)
|
||||
|
||||
// initProjectProjectExtension extends project.project with additional fields and methods.
|
||||
// Mirrors: odoo/addons/project/models/project_project.py (additional fields)
|
||||
// odoo/addons/hr_timesheet/models/project_project.py (timesheet fields)
|
||||
func initProjectProjectExtension() {
|
||||
proj := orm.ExtendModel("project.project")
|
||||
|
||||
proj.AddFields(
|
||||
orm.One2many("update_ids", "project.update", "project_id", orm.FieldOpts{
|
||||
String: "Updates",
|
||||
}),
|
||||
orm.One2many("milestone_ids", "project.milestone", "project_id", orm.FieldOpts{
|
||||
String: "Milestones",
|
||||
}),
|
||||
orm.Selection("last_update_status", []orm.SelectionItem{
|
||||
{Value: "on_track", Label: "On Track"},
|
||||
{Value: "at_risk", Label: "At Risk"},
|
||||
{Value: "off_track", Label: "Off Track"},
|
||||
{Value: "on_hold", Label: "On Hold"},
|
||||
}, orm.FieldOpts{String: "Status", Compute: "_compute_last_update_status"}),
|
||||
orm.Float("total_timesheet_hours", orm.FieldOpts{
|
||||
String: "Total Timesheet Hours", Compute: "_compute_total_timesheet_hours",
|
||||
}),
|
||||
orm.Boolean("allow_timesheets", orm.FieldOpts{String: "Timesheets", Default: true}),
|
||||
orm.Integer("update_count", orm.FieldOpts{
|
||||
String: "Update Count", Compute: "_compute_update_count",
|
||||
}),
|
||||
orm.Integer("milestone_count", orm.FieldOpts{
|
||||
String: "Milestone Count", Compute: "_compute_milestone_count",
|
||||
}),
|
||||
orm.Integer("milestone_count_reached", orm.FieldOpts{
|
||||
String: "Milestones Reached", Compute: "_compute_milestone_count_reached",
|
||||
}),
|
||||
orm.Float("allocated_hours", orm.FieldOpts{String: "Allocated Hours"}),
|
||||
orm.Float("remaining_hours", orm.FieldOpts{
|
||||
String: "Remaining Hours", Compute: "_compute_remaining_hours",
|
||||
}),
|
||||
orm.Float("progress", orm.FieldOpts{
|
||||
String: "Progress (%)", Compute: "_compute_progress",
|
||||
}),
|
||||
orm.Selection("privacy_visibility", []orm.SelectionItem{
|
||||
{Value: "followers", Label: "Invited internal users"},
|
||||
{Value: "employees", Label: "All internal users"},
|
||||
{Value: "portal", Label: "Invited portal users and all internal users"},
|
||||
}, orm.FieldOpts{String: "Visibility", Default: "portal"}),
|
||||
orm.Many2one("analytic_account_id", "account.analytic.account", orm.FieldOpts{
|
||||
String: "Analytic Account",
|
||||
}),
|
||||
orm.Many2many("tag_ids", "project.tags", orm.FieldOpts{String: "Tags"}),
|
||||
orm.One2many("task_ids", "project.task", "project_id", orm.FieldOpts{String: "Tasks"}),
|
||||
)
|
||||
|
||||
// -- _compute_task_count --
|
||||
// Mirrors: odoo/addons/project/models/project_project.py Project._compute_task_count()
|
||||
proj.RegisterCompute("task_count", func(rs *orm.Recordset) (orm.Values, error) {
|
||||
env := rs.Env()
|
||||
projID := rs.IDs()[0]
|
||||
var count int
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COUNT(*) FROM project_task WHERE project_id = $1 AND active = true`, projID).Scan(&count)
|
||||
return orm.Values{"task_count": count}, nil
|
||||
})
|
||||
|
||||
// -- _compute_last_update_status --
|
||||
// Mirrors: odoo/addons/project/models/project_project.py Project._compute_last_update_status()
|
||||
proj.RegisterCompute("last_update_status", func(rs *orm.Recordset) (orm.Values, error) {
|
||||
env := rs.Env()
|
||||
projID := rs.IDs()[0]
|
||||
var status *string
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT status FROM project_update
|
||||
WHERE project_id = $1
|
||||
ORDER BY date DESC, id DESC LIMIT 1`, projID).Scan(&status)
|
||||
if status != nil {
|
||||
return orm.Values{"last_update_status": *status}, nil
|
||||
}
|
||||
return orm.Values{"last_update_status": "on_track"}, nil
|
||||
})
|
||||
|
||||
// -- _compute_total_timesheet_hours --
|
||||
// Mirrors: odoo/addons/hr_timesheet/models/project_project.py Project._compute_total_timesheet_time()
|
||||
proj.RegisterCompute("total_timesheet_hours", func(rs *orm.Recordset) (orm.Values, error) {
|
||||
env := rs.Env()
|
||||
projID := rs.IDs()[0]
|
||||
var hours float64
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(SUM(unit_amount), 0) FROM account_analytic_line
|
||||
WHERE project_id = $1`, projID).Scan(&hours)
|
||||
return orm.Values{"total_timesheet_hours": hours}, nil
|
||||
})
|
||||
|
||||
// -- _compute_update_count --
|
||||
proj.RegisterCompute("update_count", func(rs *orm.Recordset) (orm.Values, error) {
|
||||
env := rs.Env()
|
||||
projID := rs.IDs()[0]
|
||||
var count int
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COUNT(*) FROM project_update WHERE project_id = $1`, projID).Scan(&count)
|
||||
return orm.Values{"update_count": count}, nil
|
||||
})
|
||||
|
||||
// -- _compute_milestone_count --
|
||||
proj.RegisterCompute("milestone_count", func(rs *orm.Recordset) (orm.Values, error) {
|
||||
env := rs.Env()
|
||||
projID := rs.IDs()[0]
|
||||
var count int
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COUNT(*) FROM project_milestone WHERE project_id = $1`, projID).Scan(&count)
|
||||
return orm.Values{"milestone_count": count}, nil
|
||||
})
|
||||
|
||||
// -- _compute_milestone_count_reached --
|
||||
proj.RegisterCompute("milestone_count_reached", func(rs *orm.Recordset) (orm.Values, error) {
|
||||
env := rs.Env()
|
||||
projID := rs.IDs()[0]
|
||||
var count int
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COUNT(*) FROM project_milestone WHERE project_id = $1 AND is_reached = true`, projID).Scan(&count)
|
||||
return orm.Values{"milestone_count_reached": count}, nil
|
||||
})
|
||||
|
||||
// -- _compute_remaining_hours --
|
||||
proj.RegisterCompute("remaining_hours", func(rs *orm.Recordset) (orm.Values, error) {
|
||||
env := rs.Env()
|
||||
projID := rs.IDs()[0]
|
||||
var allocated, timesheet float64
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(allocated_hours, 0) FROM project_project WHERE id = $1`, projID).Scan(&allocated)
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(SUM(unit_amount), 0) FROM account_analytic_line
|
||||
WHERE project_id = $1`, projID).Scan(×heet)
|
||||
remaining := allocated - timesheet
|
||||
if remaining < 0 {
|
||||
remaining = 0
|
||||
}
|
||||
return orm.Values{"remaining_hours": remaining}, nil
|
||||
})
|
||||
|
||||
// -- _compute_progress --
|
||||
proj.RegisterCompute("progress", func(rs *orm.Recordset) (orm.Values, error) {
|
||||
env := rs.Env()
|
||||
projID := rs.IDs()[0]
|
||||
var allocated, timesheet float64
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(allocated_hours, 0) FROM project_project WHERE id = $1`, projID).Scan(&allocated)
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(SUM(unit_amount), 0) FROM account_analytic_line
|
||||
WHERE project_id = $1`, projID).Scan(×heet)
|
||||
pct := float64(0)
|
||||
if allocated > 0 {
|
||||
pct = timesheet / allocated * 100
|
||||
if pct > 100 {
|
||||
pct = 100
|
||||
}
|
||||
}
|
||||
return orm.Values{"progress": pct}, nil
|
||||
})
|
||||
|
||||
// action_view_tasks: Open tasks of this project.
|
||||
// Mirrors: odoo/addons/project/models/project_project.py Project.action_view_tasks()
|
||||
proj.RegisterMethod("action_view_tasks", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
projID := rs.IDs()[0]
|
||||
return map[string]interface{}{
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "project.task",
|
||||
"view_mode": "kanban,list,form",
|
||||
"views": [][]interface{}{{nil, "kanban"}, {nil, "list"}, {nil, "form"}},
|
||||
"domain": []interface{}{[]interface{}{"project_id", "=", projID}},
|
||||
"target": "current",
|
||||
"name": "Tasks",
|
||||
"context": map[string]interface{}{"default_project_id": projID},
|
||||
}, nil
|
||||
})
|
||||
|
||||
// action_view_milestones: Open milestones of this project.
|
||||
// Mirrors: odoo/addons/project/models/project_project.py Project.action_view_milestones()
|
||||
proj.RegisterMethod("action_view_milestones", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
projID := rs.IDs()[0]
|
||||
return map[string]interface{}{
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "project.milestone",
|
||||
"view_mode": "list,form",
|
||||
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
|
||||
"domain": []interface{}{[]interface{}{"project_id", "=", projID}},
|
||||
"target": "current",
|
||||
"name": "Milestones",
|
||||
"context": map[string]interface{}{"default_project_id": projID},
|
||||
}, nil
|
||||
})
|
||||
|
||||
// action_view_updates: Open project updates.
|
||||
proj.RegisterMethod("action_view_updates", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
projID := rs.IDs()[0]
|
||||
return map[string]interface{}{
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "project.update",
|
||||
"view_mode": "list,form",
|
||||
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
|
||||
"domain": []interface{}{[]interface{}{"project_id", "=", projID}},
|
||||
"target": "current",
|
||||
"name": "Updates",
|
||||
"context": map[string]interface{}{"default_project_id": projID},
|
||||
}, nil
|
||||
})
|
||||
|
||||
// action_view_timesheets: Open timesheet entries for this project.
|
||||
// Mirrors: odoo/addons/hr_timesheet/models/project_project.py
|
||||
proj.RegisterMethod("action_view_timesheets", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
projID := rs.IDs()[0]
|
||||
return map[string]interface{}{
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "account.analytic.line",
|
||||
"view_mode": "list,form",
|
||||
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
|
||||
"domain": []interface{}{[]interface{}{"project_id", "=", projID}},
|
||||
"target": "current",
|
||||
"name": "Timesheets",
|
||||
"context": map[string]interface{}{"default_project_id": projID},
|
||||
}, nil
|
||||
})
|
||||
|
||||
// action_get_project_profitability: Get profitability data.
|
||||
// Mirrors: odoo/addons/project/models/project_profitability.py
|
||||
proj.RegisterMethod("action_get_project_profitability", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
env := rs.Env()
|
||||
projID := rs.IDs()[0]
|
||||
|
||||
// Compute costs from timesheets (hours * employee cost)
|
||||
var timesheetCost float64
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(SUM(aal.unit_amount * COALESCE(he.timesheet_cost, 0)), 0)
|
||||
FROM account_analytic_line aal
|
||||
LEFT JOIN hr_employee he ON he.id = aal.employee_id
|
||||
WHERE aal.project_id = $1`, projID).Scan(×heetCost)
|
||||
|
||||
// Compute revenue from linked SOs (if project has partner)
|
||||
var revenue float64
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(SUM(so.amount_total::float8), 0) FROM sale_order so
|
||||
WHERE so.state IN ('sale', 'done')
|
||||
AND so.partner_id = (SELECT partner_id FROM project_project WHERE id = $1)
|
||||
AND so.partner_id IS NOT NULL`, projID).Scan(&revenue)
|
||||
|
||||
return map[string]interface{}{
|
||||
"timesheet_cost": timesheetCost,
|
||||
"revenue": revenue,
|
||||
"margin": revenue - timesheetCost,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
// initProjectTaskExtension extends project.task with additional fields and methods.
|
||||
// Mirrors: odoo/addons/project/models/project_task.py (additional fields)
|
||||
// odoo/addons/hr_timesheet/models/project_task.py (timesheet fields)
|
||||
func initProjectTaskExtension() {
|
||||
task := orm.ExtendModel("project.task")
|
||||
|
||||
// Note: parent_id, child_ids, milestone_id, tag_ids, depend_ids already exist
|
||||
task.AddFields(
|
||||
orm.Float("planned_hours", orm.FieldOpts{String: "Initially Planned Hours"}),
|
||||
orm.Float("effective_hours", orm.FieldOpts{
|
||||
String: "Hours Spent", Compute: "_compute_effective_hours",
|
||||
}),
|
||||
orm.Float("remaining_hours", orm.FieldOpts{
|
||||
String: "Remaining Hours", Compute: "_compute_remaining_hours",
|
||||
}),
|
||||
orm.Float("progress", orm.FieldOpts{
|
||||
String: "Progress (%)", Compute: "_compute_progress",
|
||||
}),
|
||||
orm.Float("subtask_effective_hours", orm.FieldOpts{
|
||||
String: "Sub-task Hours", Compute: "_compute_subtask_effective_hours",
|
||||
}),
|
||||
orm.Float("total_hours_spent", orm.FieldOpts{
|
||||
String: "Total Hours", Compute: "_compute_total_hours_spent",
|
||||
}),
|
||||
orm.Date("date_last_stage_update", orm.FieldOpts{String: "Last Stage Update"}),
|
||||
orm.One2many("timesheet_ids", "account.analytic.line", "task_id", orm.FieldOpts{
|
||||
String: "Timesheets",
|
||||
}),
|
||||
orm.Integer("timesheet_count", orm.FieldOpts{
|
||||
String: "Timesheet Count", Compute: "_compute_timesheet_count",
|
||||
}),
|
||||
orm.Char("email_from", orm.FieldOpts{String: "Email From"}),
|
||||
orm.Boolean("allow_timesheets", orm.FieldOpts{String: "Allow Timesheets"}),
|
||||
orm.Integer("working_days_close", orm.FieldOpts{
|
||||
String: "Working Days to Close", Compute: "_compute_working_days_close",
|
||||
}),
|
||||
orm.Integer("working_days_open", orm.FieldOpts{
|
||||
String: "Working Days to Assign", Compute: "_compute_working_days_open",
|
||||
}),
|
||||
orm.Float("overtime", orm.FieldOpts{
|
||||
String: "Overtime", Compute: "_compute_overtime",
|
||||
}),
|
||||
orm.Many2one("sale_order_id", "sale.order", orm.FieldOpts{String: "Sales Order"}),
|
||||
orm.Many2one("sale_line_id", "sale.order.line", orm.FieldOpts{String: "Sales Order Item"}),
|
||||
orm.Many2one("analytic_account_id", "account.analytic.account", orm.FieldOpts{
|
||||
String: "Analytic Account",
|
||||
}),
|
||||
)
|
||||
|
||||
// -- _compute_effective_hours: Sum of timesheet hours on this task. --
|
||||
// Mirrors: odoo/addons/hr_timesheet/models/project_task.py Task._compute_effective_hours()
|
||||
task.RegisterCompute("effective_hours", func(rs *orm.Recordset) (orm.Values, error) {
|
||||
env := rs.Env()
|
||||
taskID := rs.IDs()[0]
|
||||
var hours float64
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(SUM(unit_amount), 0) FROM account_analytic_line
|
||||
WHERE task_id = $1`, taskID).Scan(&hours)
|
||||
return orm.Values{"effective_hours": hours}, nil
|
||||
})
|
||||
|
||||
// -- _compute_remaining_hours --
|
||||
// Mirrors: odoo/addons/hr_timesheet/models/project_task.py Task._compute_remaining_hours()
|
||||
task.RegisterCompute("remaining_hours", func(rs *orm.Recordset) (orm.Values, error) {
|
||||
env := rs.Env()
|
||||
taskID := rs.IDs()[0]
|
||||
var planned, effective float64
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(planned_hours, 0) FROM project_task WHERE id = $1`, taskID).Scan(&planned)
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(SUM(unit_amount), 0) FROM account_analytic_line
|
||||
WHERE task_id = $1`, taskID).Scan(&effective)
|
||||
remaining := planned - effective
|
||||
return orm.Values{"remaining_hours": remaining}, nil
|
||||
})
|
||||
|
||||
// -- _compute_progress --
|
||||
// Mirrors: odoo/addons/hr_timesheet/models/project_task.py Task._compute_progress()
|
||||
task.RegisterCompute("progress", func(rs *orm.Recordset) (orm.Values, error) {
|
||||
env := rs.Env()
|
||||
taskID := rs.IDs()[0]
|
||||
var planned float64
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(planned_hours, 0) FROM project_task WHERE id = $1`, taskID).Scan(&planned)
|
||||
if planned <= 0 {
|
||||
return orm.Values{"progress": float64(0)}, nil
|
||||
}
|
||||
var effective float64
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(SUM(unit_amount), 0) FROM account_analytic_line
|
||||
WHERE task_id = $1`, taskID).Scan(&effective)
|
||||
pct := effective / planned * 100
|
||||
if pct > 100 {
|
||||
pct = 100
|
||||
}
|
||||
return orm.Values{"progress": pct}, nil
|
||||
})
|
||||
|
||||
// -- _compute_subtask_effective_hours --
|
||||
task.RegisterCompute("subtask_effective_hours", func(rs *orm.Recordset) (orm.Values, error) {
|
||||
env := rs.Env()
|
||||
taskID := rs.IDs()[0]
|
||||
var hours float64
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(SUM(aal.unit_amount), 0)
|
||||
FROM account_analytic_line aal
|
||||
JOIN project_task pt ON pt.id = aal.task_id
|
||||
WHERE pt.parent_id = $1`, taskID).Scan(&hours)
|
||||
return orm.Values{"subtask_effective_hours": hours}, nil
|
||||
})
|
||||
|
||||
// -- _compute_total_hours_spent: own hours + subtask hours --
|
||||
task.RegisterCompute("total_hours_spent", func(rs *orm.Recordset) (orm.Values, error) {
|
||||
env := rs.Env()
|
||||
taskID := rs.IDs()[0]
|
||||
var ownHours, subtaskHours float64
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(SUM(unit_amount), 0) FROM account_analytic_line
|
||||
WHERE task_id = $1`, taskID).Scan(&ownHours)
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(SUM(aal.unit_amount), 0)
|
||||
FROM account_analytic_line aal
|
||||
JOIN project_task pt ON pt.id = aal.task_id
|
||||
WHERE pt.parent_id = $1`, taskID).Scan(&subtaskHours)
|
||||
return orm.Values{"total_hours_spent": ownHours + subtaskHours}, nil
|
||||
})
|
||||
|
||||
// -- _compute_timesheet_count --
|
||||
task.RegisterCompute("timesheet_count", func(rs *orm.Recordset) (orm.Values, error) {
|
||||
env := rs.Env()
|
||||
taskID := rs.IDs()[0]
|
||||
var count int
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COUNT(*) FROM account_analytic_line WHERE task_id = $1`, taskID).Scan(&count)
|
||||
return orm.Values{"timesheet_count": count}, nil
|
||||
})
|
||||
|
||||
// -- _compute_working_days_close --
|
||||
// Mirrors: odoo/addons/project/models/project_task.py Task._compute_elapsed()
|
||||
task.RegisterCompute("working_days_close", func(rs *orm.Recordset) (orm.Values, error) {
|
||||
env := rs.Env()
|
||||
taskID := rs.IDs()[0]
|
||||
var state string
|
||||
var createDate *time.Time
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(state, 'open'), create_date FROM project_task WHERE id = $1`, taskID).Scan(&state, &createDate)
|
||||
if state != "done" || createDate == nil {
|
||||
return orm.Values{"working_days_close": 0}, nil
|
||||
}
|
||||
days := int(time.Since(*createDate).Hours() / 24)
|
||||
return orm.Values{"working_days_close": days}, nil
|
||||
})
|
||||
|
||||
// -- _compute_working_days_open --
|
||||
task.RegisterCompute("working_days_open", func(rs *orm.Recordset) (orm.Values, error) {
|
||||
env := rs.Env()
|
||||
taskID := rs.IDs()[0]
|
||||
var dateAssign *time.Time
|
||||
var createDate *time.Time
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT date_assign, create_date FROM project_task WHERE id = $1`, taskID).Scan(&dateAssign, &createDate)
|
||||
if dateAssign == nil || createDate == nil {
|
||||
return orm.Values{"working_days_open": 0}, nil
|
||||
}
|
||||
days := int(dateAssign.Sub(*createDate).Hours() / 24)
|
||||
if days < 0 {
|
||||
days = 0
|
||||
}
|
||||
return orm.Values{"working_days_open": days}, nil
|
||||
})
|
||||
|
||||
// -- _compute_overtime --
|
||||
task.RegisterCompute("overtime", func(rs *orm.Recordset) (orm.Values, error) {
|
||||
env := rs.Env()
|
||||
taskID := rs.IDs()[0]
|
||||
var planned, effective float64
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(planned_hours, 0) FROM project_task WHERE id = $1`, taskID).Scan(&planned)
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(SUM(unit_amount), 0) FROM account_analytic_line
|
||||
WHERE task_id = $1`, taskID).Scan(&effective)
|
||||
overtime := effective - planned
|
||||
if overtime < 0 {
|
||||
overtime = 0
|
||||
}
|
||||
return orm.Values{"overtime": overtime}, nil
|
||||
})
|
||||
|
||||
// action_view_timesheets: Open timesheets for this task.
|
||||
task.RegisterMethod("action_view_timesheets", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
taskID := rs.IDs()[0]
|
||||
return map[string]interface{}{
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "account.analytic.line",
|
||||
"view_mode": "list,form",
|
||||
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
|
||||
"domain": []interface{}{[]interface{}{"task_id", "=", taskID}},
|
||||
"target": "current",
|
||||
"name": "Timesheets",
|
||||
"context": map[string]interface{}{"default_task_id": taskID},
|
||||
}, nil
|
||||
})
|
||||
|
||||
// action_open_parent_task: Open parent task form.
|
||||
task.RegisterMethod("action_open_parent_task", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
env := rs.Env()
|
||||
taskID := rs.IDs()[0]
|
||||
var parentID *int64
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT parent_id FROM project_task WHERE id = $1`, taskID).Scan(&parentID)
|
||||
if parentID == nil {
|
||||
return nil, fmt.Errorf("task has no parent")
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "project.task",
|
||||
"res_id": *parentID,
|
||||
"view_mode": "form",
|
||||
"target": "current",
|
||||
}, nil
|
||||
})
|
||||
|
||||
// action_subtask_view: Open subtasks list.
|
||||
task.RegisterMethod("action_subtask_view", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
taskID := rs.IDs()[0]
|
||||
return map[string]interface{}{
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "project.task",
|
||||
"view_mode": "list,form",
|
||||
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
|
||||
"domain": []interface{}{[]interface{}{"parent_id", "=", taskID}},
|
||||
"target": "current",
|
||||
"name": "Sub-tasks",
|
||||
"context": map[string]interface{}{"default_parent_id": taskID},
|
||||
}, nil
|
||||
})
|
||||
|
||||
// action_assign_to_me: Assign the task to the current user.
|
||||
task.RegisterMethod("action_assign_to_me", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
env := rs.Env()
|
||||
uid := env.UID()
|
||||
for _, taskID := range rs.IDs() {
|
||||
env.Tx().Exec(env.Ctx(),
|
||||
`INSERT INTO project_task_res_users_rel (project_task_id, res_users_id)
|
||||
VALUES ($1, $2) ON CONFLICT DO NOTHING`, taskID, uid)
|
||||
env.Tx().Exec(env.Ctx(),
|
||||
`UPDATE project_task SET date_assign = NOW()
|
||||
WHERE id = $1 AND date_assign IS NULL`, taskID)
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
|
||||
// action_set_stage: Change the task stage.
|
||||
// Mirrors: odoo/addons/project/models/project_task.py Task.write() stage tracking
|
||||
task.RegisterMethod("action_set_stage", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
if len(args) < 1 {
|
||||
return nil, fmt.Errorf("stage_id required")
|
||||
}
|
||||
env := rs.Env()
|
||||
stageID, _ := args[0].(float64)
|
||||
for _, taskID := range rs.IDs() {
|
||||
env.Tx().Exec(env.Ctx(),
|
||||
`UPDATE project_task SET stage_id = $1, date_last_stage_update = NOW()
|
||||
WHERE id = $2`, int64(stageID), taskID)
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
}
|
||||
|
||||
// initProjectMilestoneExtension extends project.milestone with additional fields.
|
||||
// Mirrors: odoo/addons/project/models/project_milestone.py (additional fields)
|
||||
func initProjectMilestoneExtension() {
|
||||
ms := orm.ExtendModel("project.milestone")
|
||||
|
||||
ms.AddFields(
|
||||
orm.Date("reached_date", orm.FieldOpts{String: "Reached Date"}),
|
||||
orm.Integer("task_count", orm.FieldOpts{
|
||||
String: "Task Count", Compute: "_compute_task_count",
|
||||
}),
|
||||
orm.Float("progress", orm.FieldOpts{
|
||||
String: "Progress (%)", Compute: "_compute_progress",
|
||||
}),
|
||||
orm.Integer("color", orm.FieldOpts{String: "Color Index"}),
|
||||
orm.Many2one("sale_line_id", "sale.order.line", orm.FieldOpts{String: "Sales Order Item"}),
|
||||
)
|
||||
|
||||
// action_toggle_reached: Toggle the reached status.
|
||||
ms.RegisterMethod("action_toggle_reached", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
env := rs.Env()
|
||||
for _, msID := range rs.IDs() {
|
||||
var isReached bool
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(is_reached, false) FROM project_milestone WHERE id = $1`, msID).Scan(&isReached)
|
||||
if isReached {
|
||||
env.Tx().Exec(env.Ctx(),
|
||||
`UPDATE project_milestone SET is_reached = false, reached_date = NULL WHERE id = $1`, msID)
|
||||
} else {
|
||||
env.Tx().Exec(env.Ctx(),
|
||||
`UPDATE project_milestone SET is_reached = true, reached_date = CURRENT_DATE WHERE id = $1`, msID)
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
|
||||
// _compute_task_count
|
||||
ms.RegisterCompute("task_count", func(rs *orm.Recordset) (orm.Values, error) {
|
||||
env := rs.Env()
|
||||
msID := rs.IDs()[0]
|
||||
var count int
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COUNT(*) FROM project_task WHERE milestone_id = $1`, msID).Scan(&count)
|
||||
return orm.Values{"task_count": count}, nil
|
||||
})
|
||||
|
||||
// _compute_progress: % of tasks that are done out of total linked tasks.
|
||||
ms.RegisterCompute("progress", func(rs *orm.Recordset) (orm.Values, error) {
|
||||
env := rs.Env()
|
||||
msID := rs.IDs()[0]
|
||||
var total, done int
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COUNT(*) FROM project_task WHERE milestone_id = $1`, msID).Scan(&total)
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COUNT(*) FROM project_task WHERE milestone_id = $1 AND state = 'done'`, msID).Scan(&done)
|
||||
pct := float64(0)
|
||||
if total > 0 {
|
||||
pct = float64(done) / float64(total) * 100
|
||||
}
|
||||
return orm.Values{"progress": pct}, nil
|
||||
})
|
||||
}
|
||||
|
||||
// initProjectTaskRecurrence registers project.task.recurrence.
|
||||
// Mirrors: odoo/addons/project/models/project_task_recurrence.py
|
||||
func initProjectTaskRecurrence() {
|
||||
m := orm.NewModel("project.task.recurrence", orm.ModelOpts{
|
||||
Description: "Task Recurrence",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Many2many("task_ids", "project.task", orm.FieldOpts{String: "Tasks"}),
|
||||
orm.Selection("repeat_interval", []orm.SelectionItem{
|
||||
{Value: "daily", Label: "Days"},
|
||||
{Value: "weekly", Label: "Weeks"},
|
||||
{Value: "monthly", Label: "Months"},
|
||||
{Value: "yearly", Label: "Years"},
|
||||
}, orm.FieldOpts{String: "Repeat Every", Default: "weekly"}),
|
||||
orm.Integer("repeat_number", orm.FieldOpts{String: "Repeat", Default: 1}),
|
||||
orm.Selection("repeat_type", []orm.SelectionItem{
|
||||
{Value: "forever", Label: "Forever"},
|
||||
{Value: "until", Label: "Until"},
|
||||
{Value: "after", Label: "Number of Repetitions"},
|
||||
}, orm.FieldOpts{String: "Until", Default: "forever"}),
|
||||
orm.Date("repeat_until", orm.FieldOpts{String: "End Date"}),
|
||||
orm.Integer("repeat_on_month", orm.FieldOpts{String: "Day of Month", Default: 1}),
|
||||
orm.Boolean("mon", orm.FieldOpts{String: "Monday"}),
|
||||
orm.Boolean("tue", orm.FieldOpts{String: "Tuesday"}),
|
||||
orm.Boolean("wed", orm.FieldOpts{String: "Wednesday"}),
|
||||
orm.Boolean("thu", orm.FieldOpts{String: "Thursday"}),
|
||||
orm.Boolean("fri", orm.FieldOpts{String: "Friday"}),
|
||||
orm.Boolean("sat", orm.FieldOpts{String: "Saturday"}),
|
||||
orm.Boolean("sun", orm.FieldOpts{String: "Sunday"}),
|
||||
orm.Integer("recurrence_left", orm.FieldOpts{String: "Recurrences Left"}),
|
||||
orm.Date("next_recurrence_date", orm.FieldOpts{String: "Next Recurrence Date"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initProjectSharingWizard registers a wizard for sharing projects with external users.
|
||||
// Mirrors: odoo/addons/project/wizard/project_share_wizard.py
|
||||
func initProjectSharingWizard() {
|
||||
m := orm.NewModel("project.share.wizard", orm.ModelOpts{
|
||||
Description: "Project Sharing Wizard",
|
||||
Type: orm.ModelTransient,
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Many2one("project_id", "project.project", orm.FieldOpts{
|
||||
String: "Project", Required: true,
|
||||
}),
|
||||
orm.Many2many("partner_ids", "res.partner", orm.FieldOpts{String: "Recipients"}),
|
||||
orm.Selection("access_mode", []orm.SelectionItem{
|
||||
{Value: "read", Label: "Read"},
|
||||
{Value: "edit", Label: "Edit"},
|
||||
}, orm.FieldOpts{String: "Access Mode", Default: "read"}),
|
||||
orm.Text("note", orm.FieldOpts{String: "Note"}),
|
||||
)
|
||||
|
||||
// action_share: Share the project with selected partners.
|
||||
m.RegisterMethod("action_share", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
// Placeholder: in full Odoo this would create portal access rules
|
||||
return true, nil
|
||||
})
|
||||
}
|
||||
237
addons/project/models/project_timesheet.go
Normal file
237
addons/project/models/project_timesheet.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"odoo-go/pkg/orm"
|
||||
)
|
||||
|
||||
// initProjectTimesheetExtension extends account.analytic.line with project/timesheet fields.
|
||||
// In Odoo, timesheets are account.analytic.line records with project_id set.
|
||||
// Mirrors: odoo/addons/hr_timesheet/models/hr_timesheet.py
|
||||
//
|
||||
// class AccountAnalyticLine(models.Model):
|
||||
// _inherit = 'account.analytic.line'
|
||||
func initProjectTimesheetExtension() {
|
||||
al := orm.ExtendModel("account.analytic.line")
|
||||
|
||||
al.AddFields(
|
||||
orm.Many2one("project_id", "project.project", orm.FieldOpts{
|
||||
String: "Project", Index: true,
|
||||
}),
|
||||
orm.Many2one("task_id", "project.task", orm.FieldOpts{
|
||||
String: "Task", Index: true,
|
||||
}),
|
||||
orm.Many2one("employee_id", "hr.employee", orm.FieldOpts{
|
||||
String: "Employee", Index: true,
|
||||
}),
|
||||
orm.Float("unit_amount", orm.FieldOpts{String: "Duration (Hours)"}),
|
||||
orm.Many2one("user_id", "res.users", orm.FieldOpts{String: "User"}),
|
||||
orm.Many2one("department_id", "hr.department", orm.FieldOpts{String: "Department"}),
|
||||
orm.Selection("encoding_uom_id", []orm.SelectionItem{
|
||||
{Value: "hours", Label: "Hours"},
|
||||
{Value: "days", Label: "Days"},
|
||||
}, orm.FieldOpts{String: "Encoding UoM", Default: "hours"}),
|
||||
)
|
||||
|
||||
// DefaultGet: set date to today, employee from current user
|
||||
al.DefaultGet = func(env *orm.Environment, fields []string) orm.Values {
|
||||
vals := make(orm.Values)
|
||||
vals["date"] = time.Now().Format("2006-01-02")
|
||||
if env.UID() > 0 {
|
||||
vals["user_id"] = env.UID()
|
||||
// Try to find the employee linked to this user
|
||||
var empID int64
|
||||
err := env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT id FROM hr_employee WHERE user_id = $1 LIMIT 1`, env.UID()).Scan(&empID)
|
||||
if err == nil && empID > 0 {
|
||||
vals["employee_id"] = empID
|
||||
// Also set department
|
||||
var deptID int64
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT department_id FROM hr_employee WHERE id = $1`, empID).Scan(&deptID)
|
||||
if deptID > 0 {
|
||||
vals["department_id"] = deptID
|
||||
}
|
||||
}
|
||||
}
|
||||
return vals
|
||||
}
|
||||
}
|
||||
|
||||
// initTimesheetReport registers a transient model for timesheet reporting.
|
||||
// Mirrors: odoo/addons/hr_timesheet/report/hr_timesheet_report.py
|
||||
func initTimesheetReport() {
|
||||
m := orm.NewModel("hr.timesheet.report", orm.ModelOpts{
|
||||
Description: "Timesheet Analysis Report",
|
||||
Type: orm.ModelTransient,
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Date("date_from", orm.FieldOpts{String: "Start Date"}),
|
||||
orm.Date("date_to", orm.FieldOpts{String: "End Date"}),
|
||||
orm.Many2one("project_id", "project.project", orm.FieldOpts{String: "Project"}),
|
||||
orm.Many2one("employee_id", "hr.employee", orm.FieldOpts{String: "Employee"}),
|
||||
orm.Many2one("task_id", "project.task", orm.FieldOpts{String: "Task"}),
|
||||
)
|
||||
|
||||
// get_timesheet_data: Aggregated timesheet data for reporting.
|
||||
// Returns: { by_project, by_employee, by_task, summary }
|
||||
m.RegisterMethod("get_timesheet_data", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
env := rs.Env()
|
||||
|
||||
// ── Hours by project ──
|
||||
projRows, err := env.Tx().Query(env.Ctx(), `
|
||||
SELECT COALESCE(pp.name, 'No Project') AS project,
|
||||
SUM(aal.unit_amount) AS hours,
|
||||
COUNT(*) AS entries
|
||||
FROM account_analytic_line aal
|
||||
LEFT JOIN project_project pp ON pp.id = aal.project_id
|
||||
WHERE aal.project_id IS NOT NULL
|
||||
GROUP BY pp.name
|
||||
ORDER BY hours DESC
|
||||
LIMIT 20`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("timesheet_report: by project query: %w", err)
|
||||
}
|
||||
defer projRows.Close()
|
||||
|
||||
var byProject []map[string]interface{}
|
||||
for projRows.Next() {
|
||||
var name string
|
||||
var hours float64
|
||||
var entries int64
|
||||
if err := projRows.Scan(&name, &hours, &entries); err != nil {
|
||||
continue
|
||||
}
|
||||
byProject = append(byProject, map[string]interface{}{
|
||||
"project": name,
|
||||
"hours": hours,
|
||||
"entries": entries,
|
||||
})
|
||||
}
|
||||
|
||||
// ── Hours by employee ──
|
||||
empRows, err := env.Tx().Query(env.Ctx(), `
|
||||
SELECT COALESCE(he.name, 'Unknown') AS employee,
|
||||
SUM(aal.unit_amount) AS hours,
|
||||
COUNT(*) AS entries,
|
||||
COUNT(DISTINCT aal.project_id) AS projects
|
||||
FROM account_analytic_line aal
|
||||
LEFT JOIN hr_employee he ON he.id = aal.employee_id
|
||||
WHERE aal.project_id IS NOT NULL
|
||||
GROUP BY he.name
|
||||
ORDER BY hours DESC
|
||||
LIMIT 20`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("timesheet_report: by employee query: %w", err)
|
||||
}
|
||||
defer empRows.Close()
|
||||
|
||||
var byEmployee []map[string]interface{}
|
||||
for empRows.Next() {
|
||||
var name string
|
||||
var hours float64
|
||||
var entries, projects int64
|
||||
if err := empRows.Scan(&name, &hours, &entries, &projects); err != nil {
|
||||
continue
|
||||
}
|
||||
byEmployee = append(byEmployee, map[string]interface{}{
|
||||
"employee": name,
|
||||
"hours": hours,
|
||||
"entries": entries,
|
||||
"projects": projects,
|
||||
})
|
||||
}
|
||||
|
||||
// ── Hours by task ──
|
||||
taskRows, err := env.Tx().Query(env.Ctx(), `
|
||||
SELECT COALESCE(pt.name, 'No Task') AS task,
|
||||
COALESCE(pp.name, 'No Project') AS project,
|
||||
SUM(aal.unit_amount) AS hours,
|
||||
COUNT(*) AS entries
|
||||
FROM account_analytic_line aal
|
||||
LEFT JOIN project_task pt ON pt.id = aal.task_id
|
||||
LEFT JOIN project_project pp ON pp.id = aal.project_id
|
||||
WHERE aal.project_id IS NOT NULL AND aal.task_id IS NOT NULL
|
||||
GROUP BY pt.name, pp.name
|
||||
ORDER BY hours DESC
|
||||
LIMIT 20`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("timesheet_report: by task query: %w", err)
|
||||
}
|
||||
defer taskRows.Close()
|
||||
|
||||
var byTask []map[string]interface{}
|
||||
for taskRows.Next() {
|
||||
var taskName, projName string
|
||||
var hours float64
|
||||
var entries int64
|
||||
if err := taskRows.Scan(&taskName, &projName, &hours, &entries); err != nil {
|
||||
continue
|
||||
}
|
||||
byTask = append(byTask, map[string]interface{}{
|
||||
"task": taskName,
|
||||
"project": projName,
|
||||
"hours": hours,
|
||||
"entries": entries,
|
||||
})
|
||||
}
|
||||
|
||||
// ── Summary ──
|
||||
var totalHours float64
|
||||
var totalEntries int64
|
||||
env.Tx().QueryRow(env.Ctx(), `
|
||||
SELECT COALESCE(SUM(unit_amount), 0), COUNT(*)
|
||||
FROM account_analytic_line WHERE project_id IS NOT NULL
|
||||
`).Scan(&totalHours, &totalEntries)
|
||||
|
||||
return map[string]interface{}{
|
||||
"by_project": byProject,
|
||||
"by_employee": byEmployee,
|
||||
"by_task": byTask,
|
||||
"summary": map[string]interface{}{
|
||||
"total_hours": totalHours,
|
||||
"total_entries": totalEntries,
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
|
||||
// get_timesheet_by_week: Weekly breakdown of timesheet hours.
|
||||
m.RegisterMethod("get_timesheet_by_week", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
env := rs.Env()
|
||||
|
||||
rows, err := env.Tx().Query(env.Ctx(), `
|
||||
SELECT date_trunc('week', aal.date) AS week,
|
||||
SUM(aal.unit_amount) AS hours,
|
||||
COUNT(*) AS entries,
|
||||
COUNT(DISTINCT aal.employee_id) AS employees
|
||||
FROM account_analytic_line aal
|
||||
WHERE aal.project_id IS NOT NULL
|
||||
GROUP BY week
|
||||
ORDER BY week DESC
|
||||
LIMIT 12`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("timesheet_report: weekly query: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var results []map[string]interface{}
|
||||
for rows.Next() {
|
||||
var week time.Time
|
||||
var hours float64
|
||||
var entries, employees int64
|
||||
if err := rows.Scan(&week, &hours, &entries, &employees); err != nil {
|
||||
continue
|
||||
}
|
||||
results = append(results, map[string]interface{}{
|
||||
"week": week.Format("2006-01-02"),
|
||||
"hours": hours,
|
||||
"entries": entries,
|
||||
"employees": employees,
|
||||
})
|
||||
}
|
||||
return results, nil
|
||||
})
|
||||
}
|
||||
69
addons/project/models/project_update.go
Normal file
69
addons/project/models/project_update.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"odoo-go/pkg/orm"
|
||||
)
|
||||
|
||||
// initProjectUpdate registers the project.update model.
|
||||
// Mirrors: odoo/addons/project/models/project_update.py
|
||||
//
|
||||
// class ProjectUpdate(models.Model):
|
||||
// _name = 'project.update'
|
||||
// _description = 'Project Update'
|
||||
// _order = 'date desc'
|
||||
func initProjectUpdate() {
|
||||
m := orm.NewModel("project.update", orm.ModelOpts{
|
||||
Description: "Project Update",
|
||||
Order: "date desc, id desc",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Many2one("project_id", "project.project", orm.FieldOpts{
|
||||
String: "Project", Required: true, OnDelete: orm.OnDeleteCascade, Index: true,
|
||||
}),
|
||||
orm.Char("name", orm.FieldOpts{String: "Title", Required: true}),
|
||||
orm.Selection("status", []orm.SelectionItem{
|
||||
{Value: "on_track", Label: "On Track"},
|
||||
{Value: "at_risk", Label: "At Risk"},
|
||||
{Value: "off_track", Label: "Off Track"},
|
||||
{Value: "on_hold", Label: "On Hold"},
|
||||
}, orm.FieldOpts{String: "Status", Required: true, Default: "on_track"}),
|
||||
orm.Date("date", orm.FieldOpts{String: "Date", Required: true}),
|
||||
orm.HTML("description", orm.FieldOpts{String: "Description"}),
|
||||
orm.Many2one("user_id", "res.users", orm.FieldOpts{String: "Author"}),
|
||||
orm.Float("progress", orm.FieldOpts{String: "Progress (%)"}),
|
||||
orm.Integer("color", orm.FieldOpts{String: "Color Index"}),
|
||||
)
|
||||
|
||||
// DefaultGet: set date to today, user to current user
|
||||
m.DefaultGet = func(env *orm.Environment, fields []string) orm.Values {
|
||||
vals := make(orm.Values)
|
||||
vals["date"] = time.Now().Format("2006-01-02")
|
||||
if env.UID() > 0 {
|
||||
vals["user_id"] = env.UID()
|
||||
}
|
||||
return vals
|
||||
}
|
||||
|
||||
// action_open_project: Return an action to open the project.
|
||||
m.RegisterMethod("action_open_project", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
env := rs.Env()
|
||||
updateID := rs.IDs()[0]
|
||||
var projectID int64
|
||||
err := env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT project_id FROM project_update WHERE id = $1`, updateID).Scan(&projectID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("project_update: read update %d: %w", updateID, err)
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "project.project",
|
||||
"res_id": projectID,
|
||||
"view_mode": "form",
|
||||
"target": "current",
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user