Add timesheet / AI-audit time tracking (wraps account.analytic.line)
- fl.timesheet via delegation inheritance on account.analytic.line so billable hours flow through standard Odoo Accounting; duration_hours maps to unit_amount - Fields: case_id, employee_id, is_billable, ai_agent, duration_hours, computed hourly_rate/billable_amount (rate from hr.employee.fl_hourly_rate, else firm default ir.config_parameter fl_timesheet.default_hourly_rate) - _resolve_analytic_account: prefers the case project's analytic account (version-agnostic field lookup), falls back to a cached firm account under any available analytic plan — handles the required account_id on the wrapped line - Add 'analytic' to manifest depends; ACL for fl.timesheet and account.analytic.line (admin + paralegal) so non-admins can post entries - fl.case: timesheet_ids + total_billable_hours/amount + total_ai_audit_hours + currency_id; new Time & Billing tab; Timesheets menu + standalone views - Both AI agents now log non-billable audit entries via sudo() (paralegal + attorney, ai_agent set); logging stays a guarded no-op if creation fails Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@
|
|||||||
'crm',
|
'crm',
|
||||||
'account',
|
'account',
|
||||||
'hr_expense',
|
'hr_expense',
|
||||||
|
'analytic',
|
||||||
# 'sign', # Odoo Sign — enable when confirmed installed
|
# 'sign', # Odoo Sign — enable when confirmed installed
|
||||||
# 'queue_job', # OCA queue_job — install from https://github.com/OCA/queue
|
# 'queue_job', # OCA queue_job — install from https://github.com/OCA/queue
|
||||||
],
|
],
|
||||||
@@ -56,6 +57,7 @@
|
|||||||
'views/fl_wizard_views.xml',
|
'views/fl_wizard_views.xml',
|
||||||
'views/fl_discovery_suggest_views.xml',
|
'views/fl_discovery_suggest_views.xml',
|
||||||
'views/fl_conflict_check_views.xml',
|
'views/fl_conflict_check_views.xml',
|
||||||
|
'views/fl_timesheet_views.xml',
|
||||||
'views/menu_views.xml',
|
'views/menu_views.xml',
|
||||||
# Phase 4 — QWeb PDF Reports
|
# Phase 4 — QWeb PDF Reports
|
||||||
'report/report_financial_affidavit_short.xml',
|
'report/report_financial_affidavit_short.xml',
|
||||||
|
|||||||
@@ -18,3 +18,4 @@ from . import fl_case
|
|||||||
from . import fl_conflict_check
|
from . import fl_conflict_check
|
||||||
from . import fl_paralegal_agent
|
from . import fl_paralegal_agent
|
||||||
from . import fl_attorney_agent
|
from . import fl_attorney_agent
|
||||||
|
from . import fl_timesheet
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class FlAttorneyAgent(models.AbstractModel):
|
|||||||
statutes = self._candidate_statutes(case)
|
statutes = self._candidate_statutes(case)
|
||||||
caselaw = self._candidate_caselaw(case)
|
caselaw = self._candidate_caselaw(case)
|
||||||
|
|
||||||
|
ai_used = False
|
||||||
try:
|
try:
|
||||||
context = self._build_context(case, statutes, caselaw)
|
context = self._build_context(case, statutes, caselaw)
|
||||||
result = self.env['fl.ai.engine'].call_claude_json(
|
result = self.env['fl.ai.engine'].call_claude_json(
|
||||||
@@ -69,13 +70,31 @@ class FlAttorneyAgent(models.AbstractModel):
|
|||||||
max_tokens=3000,
|
max_tokens=3000,
|
||||||
)
|
)
|
||||||
self._store_memo(case, analysis, result, statutes, caselaw)
|
self._store_memo(case, analysis, result, statutes, caselaw)
|
||||||
|
ai_used = True
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
_logger.error("Attorney agent failed for case %s: %s",
|
_logger.error("Attorney agent failed for case %s: %s",
|
||||||
case.id, exc, exc_info=True)
|
case.id, exc, exc_info=True)
|
||||||
self._store_fallback(case, analysis, statutes, caselaw, str(exc))
|
self._store_fallback(case, analysis, statutes, caselaw, str(exc))
|
||||||
|
|
||||||
|
self._log_ai_time(case, 'Attorney strategy memo', ai_used)
|
||||||
return analysis
|
return analysis
|
||||||
|
|
||||||
|
def _log_ai_time(self, case, note, ai_used):
|
||||||
|
"""Log a non-billable AI audit entry. No-op until fl.timesheet exists."""
|
||||||
|
if 'fl.timesheet' not in self.env:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.env['fl.timesheet'].sudo().create({
|
||||||
|
'case_id': case.id,
|
||||||
|
'name': note,
|
||||||
|
'is_billable': False,
|
||||||
|
'ai_agent': 'attorney',
|
||||||
|
'duration_hours': 0.1 if ai_used else 0.02,
|
||||||
|
})
|
||||||
|
except Exception as exc: # never block on audit logging
|
||||||
|
_logger.warning("Attorney AI-time logging skipped for case %s: %s",
|
||||||
|
case.id, exc)
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
# Candidate library (grounds the model in real records)
|
# Candidate library (grounds the model in real records)
|
||||||
# ──────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -472,6 +472,28 @@ class FlCase(models.Model):
|
|||||||
compute='_compute_total_expenses', store=True
|
compute='_compute_total_expenses', store=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════
|
||||||
|
# TIME & BILLING
|
||||||
|
# ══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
currency_id = fields.Many2one(
|
||||||
|
'res.currency', string='Currency',
|
||||||
|
default=lambda self: self.env.company.currency_id
|
||||||
|
)
|
||||||
|
timesheet_ids = fields.One2many(
|
||||||
|
'fl.timesheet', 'case_id', string='Timesheet Entries'
|
||||||
|
)
|
||||||
|
total_billable_hours = fields.Float(
|
||||||
|
string='Billable Hours', compute='_compute_timesheet_totals'
|
||||||
|
)
|
||||||
|
total_billable_amount = fields.Monetary(
|
||||||
|
string='Billable Amount', compute='_compute_timesheet_totals',
|
||||||
|
currency_field='currency_id'
|
||||||
|
)
|
||||||
|
total_ai_audit_hours = fields.Float(
|
||||||
|
string='AI Audit Hours', compute='_compute_timesheet_totals'
|
||||||
|
)
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════
|
||||||
# POST-ORDER
|
# POST-ORDER
|
||||||
# ══════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════
|
||||||
@@ -804,6 +826,22 @@ class FlCase(models.Model):
|
|||||||
for rec in self:
|
for rec in self:
|
||||||
rec.total_expenses = sum(rec.expense_ids.mapped('total_amount'))
|
rec.total_expenses = sum(rec.expense_ids.mapped('total_amount'))
|
||||||
|
|
||||||
|
@api.depends(
|
||||||
|
'timesheet_ids.duration_hours',
|
||||||
|
'timesheet_ids.is_billable',
|
||||||
|
'timesheet_ids.billable_amount',
|
||||||
|
'timesheet_ids.ai_agent',
|
||||||
|
)
|
||||||
|
def _compute_timesheet_totals(self):
|
||||||
|
for rec in self:
|
||||||
|
billable = rec.timesheet_ids.filtered(lambda t: t.is_billable)
|
||||||
|
rec.total_billable_hours = sum(billable.mapped('duration_hours'))
|
||||||
|
rec.total_billable_amount = sum(billable.mapped('billable_amount'))
|
||||||
|
rec.total_ai_audit_hours = sum(
|
||||||
|
rec.timesheet_ids.filtered(lambda t: t.ai_agent)
|
||||||
|
.mapped('duration_hours')
|
||||||
|
)
|
||||||
|
|
||||||
@api.depends('filing_date')
|
@api.depends('filing_date')
|
||||||
def _compute_retroactivity_date(self):
|
def _compute_retroactivity_date(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ class FlParalegalAgent(models.AbstractModel):
|
|||||||
if 'fl.timesheet' not in self.env:
|
if 'fl.timesheet' not in self.env:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
self.env['fl.timesheet'].create({
|
self.env['fl.timesheet'].sudo().create({
|
||||||
'case_id': case.id,
|
'case_id': case.id,
|
||||||
'name': note,
|
'name': note,
|
||||||
'is_billable': False,
|
'is_billable': False,
|
||||||
|
|||||||
129
activeblue_familylaw/models/fl_timesheet.py
Normal file
129
activeblue_familylaw/models/fl_timesheet.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class HrEmployee(models.Model):
|
||||||
|
_inherit = 'hr.employee'
|
||||||
|
|
||||||
|
fl_hourly_rate = fields.Float(
|
||||||
|
string='Family Law Billing Rate ($/hr)',
|
||||||
|
help='Hourly billing rate used for family-law case timesheets. '
|
||||||
|
'Falls back to the firm default (ir.config_parameter '
|
||||||
|
'fl_timesheet.default_hourly_rate) when unset.'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FlTimesheet(models.Model):
|
||||||
|
"""
|
||||||
|
Family-law timesheet / AI audit entry.
|
||||||
|
|
||||||
|
Wraps account.analytic.line via delegation inheritance: each fl.timesheet
|
||||||
|
owns one analytic line, so billable hours flow through standard Odoo
|
||||||
|
Accounting. duration_hours maps to the analytic line's unit_amount.
|
||||||
|
AI agents create non-billable entries (is_billable=False, ai_agent set) for
|
||||||
|
an audit trail; people create billable entries from the case Timesheet tab.
|
||||||
|
"""
|
||||||
|
_name = 'fl.timesheet'
|
||||||
|
_description = 'Family Law Timesheet / AI Audit Entry'
|
||||||
|
_inherits = {'account.analytic.line': 'analytic_line_id'}
|
||||||
|
_order = 'date desc, id desc'
|
||||||
|
|
||||||
|
analytic_line_id = fields.Many2one(
|
||||||
|
'account.analytic.line', string='Analytic Line',
|
||||||
|
required=True, ondelete='cascade', auto_join=True, index=True
|
||||||
|
)
|
||||||
|
case_id = fields.Many2one(
|
||||||
|
'fl.case', string='Case', required=True, ondelete='cascade', index=True
|
||||||
|
)
|
||||||
|
employee_id = fields.Many2one('hr.employee', string='Employee')
|
||||||
|
is_billable = fields.Boolean(
|
||||||
|
string='Billable', default=True,
|
||||||
|
help='Uncheck for non-billable entries (e.g. AI audit time).'
|
||||||
|
)
|
||||||
|
ai_agent = fields.Selection([
|
||||||
|
('paralegal', 'Paralegal Agent'),
|
||||||
|
('attorney', 'Attorney Agent'),
|
||||||
|
], string='AI Agent',
|
||||||
|
help='Set when an AI agent auto-logged this entry for the audit trail.')
|
||||||
|
duration_hours = fields.Float(string='Duration (Hours)', default=0.0)
|
||||||
|
|
||||||
|
hourly_rate = fields.Float(
|
||||||
|
string='Rate ($/hr)', compute='_compute_billable_amount'
|
||||||
|
)
|
||||||
|
billable_amount = fields.Monetary(
|
||||||
|
string='Amount', compute='_compute_billable_amount',
|
||||||
|
currency_field='currency_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('duration_hours', 'is_billable', 'employee_id')
|
||||||
|
def _compute_billable_amount(self):
|
||||||
|
default_rate = float(self.env['ir.config_parameter'].sudo().get_param(
|
||||||
|
'fl_timesheet.default_hourly_rate', 0.0) or 0.0)
|
||||||
|
for rec in self:
|
||||||
|
rate = rec.employee_id.fl_hourly_rate or default_rate
|
||||||
|
rec.hourly_rate = rate
|
||||||
|
rec.billable_amount = (
|
||||||
|
rec.duration_hours * rate if rec.is_billable else 0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
# CRUD — keep the wrapped analytic line in sync
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
for vals in vals_list:
|
||||||
|
self._prepare_line_vals(vals)
|
||||||
|
return super().create(vals_list)
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
if 'duration_hours' in vals:
|
||||||
|
vals['unit_amount'] = vals['duration_hours']
|
||||||
|
return super().write(vals)
|
||||||
|
|
||||||
|
def _prepare_line_vals(self, vals):
|
||||||
|
"""Fill the analytic line's required fields before delegation create."""
|
||||||
|
case = self.env['fl.case'].browse(vals['case_id']) if vals.get('case_id') \
|
||||||
|
else self.env['fl.case']
|
||||||
|
if not vals.get('account_id'):
|
||||||
|
vals['account_id'] = self._resolve_analytic_account(case).id
|
||||||
|
if not vals.get('name'):
|
||||||
|
if vals.get('ai_agent'):
|
||||||
|
label = dict(self._fields['ai_agent'].selection).get(
|
||||||
|
vals['ai_agent'], vals['ai_agent'])
|
||||||
|
vals['name'] = f'AI audit: {label}'
|
||||||
|
else:
|
||||||
|
vals['name'] = case.name or 'Time entry'
|
||||||
|
if 'duration_hours' in vals:
|
||||||
|
vals['unit_amount'] = vals['duration_hours']
|
||||||
|
|
||||||
|
def _resolve_analytic_account(self, case):
|
||||||
|
"""
|
||||||
|
Resolve the analytic account for the wrapped line. Prefers the case
|
||||||
|
project's analytic account (field name varies by version), falling back
|
||||||
|
to a cached firm-wide account created under any available analytic plan.
|
||||||
|
"""
|
||||||
|
Account = self.env['account.analytic.account'].sudo()
|
||||||
|
|
||||||
|
project = case.project_id if case else self.env['project.project']
|
||||||
|
if project:
|
||||||
|
for fname in ('account_id', 'analytic_account_id'):
|
||||||
|
acc = getattr(project, fname, False)
|
||||||
|
if acc:
|
||||||
|
return acc
|
||||||
|
|
||||||
|
param = self.env['ir.config_parameter'].sudo()
|
||||||
|
acc_id = param.get_param('fl_timesheet.analytic_account_id')
|
||||||
|
if acc_id:
|
||||||
|
acc = Account.browse(int(acc_id))
|
||||||
|
if acc.exists():
|
||||||
|
return acc
|
||||||
|
|
||||||
|
Plan = self.env['account.analytic.plan'].sudo()
|
||||||
|
plan = Plan.search([], limit=1) or Plan.create({'name': 'Family Law'})
|
||||||
|
acc = Account.create({
|
||||||
|
'name': 'Family Law Cases',
|
||||||
|
'plan_id': plan.id,
|
||||||
|
'company_id': self.env.company.id,
|
||||||
|
})
|
||||||
|
param.set_param('fl_timesheet.analytic_account_id', acc.id)
|
||||||
|
return acc
|
||||||
@@ -81,6 +81,12 @@ access_fl_income_withholding_petitioner,fl.income.withholding petitioner,model_f
|
|||||||
# ── fl.conflict.check ────────────────────────────────────────────────────────
|
# ── fl.conflict.check ────────────────────────────────────────────────────────
|
||||||
access_fl_conflict_check_admin,fl.conflict.check admin,model_fl_conflict_check,group_admin,1,1,1,1
|
access_fl_conflict_check_admin,fl.conflict.check admin,model_fl_conflict_check,group_admin,1,1,1,1
|
||||||
access_fl_conflict_check_paralegal,fl.conflict.check paralegal,model_fl_conflict_check,group_paralegal,1,1,1,0
|
access_fl_conflict_check_paralegal,fl.conflict.check paralegal,model_fl_conflict_check,group_paralegal,1,1,1,0
|
||||||
|
# ── fl.timesheet ─────────────────────────────────────────────────────────────
|
||||||
|
access_fl_timesheet_admin,fl.timesheet admin,model_fl_timesheet,group_admin,1,1,1,1
|
||||||
|
access_fl_timesheet_paralegal,fl.timesheet paralegal,model_fl_timesheet,group_paralegal,1,1,1,0
|
||||||
|
# ── account.analytic.line (timesheet wraps it — ensure non-admins can post) ───
|
||||||
|
access_account_analytic_line_fl_admin,account.analytic.line fl admin,analytic.model_account_analytic_line,group_admin,1,1,1,1
|
||||||
|
access_account_analytic_line_fl_paralegal,account.analytic.line fl paralegal,analytic.model_account_analytic_line,group_paralegal,1,1,1,0
|
||||||
# ── fl.intake.wizard ─────────────────────────────────────────────────────────
|
# ── fl.intake.wizard ─────────────────────────────────────────────────────────
|
||||||
access_fl_intake_wizard_admin,fl.intake.wizard admin,model_fl_intake_wizard,group_admin,1,1,1,1
|
access_fl_intake_wizard_admin,fl.intake.wizard admin,model_fl_intake_wizard,group_admin,1,1,1,1
|
||||||
access_fl_intake_wizard_paralegal,fl.intake.wizard paralegal,model_fl_intake_wizard,group_paralegal,1,1,1,1
|
access_fl_intake_wizard_paralegal,fl.intake.wizard paralegal,model_fl_intake_wizard,group_paralegal,1,1,1,1
|
||||||
|
|||||||
|
@@ -310,6 +310,34 @@
|
|||||||
<field name="total_expenses" readonly="1"/>
|
<field name="total_expenses" readonly="1"/>
|
||||||
</page>
|
</page>
|
||||||
|
|
||||||
|
<!-- TAB 9: Time & Billing -->
|
||||||
|
<page string="Time & Billing" name="timesheets">
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="total_billable_hours" readonly="1"/>
|
||||||
|
<field name="total_billable_amount" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="total_ai_audit_hours" readonly="1"/>
|
||||||
|
<field name="currency_id" invisible="1"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<field name="timesheet_ids">
|
||||||
|
<tree string="Timesheet Entries" editable="bottom"
|
||||||
|
decoration-muted="is_billable == False">
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="duration_hours" sum="Hours"/>
|
||||||
|
<field name="is_billable"/>
|
||||||
|
<field name="ai_agent" readonly="1"/>
|
||||||
|
<field name="hourly_rate" readonly="1"/>
|
||||||
|
<field name="billable_amount" readonly="1" sum="Amount"/>
|
||||||
|
<field name="currency_id" column_invisible="1"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
|
||||||
</notebook>
|
</notebook>
|
||||||
</sheet>
|
</sheet>
|
||||||
<div class="oe_chatter">
|
<div class="oe_chatter">
|
||||||
|
|||||||
84
activeblue_familylaw/views/fl_timesheet_views.xml
Normal file
84
activeblue_familylaw/views/fl_timesheet_views.xml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<record id="view_fl_timesheet_tree" model="ir.ui.view">
|
||||||
|
<field name="name">fl.timesheet.tree</field>
|
||||||
|
<field name="model">fl.timesheet</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="Timesheet Entries" editable="bottom"
|
||||||
|
decoration-muted="is_billable == False">
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="case_id"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="duration_hours" sum="Hours"/>
|
||||||
|
<field name="is_billable"/>
|
||||||
|
<field name="ai_agent" readonly="1"/>
|
||||||
|
<field name="hourly_rate" readonly="1"/>
|
||||||
|
<field name="billable_amount" readonly="1" sum="Amount"/>
|
||||||
|
<field name="currency_id" column_invisible="1"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_fl_timesheet_form" model="ir.ui.view">
|
||||||
|
<field name="name">fl.timesheet.form</field>
|
||||||
|
<field name="model">fl.timesheet</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Timesheet Entry">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="case_id"/>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="name"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="duration_hours"/>
|
||||||
|
<field name="is_billable"/>
|
||||||
|
<field name="ai_agent" readonly="1"/>
|
||||||
|
<field name="hourly_rate" readonly="1"/>
|
||||||
|
<field name="billable_amount" readonly="1"/>
|
||||||
|
<field name="currency_id" invisible="1"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_fl_timesheet_search" model="ir.ui.view">
|
||||||
|
<field name="name">fl.timesheet.search</field>
|
||||||
|
<field name="model">fl.timesheet</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Search Timesheets">
|
||||||
|
<field name="case_id"/>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<filter string="Billable" name="billable"
|
||||||
|
domain="[('is_billable', '=', True)]"/>
|
||||||
|
<filter string="AI Audit" name="ai_audit"
|
||||||
|
domain="[('ai_agent', '!=', False)]"/>
|
||||||
|
<group expand="0" string="Group By">
|
||||||
|
<filter string="Case" name="group_case"
|
||||||
|
context="{'group_by': 'case_id'}"/>
|
||||||
|
<filter string="Employee" name="group_employee"
|
||||||
|
context="{'group_by': 'employee_id'}"/>
|
||||||
|
<filter string="AI Agent" name="group_ai"
|
||||||
|
context="{'group_by': 'ai_agent'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_fl_timesheet_list" model="ir.actions.act_window">
|
||||||
|
<field name="name">Timesheets</field>
|
||||||
|
<field name="res_model">fl.timesheet</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
<field name="search_view_id" ref="view_fl_timesheet_search"/>
|
||||||
|
<field name="context">{'search_default_billable': 1}</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
@@ -94,6 +94,13 @@
|
|||||||
action="action_fl_conflict_check_list"
|
action="action_fl_conflict_check_list"
|
||||||
sequence="70"/>
|
sequence="70"/>
|
||||||
|
|
||||||
|
<menuitem
|
||||||
|
id="menu_fl_timesheets"
|
||||||
|
name="Timesheets"
|
||||||
|
parent="menu_fl_cases"
|
||||||
|
action="action_fl_timesheet_list"
|
||||||
|
sequence="80"/>
|
||||||
|
|
||||||
<!-- ══════════════════════════════════════════════════════
|
<!-- ══════════════════════════════════════════════════════
|
||||||
SUPPORT CALCULATOR SUB-MENU
|
SUPPORT CALCULATOR SUB-MENU
|
||||||
══════════════════════════════════════════════════════ -->
|
══════════════════════════════════════════════════════ -->
|
||||||
|
|||||||
Reference in New Issue
Block a user