Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,36 @@ def _compute_amount_totals(self):
record.expense_appropriation_ids.mapped("amount_net")
)

def iter_signed_lines(self, budget_type):
"""Yield ``(line, sign)`` pairs across this compilation's appropriations.

``sign`` is ``+1`` for regular lines and ``-1`` for deduct lines, so
summing ``line.balance * sign`` matches the appropriation's
``amount_net`` (the value used by the compilation aggregate totals).
"""
self.ensure_one()
field = (
"revenue_appropriation_ids"
if budget_type == "revenue"
else "expense_appropriation_ids"
)
for appropriation in self[field]:
for line in appropriation.line_ids:
yield line, 1
for line in appropriation.deduct_line_ids:
yield line, -1

def top_level_department_id(self):
"""Return the top-level ancestor id of this compilation's department."""
self.ensure_one()
department = self.department_analytic_id
if department and department.parent_path:
try:
return int(department.parent_path.strip("/").split("/")[0])
except (ValueError, IndexError):
return None
return None

@api.depends("revenue_appropriation_ids")
def _compute_f4_revenue_data(self):
F4Model = self.env["budget.appropriation.f4.report"]
Expand Down
19 changes: 9 additions & 10 deletions budget_appropriation_summary/models/summary_f10w_expense.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,18 +281,17 @@ def _build_activity_expense_totals(self, summary, expense_type_map):
Returns:
dict: (activity_id, expense_type_code) -> balance
"""
lines = summary.expense_appropriation_ids.mapped("line_ids")
totals = {}
for compilation in summary.compilation_ids:
for line, sign in compilation.iter_signed_lines("expense"):
activity = line.activity_analytic_id
account_id = line.account_id.id

for line in lines:
activity = line.activity_analytic_id
account_id = line.account_id.id
if not activity or account_id not in expense_type_map:
continue

if not activity or account_id not in expense_type_map:
continue

expense_type_code = expense_type_map[account_id]
key = (activity.id, expense_type_code)
totals[key] = totals.get(key, 0) + line.balance
expense_type_code = expense_type_map[account_id]
key = (activity.id, expense_type_code)
totals[key] = totals.get(key, 0) + line.balance * sign

return totals
18 changes: 7 additions & 11 deletions budget_appropriation_summary/models/summary_f11w_expense.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,23 +223,19 @@ def _build_dept_column_totals(self, summary, dept_map, fund_path_map):
Returns:
dict: (dept_id, column_code) -> balance
"""
lines = summary.expense_appropriation_ids.mapped("line_ids")
totals = {}

for line in lines:
top_dept_id = self._extract_top_level_dept_id(line.department_analytic_id)

for compilation in summary.compilation_ids:
top_dept_id = compilation.top_level_department_id()
if not top_dept_id or top_dept_id not in dept_map:
continue

# Check fund
fund = line.fund_analytic_id
if fund:
# Check if fund matches directly or is a child of one of our funds
for line, sign in compilation.iter_signed_lines("expense"):
fund = line.fund_analytic_id
if not fund:
continue
fund_code = self._get_fund_code(fund, fund_path_map)
if fund_code:
key = (top_dept_id, fund_code)
totals[key] = totals.get(key, 0) + line.balance
totals[key] = totals.get(key, 0) + line.balance * sign

return totals

Expand Down
15 changes: 10 additions & 5 deletions budget_appropriation_summary/models/summary_f2_revenue.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,18 @@ def get_data(self, summary_id):
}

def _build_category_totals(self, summary):
"""Build category_code -> balance mapping from all compilations."""
lines = summary.revenue_appropriation_ids.mapped("line_ids")
"""Build category_code -> amount mapping from all compilations.

Signed line sum (line_ids - deduct_line_ids) per compilation so the
grand total matches the compilation's amount_revenue_total (amount_net).
"""
account_totals = {}
for line in lines:
acc_id = line.account_id.id
account_totals[acc_id] = account_totals.get(acc_id, 0) + line.balance
for compilation in summary.compilation_ids:
for line, sign in compilation.iter_signed_lines("revenue"):
acc_id = line.account_id.id
account_totals[acc_id] = (
account_totals.get(acc_id, 0) + line.balance * sign
)

totals = {}
for code, _ in self.REVENUE_CATEGORIES:
Expand Down
19 changes: 12 additions & 7 deletions budget_appropriation_summary/models/summary_f4p_revenue.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,22 +117,27 @@ def get_data(self, summary_id):
}

def _build_category_dept_totals(self, summary, dept_map):
lines = summary.revenue_appropriation_ids.mapped("line_ids")
"""Build (category_code, top_dept_id) -> signed amount mapping.

Iterates compilations so department is taken from the compilation
(not from individual lines) and deduct_line_ids are subtracted —
keeping totals aligned with compilation.amount_revenue_total.
"""
category_accounts = {}
for code, _ in self.REVENUE_CATEGORIES:
category_accounts[code] = set(self._get_accounts_in_category(code))

