Files
Odoo-Modules/Fusion Accounting/models/budget.py
2026-02-22 01:22:18 -05:00

221 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Part of Fusion Accounting. See LICENSE file for full copyright and licensing details.
from odoo import api, Command, fields, models, _
from odoo.exceptions import ValidationError
from odoo.tools import date_utils, float_is_zero, float_round
class FusionBudget(models.Model):
"""Represents a financial budget linked to accounting reports.
A budget groups together individual budget line items, each targeting
a specific account and month. Budgets are company-specific and appear
as additional columns in accounting reports.
"""
_name = 'account.report.budget'
_description = "Fusion Report Budget"
_order = 'sequence, id'
name = fields.Char(
string="Budget Name",
required=True,
)
sequence = fields.Integer(
string="Display Order",
default=10,
)
company_id = fields.Many2one(
comodel_name='res.company',
string="Company",
required=True,
default=lambda self: self.env.company,
)
item_ids = fields.One2many(
comodel_name='account.report.budget.item',
inverse_name='budget_id',
string="Budget Lines",
)
# --------------------------------------------------
# CRUD
# --------------------------------------------------
@api.model_create_multi
def create(self, vals_list):
"""Override create to sanitize the budget name by stripping whitespace."""
for record_vals in vals_list:
raw_name = record_vals.get('name')
if raw_name:
record_vals['name'] = raw_name.strip()
return super().create(vals_list)
# --------------------------------------------------
# Constraints
# --------------------------------------------------
@api.constrains('name')
def _check_budget_name_not_empty(self):
"""Ensure every budget record has a non-empty name."""
for record in self:
if not record.name or not record.name.strip():
raise ValidationError(
_("A budget must have a non-empty name.")
)
# --------------------------------------------------
# Duplication helpers
# --------------------------------------------------
def copy_data(self, default=None):
"""Append '(copy)' suffix to duplicated budget names."""
data_list = super().copy_data(default=default)
result = []
for budget, vals in zip(self, data_list):
vals['name'] = _("%s (copy)", budget.name)
result.append(vals)
return result
def copy(self, default=None):
"""Duplicate budgets together with their line items."""
duplicated_budgets = super().copy(default)
for source_budget, target_budget in zip(self, duplicated_budgets):
for line in source_budget.item_ids:
line.copy({
'budget_id': target_budget.id,
'account_id': line.account_id.id,
'amount': line.amount,
'date': line.date,
})
return duplicated_budgets
# --------------------------------------------------
# Budget item management (called from report engine)
# --------------------------------------------------
def _create_or_update_budget_items(
self, value_to_set, account_id, rounding, date_from, date_to
):
"""Distribute a target amount across monthly budget items.
When the user edits a budget cell in the report view, this method
calculates the difference between the desired total and the existing
total for the given account/date range, then distributes that delta
evenly across the months in the range.
Existing items within the range are updated in place; new items are
created for months that don't have one yet.
Args:
value_to_set: The desired total amount for the date range.
account_id: The ``account.account`` record id.
rounding: Number of decimal digits for monetary precision.
date_from: Start date (inclusive) of the budget period.
date_to: End date (inclusive) of the budget period.
"""
self.ensure_one()
period_start = fields.Date.to_date(date_from)
period_end = fields.Date.to_date(date_to)
BudgetItem = self.env['account.report.budget.item']
# Fetch all items that already cover (part of) the requested range
matching_items = BudgetItem.search_fetch(
[
('budget_id', '=', self.id),
('account_id', '=', account_id),
('date', '>=', period_start),
('date', '<=', period_end),
],
['id', 'amount'],
)
current_total = sum(matching_items.mapped('amount'))
# Calculate the remaining amount to distribute
remaining_delta = value_to_set - current_total
if float_is_zero(remaining_delta, precision_digits=rounding):
return
# Build a list of first-of-month dates spanning the period
month_starts = [
date_utils.start_of(d, 'month')
for d in date_utils.date_range(period_start, period_end)
]
month_count = len(month_starts)
# Spread the delta equally across months (rounding down),
# then assign any leftover cents to the final month.
per_month = float_round(
remaining_delta / month_count,
precision_digits=rounding,
rounding_method='DOWN',
)
monthly_portions = [per_month] * month_count
distributed_sum = float_round(sum(monthly_portions), precision_digits=rounding)
monthly_portions[-1] += float_round(
remaining_delta - distributed_sum,
precision_digits=rounding,
)
# Pair existing items with months and amounts; create or update as needed
write_commands = []
idx = 0
for month_date, portion in zip(month_starts, monthly_portions):
if idx < len(matching_items):
# Update an existing item
existing = matching_items[idx]
write_commands.append(
Command.update(existing.id, {
'amount': existing.amount + portion,
})
)
else:
# No existing item for this slot create a new one
write_commands.append(
Command.create({
'account_id': account_id,
'amount': portion,
'date': month_date,
})
)
idx += 1
if write_commands:
self.item_ids = write_commands
# Ensure the ORM flushes new records to the database so
# subsequent queries within the same request see them.
BudgetItem.flush_model()
class FusionBudgetItem(models.Model):
"""A single monthly budget entry for one account within a budget.
Each item records a monetary amount allocated to a specific
``account.account`` for a particular month. The ``date`` field
stores the first day of the relevant month.
"""
_name = 'account.report.budget.item'
_description = "Fusion Report Budget Line"
budget_id = fields.Many2one(
comodel_name='account.report.budget',
string="Parent Budget",
required=True,
ondelete='cascade',
)
account_id = fields.Many2one(
comodel_name='account.account',
string="Account",
required=True,
)
date = fields.Date(
string="Month",
required=True,
)
amount = fields.Float(
string="Budgeted Amount",
default=0.0,
)