Initial commit
This commit is contained in:
879
Fusion Accounting/models/account_generic_tax_report.py
Normal file
879
Fusion Accounting/models/account_generic_tax_report.py
Normal file
@@ -0,0 +1,879 @@
|
||||
# Fusion Accounting - Generic Tax Report Handlers
|
||||
# Base tax-report handler, generic handler, and account/tax grouping variants
|
||||
|
||||
import ast
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import models, api, fields, Command, _
|
||||
from odoo.addons.web.controllers.utils import clean_action
|
||||
from odoo.exceptions import UserError, RedirectWarning
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import SQL
|
||||
|
||||
|
||||
class FusionTaxReportHandler(models.AbstractModel):
|
||||
"""Base handler providing the Closing Entry button and tax-period
|
||||
configuration for all tax reports (generic and country-specific)."""
|
||||
|
||||
_name = 'account.tax.report.handler'
|
||||
_inherit = 'account.report.custom.handler'
|
||||
_description = 'Account Report Handler for Tax Reports'
|
||||
|
||||
def _custom_options_initializer(self, report, options, previous_options):
|
||||
period_type_map = {'monthly': 'month', 'trimester': 'quarter', 'year': 'year'}
|
||||
|
||||
options['buttons'].append({
|
||||
'name': _('Closing Entry'),
|
||||
'action': 'action_periodic_vat_entries',
|
||||
'sequence': 110,
|
||||
'always_show': True,
|
||||
})
|
||||
self._enable_export_buttons_for_common_vat_groups_in_branches(options)
|
||||
|
||||
start_day, start_month = self.env.company._get_tax_closing_start_date_attributes(report)
|
||||
tax_period = self.env.company._get_tax_periodicity(report)
|
||||
options['tax_periodicity'] = {
|
||||
'periodicity': tax_period,
|
||||
'months_per_period': self.env.company._get_tax_periodicity_months_delay(report),
|
||||
'start_day': start_day,
|
||||
'start_month': start_month,
|
||||
}
|
||||
|
||||
options['show_tax_period_filter'] = (
|
||||
tax_period not in period_type_map or start_day != 1 or start_month != 1
|
||||
)
|
||||
if not options['show_tax_period_filter']:
|
||||
std_period = period_type_map[tax_period]
|
||||
options['date']['filter'] = options['date']['filter'].replace('tax_period', std_period)
|
||||
options['date']['period_type'] = options['date']['period_type'].replace('tax_period', std_period)
|
||||
|
||||
def _get_custom_display_config(self):
|
||||
cfg = defaultdict(dict)
|
||||
cfg['templates']['AccountReportFilters'] = 'fusion_accounting.GenericTaxReportFiltersCustomizable'
|
||||
return cfg
|
||||
|
||||
def _customize_warnings(self, report, options, all_column_groups_expression_totals, warnings):
|
||||
if 'fusion_accounting.common_warning_draft_in_period' in warnings:
|
||||
has_non_closing_drafts = self.env['account.move'].search_count([
|
||||
('state', '=', 'draft'),
|
||||
('date', '<=', options['date']['date_to']),
|
||||
('tax_closing_report_id', '=', False),
|
||||
], limit=1)
|
||||
if not has_non_closing_drafts:
|
||||
warnings.pop('fusion_accounting.common_warning_draft_in_period')
|
||||
|
||||
qry = report._get_report_query(options, 'strict_range')
|
||||
inactive_rows = self.env.execute_query(SQL("""
|
||||
SELECT 1
|
||||
FROM %s
|
||||
JOIN account_account_tag_account_move_line_rel aml_tag
|
||||
ON account_move_line.id = aml_tag.account_move_line_id
|
||||
JOIN account_account_tag tag
|
||||
ON aml_tag.account_account_tag_id = tag.id
|
||||
WHERE %s AND NOT tag.active
|
||||
LIMIT 1
|
||||
""", qry.from_clause, qry.where_clause))
|
||||
if inactive_rows:
|
||||
warnings['fusion_accounting.tax_report_warning_inactive_tags'] = {}
|
||||
|
||||
# ================================================================
|
||||
# TAX CLOSING
|
||||
# ================================================================
|
||||
|
||||
def _is_period_equal_to_options(self, report, options):
|
||||
opt_to = fields.Date.from_string(options['date']['date_to'])
|
||||
opt_from = fields.Date.from_string(options['date']['date_from'])
|
||||
boundary_from, boundary_to = self.env.company._get_tax_closing_period_boundaries(opt_to, report)
|
||||
return boundary_from == opt_from and boundary_to == opt_to
|
||||
|
||||
def action_periodic_vat_entries(self, options, from_post=False):
|
||||
report = self.env['account.report'].browse(options['report_id'])
|
||||
if (
|
||||
options['date']['period_type'] != 'tax_period'
|
||||
and not self._is_period_equal_to_options(report, options)
|
||||
and not self.env.context.get('override_tax_closing_warning')
|
||||
):
|
||||
if len(options['companies']) > 1 and (
|
||||
report.filter_multi_company != 'tax_units'
|
||||
or not (report.country_id and options['available_tax_units'])
|
||||
):
|
||||
warning_msg = _(
|
||||
"You're about to generate closing entries for multiple companies. "
|
||||
"Each will follow its own tax periodicity."
|
||||
)
|
||||
else:
|
||||
warning_msg = _(
|
||||
"The selected dates don't match a tax period. The closing entry "
|
||||
"will target the closest matching period."
|
||||
)
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'fusion_accounting.redirect_action',
|
||||
'target': 'new',
|
||||
'params': {
|
||||
'depending_action': self.with_context(
|
||||
override_tax_closing_warning=True,
|
||||
).action_periodic_vat_entries(options),
|
||||
'message': warning_msg,
|
||||
'button_text': _("Proceed"),
|
||||
},
|
||||
'context': {'dialog_size': 'medium', 'override_tax_closing_warning': True},
|
||||
}
|
||||
|
||||
generated_moves = self._get_periodic_vat_entries(options, from_post=from_post)
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("account.action_move_journal_line")
|
||||
action = clean_action(action, env=self.env)
|
||||
action.pop('domain', None)
|
||||
|
||||
if len(generated_moves) == 1:
|
||||
action['views'] = [(self.env.ref('account.view_move_form').id, 'form')]
|
||||
action['res_id'] = generated_moves.id
|
||||
else:
|
||||
action['domain'] = [('id', 'in', generated_moves.ids)]
|
||||
action['context'] = dict(ast.literal_eval(action['context']))
|
||||
action['context'].pop('search_default_posted', None)
|
||||
return action
|
||||
|
||||
def _get_periodic_vat_entries(self, options, from_post=False):
|
||||
report = self.env['account.report'].browse(options['report_id'])
|
||||
if options.get('integer_rounding'):
|
||||
options['integer_rounding_enabled'] = True
|
||||
|
||||
result_moves = self.env['account.move']
|
||||
company_set = self.env['res.company'].browse(report.get_report_company_ids(options))
|
||||
|
||||
existing = self._get_tax_closing_entries_for_closed_period(
|
||||
report, options, company_set, posted_only=False,
|
||||
)
|
||||
result_moves += existing
|
||||
result_moves += self._generate_tax_closing_entries(
|
||||
report, options,
|
||||
companies=company_set - existing.company_id,
|
||||
from_post=from_post,
|
||||
)
|
||||
return result_moves
|
||||
|
||||
def _generate_tax_closing_entries(self, report, options, closing_moves=None, companies=None, from_post=False):
|
||||
if companies is None:
|
||||
companies = self.env['res.company'].browse(report.get_report_company_ids(options))
|
||||
if closing_moves is None:
|
||||
closing_moves = self.env['account.move']
|
||||
|
||||
period_end = fields.Date.from_string(options['date']['date_to'])
|
||||
moves_by_company = defaultdict(lambda: self.env['account.move'])
|
||||
|
||||
remaining_cos = companies.filtered(lambda c: c not in closing_moves.company_id)
|
||||
if closing_moves:
|
||||
for mv in closing_moves.filtered(lambda m: m.state == 'draft'):
|
||||
moves_by_company[mv.company_id] |= mv
|
||||
|
||||
for co in remaining_cos:
|
||||
include_dom, fpos_set = self._get_fpos_info_for_tax_closing(co, report, options)
|
||||
co_moves = co._get_and_update_tax_closing_moves(
|
||||
period_end, report, fiscal_positions=fpos_set, include_domestic=include_dom,
|
||||
)
|
||||
moves_by_company[co] = co_moves
|
||||
closing_moves += co_moves
|
||||
|
||||
for co, co_moves in moves_by_company.items():
|
||||
countries = self.env['res.country']
|
||||
for mv in co_moves:
|
||||
if mv.fiscal_position_id.foreign_vat:
|
||||
countries |= mv.fiscal_position_id.country_id
|
||||
else:
|
||||
countries |= co.account_fiscal_country_id
|
||||
|
||||
if self.env['account.tax.group']._check_misconfigured_tax_groups(co, countries):
|
||||
self._redirect_to_misconfigured_tax_groups(co, countries)
|
||||
|
||||
for mv in co_moves:
|
||||
if from_post and mv == moves_by_company.get(self.env.company):
|
||||
continue
|
||||
|
||||
mv_opts = {
|
||||
**options,
|
||||
'fiscal_position': mv.fiscal_position_id.id if mv.fiscal_position_id else 'domestic',
|
||||
}
|
||||
line_cmds, tg_subtotals = self._compute_vat_closing_entry(co, mv_opts)
|
||||
line_cmds += self._add_tax_group_closing_items(tg_subtotals, mv)
|
||||
|
||||
if mv.line_ids:
|
||||
line_cmds += [Command.delete(aml.id) for aml in mv.line_ids]
|
||||
|
||||
if line_cmds:
|
||||
mv.write({'line_ids': line_cmds})
|
||||
|
||||
return closing_moves
|
||||
|
||||
def _get_tax_closing_entries_for_closed_period(self, report, options, companies, posted_only=True):
|
||||
found = self.env['account.move']
|
||||
for co in companies:
|
||||
_s, p_end = co._get_tax_closing_period_boundaries(
|
||||
fields.Date.from_string(options['date']['date_to']), report,
|
||||
)
|
||||
inc_dom, fpos = self._get_fpos_info_for_tax_closing(co, report, options)
|
||||
fpos_ids = fpos.ids + ([False] if inc_dom else [])
|
||||
state_cond = ('state', '=', 'posted') if posted_only else ('state', '!=', 'cancel')
|
||||
found += self.env['account.move'].search([
|
||||
('company_id', '=', co.id),
|
||||
('fiscal_position_id', 'in', fpos_ids),
|
||||
('date', '=', p_end),
|
||||
('tax_closing_report_id', '=', options['report_id']),
|
||||
state_cond,
|
||||
], limit=1)
|
||||
return found
|
||||
|
||||
@api.model
|
||||
def _compute_vat_closing_entry(self, company, options):
|
||||
self = self.with_company(company)
|
||||
self.env['account.tax'].flush_model(['name', 'tax_group_id'])
|
||||
self.env['account.tax.repartition.line'].flush_model(['use_in_tax_closing'])
|
||||
self.env['account.move.line'].flush_model([
|
||||
'account_id', 'debit', 'credit', 'move_id', 'tax_line_id',
|
||||
'date', 'company_id', 'display_type', 'parent_state',
|
||||
])
|
||||
self.env['account.move'].flush_model(['state'])
|
||||
|
||||
adjusted_opts = {**options, 'all_entries': False, 'date': dict(options['date'])}
|
||||
report = self.env['account.report'].browse(options['report_id'])
|
||||
p_start, p_end = company._get_tax_closing_period_boundaries(
|
||||
fields.Date.from_string(options['date']['date_to']), report,
|
||||
)
|
||||
adjusted_opts['date']['date_from'] = fields.Date.to_string(p_start)
|
||||
adjusted_opts['date']['date_to'] = fields.Date.to_string(p_end)
|
||||
adjusted_opts['date']['period_type'] = 'custom'
|
||||
adjusted_opts['date']['filter'] = 'custom'
|
||||
adjusted_opts = report.with_context(
|
||||
allowed_company_ids=company.ids,
|
||||
).get_options(previous_options=adjusted_opts)
|
||||
adjusted_opts['fiscal_position'] = options['fiscal_position']
|
||||
|
||||
qry = self.env.ref('account.generic_tax_report')._get_report_query(
|
||||
adjusted_opts, 'strict_range',
|
||||
domain=self._get_vat_closing_entry_additional_domain(),
|
||||
)
|
||||
tax_name_expr = self.env['account.tax']._field_to_sql('tax', 'name')
|
||||
stmt = SQL("""
|
||||
SELECT "account_move_line".tax_line_id as tax_id,
|
||||
tax.tax_group_id as tax_group_id,
|
||||
%(tax_name)s as tax_name,
|
||||
"account_move_line".account_id,
|
||||
COALESCE(SUM("account_move_line".balance), 0) as amount
|
||||
FROM account_tax tax, account_tax_repartition_line repartition, %(tbl)s
|
||||
WHERE %(where)s
|
||||
AND tax.id = "account_move_line".tax_line_id
|
||||
AND repartition.id = "account_move_line".tax_repartition_line_id
|
||||
AND repartition.use_in_tax_closing
|
||||
GROUP BY tax.tax_group_id, "account_move_line".tax_line_id, tax.name, "account_move_line".account_id
|
||||
""", tax_name=tax_name_expr, tbl=qry.from_clause, where=qry.where_clause)
|
||||
self.env.cr.execute(stmt)
|
||||
raw_results = self.env.cr.dictfetchall()
|
||||
raw_results = self._postprocess_vat_closing_entry_results(company, adjusted_opts, raw_results)
|
||||
|
||||
tg_ids = [r['tax_group_id'] for r in raw_results]
|
||||
tax_groups = {}
|
||||
for tg, row in zip(self.env['account.tax.group'].browse(tg_ids), raw_results):
|
||||
tax_groups.setdefault(tg, {}).setdefault(row.get('tax_id'), []).append(
|
||||
(row.get('tax_name'), row.get('account_id'), row.get('amount'))
|
||||
)
|
||||
|
||||
line_cmds = []
|
||||
tg_subtotals = {}
|
||||
cur = self.env.company.currency_id
|
||||
|
||||
for tg, tax_entries in tax_groups.items():
|
||||
if not tg.tax_receivable_account_id or not tg.tax_payable_account_id:
|
||||
continue
|
||||
tg_total = 0
|
||||
for _tid, vals_list in tax_entries.items():
|
||||
for t_name, acct_id, amt in vals_list:
|
||||
line_cmds.append((0, 0, {
|
||||
'name': t_name,
|
||||
'debit': abs(amt) if amt < 0 else 0,
|
||||
'credit': amt if amt > 0 else 0,
|
||||
'account_id': acct_id,
|
||||
}))
|
||||
tg_total += amt
|
||||
|
||||
if not cur.is_zero(tg_total):
|
||||
key = (
|
||||
tg.advance_tax_payment_account_id.id or False,
|
||||
tg.tax_receivable_account_id.id,
|
||||
tg.tax_payable_account_id.id,
|
||||
)
|
||||
tg_subtotals[key] = tg_subtotals.get(key, 0) + tg_total
|
||||
|
||||
if not line_cmds:
|
||||
rep_in = self.env['account.tax.repartition.line'].search([
|
||||
*self.env['account.tax.repartition.line']._check_company_domain(company),
|
||||
('repartition_type', '=', 'tax'),
|
||||
('document_type', '=', 'invoice'),
|
||||
('tax_id.type_tax_use', '=', 'purchase'),
|
||||
], limit=1)
|
||||
rep_out = self.env['account.tax.repartition.line'].search([
|
||||
*self.env['account.tax.repartition.line']._check_company_domain(company),
|
||||
('repartition_type', '=', 'tax'),
|
||||
('document_type', '=', 'invoice'),
|
||||
('tax_id.type_tax_use', '=', 'sale'),
|
||||
], limit=1)
|
||||
if rep_out.account_id and rep_in.account_id:
|
||||
line_cmds = [
|
||||
Command.create({'name': _('Tax Received Adjustment'), 'debit': 0, 'credit': 0.0, 'account_id': rep_out.account_id.id}),
|
||||
Command.create({'name': _('Tax Paid Adjustment'), 'debit': 0.0, 'credit': 0, 'account_id': rep_in.account_id.id}),
|
||||
]
|
||||
|
||||
return line_cmds, tg_subtotals
|
||||
|
||||
def _get_vat_closing_entry_additional_domain(self):
|
||||
return []
|
||||
|
||||
def _postprocess_vat_closing_entry_results(self, company, options, results):
|
||||
return results
|
||||
|
||||
def _vat_closing_entry_results_rounding(self, company, options, results, rounding_accounts, vat_results_summary):
|
||||
if not rounding_accounts.get('profit') or not rounding_accounts.get('loss'):
|
||||
return results
|
||||
|
||||
total_amt = sum(r['amount'] for r in results)
|
||||
last_tg_id = results[-1]['tax_group_id'] if results else None
|
||||
report = self.env['account.report'].browse(options['report_id'])
|
||||
|
||||
for ln in report._get_lines(options):
|
||||
mdl, rec_id = report._get_model_info_from_id(ln['id'])
|
||||
if mdl != 'account.report.line':
|
||||
continue
|
||||
for (op_type, rpt_line_id, col_label) in vat_results_summary:
|
||||
for col in ln['columns']:
|
||||
if rec_id != rpt_line_id or col['expression_label'] != col_label:
|
||||
continue
|
||||
if op_type in {'due', 'total'}:
|
||||
total_amt += col['no_format']
|
||||
elif op_type == 'deductible':
|
||||
total_amt -= col['no_format']
|
||||
|
||||
diff = company.currency_id.round(total_amt)
|
||||
if not company.currency_id.is_zero(diff):
|
||||
results.append({
|
||||
'tax_name': _('Difference from rounding taxes'),
|
||||
'amount': diff * -1,
|
||||
'tax_group_id': last_tg_id,
|
||||
'account_id': rounding_accounts['profit'].id if diff < 0 else rounding_accounts['loss'].id,
|
||||
})
|
||||
return results
|
||||
|
||||
@api.model
|
||||
def _add_tax_group_closing_items(self, tg_subtotals, closing_move):
|
||||
sql_balance = '''
|
||||
SELECT SUM(aml.balance) AS balance
|
||||
FROM account_move_line aml
|
||||
LEFT JOIN account_move move ON move.id = aml.move_id
|
||||
WHERE aml.account_id = %s AND aml.date <= %s AND move.state = 'posted' AND aml.company_id = %s
|
||||
'''
|
||||
cur = closing_move.company_id.currency_id
|
||||
cmds = []
|
||||
balanced_accounts = []
|
||||
|
||||
def _balance_account(acct_id, lbl):
|
||||
self.env.cr.execute(sql_balance, (acct_id, closing_move.date, closing_move.company_id.id))
|
||||
row = self.env.cr.dictfetchone()
|
||||
bal = row.get('balance') or 0
|
||||
if not cur.is_zero(bal):
|
||||
cmds.append((0, 0, {
|
||||
'name': lbl,
|
||||
'debit': abs(bal) if bal < 0 else 0,
|
||||
'credit': abs(bal) if bal > 0 else 0,
|
||||
'account_id': acct_id,
|
||||
}))
|
||||
return bal
|
||||
|
||||
for key, val in tg_subtotals.items():
|
||||
running = val
|
||||
if key[0] and key[0] not in balanced_accounts:
|
||||
running += _balance_account(key[0], _('Balance tax advance payment account'))
|
||||
balanced_accounts.append(key[0])
|
||||
if key[1] and key[1] not in balanced_accounts:
|
||||
running += _balance_account(key[1], _('Balance tax current account (receivable)'))
|
||||
balanced_accounts.append(key[1])
|
||||
if key[2] and key[2] not in balanced_accounts:
|
||||
running += _balance_account(key[2], _('Balance tax current account (payable)'))
|
||||
balanced_accounts.append(key[2])
|
||||
if not cur.is_zero(running):
|
||||
cmds.append(Command.create({
|
||||
'name': _('Payable tax amount') if running < 0 else _('Receivable tax amount'),
|
||||
'debit': running if running > 0 else 0,
|
||||
'credit': abs(running) if running < 0 else 0,
|
||||
'account_id': key[2] if running < 0 else key[1],
|
||||
}))
|
||||
return cmds
|
||||
|
||||
@api.model
|
||||
def _redirect_to_misconfigured_tax_groups(self, company, countries):
|
||||
raise RedirectWarning(
|
||||
_('Please specify the accounts necessary for the Tax Closing Entry.'),
|
||||
{
|
||||
'type': 'ir.actions.act_window', 'name': 'Tax groups',
|
||||
'res_model': 'account.tax.group', 'view_mode': 'list',
|
||||
'views': [[False, 'list']],
|
||||
'domain': ['|', ('country_id', 'in', countries.ids), ('country_id', '=', False)],
|
||||
},
|
||||
_('Configure your TAX accounts - %s', company.display_name),
|
||||
)
|
||||
|
||||
def _get_fpos_info_for_tax_closing(self, company, report, options):
|
||||
if options['fiscal_position'] == 'domestic':
|
||||
fpos = self.env['account.fiscal.position']
|
||||
elif options['fiscal_position'] == 'all':
|
||||
fpos = self.env['account.fiscal.position'].search([
|
||||
*self.env['account.fiscal.position']._check_company_domain(company),
|
||||
('foreign_vat', '!=', False),
|
||||
])
|
||||
else:
|
||||
fpos = self.env['account.fiscal.position'].browse([options['fiscal_position']])
|
||||
|
||||
if options['fiscal_position'] == 'all':
|
||||
fiscal_country = company.account_fiscal_country_id
|
||||
include_dom = (
|
||||
not fpos or not report.country_id
|
||||
or fiscal_country == fpos[0].country_id
|
||||
)
|
||||
else:
|
||||
include_dom = options['fiscal_position'] == 'domestic'
|
||||
|
||||
return include_dom, fpos
|
||||
|
||||
def _get_amls_with_archived_tags_domain(self, options):
|
||||
domain = [
|
||||
('tax_tag_ids.active', '=', False),
|
||||
('parent_state', '=', 'posted'),
|
||||
('date', '>=', options['date']['date_from']),
|
||||
]
|
||||
if options['date']['mode'] == 'single':
|
||||
domain.append(('date', '<=', options['date']['date_to']))
|
||||
return domain
|
||||
|
||||
def action_open_amls_with_archived_tags(self, options, params=None):
|
||||
return {
|
||||
'name': _("Journal items with archived tax tags"),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'account.move.line',
|
||||
'domain': self._get_amls_with_archived_tags_domain(options),
|
||||
'context': {'active_test': False},
|
||||
'views': [(self.env.ref('fusion_accounting.view_archived_tag_move_tree').id, 'list')],
|
||||
}
|
||||
|
||||
|
||||
class FusionGenericTaxReportHandler(models.AbstractModel):
|
||||
"""Handler for the standard generic tax report (Tax -> Tax grouping)."""
|
||||
|
||||
_name = 'account.generic.tax.report.handler'
|
||||
_inherit = 'account.tax.report.handler'
|
||||
_description = 'Generic Tax Report Custom Handler'
|
||||
|
||||
def _get_custom_display_config(self):
|
||||
cfg = super()._get_custom_display_config()
|
||||
cfg['css_custom_class'] = 'generic_tax_report'
|
||||
cfg['templates']['AccountReportLineName'] = 'fusion_accounting.TaxReportLineName'
|
||||
return cfg
|
||||
|
||||
def _custom_options_initializer(self, report, options, previous_options=None):
|
||||
super()._custom_options_initializer(report, options, previous_options=previous_options)
|
||||
if (
|
||||
not report.country_id
|
||||
and len(options['available_vat_fiscal_positions']) <= (0 if options['allow_domestic'] else 1)
|
||||
and len(options['companies']) <= 1
|
||||
):
|
||||
options['allow_domestic'] = False
|
||||
options['fiscal_position'] = 'all'
|
||||
|
||||
def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None):
|
||||
return self._get_dynamic_lines(report, options, 'default', warnings)
|
||||
|
||||
def _caret_options_initializer(self):
|
||||
return {
|
||||
'generic_tax_report': [
|
||||
{'name': _("Audit"), 'action': 'caret_option_audit_tax'},
|
||||
],
|
||||
}
|
||||
|
||||
def _get_dynamic_lines(self, report, options, grouping, warnings=None):
|
||||
opts_per_cg = report._split_options_per_column_group(options)
|
||||
|
||||
if grouping == 'tax_account':
|
||||
gb_fields = [('src_tax', 'type_tax_use'), ('src_tax', 'id'), ('account', 'id')]
|
||||
comodel_list = [None, 'account.tax', 'account.account']
|
||||
elif grouping == 'account_tax':
|
||||
gb_fields = [('src_tax', 'type_tax_use'), ('account', 'id'), ('src_tax', 'id')]
|
||||
comodel_list = [None, 'account.account', 'account.tax']
|
||||
else:
|
||||
gb_fields = [('src_tax', 'type_tax_use'), ('src_tax', 'id')]
|
||||
comodel_list = [None, 'account.tax']
|
||||
|
||||
if grouping in ('tax_account', 'account_tax'):
|
||||
amount_tree = self._read_generic_tax_report_amounts(report, opts_per_cg, gb_fields)
|
||||
else:
|
||||
amount_tree = self._read_generic_tax_report_amounts_no_tax_details(report, options, opts_per_cg)
|
||||
|
||||
id_sets = [set() for _ in gb_fields]
|
||||
|
||||
def _collect_ids(node, depth=0):
|
||||
for k, v in node.items():
|
||||
if k:
|
||||
id_sets[depth].add(k)
|
||||
if v.get('children'):
|
||||
_collect_ids(v['children'], depth + 1)
|
||||
|
||||
_collect_ids(amount_tree)
|
||||
|
||||
sort_maps = []
|
||||
for i, cm in enumerate(comodel_list):
|
||||
if cm:
|
||||
recs = self.env[cm].with_context(active_test=False).search([('id', 'in', tuple(id_sets[i]))])
|
||||
sort_maps.append({r.id: (r, j) for j, r in enumerate(recs)})
|
||||
else:
|
||||
sel = self.env['account.tax']._fields['type_tax_use'].selection
|
||||
sort_maps.append({v[0]: (v, j) for j, v in enumerate(sel) if v[0] in id_sets[i]})
|
||||
|
||||
output = []
|
||||
self._populate_lines_recursively(report, options, output, sort_maps, gb_fields, amount_tree, warnings=warnings)
|
||||
return output
|
||||
|
||||
# ================================================================
|
||||
# AMOUNT COMPUTATION
|
||||
# ================================================================
|
||||
|
||||
@api.model
|
||||
def _read_generic_tax_report_amounts_no_tax_details(self, report, options, opts_per_cg):
|
||||
co_ids = report.get_report_company_ids(options)
|
||||
co_domain = self.env['account.tax']._check_company_domain(co_ids)
|
||||
co_where = self.env['account.tax'].with_context(active_test=False)._where_calc(co_domain)
|
||||
self.env.cr.execute(SQL('''
|
||||
SELECT account_tax.id, account_tax.type_tax_use,
|
||||
ARRAY_AGG(child_tax.id) AS child_tax_ids,
|
||||
ARRAY_AGG(DISTINCT child_tax.type_tax_use) AS child_types
|
||||
FROM account_tax_filiation_rel account_tax_rel
|
||||
JOIN account_tax ON account_tax.id = account_tax_rel.parent_tax
|
||||
JOIN account_tax child_tax ON child_tax.id = account_tax_rel.child_tax
|
||||
WHERE account_tax.amount_type = 'group' AND %s
|
||||
GROUP BY account_tax.id
|
||||
''', co_where.where_clause or SQL("TRUE")))
|
||||
|
||||
group_info = {}
|
||||
child_map = {}
|
||||
for row in self.env.cr.dictfetchall():
|
||||
row['to_expand'] = row['child_types'] != ['none']
|
||||
group_info[row['id']] = row
|
||||
for cid in row['child_tax_ids']:
|
||||
child_map[cid] = row['id']
|
||||
|
||||
results = defaultdict(lambda: {
|
||||
'base_amount': {cg: 0.0 for cg in options['column_groups']},
|
||||
'tax_amount': {cg: 0.0 for cg in options['column_groups']},
|
||||
'tax_non_deductible': {cg: 0.0 for cg in options['column_groups']},
|
||||
'tax_deductible': {cg: 0.0 for cg in options['column_groups']},
|
||||
'tax_due': {cg: 0.0 for cg in options['column_groups']},
|
||||
'children': defaultdict(lambda: {
|
||||
'base_amount': {cg: 0.0 for cg in options['column_groups']},
|
||||
'tax_amount': {cg: 0.0 for cg in options['column_groups']},
|
||||
'tax_non_deductible': {cg: 0.0 for cg in options['column_groups']},
|
||||
'tax_deductible': {cg: 0.0 for cg in options['column_groups']},
|
||||
'tax_due': {cg: 0.0 for cg in options['column_groups']},
|
||||
}),
|
||||
})
|
||||
|
||||
for cg_key, cg_opts in opts_per_cg.items():
|
||||
qry = report._get_report_query(cg_opts, 'strict_range')
|
||||
|
||||
# Base amounts
|
||||
self.env.cr.execute(SQL('''
|
||||
SELECT tax.id AS tax_id, tax.type_tax_use AS tax_type_tax_use,
|
||||
src_group_tax.id AS src_group_tax_id, src_group_tax.type_tax_use AS src_group_tax_type_tax_use,
|
||||
src_tax.id AS src_tax_id, src_tax.type_tax_use AS src_tax_type_tax_use,
|
||||
SUM(account_move_line.balance) AS base_amount
|
||||
FROM %(tbl)s
|
||||
JOIN account_move_line_account_tax_rel tax_rel ON account_move_line.id = tax_rel.account_move_line_id
|
||||
JOIN account_tax tax ON tax.id = tax_rel.account_tax_id
|
||||
LEFT JOIN account_tax src_tax ON src_tax.id = account_move_line.tax_line_id
|
||||
LEFT JOIN account_tax src_group_tax ON src_group_tax.id = account_move_line.group_tax_id
|
||||
WHERE %(where)s
|
||||
AND (account_move_line__move_id.always_tax_exigible OR account_move_line__move_id.tax_cash_basis_rec_id IS NOT NULL OR tax.tax_exigibility != 'on_payment')
|
||||
AND (
|
||||
(account_move_line.tax_line_id IS NOT NULL AND (src_tax.type_tax_use IN ('sale','purchase') OR src_group_tax.type_tax_use IN ('sale','purchase')))
|
||||
OR (account_move_line.tax_line_id IS NULL AND tax.type_tax_use IN ('sale','purchase'))
|
||||
)
|
||||
GROUP BY tax.id, src_group_tax.id, src_tax.id
|
||||
ORDER BY src_group_tax.sequence, src_group_tax.id, src_tax.sequence, src_tax.id, tax.sequence, tax.id
|
||||
''', tbl=qry.from_clause, where=qry.where_clause))
|
||||
|
||||
groups_with_extra = set()
|
||||
for r in self.env.cr.dictfetchall():
|
||||
is_tax_ln = bool(r['src_tax_id'])
|
||||
if is_tax_ln:
|
||||
if r['src_group_tax_id'] and not group_info.get(r['src_group_tax_id'], {}).get('to_expand') and r['tax_id'] in group_info.get(r['src_group_tax_id'], {}).get('child_tax_ids', []):
|
||||
pass
|
||||
elif r['tax_type_tax_use'] == 'none' and child_map.get(r['tax_id']):
|
||||
gid = child_map[r['tax_id']]
|
||||
if gid not in groups_with_extra:
|
||||
gi = group_info[gid]
|
||||
results[gi['type_tax_use']]['children'][gid]['base_amount'][cg_key] += r['base_amount']
|
||||
groups_with_extra.add(gid)
|
||||
else:
|
||||
ttu = r['src_group_tax_type_tax_use'] or r['src_tax_type_tax_use']
|
||||
results[ttu]['children'][r['tax_id']]['base_amount'][cg_key] += r['base_amount']
|
||||
else:
|
||||
if r['tax_id'] in group_info and group_info[r['tax_id']]['to_expand']:
|
||||
gi = group_info[r['tax_id']]
|
||||
for child_id in gi['child_tax_ids']:
|
||||
results[gi['type_tax_use']]['children'][child_id]['base_amount'][cg_key] += r['base_amount']
|
||||
else:
|
||||
results[r['tax_type_tax_use']]['children'][r['tax_id']]['base_amount'][cg_key] += r['base_amount']
|
||||
|
||||
# Tax amounts
|
||||
sel_ded = join_ded = gb_ded = SQL()
|
||||
if cg_opts.get('account_journal_report_tax_deductibility_columns'):
|
||||
sel_ded = SQL(", repartition.use_in_tax_closing AS trl_tax_closing, SIGN(repartition.factor_percent) AS trl_factor")
|
||||
join_ded = SQL("JOIN account_tax_repartition_line repartition ON account_move_line.tax_repartition_line_id = repartition.id")
|
||||
gb_ded = SQL(', repartition.use_in_tax_closing, SIGN(repartition.factor_percent)')
|
||||
|
||||
self.env.cr.execute(SQL('''
|
||||
SELECT tax.id AS tax_id, tax.type_tax_use AS tax_type_tax_use,
|
||||
group_tax.id AS group_tax_id, group_tax.type_tax_use AS group_tax_type_tax_use,
|
||||
SUM(account_move_line.balance) AS tax_amount %(sel_ded)s
|
||||
FROM %(tbl)s
|
||||
JOIN account_tax tax ON tax.id = account_move_line.tax_line_id
|
||||
%(join_ded)s
|
||||
LEFT JOIN account_tax group_tax ON group_tax.id = account_move_line.group_tax_id
|
||||
WHERE %(where)s
|
||||
AND (account_move_line__move_id.always_tax_exigible OR account_move_line__move_id.tax_cash_basis_rec_id IS NOT NULL OR tax.tax_exigibility != 'on_payment')
|
||||
AND ((group_tax.id IS NULL AND tax.type_tax_use IN ('sale','purchase')) OR (group_tax.id IS NOT NULL AND group_tax.type_tax_use IN ('sale','purchase')))
|
||||
GROUP BY tax.id, group_tax.id %(gb_ded)s
|
||||
''', sel_ded=sel_ded, tbl=qry.from_clause, join_ded=join_ded, where=qry.where_clause, gb_ded=gb_ded))
|
||||
|
||||
for r in self.env.cr.dictfetchall():
|
||||
tid = r['tax_id']
|
||||
if r['group_tax_id']:
|
||||
ttu = r['group_tax_type_tax_use']
|
||||
if not group_info.get(r['group_tax_id'], {}).get('to_expand'):
|
||||
tid = r['group_tax_id']
|
||||
else:
|
||||
ttu = r['group_tax_type_tax_use'] or r['tax_type_tax_use']
|
||||
|
||||
results[ttu]['tax_amount'][cg_key] += r['tax_amount']
|
||||
results[ttu]['children'][tid]['tax_amount'][cg_key] += r['tax_amount']
|
||||
|
||||
if cg_opts.get('account_journal_report_tax_deductibility_columns'):
|
||||
detail_label = False
|
||||
if r['trl_factor'] > 0 and ttu == 'purchase':
|
||||
detail_label = 'tax_deductible' if r['trl_tax_closing'] else 'tax_non_deductible'
|
||||
elif r['trl_tax_closing'] and (r['trl_factor'] > 0, ttu) in ((False, 'purchase'), (True, 'sale')):
|
||||
detail_label = 'tax_due'
|
||||
if detail_label:
|
||||
results[ttu][detail_label][cg_key] += r['tax_amount'] * r['trl_factor']
|
||||
results[ttu]['children'][tid][detail_label][cg_key] += r['tax_amount'] * r['trl_factor']
|
||||
|
||||
return results
|
||||
|
||||
def _read_generic_tax_report_amounts(self, report, opts_per_cg, gb_fields):
|
||||
needs_group = False
|
||||
select_parts, gb_parts = [], []
|
||||
for alias, fld in gb_fields:
|
||||
select_parts.append(SQL("%s AS %s", SQL.identifier(alias, fld), SQL.identifier(f'{alias}_{fld}')))
|
||||
gb_parts.append(SQL.identifier(alias, fld))
|
||||
if alias == 'src_tax':
|
||||
select_parts.append(SQL("%s AS %s", SQL.identifier('tax', fld), SQL.identifier(f'tax_{fld}')))
|
||||
gb_parts.append(SQL.identifier('tax', fld))
|
||||
needs_group = True
|
||||
|
||||
expand_set = set()
|
||||
if needs_group:
|
||||
groups = self.env['account.tax'].with_context(active_test=False).search([('amount_type', '=', 'group')])
|
||||
for g in groups:
|
||||
if set(g.children_tax_ids.mapped('type_tax_use')) != {'none'}:
|
||||
expand_set.add(g.id)
|
||||
|
||||
tree = {}
|
||||
for cg_key, cg_opts in opts_per_cg.items():
|
||||
qry = report._get_report_query(cg_opts, 'strict_range')
|
||||
td_qry = self.env['account.move.line']._get_query_tax_details(qry.from_clause, qry.where_clause)
|
||||
seen_keys = set()
|
||||
|
||||
self.env.cr.execute(SQL('''
|
||||
SELECT %(sel)s, trl.document_type = 'refund' AS is_refund,
|
||||
SUM(CASE WHEN tdr.display_type = 'rounding' THEN 0 ELSE tdr.base_amount END) AS base_amount,
|
||||
SUM(tdr.tax_amount) AS tax_amount
|
||||
FROM (%(td)s) AS tdr
|
||||
JOIN account_tax_repartition_line trl ON trl.id = tdr.tax_repartition_line_id
|
||||
JOIN account_tax tax ON tax.id = tdr.tax_id
|
||||
JOIN account_tax src_tax ON src_tax.id = COALESCE(tdr.group_tax_id, tdr.tax_id) AND src_tax.type_tax_use IN ('sale','purchase')
|
||||
JOIN account_account account ON account.id = tdr.base_account_id
|
||||
WHERE tdr.tax_exigible
|
||||
GROUP BY tdr.tax_repartition_line_id, trl.document_type, %(gb)s
|
||||
ORDER BY src_tax.sequence, src_tax.id, tax.sequence, tax.id
|
||||
''', sel=SQL(',').join(select_parts), td=td_qry, gb=SQL(',').join(gb_parts)))
|
||||
|
||||
for row in self.env.cr.dictfetchall():
|
||||
node = tree
|
||||
cum_key = [row['is_refund']]
|
||||
for alias, fld in gb_fields:
|
||||
gk = f'{alias}_{fld}'
|
||||
if gk == 'src_tax_id' and row['src_tax_id'] in expand_set:
|
||||
cum_key.append(row[gk])
|
||||
gk = 'tax_id'
|
||||
rk = row[gk]
|
||||
cum_key.append(rk)
|
||||
ck_tuple = tuple(cum_key)
|
||||
node.setdefault(rk, {
|
||||
'base_amount': {k: 0.0 for k in cg_opts['column_groups']},
|
||||
'tax_amount': {k: 0.0 for k in cg_opts['column_groups']},
|
||||
'children': {},
|
||||
})
|
||||
sub = node[rk]
|
||||
if ck_tuple not in seen_keys:
|
||||
sub['base_amount'][cg_key] += row['base_amount']
|
||||
sub['tax_amount'][cg_key] += row['tax_amount']
|
||||
node = sub['children']
|
||||
seen_keys.add(ck_tuple)
|
||||
return tree
|
||||
|
||||
def _populate_lines_recursively(
|
||||
self, report, options, lines, sort_maps, gb_fields, node,
|
||||
index=0, type_tax_use=None, parent_line_id=None, warnings=None,
|
||||
):
|
||||
if index == len(gb_fields):
|
||||
return
|
||||
alias, fld = gb_fields[index]
|
||||
gk = f'{alias}_{fld}'
|
||||
smap = sort_maps[index]
|
||||
sorted_keys = sorted(node.keys(), key=lambda k: smap[k][1])
|
||||
|
||||
for key in sorted_keys:
|
||||
if gk == 'src_tax_type_tax_use':
|
||||
type_tax_use = key
|
||||
sign = -1 if type_tax_use == 'sale' else 1
|
||||
|
||||
amounts = node[key]
|
||||
cols = []
|
||||
for col in options['columns']:
|
||||
el = col.get('expression_label')
|
||||
if el == 'net':
|
||||
cv = sign * amounts['base_amount'][col['column_group_key']] if index == len(gb_fields) - 1 else ''
|
||||
if el == 'tax':
|
||||
cv = sign * amounts['tax_amount'][col['column_group_key']]
|
||||
cols.append(report._build_column_dict(cv, col, options=options))
|
||||
|
||||
if el == 'tax' and options.get('account_journal_report_tax_deductibility_columns'):
|
||||
for dt in ('tax_non_deductible', 'tax_deductible', 'tax_due'):
|
||||
cols.append(report._build_column_dict(
|
||||
col_value=sign * amounts[dt][col['column_group_key']],
|
||||
col_data={'figure_type': 'monetary', 'column_group_key': col['column_group_key'], 'expression_label': dt},
|
||||
options=options,
|
||||
))
|
||||
|
||||
defaults = {'columns': cols, 'level': index if index == 0 else index + 1, 'unfoldable': False}
|
||||
rpt_line = self._build_report_line(report, options, defaults, gk, smap[key][0], parent_line_id, warnings)
|
||||
|
||||
if gk == 'src_tax_id':
|
||||
rpt_line['caret_options'] = 'generic_tax_report'
|
||||
|
||||
lines.append((0, rpt_line))
|
||||
self._populate_lines_recursively(
|
||||
report, options, lines, sort_maps, gb_fields,
|
||||
amounts.get('children'), index=index + 1,
|
||||
type_tax_use=type_tax_use, parent_line_id=rpt_line['id'],
|
||||
warnings=warnings,
|
||||
)
|
||||
|
||||
def _build_report_line(self, report, options, defaults, gk, value, parent_id, warnings=None):
|
||||
ln = dict(defaults)
|
||||
if parent_id is not None:
|
||||
ln['parent_id'] = parent_id
|
||||
|
||||
if gk == 'src_tax_type_tax_use':
|
||||
ln['id'] = report._get_generic_line_id(None, None, markup=value[0], parent_line_id=parent_id)
|
||||
ln['name'] = value[1]
|
||||
elif gk == 'src_tax_id':
|
||||
tax = value
|
||||
ln['id'] = report._get_generic_line_id(tax._name, tax.id, parent_line_id=parent_id)
|
||||
if tax.amount_type == 'percent':
|
||||
ln['name'] = f"{tax.name} ({tax.amount}%)"
|
||||
if warnings is not None:
|
||||
self._check_line_consistency(report, options, ln, tax, warnings)
|
||||
elif tax.amount_type == 'fixed':
|
||||
ln['name'] = f"{tax.name} ({tax.amount})"
|
||||
else:
|
||||
ln['name'] = tax.name
|
||||
if options.get('multi-company'):
|
||||
ln['name'] = f"{ln['name']} - {tax.company_id.display_name}"
|
||||
elif gk == 'account_id':
|
||||
acct = value
|
||||
ln['id'] = report._get_generic_line_id(acct._name, acct.id, parent_line_id=parent_id)
|
||||
ln['name'] = f"{acct.display_name} - {acct.company_id.display_name}" if options.get('multi-company') else acct.display_name
|
||||
return ln
|
||||
|
||||
def _check_line_consistency(self, report, options, ln, tax, warnings=None):
|
||||
eff_rate = tax.amount * sum(
|
||||
tax.invoice_repartition_line_ids.filtered(
|
||||
lambda r: r.repartition_type == 'tax'
|
||||
).mapped('factor')
|
||||
) / 100
|
||||
for cg_key, cg_opts in report._split_options_per_column_group(options).items():
|
||||
net = next((c['no_format'] for c in ln['columns'] if c['column_group_key'] == cg_key and c['expression_label'] == 'net'), 0)
|
||||
tax_val = next((c['no_format'] for c in ln['columns'] if c['column_group_key'] == cg_key and c['expression_label'] == 'tax'), 0)
|
||||
if net == '':
|
||||
continue
|
||||
cur = self.env.company.currency_id
|
||||
expected = float(net or 0) * eff_rate
|
||||
if cur.compare_amounts(expected, tax_val):
|
||||
err = abs(abs(tax_val) - abs(expected)) / float(net or 1)
|
||||
if err > 0.001:
|
||||
ln['alert'] = True
|
||||
warnings['fusion_accounting.tax_report_warning_lines_consistency'] = {'alert_type': 'danger'}
|
||||
return
|
||||
|
||||
# ================================================================
|
||||
# CARET / AUDIT
|
||||
# ================================================================
|
||||
|
||||
def caret_option_audit_tax(self, options, params):
|
||||
report = self.env['account.report'].browse(options['report_id'])
|
||||
mdl, tax_id = report._get_model_info_from_id(params['line_id'])
|
||||
if mdl != 'account.tax':
|
||||
raise UserError(_("Cannot audit tax from a non-tax model."))
|
||||
|
||||
tax = self.env['account.tax'].browse(tax_id)
|
||||
if tax.amount_type == 'group':
|
||||
affect_domain = [('tax_ids', 'in', tax.children_tax_ids.ids), ('tax_repartition_line_id', '!=', False)]
|
||||
else:
|
||||
affect_domain = [('tax_ids', '=', tax.id), ('tax_ids.type_tax_use', '=', tax.type_tax_use), ('tax_repartition_line_id', '!=', False)]
|
||||
|
||||
domain = report._get_options_domain(options, 'strict_range') + expression.OR((
|
||||
[('tax_ids', 'in', tax.ids), ('tax_ids.type_tax_use', '=', tax.type_tax_use), ('tax_repartition_line_id', '=', False)],
|
||||
[('group_tax_id', '=', tax.id) if tax.amount_type == 'group' else ('tax_line_id', '=', tax.id)],
|
||||
affect_domain,
|
||||
))
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Journal Items for Tax Audit'),
|
||||
'res_model': 'account.move.line',
|
||||
'views': [[self.env.ref('account.view_move_line_tax_audit_tree').id, 'list']],
|
||||
'domain': domain,
|
||||
'context': {**self.env.context, 'search_default_group_by_account': 2, 'expand': 1},
|
||||
}
|
||||
|
||||
|
||||
class FusionGenericTaxReportHandlerAT(models.AbstractModel):
|
||||
_name = 'account.generic.tax.report.handler.account.tax'
|
||||
_inherit = 'account.generic.tax.report.handler'
|
||||
_description = 'Generic Tax Report Custom Handler (Account -> Tax)'
|
||||
|
||||
def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None):
|
||||
return super()._get_dynamic_lines(report, options, 'account_tax', warnings)
|
||||
|
||||
|
||||
class FusionGenericTaxReportHandlerTA(models.AbstractModel):
|
||||
_name = 'account.generic.tax.report.handler.tax.account'
|
||||
_inherit = 'account.generic.tax.report.handler'
|
||||
_description = 'Generic Tax Report Custom Handler (Tax -> Account)'
|
||||
|
||||
def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None):
|
||||
return super()._get_dynamic_lines(report, options, 'tax_account', warnings)
|
||||
Reference in New Issue
Block a user