totals = {}
for line in lines:
acc_id = line.account_id.id
top_dept_id = self._extract_top_level_dept_id(line.department_analytic_id)

if top_dept_id and top_dept_id in dept_map:
for compilation in summary.compilation_ids:
top_dept_id = compilation.top_level_department_id()
if not top_dept_id or top_dept_id not in dept_map:
continue
for line, sign in compilation.iter_signed_lines("revenue"):
acc_id = line.account_id.id
for code, _ in self.REVENUE_CATEGORIES:
if acc_id in category_accounts[code]:
key = (code, top_dept_id)
totals[key] = totals.get(key, 0) + line.balance
totals[key] = totals.get(key, 0) + line.balance * sign
break

return totals
Expand Down
18 changes: 10 additions & 8 deletions budget_appropriation_summary/models/summary_f4w_revenue.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,18 +129,20 @@ def get_data(self, summary_id):
}

def _build_dept_category_totals(self, summary, dept_map, category_accounts):
lines = summary.revenue_appropriation_ids.mapped("line_ids")
"""Iterate compilations (not raw appropriations) so dept attribution
comes from the compilation and deduct lines reduce amounts to match
compilation.amount_revenue_total (amount_net)."""
totals = {}

for line in lines:
acc_id = line.account_id.id
top_dept_id = self._extract_top_level_dept_id(line.department_analytic_id)

if top_dept_id and top_dept_id in dept_map:
for compilation in summary.compilation_ids:
top_dept_id = compilation.top_level_department_id()
if not top_dept_id or top_dept_id not in dept_map:
continue
for line, sign in compilation.iter_signed_lines("revenue"):
acc_id = line.account_id.id
for code, _ in self.REVENUE_CATEGORIES:
if acc_id in category_accounts[code]:
key = (top_dept_id, code)
totals[key] = totals.get(key, 0) + line.balance
totals[key] = totals.get(key, 0) + line.balance * sign
break

return totals
Expand Down
21 changes: 12 additions & 9 deletions budget_appropriation_summary/models/summary_f5p_expense.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,25 +257,28 @@ def _build_category_dept_totals(self, summary, dept_map):
Returns:
dict: (type_code, cat_name, dept_id) -> balance
"""
lines = summary.expense_appropriation_ids.mapped("line_ids")

# Pre-compute account_id -> (type_code, cat_name) mapping
account_category_map = {}
for type_code, cat_name, mapping_type, codes in self.EXPENSE_CATEGORIES:
account_ids = self._get_accounts_for_category(mapping_type, codes)
for acc_id in account_ids:
account_category_map[acc_id] = (type_code, cat_name)

# Aggregate
# Iterate compilations so department attribution follows the
# compilation (not the line) and deduct_line_ids are subtracted
# — matching compilation.amount_expense_total (amount_net).
totals = {}
for line in lines:
acc_id = line.account_id.id
top_dept_id = self._extract_top_level_dept_id(line.department_analytic_id)

if acc_id in account_category_map and top_dept_id and top_dept_id in dept_map:
for compilation in summary.compilation_ids:
top_dept_id = compilation.top_level_department_id()
if not top_dept_id or top_dept_id not in dept_map:
continue
for line, sign in compilation.iter_signed_lines("expense"):
acc_id = line.account_id.id
if acc_id not in account_category_map:
continue
type_code, cat_name = account_category_map[acc_id]
key = (type_code, cat_name, top_dept_id)
totals[key] = totals.get(key, 0) + line.balance
totals[key] = totals.get(key, 0) + line.balance * sign

return totals

Expand Down
26 changes: 16 additions & 10 deletions budget_appropriation_summary/models/summary_f5w_expense.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,6 @@ def _build_activity_totals(self, summary):
Returns:
dict: xml_id -> balance
"""
lines = summary.expense_appropriation_ids.mapped("line_ids")

# Pre-compute parent_path_prefix -> xml_id mapping
# Using parent_path prefix (e.g., "123/") to match all descendants
path_prefix_map = {}
Expand All @@ -157,14 +155,22 @@ def _build_activity_totals(self, summary):
else:
_logger.warning("Activity plan with xml_id '%s' not found", xml_id)

# Aggregate
# Iterate compilations with signed lines (deducts subtracted) so
# totals reconcile with compilation.amount_expense_total.
totals = {}
for line in lines:
activity = line.activity_analytic_id
# Check which plan this activity belongs to
for prefix, xml_id in path_prefix_map.items():
if prefix in activity.parent_path or activity.parent_path.startswith(prefix):
totals[xml_id] = totals.get(xml_id, 0) + line.balance
break
for compilation in summary.compilation_ids:
for line, sign in compilation.iter_signed_lines("expense"):
activity = line.activity_analytic_id
if not activity or not activity.parent_path:
continue
# parent_path.startswith(prefix) is the only valid ancestor
# check — `prefix in parent_path` is a substring match and
# can collide when ids share digits (e.g. "10/" vs "110/").
for prefix, xml_id in path_prefix_map.items():
if activity.parent_path.startswith(prefix):
totals[xml_id] = (
totals.get(xml_id, 0) + line.balance * sign
)
break

