1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/account_reports/models/account_deferred_reports.py
2024-12-10 09:04:09 +07:00

549 lines
27 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import calendar
from collections import defaultdict
from dateutil.relativedelta import relativedelta
from odoo import models, fields, _, api, Command
from odoo.exceptions import UserError
from odoo.tools import groupby
from odoo.addons.account_accountant.models.account_move import DEFERRED_DATE_MIN, DEFERRED_DATE_MAX
class DeferredReportCustomHandler(models.AbstractModel):
_name = 'account.deferred.report.handler'
_inherit = 'account.report.custom.handler'
_description = 'Deferred Expense Report Custom Handler'
def _get_deferred_report_type(self):
raise NotImplementedError("This method is not implemented in the deferred report handler.")
############################################
# DEFERRED COMMON (DISPLAY AND GENERATION) #
############################################
def _get_domain(self, report, options, filter_already_generated=False, filter_not_started=False):
domain = report._get_options_domain(options, "from_beginning")
account_types = ('expense', 'expense_depreciation', 'expense_direct_cost') if self._get_deferred_report_type() == 'expense' else ('income', 'income_other')
domain += [
('account_id.account_type', 'in', account_types),
('deferred_start_date', '!=', False),
('deferred_end_date', '!=', False),
('deferred_end_date', '>=', options['date']['date_from']),
('move_id.date', '<=', options['date']['date_to']),
]
domain += [ # Exclude if entirely inside the period
'!', '&', '&', '&', '&', '&',
('deferred_start_date', '>=', options['date']['date_from']),
('deferred_start_date', '<=', options['date']['date_to']),
('deferred_end_date', '>=', options['date']['date_from']),
('deferred_end_date', '<=', options['date']['date_to']),
('move_id.date', '>=', options['date']['date_from']),
('move_id.date', '<=', options['date']['date_to']),
]
if filter_already_generated:
domain += [
('deferred_end_date', '>=', options['date']['date_from']),
'!',
'&',
('move_id.deferred_move_ids.date', '=', options['date']['date_to']),
('move_id.deferred_move_ids.state', '=', 'posted'),
]
if filter_not_started:
domain += [('deferred_start_date', '>', options['date']['date_to'])]
return domain
@api.model
def _get_select(self):
return [
"account_move_line.id AS line_id",
"account_move_line.account_id AS account_id",
"account_move_line.partner_id AS partner_id",
"account_move_line.product_id AS product_id",
"account_move_line.name AS line_name",
"account_move_line.deferred_start_date AS deferred_start_date",
"account_move_line.deferred_end_date AS deferred_end_date",
"account_move_line.deferred_end_date - account_move_line.deferred_start_date AS diff_days",
"account_move_line.balance AS balance",
"account_move_line.analytic_distribution AS analytic_distribution",
"account_move_line__move_id.id as move_id",
"account_move_line__move_id.name AS move_name",
"account_move_line__account_id.name AS account_name",
]
def _get_lines(self, report, options, filter_already_generated=False):
domain = self._get_domain(report, options, filter_already_generated)
tables, where_clause, where_params = report._query_get(options, domain=domain, date_scope='from_beginning')
select_clause = ', '.join(self._get_select())
query = f"""
SELECT {select_clause}
FROM {tables}
WHERE {where_clause}
ORDER BY "account_move_line"."deferred_start_date", "account_move_line"."id"
"""
self.env.cr.execute(query, where_params)
res = self.env.cr.dictfetchall()
return res
@api.model
def _get_grouping_keys_deferred_lines(self, filter_already_generated=False):
return ('account_id',)
@api.model
def _group_by_deferred_keys(self, line, filter_already_generated=False):
return tuple(line[k] for k in self._get_grouping_keys_deferred_lines(filter_already_generated))
@api.model
def _get_grouping_keys_deferral_lines(self):
return ()
@api.model
def _group_by_deferral_keys(self, line):
return tuple(line[k] for k in self._get_grouping_keys_deferral_lines())
@api.model
def _group_deferred_amounts_by_account(self, deferred_amounts_by_line, periods, is_reverse, filter_already_generated=False):
"""
Groups the deferred amounts by account and computes the totals for each account for each period.
And the total for all accounts for each period.
E.g. (where period1 = (date1, date2, label1), period2 = (date2, date3, label2), ...)
{
self._get_grouping_keys_deferred_lines(): {
'account_id': account1, 'amount_total': 600, period_1: 200, period_2: 400
},
self._get_grouping_keys_deferred_lines(): {
'account_id': account2, 'amount_total': 700, period_1: 300, period_2: 400
},
}, {'totals_aggregated': 1300, period_1: 500, period_2: 800}
"""
deferred_amounts_by_line = groupby(deferred_amounts_by_line, key=lambda x: self._group_by_deferred_keys(x, filter_already_generated))
totals_per_key = {} # {key: {**self._get_grouping_keys_deferral_lines(), total, before, current, later}}
totals_aggregated_by_period = {period: 0 for period in periods + ['totals_aggregated']}
sign = 1 if is_reverse else -1
for key, lines_per_key in deferred_amounts_by_line:
lines_per_key = list(lines_per_key)
current_key_totals = self._get_current_key_totals_dict(lines_per_key, sign)
totals_aggregated_by_period['totals_aggregated'] += current_key_totals['amount_total']
for period in periods:
current_key_totals[period] = sign * sum(line[period] for line in lines_per_key)
totals_aggregated_by_period[period] += self.env.company.currency_id.round(current_key_totals[period])
totals_per_key[key] = current_key_totals
return totals_per_key, totals_aggregated_by_period
###########################
# DEFERRED REPORT DISPLAY #
###########################
def _get_custom_display_config(self):
return {
'templates': {
'AccountReportFilters': 'account_reports.DeferredFilters',
},
}
def _custom_options_initializer(self, report, options, previous_options=None):
super()._custom_options_initializer(report, options, previous_options=previous_options)
options_per_col_group = report._split_options_per_column_group(options)
for column_dict in options['columns']:
column_options = options_per_col_group[column_dict['column_group_key']]
column_dict['name'] = column_options['date']['string']
column_dict['date_from'] = column_options['date']['date_from']
column_dict['date_to'] = column_options['date']['date_to']
options['columns'] = list(reversed(options['columns']))
total_column = [{
**options['columns'][0],
'name': _('Total'),
'expression_label': 'total',
'date_from': DEFERRED_DATE_MIN,
'date_to': DEFERRED_DATE_MAX,
}]
not_started_column = [{
**options['columns'][0],
'name': _('Not Started'),
'expression_label': 'not_started',
'date_from': options['columns'][-1]['date_to'],
'date_to': DEFERRED_DATE_MAX,
}]
before_column = [{
**options['columns'][0],
'name': _('Before'),
'expression_label': 'before',
'date_from': DEFERRED_DATE_MIN,
'date_to': options['columns'][0]['date_from'],
}]
later_column = [{
**options['columns'][0],
'name': _('Later'),
'expression_label': 'later',
'date_from': options['columns'][-1]['date_to'],
'date_to': DEFERRED_DATE_MAX,
}]
options['columns'] = total_column + not_started_column + before_column + options['columns'] + later_column
options['column_headers'] = []
options['deferred_report_type'] = self._get_deferred_report_type()
if (
self._get_deferred_report_type() == 'expense' and self.env.company.generate_deferred_expense_entries_method == 'manual'
or self._get_deferred_report_type() == 'revenue' and self.env.company.generate_deferred_revenue_entries_method == 'manual'
):
options['buttons'].append({'name': _('Generate entry'), 'action': 'action_generate_entry', 'sequence': 80, 'always_show': True})
def action_audit_cell(self, options, params):
""" Open a list of invoices/bills and/or deferral entries for the clicked cell in a deferred report.
Specifically, we show the following lines, grouped by their journal entry, filtered by the column date bounds:
- Total: Lines of all invoices/bills being deferred in the current period
- Not Started: Lines of all deferral entries for which the original invoice/bill date is before or in the
current period, but the deferral only starts after the current period, as well as the lines of
their original invoices/bills
- Before: Lines of all deferral entries with a date before the current period, created by invoices/bills also
being deferred in the current period, as well as the lines of their original invoices/bills
- Current: Lines of all deferral entries in the current period, as well as these of their original
invoices/bills
- Later: Lines of all deferral entries with a date after the current period, created by invoices/bills also
being deferred in the current period, as well as the lines of their original invoices/bills
:param dict options: the report's `options`
:param dict params: a dict containing:
`calling_line_dict_id`: line id containing the optional account of the cell
`column_group_id`: the column group id of the cell
`expression_label`: the expression label of the cell
"""
report = self.env['account.report'].browse(options['report_id'])
column_values = next(
(column for column in options['columns'] if (
column['column_group_key'] == params.get('column_group_key')
and column['expression_label'] == params.get('expression_label')
)),
None
)
if not column_values:
return
column_date_from = fields.Date.to_date(column_values['date_from'])
column_date_to = fields.Date.to_date(column_values['date_to'])
report_date_from = fields.Date.to_date(options['date']['date_from'])
report_date_to = fields.Date.to_date(options['date']['date_to'])
# Corrections for comparisons
if column_values['expression_label'] in ('not_started', 'later'):
# Not Started and Later period start one day after `report_date_to`
column_date_from = report_date_to + relativedelta(days=1)
if column_values['expression_label'] == 'before':
# Before period ends one day before `report_date_from`
column_date_to = report_date_from - relativedelta(days=1)
# calling_line_dict_id is of the format `~account.report~15|~account.account~25`
model, account_id = report._get_model_info_from_id(params.get('calling_line_dict_id'))
if model != 'account.account':
account_id = None
# Find the original lines to be deferred in the report period
original_move_lines_domain = self._get_domain(
report, options, filter_not_started=column_values['expression_label'] == 'not_started'
)
if account_id:
# We're auditing a specific account, so we only want moves containing this account
original_move_lines_domain.append(('account_id', '=', account_id))
# We're getting all lines from the concerned moves. They are filtered later for flexibility.
original_move = self.env['account.move.line'].search(original_move_lines_domain).move_id
# For the Total period only show the original move lines
line_ids = original_move.line_ids.ids
# Show both the original move lines and deferral move lines for all other periods
if not column_values['expression_label'] == 'total':
line_ids += original_move.deferred_move_ids.line_ids.ids
return {
'type': 'ir.actions.act_window',
'name': _('Deferred Entries'),
'res_model': 'account.move.line',
'domain': [('id', 'in', line_ids)],
'views': [(False, 'list'), (False, 'form'), (False, 'pivot'), (False, 'graph'), (False, 'kanban')],
# Most filters are set here to allow auditing flexibility to the user
'context': {
'search_default_pl_accounts': True,
'search_default_account_id': account_id,
'date_from': column_date_from,
'date_to': column_date_to,
'search_default_date_between': True,
'expand': True,
}
}
def _caret_options_initializer(self):
return {
'deferred_caret': [
{'name': _("Journal Items"), 'action': 'open_journal_items'},
],
}
def open_journal_items(self, options, params):
report = self.env['account.report'].browse(options['report_id'])
action = report.open_journal_items(options=options, params=params)
action.get('context', {}).pop('search_default_date_between', None)
action['domain'] = action.get('domain', []) + self._get_domain(report, options)
return action
def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None):
def get_columns(totals):
return [
{
**report._build_column_dict(
totals[(
fields.Date.to_date(column['date_from']),
fields.Date.to_date(column['date_to']),
column['expression_label']
)],
column,
options=options,
currency=self.env.company.currency_id,
),
'auditable': True,
}
for column in options['columns']
]
if warnings is not None:
already_generated = (
(
self._get_deferred_report_type() == 'expense' and self.env.company.generate_deferred_expense_entries_method == 'manual'
or self._get_deferred_report_type() == 'revenue' and self.env.company.generate_deferred_revenue_entries_method == 'manual'
)
and self.env['account.move'].search_count(
report._get_generated_deferral_entries_domain(options)
)
)
if already_generated:
warnings['account_reports.deferred_report_warning_already_posted'] = {'alert_type': 'warning'}
lines = self._get_lines(report, options)
periods = [
(
fields.Date.from_string(column['date_from']),
fields.Date.from_string(column['date_to']),
column['expression_label'],
)
for column in options['columns']
]
deferred_amounts_by_line = self.env['account.move']._get_deferred_amounts_by_line(lines, periods)
totals_per_account, totals_all_accounts = self._group_deferred_amounts_by_account(deferred_amounts_by_line, periods, self._get_deferred_report_type() == 'expense')
report_lines = []
for totals_account in totals_per_account.values():
account = self.env['account.account'].browse(totals_account['account_id'])
report_lines.append((0, {
'id': report._get_generic_line_id('account.account', account.id),
'name': f"{account.code} {account.name}",
'caret_options': 'deferred_caret',
'level': 1,
'columns': get_columns(totals_account),
}))
if totals_per_account:
report_lines.append((0, {
'id': report._get_generic_line_id(None, None, markup='total'),
'name': 'Total',
'level': 1,
'columns': get_columns(totals_all_accounts),
}))
return report_lines
#######################
# DEFERRED GENERATION #
#######################
def action_generate_entry(self, options):
new_deferred_moves = self._generate_deferral_entry(options)
return {
'name': _('Deferred Entries'),
'type': 'ir.actions.act_window',
'views': [(False, "tree"), (False, "form")],
'domain': [('id', 'in', new_deferred_moves.ids)],
'res_model': 'account.move',
'context': {
'search_default_group_by_move': True,
'expand': True,
},
'target': 'current',
}
def _generate_deferral_entry(self, options):
journal = self.env.company.deferred_journal_id
if not journal:
raise UserError(_("Please set the deferred journal in the accounting settings."))
date_from = fields.Date.to_date(DEFERRED_DATE_MIN)
date_to = fields.Date.from_string(options['date']['date_to'])
if date_to.day != calendar.monthrange(date_to.year, date_to.month)[1]:
raise UserError(_("You cannot generate entries for a period that does not end at the end of the month."))
if self.env.company._get_violated_lock_dates(date_to, False):
raise UserError(_("You cannot generate entries for a period that is locked."))
options['all_entries'] = False # We only want to create deferrals for posted moves
report = self.env["account.report"].browse(options["report_id"])
self.env['account.move.line'].flush_model()
lines = self._get_lines(report, options, filter_already_generated=True)
deferral_entry_period = self.env['account.report']._get_dates_period(date_from, date_to, 'range', period_type='month')
ref = _("Grouped Deferral Entry of %s", deferral_entry_period['string'])
ref_rev = _("Reversal of Grouped Deferral Entry of %s", deferral_entry_period['string'])
deferred_account = self.env.company.deferred_expense_account_id if self._get_deferred_report_type() == 'expense' else self.env.company.deferred_revenue_account_id
move_lines, original_move_ids = self._get_deferred_lines(lines, deferred_account, (date_from, date_to, 'current'), self._get_deferred_report_type() == 'expense', ref)
if not move_lines:
raise UserError(_("No entry to generate."))
deferred_move = self.env['account.move'].with_context(skip_account_deprecation_check=True).create({
'move_type': 'entry',
'deferred_original_move_ids': [Command.set(original_move_ids)],
'journal_id': journal.id,
'date': date_to,
'auto_post': 'at_date',
'ref': ref,
})
# We write the lines after creation, to make sure the `deferred_original_move_ids` is set.
# This way we can avoid adding taxes for deferred moves.
deferred_move.write({'line_ids': move_lines})
reverse_move = deferred_move._reverse_moves()
reverse_move.write({
'date': deferred_move.date + relativedelta(days=1),
'ref': ref_rev,
})
reverse_move.line_ids.name = ref_rev
new_deferred_moves = deferred_move + reverse_move
# We create the relation (original deferred move, deferral entry)
# using SQL. This avoids a MemoryError using the ORM which will
# load huge amounts of moves in memory for nothing
self.env.cr.execute_values("""
INSERT INTO account_move_deferred_rel(original_move_id, deferred_move_id)
VALUES %s
ON CONFLICT DO NOTHING
""", [
(original_move_id, deferral_move.id)
for original_move_id in original_move_ids
for deferral_move in new_deferred_moves
])
(deferred_move + reverse_move)._post(soft=True)
return new_deferred_moves
@api.model
def _get_current_key_totals_dict(self, lines_per_key, sign):
return {
'account_id': lines_per_key[0]['account_id'],
'product_id': lines_per_key[0]['product_id'],
'amount_total': sign * sum(line['balance'] for line in lines_per_key),
'move_ids': {line['move_id'] for line in lines_per_key},
}
@api.model
def _get_deferred_lines(self, lines, deferred_account, period, is_reverse, ref):
"""
Returns a list of Command objects to create the deferred lines of a single given period.
And the move_ids of the original lines that created these deferred
(to keep track of the original invoice in the deferred entries).
"""
if not deferred_account:
raise UserError(_("Please set the deferred accounts in the accounting settings."))
deferred_amounts_by_line = self.env['account.move']._get_deferred_amounts_by_line(lines, [period])
deferred_amounts_by_key, deferred_amounts_totals = self._group_deferred_amounts_by_account(deferred_amounts_by_line, [period], is_reverse, filter_already_generated=True)
if deferred_amounts_totals['totals_aggregated'] == deferred_amounts_totals[period]:
return [], set()
# compute analytic distribution to populate on deferred lines
# structure: {self._get_grouping_keys_deferred_lines(): [analytic distribution]}
# dict of keys: self._get_grouping_keys_deferred_lines()
# values: dict of keys: "account.analytic.account.id" (string)
# values: float
anal_dist_by_key = defaultdict(lambda: defaultdict(float))
# using another var for the analytic distribution of the deferral account
deferred_anal_dist = defaultdict(lambda: defaultdict(float))
for line in lines:
if not line['analytic_distribution']:
continue
# Analytic distribution should be computed from the lines with the same _get_grouping_keys_deferred_lines(), except for
# the deferred line with the deferral account which will use _get_grouping_keys_deferral_lines()
full_ratio = (line['balance'] / deferred_amounts_totals['totals_aggregated']) if deferred_amounts_totals['totals_aggregated'] else 0
key_amount = deferred_amounts_by_key.get(self._group_by_deferred_keys(line, True))
key_ratio = (line['balance'] / key_amount['amount_total']) if key_amount and key_amount['amount_total'] else 0
for account_id, distribution in line['analytic_distribution'].items():
anal_dist_by_key[self._group_by_deferred_keys(line, True)][account_id] += distribution * key_ratio
deferred_anal_dist[self._group_by_deferral_keys(line)][account_id] += distribution * full_ratio
remaining_balance = 0
deferred_lines = []
original_move_ids = set()
for key, line in deferred_amounts_by_key.items():
for balance in (-line['amount_total'], line[period]):
if balance != 0 and line[period] != line['amount_total']:
original_move_ids |= line['move_ids']
deferred_balance = self.env.company.currency_id.round((1 if is_reverse else -1) * balance)
deferred_lines.append(
Command.create(
self.env['account.move.line']._get_deferred_lines_values(
account_id=line['account_id'],
balance=deferred_balance,
ref=ref,
analytic_distribution=anal_dist_by_key[key] or False,
line=line,
)
)
)
remaining_balance += deferred_balance
grouped_by_key = {
key: list(value)
for key, value in groupby(
deferred_amounts_by_key.values(),
key=self._group_by_deferral_keys,
)
}
deferral_lines = []
for key, lines_per_key in grouped_by_key.items():
balance = 0
for line in lines_per_key:
if line[period] != line['amount_total']:
balance += self.env.company.currency_id.round((1 if is_reverse else -1) * (line['amount_total'] - line[period]))
deferral_lines.append(
Command.create(
self.env['account.move.line']._get_deferred_lines_values(
account_id=deferred_account.id,
balance=balance,
ref=ref,
analytic_distribution=deferred_anal_dist[key] or False,
line=lines_per_key[0],
)
)
)
remaining_balance += balance
if not self.env.company.currency_id.is_zero(remaining_balance):
deferral_lines.append(
Command.create({
'account_id': deferred_account.id,
'balance': -remaining_balance,
'name': ref,
})
)
return deferred_lines + deferral_lines, original_move_ids
class DeferredExpenseCustomHandler(models.AbstractModel):
_name = 'account.deferred.expense.report.handler'
_inherit = 'account.deferred.report.handler'
_description = 'Deferred Expense Custom Handler'
def _get_deferred_report_type(self):
return 'expense'
class DeferredRevenueCustomHandler(models.AbstractModel):
_name = 'account.deferred.revenue.report.handler'
_inherit = 'account.deferred.report.handler'
_description = 'Deferred Revenue Custom Handler'
def _get_deferred_report_type(self):
return 'revenue'