Initial commit
This commit is contained in:
220
Fusion Accounting/models/budget.py
Normal file
220
Fusion Accounting/models/budget.py
Normal file
@@ -0,0 +1,220 @@
|
||||
# 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,
|
||||
)
|
||||
Reference in New Issue
Block a user