return totals
17 changes: 9 additions & 8 deletions budget_appropriation_summary/models/summary_f7w_expense.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,22 +209,23 @@ def _build_category_totals(self, summary):
Returns:
dict: (type_code, cat_name) -> balance
"""
lines = summary.expense_appropriation_ids.mapped("line_ids")

# Pre-compute account_id -> (type_code, cat_name) mapping
account_category_map = {}
for type_code, cat_name, mapping_type, codes in self.EXPENSE_CATEGORIES:
account_ids = self._get_accounts_for_category(mapping_type, codes)
for acc_id in account_ids:
account_category_map[acc_id] = (type_code, cat_name)

# Aggregate
# Iterate compilations with signed lines so deduct_line_ids are
# subtracted — keeping totals aligned with compilation
# amount_expense_total (amount_net).
totals = {}
for line in lines:
acc_id = line.account_id.id
if acc_id in account_category_map:
key = account_category_map[acc_id]
totals[key] = totals.get(key, 0) + line.balance
for compilation in summary.compilation_ids:
for line, sign in compilation.iter_signed_lines("expense"):
acc_id = line.account_id.id
if acc_id in account_category_map:
key = account_category_map[acc_id]
totals[key] = totals.get(key, 0) + line.balance * sign

return totals

Expand Down
20 changes: 10 additions & 10 deletions budget_appropriation_summary/models/summary_f8w_expense.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,19 +230,19 @@ def _build_dept_activity_totals(self, summary, dept_map, activity_path_map):
Returns:
dict: (dept_id, xml_id) -> balance
"""
lines = summary.expense_appropriation_ids.mapped("line_ids")
totals = {}

for line in lines:
top_dept_id = self._extract_top_level_dept_id(line.department_analytic_id)
activity = line.activity_analytic_id

if top_dept_id and top_dept_id in dept_map and activity and activity.parent_path:
# Find which activity plan this belongs to
for compilation in summary.compilation_ids:
top_dept_id = compilation.top_level_department_id()
if not top_dept_id or top_dept_id not in dept_map:
continue
for line, sign in compilation.iter_signed_lines("expense"):
activity = line.activity_analytic_id
if not activity or not activity.parent_path:
continue
for prefix, xml_id in activity_path_map.items():
if prefix in activity.parent_path or activity.parent_path.startswith(prefix):
if activity.parent_path.startswith(prefix):
key = (top_dept_id, xml_id)
totals[key] = totals.get(key, 0) + line.balance
totals[key] = totals.get(key, 0) + line.balance * sign
break

return totals
Expand Down
32 changes: 16 additions & 16 deletions budget_appropriation_summary/models/summary_f9w_expense.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,29 +241,29 @@ def _build_dept_column_totals(self, summary, dept_map, activity_path_map, reserv
Returns:
dict: (dept_id, column_code) -> balance
"""
lines = summary.expense_appropriation_ids.mapped("line_ids")
totals = {}

for line in lines:
top_dept_id = self._extract_top_level_dept_id(line.department_analytic_id)

for compilation in summary.compilation_ids:
top_dept_id = compilation.top_level_department_id()
if not top_dept_id or top_dept_id not in dept_map:
continue

# Check activity plans (4 columns)
activity = line.activity_analytic_id
if activity and activity.parent_path:
for line, sign in compilation.iter_signed_lines("expense"):
# Reserve-fund lines must not also count toward an activity
# plan column, or the department total doubles. Classify
# each line into exactly one column.
if line.account_id and line.account_id.id in reserve_fund_account_ids:
key = (top_dept_id, self.RESERVE_FUND[1])
totals[key] = totals.get(key, 0) + line.balance * sign
continue

activity = line.activity_analytic_id
if not activity or not activity.parent_path:
continue
for prefix, code in activity_path_map.items():
if prefix in activity.parent_path or activity.parent_path.startswith(prefix):
if activity.parent_path.startswith(prefix):
key = (top_dept_id, code)
totals[key] = totals.get(key, 0) + line.balance
totals[key] = totals.get(key, 0) + line.balance * sign
break

# Check reserve fund (1 column)
if line.account_id and line.account_id.id in reserve_fund_account_ids:
key = (top_dept_id, self.RESERVE_FUND[1])
totals[key] = totals.get(key, 0) + line.balance

return totals

def _get_top_level_departments(self):
Expand Down