931 lines
45 KiB
Python
931 lines
45 KiB
Python
# Fusion Accounting - Journal Report Handler
|
|
# Full journal audit with tax summaries, PDF/XLSX export, bank journal support
|
|
|
|
import io
|
|
import datetime
|
|
|
|
from PIL import ImageFont
|
|
from markupsafe import Markup
|
|
from collections import defaultdict
|
|
|
|
from odoo import models, _
|
|
from odoo.tools import SQL
|
|
from odoo.tools.misc import file_path
|
|
try:
|
|
from odoo.tools.misc import xlsxwriter
|
|
except ImportError:
|
|
import xlsxwriter
|
|
|
|
XLSX_GRAY_200 = '#EEEEEE'
|
|
XLSX_BORDER_COLOR = '#B4B4B4'
|
|
XLSX_FONT_SIZE_DEFAULT = 8
|
|
XLSX_FONT_SIZE_HEADING = 11
|
|
|
|
|
|
class FusionJournalReportHandler(models.AbstractModel):
|
|
"""Custom handler for the Journal Audit report. Produces detailed
|
|
per-journal line listings, tax summaries (per-journal and global),
|
|
and supports PDF and XLSX export."""
|
|
|
|
_name = "account.journal.report.handler"
|
|
_inherit = "account.report.custom.handler"
|
|
_description = "Journal Report Custom Handler"
|
|
|
|
# ================================================================
|
|
# OPTIONS
|
|
# ================================================================
|
|
|
|
def _custom_options_initializer(self, report, options, previous_options):
|
|
options['ignore_totals_below_sections'] = True
|
|
options['show_payment_lines'] = previous_options.get('show_payment_lines', True)
|
|
|
|
def _get_custom_display_config(self):
|
|
return {
|
|
'css_custom_class': 'journal_report',
|
|
'pdf_css_custom_class': 'journal_report_pdf',
|
|
'components': {
|
|
'AccountReportLine': 'fusion_accounting.JournalReportLine',
|
|
},
|
|
'templates': {
|
|
'AccountReportFilters': 'fusion_accounting.JournalReportFilters',
|
|
'AccountReportLineName': 'fusion_accounting.JournalReportLineName',
|
|
},
|
|
}
|
|
|
|
# ================================================================
|
|
# CUSTOM ENGINE
|
|
# ================================================================
|
|
|
|
def _report_custom_engine_journal_report(
|
|
self, expressions, options, date_scope, current_groupby,
|
|
next_groupby, offset=0, limit=None, warnings=None,
|
|
):
|
|
def _assemble_result(groupby_key, row):
|
|
if groupby_key == 'account_id':
|
|
code = row['account_code'][0]
|
|
elif groupby_key == 'journal_id':
|
|
code = row['journal_code'][0]
|
|
else:
|
|
code = None
|
|
return row['grouping_key'], {
|
|
'code': code,
|
|
'credit': row['credit'],
|
|
'debit': row['debit'],
|
|
'balance': row['balance'] if groupby_key == 'account_id' else None,
|
|
}
|
|
|
|
report = self.env['account.report'].browse(options['report_id'])
|
|
report._check_groupby_fields(
|
|
(next_groupby.split(',') if next_groupby else [])
|
|
+ ([current_groupby] if current_groupby else []),
|
|
)
|
|
|
|
if not current_groupby:
|
|
return {'code': None, 'debit': None, 'credit': None, 'balance': None}
|
|
|
|
qry = report._get_report_query(options, 'strict_range')
|
|
acct_alias = qry.join(
|
|
lhs_alias='account_move_line', lhs_column='account_id',
|
|
rhs_table='account_account', rhs_column='id', link='account_id',
|
|
)
|
|
acct_code = self.env['account.account']._field_to_sql(acct_alias, 'code', qry)
|
|
gb_col = SQL.identifier('account_move_line', current_groupby)
|
|
sel_gb = SQL('%s AS grouping_key', gb_col)
|
|
|
|
stmt = SQL(
|
|
"""
|
|
SELECT %(sel_gb)s,
|
|
ARRAY_AGG(DISTINCT %(acct_code)s) AS account_code,
|
|
ARRAY_AGG(DISTINCT j.code) AS journal_code,
|
|
SUM("account_move_line".debit) AS debit,
|
|
SUM("account_move_line".credit) AS credit,
|
|
SUM("account_move_line".balance) AS balance
|
|
FROM %(tbl)s
|
|
JOIN account_move am ON am.id = account_move_line.move_id
|
|
JOIN account_journal j ON j.id = am.journal_id
|
|
JOIN res_company cp ON cp.id = am.company_id
|
|
WHERE %(pmt_filter)s AND %(where)s
|
|
GROUP BY %(gb_col)s
|
|
ORDER BY %(gb_col)s
|
|
""",
|
|
sel_gb=sel_gb,
|
|
acct_code=acct_code,
|
|
tbl=qry.from_clause,
|
|
where=qry.where_clause,
|
|
pmt_filter=self._get_payment_lines_filter_case_statement(options),
|
|
gb_col=gb_col,
|
|
)
|
|
self.env.cr.execute(stmt)
|
|
return [_assemble_result(current_groupby, r) for r in self.env.cr.dictfetchall()]
|
|
|
|
# ================================================================
|
|
# LINE POST-PROCESSING
|
|
# ================================================================
|
|
|
|
def _custom_line_postprocessor(self, report, options, lines):
|
|
"""Inject tax summary sub-tables after journal account sections
|
|
and append a global tax summary when applicable."""
|
|
enriched = []
|
|
for idx, ln in enumerate(lines):
|
|
enriched.append(ln)
|
|
line_model, res_id = report._get_model_info_from_id(ln['id'])
|
|
|
|
if line_model == 'account.journal':
|
|
ln['journal_id'] = res_id
|
|
elif line_model == 'account.account':
|
|
id_map = report._get_res_ids_from_line_id(
|
|
ln['id'], ['account.journal', 'account.account'],
|
|
)
|
|
ln['journal_id'] = id_map['account.journal']
|
|
ln['account_id'] = id_map['account.account']
|
|
ln['date'] = options['date']
|
|
|
|
jnl = self.env['account.journal'].browse(ln['journal_id'])
|
|
is_last_acct = (
|
|
idx + 1 == len(lines)
|
|
or report._get_model_info_from_id(lines[idx + 1]['id'])[0] != 'account.account'
|
|
)
|
|
if is_last_acct and self._section_has_tax(options, jnl.id):
|
|
enriched.append({
|
|
'id': report._get_generic_line_id(
|
|
False, False,
|
|
parent_line_id=ln['parent_id'],
|
|
markup='tax_report_section',
|
|
),
|
|
'name': '',
|
|
'parent_id': ln['parent_id'],
|
|
'journal_id': jnl.id,
|
|
'is_tax_section_line': True,
|
|
'columns': [],
|
|
'colspan': len(options['columns']) + 1,
|
|
'level': 4,
|
|
**self._get_tax_summary_section(
|
|
options, {'id': jnl.id, 'type': jnl.type},
|
|
),
|
|
})
|
|
|
|
if report._get_model_info_from_id(lines[0]['id'])[0] == 'account.report.line':
|
|
if self._section_has_tax(options, False):
|
|
enriched.append({
|
|
'id': report._get_generic_line_id(False, False, markup='tax_report_section_heading'),
|
|
'name': _('Global Tax Summary'),
|
|
'level': 0,
|
|
'columns': [],
|
|
'unfoldable': False,
|
|
'colspan': len(options['columns']) + 1,
|
|
})
|
|
enriched.append({
|
|
'id': report._get_generic_line_id(False, False, markup='tax_report_section'),
|
|
'name': '',
|
|
'is_tax_section_line': True,
|
|
'columns': [],
|
|
'colspan': len(options['columns']) + 1,
|
|
'level': 4,
|
|
'class': 'o_account_reports_ja_subtable',
|
|
**self._get_tax_summary_section(options),
|
|
})
|
|
|
|
return enriched
|
|
|
|
# ================================================================
|
|
# PDF EXPORT
|
|
# ================================================================
|
|
|
|
def export_to_pdf(self, options):
|
|
report = self.env['account.report'].browse(options['report_id'])
|
|
base_url = report.get_base_url()
|
|
print_opts = {
|
|
**report.get_options(previous_options={**options, 'export_mode': 'print'}),
|
|
'css_custom_class': self._get_custom_display_config().get(
|
|
'pdf_css_custom_class', 'journal_report_pdf',
|
|
),
|
|
}
|
|
ctx = {'mode': 'print', 'base_url': base_url, 'company': self.env.company}
|
|
|
|
footer_html = self.env['ir.actions.report']._render_template(
|
|
'fusion_accounting.internal_layout', values=ctx,
|
|
)
|
|
footer_html = self.env['ir.actions.report']._render_template(
|
|
'web.minimal_layout',
|
|
values=dict(ctx, subst=True, body=Markup(footer_html.decode())),
|
|
)
|
|
|
|
doc_data = self._generate_document_data_for_export(report, print_opts, 'pdf')
|
|
body_html = self.env['ir.qweb']._render(
|
|
'fusion_accounting.journal_report_pdf_export_main',
|
|
{'report': report, 'options': print_opts, 'base_url': base_url, 'document_data': doc_data},
|
|
)
|
|
|
|
pdf_bytes = io.BytesIO(
|
|
self.env['ir.actions.report']._run_wkhtmltopdf(
|
|
[body_html],
|
|
footer=footer_html.decode(),
|
|
landscape=False,
|
|
specific_paperformat_args={
|
|
'data-report-margin-top': 10,
|
|
'data-report-header-spacing': 10,
|
|
'data-report-margin-bottom': 15,
|
|
},
|
|
)
|
|
)
|
|
result = pdf_bytes.getvalue()
|
|
pdf_bytes.close()
|
|
|
|
return {
|
|
'file_name': report.get_default_report_filename(print_opts, 'pdf'),
|
|
'file_content': result,
|
|
'file_type': 'pdf',
|
|
}
|
|
|
|
# ================================================================
|
|
# XLSX EXPORT
|
|
# ================================================================
|
|
|
|
def export_to_xlsx(self, options, response=None):
|
|
wb_buffer = io.BytesIO()
|
|
wb = xlsxwriter.Workbook(wb_buffer, {'in_memory': True, 'strings_to_formulas': False})
|
|
report = self.env['account.report'].search([('id', '=', options['report_id'])], limit=1)
|
|
print_opts = report.get_options(previous_options={**options, 'export_mode': 'print'})
|
|
doc_data = self._generate_document_data_for_export(report, print_opts, 'xlsx')
|
|
|
|
font_cache = {}
|
|
for sz in (XLSX_FONT_SIZE_HEADING, XLSX_FONT_SIZE_DEFAULT):
|
|
font_cache[sz] = defaultdict()
|
|
for variant in ('Reg', 'Bol', 'RegIta', 'BolIta'):
|
|
try:
|
|
path = f'web/static/fonts/lato/Lato-{variant}-webfont.ttf'
|
|
font_cache[sz][variant] = ImageFont.truetype(file_path(path), sz)
|
|
except (OSError, FileNotFoundError):
|
|
font_cache[sz][variant] = ImageFont.load_default()
|
|
|
|
for jv in doc_data['journals_vals']:
|
|
cx, cy = 0, 0
|
|
ws = wb.add_worksheet(jv['name'][:31])
|
|
cols = jv['columns']
|
|
|
|
for col in cols:
|
|
alignment = 'right' if 'o_right_alignment' in col.get('class', '') else 'left'
|
|
self._write_cell(
|
|
cx, cy, col['name'], 1, False, report, font_cache, wb, ws,
|
|
XLSX_FONT_SIZE_HEADING, True, XLSX_GRAY_200, alignment, 2, 2,
|
|
)
|
|
cx += 1
|
|
|
|
cy += 1
|
|
cx = 0
|
|
for row in jv['lines'][:-1]:
|
|
first_aml = False
|
|
for col in cols:
|
|
top_bdr = 1 if first_aml else 0
|
|
alignment = 'right' if 'o_right_alignment' in col.get('class', '') else 'left'
|
|
|
|
if row.get(col['label'], {}).get('data'):
|
|
val = row[col['label']]['data']
|
|
is_dt = isinstance(val, datetime.date)
|
|
is_bold = False
|
|
|
|
if row[col['label']].get('class') and 'o_bold' in row[col['label']]['class']:
|
|
first_aml = True
|
|
top_bdr = 1
|
|
is_bold = True
|
|
|
|
self._write_cell(
|
|
cx, cy, val, 1, is_dt, report, font_cache, wb, ws,
|
|
XLSX_FONT_SIZE_DEFAULT, is_bold, 'white', alignment, 0, top_bdr, XLSX_BORDER_COLOR,
|
|
)
|
|
else:
|
|
self._write_cell(
|
|
cx, cy, '', 1, False, report, font_cache, wb, ws,
|
|
XLSX_FONT_SIZE_DEFAULT, False, 'white', alignment, 0, top_bdr, XLSX_BORDER_COLOR,
|
|
)
|
|
cx += 1
|
|
cx = 0
|
|
cy += 1
|
|
|
|
# Total row
|
|
total_row = jv['lines'][-1]
|
|
for col in cols:
|
|
val = total_row.get(col['label'], {}).get('data', '')
|
|
alignment = 'right' if 'o_right_alignment' in col.get('class', '') else 'left'
|
|
self._write_cell(
|
|
cx, cy, val, 1, False, report, font_cache, wb, ws,
|
|
XLSX_FONT_SIZE_DEFAULT, True, XLSX_GRAY_200, alignment, 2, 2,
|
|
)
|
|
cx += 1
|
|
cx = 0
|
|
|
|
ws.set_default_row(20)
|
|
ws.set_row(0, 30)
|
|
|
|
if jv.get('tax_summary'):
|
|
self._write_tax_summaries_to_sheet(
|
|
report, wb, ws, font_cache, len(cols) + 1, 1, jv['tax_summary'],
|
|
)
|
|
|
|
if doc_data.get('global_tax_summary'):
|
|
self._write_tax_summaries_to_sheet(
|
|
report, wb, wb.add_worksheet(_('Global Tax Summary')[:31]),
|
|
font_cache, 0, 0, doc_data['global_tax_summary'],
|
|
)
|
|
|
|
wb.close()
|
|
wb_buffer.seek(0)
|
|
xlsx_bytes = wb_buffer.read()
|
|
wb_buffer.close()
|
|
|
|
return {
|
|
'file_name': report.get_default_report_filename(options, 'xlsx'),
|
|
'file_content': xlsx_bytes,
|
|
'file_type': 'xlsx',
|
|
}
|
|
|
|
def _write_cell(
|
|
self, x, y, value, colspan, is_datetime, report, fonts, workbook,
|
|
sheet, font_size, bold=False, bg_color='white', align='left',
|
|
border_bottom=0, border_top=0, border_color='0x000000',
|
|
):
|
|
"""Write a styled value to the specified worksheet cell."""
|
|
fmt = workbook.add_format({
|
|
'font_name': 'Arial', 'font_size': font_size, 'bold': bold,
|
|
'bg_color': bg_color, 'align': align,
|
|
'bottom': border_bottom, 'top': border_top, 'border_color': border_color,
|
|
})
|
|
if colspan == 1:
|
|
if is_datetime:
|
|
fmt.set_num_format('yyyy-mm-dd')
|
|
sheet.write_datetime(y, x, value, fmt)
|
|
else:
|
|
if isinstance(value, str):
|
|
value = value.replace('\n', ' ')
|
|
report._set_xlsx_cell_sizes(sheet, fonts[font_size], x, y, value, fmt, colspan > 1)
|
|
sheet.write(y, x, value, fmt)
|
|
else:
|
|
sheet.merge_range(y, x, y, x + colspan - 1, value, fmt)
|
|
|
|
def _write_tax_summaries_to_sheet(self, report, workbook, sheet, fonts, start_x, start_y, tax_summary):
|
|
cx, cy = start_x, start_y
|
|
|
|
taxes = tax_summary.get('tax_report_lines')
|
|
if taxes:
|
|
ar_start = start_x + 1
|
|
cols = []
|
|
if len(taxes) > 1:
|
|
ar_start += 1
|
|
cols.append(_('Country'))
|
|
cols += [_('Name'), _('Base Amount'), _('Tax Amount')]
|
|
if tax_summary.get('tax_non_deductible_column'):
|
|
cols.append(_('Non-Deductible'))
|
|
if tax_summary.get('tax_deductible_column'):
|
|
cols.append(_('Deductible'))
|
|
if tax_summary.get('tax_due_column'):
|
|
cols.append(_('Due'))
|
|
|
|
self._write_cell(cx, cy, _('Taxes Applied'), len(cols), False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_HEADING, True, 'white', 'left', 2)
|
|
cy += 1
|
|
for c in cols:
|
|
a = 'right' if cx >= ar_start else 'left'
|
|
self._write_cell(cx, cy, c, 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, True, XLSX_GRAY_200, a, 2)
|
|
cx += 1
|
|
cx = start_x
|
|
cy += 1
|
|
|
|
for country in taxes:
|
|
first_country_line = True
|
|
for tax in taxes[country]:
|
|
if len(taxes) > 1:
|
|
if first_country_line:
|
|
first_country_line = False
|
|
self._write_cell(cx, cy, country, 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, True, 'white', 'left', 1, 0, XLSX_BORDER_COLOR)
|
|
cx += 1
|
|
self._write_cell(cx, cy, tax['name'], 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, True, 'white', 'left', 1, 0, XLSX_BORDER_COLOR)
|
|
self._write_cell(cx+1, cy, tax['base_amount'], 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, False, 'white', 'right', 1, 0, XLSX_BORDER_COLOR)
|
|
self._write_cell(cx+2, cy, tax['tax_amount'], 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, False, 'white', 'right', 1, 0, XLSX_BORDER_COLOR)
|
|
cx += 3
|
|
if tax_summary.get('tax_non_deductible_column'):
|
|
self._write_cell(cx, cy, tax['tax_non_deductible'], 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, False, 'white', 'right', 1, 0, XLSX_BORDER_COLOR)
|
|
cx += 1
|
|
if tax_summary.get('tax_deductible_column'):
|
|
self._write_cell(cx, cy, tax['tax_deductible'], 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, False, 'white', 'right', 1, 0, XLSX_BORDER_COLOR)
|
|
cx += 1
|
|
if tax_summary.get('tax_due_column'):
|
|
self._write_cell(cx, cy, tax['tax_due'], 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, False, 'white', 'right', 1, 0, XLSX_BORDER_COLOR)
|
|
cx = start_x
|
|
cy += 1
|
|
|
|
cx = start_x
|
|
cy += 2
|
|
|
|
grids = tax_summary.get('tax_grid_summary_lines')
|
|
if grids:
|
|
ar_start = start_x + 1
|
|
gcols = []
|
|
if len(grids) > 1:
|
|
ar_start += 1
|
|
gcols.append(_('Country'))
|
|
gcols += [_('Grid'), _('+'), _('-'), _('Impact On Grid')]
|
|
|
|
self._write_cell(cx, cy, _('Impact On Grid'), len(gcols), False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_HEADING, True, 'white', 'left', 2)
|
|
cy += 1
|
|
for c in gcols:
|
|
a = 'right' if cx >= ar_start else 'left'
|
|
self._write_cell(cx, cy, c, 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, True, XLSX_GRAY_200, a, 2)
|
|
cx += 1
|
|
cx = start_x
|
|
cy += 1
|
|
|
|
for country in grids:
|
|
first_line = True
|
|
for grid_name in grids[country]:
|
|
if len(grids) > 1:
|
|
if first_line:
|
|
first_line = False
|
|
self._write_cell(cx, cy, country, 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, True, 'white', 'left', 1, 0, XLSX_BORDER_COLOR)
|
|
cx += 1
|
|
self._write_cell(cx, cy, grid_name, 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, True, 'white', 'left', 1, 0, XLSX_BORDER_COLOR)
|
|
self._write_cell(cx+1, cy, grids[country][grid_name].get('+', 0), 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, False, 'white', 'right', 1, 0, XLSX_BORDER_COLOR)
|
|
self._write_cell(cx+2, cy, grids[country][grid_name].get('-', 0), 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, False, 'white', 'right', 1, 0, XLSX_BORDER_COLOR)
|
|
self._write_cell(cx+3, cy, grids[country][grid_name]['impact'], 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, False, 'white', 'right', 1, 0, XLSX_BORDER_COLOR)
|
|
cx = start_x
|
|
cy += 1
|
|
|
|
# ================================================================
|
|
# DOCUMENT DATA GENERATION
|
|
# ================================================================
|
|
|
|
def _generate_document_data_for_export(self, report, options, export_type='pdf'):
|
|
"""Produce all data needed for journal report export (PDF or XLSX)."""
|
|
self.env.flush_all()
|
|
qry = report._get_report_query(options, 'strict_range')
|
|
acct_alias = qry.join(
|
|
lhs_alias='account_move_line', lhs_column='account_id',
|
|
rhs_table='account_account', rhs_column='id', link='account_id',
|
|
)
|
|
acct_code = self.env['account.account']._field_to_sql(acct_alias, 'code', qry)
|
|
acct_name = self.env['account.account']._field_to_sql(acct_alias, 'name')
|
|
|
|
stmt = SQL(
|
|
"""
|
|
SELECT
|
|
account_move_line.id AS move_line_id,
|
|
account_move_line.name,
|
|
account_move_line.date,
|
|
account_move_line.invoice_date,
|
|
account_move_line.amount_currency,
|
|
account_move_line.tax_base_amount,
|
|
account_move_line.currency_id AS move_line_currency,
|
|
am.id AS move_id,
|
|
am.name AS move_name,
|
|
am.journal_id,
|
|
am.currency_id AS move_currency,
|
|
am.amount_total_in_currency_signed AS amount_currency_total,
|
|
am.currency_id != cp.currency_id AS is_multicurrency,
|
|
p.name AS partner_name,
|
|
%(acct_code)s AS account_code,
|
|
%(acct_name)s AS account_name,
|
|
%(acct_alias)s.account_type AS account_type,
|
|
COALESCE(account_move_line.debit, 0) AS debit,
|
|
COALESCE(account_move_line.credit, 0) AS credit,
|
|
COALESCE(account_move_line.balance, 0) AS balance,
|
|
%(j_name)s AS journal_name,
|
|
j.code AS journal_code,
|
|
j.type AS journal_type,
|
|
cp.currency_id AS company_currency,
|
|
CASE WHEN j.type = 'sale' THEN am.payment_reference
|
|
WHEN j.type = 'purchase' THEN am.ref END AS reference,
|
|
array_remove(array_agg(DISTINCT %(tax_name)s), NULL) AS taxes,
|
|
array_remove(array_agg(DISTINCT %(tag_name)s), NULL) AS tax_grids
|
|
FROM %(tbl)s
|
|
JOIN account_move am ON am.id = account_move_line.move_id
|
|
LEFT JOIN res_partner p ON p.id = account_move_line.partner_id
|
|
JOIN account_journal j ON j.id = am.journal_id
|
|
JOIN res_company cp ON cp.id = am.company_id
|
|
LEFT JOIN account_move_line_account_tax_rel aml_at_rel ON aml_at_rel.account_move_line_id = account_move_line.id
|
|
LEFT JOIN account_tax parent_tax ON parent_tax.id = aml_at_rel.account_tax_id and parent_tax.amount_type = 'group'
|
|
LEFT JOIN account_tax_filiation_rel tax_filiation_rel ON tax_filiation_rel.parent_tax = parent_tax.id
|
|
LEFT JOIN account_tax tax ON (tax.id = aml_at_rel.account_tax_id and tax.amount_type != 'group') or tax.id = tax_filiation_rel.child_tax
|
|
LEFT JOIN account_account_tag_account_move_line_rel tag_rel ON tag_rel.account_move_line_id = account_move_line.id
|
|
LEFT JOIN account_account_tag tag ON tag_rel.account_account_tag_id = tag.id
|
|
LEFT JOIN res_currency journal_curr ON journal_curr.id = j.currency_id
|
|
WHERE %(pmt_filter)s AND %(where)s
|
|
GROUP BY "account_move_line".id, am.id, p.id, %(acct_alias)s.id, j.id, cp.id, journal_curr.id, account_code, account_name
|
|
ORDER BY
|
|
CASE j.type WHEN 'sale' THEN 1 WHEN 'purchase' THEN 2 WHEN 'general' THEN 3 WHEN 'bank' THEN 4 ELSE 5 END,
|
|
j.sequence,
|
|
CASE WHEN am.name = '/' THEN 1 ELSE 0 END, am.date, am.name,
|
|
CASE %(acct_alias)s.account_type
|
|
WHEN 'liability_payable' THEN 1 WHEN 'asset_receivable' THEN 1
|
|
WHEN 'liability_credit_card' THEN 5 WHEN 'asset_cash' THEN 5 ELSE 2 END,
|
|
account_move_line.tax_line_id NULLS FIRST
|
|
""",
|
|
tbl=qry.from_clause,
|
|
pmt_filter=self._get_payment_lines_filter_case_statement(options),
|
|
where=qry.where_clause,
|
|
acct_code=acct_code,
|
|
acct_name=acct_name,
|
|
acct_alias=SQL.identifier(acct_alias),
|
|
j_name=self.env['account.journal']._field_to_sql('j', 'name'),
|
|
tax_name=self.env['account.tax']._field_to_sql('tax', 'name'),
|
|
tag_name=self.env['account.account.tag']._field_to_sql('tag', 'name'),
|
|
)
|
|
self.env.cr.execute(stmt)
|
|
|
|
by_journal = {}
|
|
for row in self.env.cr.dictfetchall():
|
|
by_journal.setdefault(row['journal_id'], {}).setdefault(row['move_id'], []).append(row)
|
|
|
|
journals_vals = []
|
|
any_has_taxes = False
|
|
for jnl_moves in by_journal.values():
|
|
move_lists = list(jnl_moves.values())
|
|
first = move_lists[0][0]
|
|
jv = {
|
|
'id': first['journal_id'],
|
|
'name': first['journal_name'],
|
|
'code': first['journal_code'],
|
|
'type': first['journal_type'],
|
|
}
|
|
if self._section_has_tax(options, jv['id']):
|
|
jv['tax_summary'] = self._get_tax_summary_section(options, jv)
|
|
any_has_taxes = True
|
|
jv['lines'] = self._get_export_lines_for_journal(report, options, export_type, jv, move_lists)
|
|
jv['columns'] = self._get_columns_for_journal(jv, export_type)
|
|
journals_vals.append(jv)
|
|
|
|
return {
|
|
'journals_vals': journals_vals,
|
|
'global_tax_summary': self._get_tax_summary_section(options) if any_has_taxes else False,
|
|
}
|
|
|
|
def _get_columns_for_journal(self, journal, export_type='pdf'):
|
|
cols = [{'name': _('Document'), 'label': 'document'}]
|
|
if export_type == 'pdf':
|
|
cols.append({'name': _('Account'), 'label': 'account_label'})
|
|
else:
|
|
cols.extend([
|
|
{'name': _('Account Code'), 'label': 'account_code'},
|
|
{'name': _('Account Label'), 'label': 'account_label'},
|
|
])
|
|
cols.extend([
|
|
{'name': _('Name'), 'label': 'name'},
|
|
{'name': _('Debit'), 'label': 'debit', 'class': 'o_right_alignment '},
|
|
{'name': _('Credit'), 'label': 'credit', 'class': 'o_right_alignment '},
|
|
])
|
|
if journal.get('tax_summary'):
|
|
cols.append({'name': _('Taxes'), 'label': 'taxes'})
|
|
if journal['tax_summary'].get('tax_grid_summary_lines'):
|
|
cols.append({'name': _('Tax Grids'), 'label': 'tax_grids'})
|
|
if journal['type'] == 'bank':
|
|
cols.append({'name': _('Balance'), 'label': 'balance', 'class': 'o_right_alignment '})
|
|
if journal.get('multicurrency_column'):
|
|
cols.append({'name': _('Amount Currency'), 'label': 'amount_currency', 'class': 'o_right_alignment '})
|
|
return cols
|
|
|
|
def _get_export_lines_for_journal(self, report, options, export_type, journal_vals, move_lists):
|
|
if journal_vals['type'] == 'bank':
|
|
return self._get_export_lines_for_bank_journal(report, options, export_type, journal_vals, move_lists)
|
|
|
|
sum_cr, sum_dr = 0, 0
|
|
rows = []
|
|
for i, aml_list in enumerate(move_lists):
|
|
for j, entry in enumerate(aml_list):
|
|
doc = False
|
|
if j == 0:
|
|
doc = entry['move_name']
|
|
elif j == 1:
|
|
doc = entry['date']
|
|
row = self._get_base_line(report, options, export_type, doc, entry, j, i % 2 != 0, journal_vals.get('tax_summary'))
|
|
sum_cr += entry['credit']
|
|
sum_dr += entry['debit']
|
|
rows.append(row)
|
|
|
|
first_entry = aml_list[0]
|
|
if first_entry['is_multicurrency']:
|
|
mc_label = _('Amount in currency: %s', report._format_value(options, first_entry['amount_currency_total'], 'monetary', format_params={'currency_id': first_entry['move_currency']}))
|
|
if len(aml_list) <= 2:
|
|
rows.append({'document': {'data': mc_label}, 'line_class': 'o_even ' if i % 2 == 0 else 'o_odd ', 'amount': {'data': first_entry['amount_currency_total']}, 'currency_id': {'data': first_entry['move_currency']}})
|
|
else:
|
|
rows[-1]['document'] = {'data': mc_label}
|
|
rows[-1]['amount'] = {'data': first_entry['amount_currency_total']}
|
|
rows[-1]['currency_id'] = {'data': first_entry['move_currency']}
|
|
|
|
rows.append({})
|
|
rows.append({
|
|
'name': {'data': _('Total')},
|
|
'debit': {'data': report._format_value(options, sum_dr, 'monetary')},
|
|
'credit': {'data': report._format_value(options, sum_cr, 'monetary')},
|
|
})
|
|
return rows
|
|
|
|
def _get_export_lines_for_bank_journal(self, report, options, export_type, journal_vals, move_lists):
|
|
rows = []
|
|
running_balance = self._query_bank_journal_initial_balance(options, journal_vals['id'])
|
|
rows.append({'name': {'data': _('Starting Balance')}, 'balance': {'data': report._format_value(options, running_balance, 'monetary')}})
|
|
|
|
sum_cr, sum_dr = 0, 0
|
|
for i, aml_list in enumerate(move_lists):
|
|
is_unreconciled = not any(ln for ln in aml_list if ln['account_type'] in ('liability_credit_card', 'asset_cash'))
|
|
for j, entry in enumerate(aml_list):
|
|
if entry['account_type'] not in ('liability_credit_card', 'asset_cash'):
|
|
doc = ''
|
|
if j == 0:
|
|
doc = f'{entry["move_name"]} ({entry["date"]})'
|
|
row = self._get_base_line(report, options, export_type, doc, entry, j, i % 2 != 0, journal_vals.get('tax_summary'))
|
|
sum_cr += entry['credit']
|
|
sum_dr += entry['debit']
|
|
|
|
if not is_unreconciled:
|
|
line_bal = -entry['balance']
|
|
running_balance += line_bal
|
|
row['balance'] = {
|
|
'data': report._format_value(options, running_balance, 'monetary'),
|
|
'class': 'o_muted ' if self.env.company.currency_id.is_zero(line_bal) else '',
|
|
}
|
|
|
|
if self.env.user.has_group('base.group_multi_currency') and entry['move_line_currency'] != entry['company_currency']:
|
|
journal_vals['multicurrency_column'] = True
|
|
mc_amt = -entry['amount_currency'] if not is_unreconciled else entry['amount_currency']
|
|
mc_cur = self.env['res.currency'].browse(entry['move_line_currency'])
|
|
row['amount_currency'] = {
|
|
'data': report._format_value(options, mc_amt, 'monetary', format_params={'currency_id': mc_cur.id}),
|
|
'class': 'o_muted ' if mc_cur.is_zero(mc_amt) else '',
|
|
}
|
|
rows.append(row)
|
|
|
|
rows.append({})
|
|
rows.append({'name': {'data': _('Total')}, 'balance': {'data': report._format_value(options, running_balance, 'monetary')}})
|
|
return rows
|
|
|
|
def _get_base_line(self, report, options, export_type, document, entry, line_idx, is_even, has_taxes):
|
|
co_cur = self.env.company.currency_id
|
|
label = entry['name'] or entry['reference']
|
|
acct_label = entry['partner_name'] or entry['account_name']
|
|
if entry['partner_name'] and entry['account_type'] == 'asset_receivable':
|
|
fmt_label = _('AR %s', acct_label)
|
|
elif entry['partner_name'] and entry['account_type'] == 'liability_payable':
|
|
fmt_label = _('AP %s', acct_label)
|
|
else:
|
|
acct_label = entry['account_name']
|
|
fmt_label = _('G %s', entry["account_code"])
|
|
|
|
row = {
|
|
'line_class': 'o_even ' if is_even else 'o_odd ',
|
|
'document': {'data': document, 'class': 'o_bold ' if line_idx == 0 else ''},
|
|
'account_code': {'data': entry['account_code']},
|
|
'account_label': {'data': acct_label if export_type != 'pdf' else fmt_label},
|
|
'name': {'data': label},
|
|
'debit': {'data': report._format_value(options, entry['debit'], 'monetary'), 'class': 'o_muted ' if co_cur.is_zero(entry['debit']) else ''},
|
|
'credit': {'data': report._format_value(options, entry['credit'], 'monetary'), 'class': 'o_muted ' if co_cur.is_zero(entry['credit']) else ''},
|
|
}
|
|
if has_taxes:
|
|
tax_display = ''
|
|
if entry['taxes']:
|
|
tax_display = _('T: %s', ', '.join(entry['taxes']))
|
|
elif entry['tax_base_amount'] is not None:
|
|
tax_display = _('B: %s', report._format_value(options, entry['tax_base_amount'], 'monetary'))
|
|
row['taxes'] = {'data': tax_display}
|
|
row['tax_grids'] = {'data': ', '.join(entry['tax_grids'])}
|
|
return row
|
|
|
|
# ================================================================
|
|
# QUERY HELPERS
|
|
# ================================================================
|
|
|
|
def _get_payment_lines_filter_case_statement(self, options):
|
|
if not options.get('show_payment_lines'):
|
|
return SQL("""
|
|
(j.type != 'bank' OR EXISTS(
|
|
SELECT 1
|
|
FROM account_move_line
|
|
JOIN account_account acc ON acc.id = account_move_line.account_id
|
|
WHERE account_move_line.move_id = am.id
|
|
AND acc.account_type IN ('liability_credit_card', 'asset_cash')
|
|
))
|
|
""")
|
|
return SQL('TRUE')
|
|
|
|
def _query_bank_journal_initial_balance(self, options, journal_id):
|
|
report = self.env.ref('fusion_accounting.journal_report')
|
|
qry = report._get_report_query(options, 'to_beginning_of_period', domain=[('journal_id', '=', journal_id)])
|
|
stmt = SQL("""
|
|
SELECT COALESCE(SUM(account_move_line.balance), 0) AS balance
|
|
FROM %(tbl)s
|
|
JOIN account_journal journal ON journal.id = "account_move_line".journal_id
|
|
AND account_move_line.account_id = journal.default_account_id
|
|
WHERE %(where)s
|
|
GROUP BY journal.id
|
|
""", tbl=qry.from_clause, where=qry.where_clause)
|
|
self.env.cr.execute(stmt)
|
|
rows = self.env.cr.dictfetchall()
|
|
return rows[0]['balance'] if rows else 0
|
|
|
|
# ================================================================
|
|
# TAX SUMMARIES
|
|
# ================================================================
|
|
|
|
def _section_has_tax(self, options, journal_id):
|
|
report = self.env['account.report'].browse(options.get('report_id'))
|
|
domain = [('tax_ids', '!=', False)]
|
|
if journal_id:
|
|
domain.append(('journal_id', '=', journal_id))
|
|
domain += report._get_options_domain(options, 'strict_range')
|
|
return bool(self.env['account.move.line'].search_count(domain, limit=1))
|
|
|
|
def _get_tax_summary_section(self, options, journal_vals=None):
|
|
td = {
|
|
'date_from': options.get('date', {}).get('date_from'),
|
|
'date_to': options.get('date', {}).get('date_to'),
|
|
}
|
|
if journal_vals:
|
|
td['journal_id'] = journal_vals['id']
|
|
td['journal_type'] = journal_vals['type']
|
|
|
|
tax_lines = self._get_generic_tax_summary_for_sections(options, td)
|
|
nd_col = any(ln.get('tax_non_deductible_no_format') for vals in tax_lines.values() for ln in vals)
|
|
ded_col = any(ln.get('tax_deductible_no_format') for vals in tax_lines.values() for ln in vals)
|
|
due_col = any(ln.get('tax_due_no_format') for vals in tax_lines.values() for ln in vals)
|
|
|
|
return {
|
|
'tax_report_lines': tax_lines,
|
|
'tax_non_deductible_column': nd_col,
|
|
'tax_deductible_column': ded_col,
|
|
'tax_due_column': due_col,
|
|
'extra_columns': int(nd_col) + int(ded_col) + int(due_col),
|
|
'tax_grid_summary_lines': self._get_tax_grids_summary(options, td),
|
|
}
|
|
|
|
def _get_generic_tax_report_options(self, options, data):
|
|
generic_rpt = self.env.ref('account.generic_tax_report')
|
|
prev = options.copy()
|
|
prev.update({
|
|
'selected_variant_id': generic_rpt.id,
|
|
'date_from': data.get('date_from'),
|
|
'date_to': data.get('date_to'),
|
|
})
|
|
tax_opts = generic_rpt.get_options(prev)
|
|
jnl_rpt = self.env['account.report'].browse(options['report_id'])
|
|
tax_opts['forced_domain'] = tax_opts.get('forced_domain', []) + jnl_rpt._get_options_domain(options, 'strict_range')
|
|
|
|
if data.get('journal_id') or data.get('journal_type'):
|
|
tax_opts['journals'] = [{
|
|
'id': data.get('journal_id'),
|
|
'model': 'account.journal',
|
|
'type': data.get('journal_type'),
|
|
'selected': True,
|
|
}]
|
|
return tax_opts
|
|
|
|
def _get_tax_grids_summary(self, options, data):
|
|
report = self.env.ref('fusion_accounting.journal_report')
|
|
tax_opts = self._get_generic_tax_report_options(options, data)
|
|
qry = report._get_report_query(tax_opts, 'strict_range')
|
|
country_nm = self.env['res.country']._field_to_sql('country', 'name')
|
|
tag_nm = self.env['account.account.tag']._field_to_sql('tag', 'name')
|
|
stmt = SQL("""
|
|
WITH tag_info (country_name, tag_id, tag_name, tag_sign, balance) AS (
|
|
SELECT %(cn)s AS country_name, tag.id, %(tn)s AS name,
|
|
CASE WHEN tag.tax_negate IS TRUE THEN '-' ELSE '+' END,
|
|
SUM(COALESCE("account_move_line".balance, 0)
|
|
* CASE WHEN "account_move_line".tax_tag_invert THEN -1 ELSE 1 END) AS balance
|
|
FROM account_account_tag tag
|
|
JOIN account_account_tag_account_move_line_rel rel ON tag.id = rel.account_account_tag_id
|
|
JOIN res_country country ON country.id = tag.country_id
|
|
, %(tbl)s
|
|
WHERE %(where)s AND applicability = 'taxes' AND "account_move_line".id = rel.account_move_line_id
|
|
GROUP BY country_name, tag.id
|
|
)
|
|
SELECT country_name, tag_id, REGEXP_REPLACE(tag_name, '^[+-]', '') AS name, balance, tag_sign AS sign
|
|
FROM tag_info ORDER BY country_name, name
|
|
""", cn=country_nm, tn=tag_nm, tbl=qry.from_clause, where=qry.where_clause)
|
|
self.env.cr.execute(stmt)
|
|
rows = self.env.cr.fetchall()
|
|
|
|
result = {}
|
|
opp = {'+': '-', '-': '+'}
|
|
for cname, _tid, gname, bal, sign in rows:
|
|
result.setdefault(cname, {}).setdefault(gname, {})
|
|
result[cname][gname].setdefault('tag_ids', []).append(_tid)
|
|
result[cname][gname][sign] = report._format_value(options, bal, 'monetary')
|
|
if opp[sign] not in result[cname][gname]:
|
|
result[cname][gname][opp[sign]] = report._format_value(options, 0, 'monetary')
|
|
result[cname][gname][sign + '_no_format'] = bal
|
|
result[cname][gname]['impact'] = report._format_value(options, result[cname][gname].get('+_no_format', 0) - result[cname][gname].get('-_no_format', 0), 'monetary')
|
|
return result
|
|
|
|
def _get_generic_tax_summary_for_sections(self, options, data):
|
|
report = self.env['account.report'].browse(options['report_id'])
|
|
tax_opts = self._get_generic_tax_report_options(options, data)
|
|
tax_opts['account_journal_report_tax_deductibility_columns'] = True
|
|
tax_rpt = self.env.ref('account.generic_tax_report')
|
|
rpt_lines = tax_rpt._get_lines(tax_opts)
|
|
|
|
tax_vals = {}
|
|
for rln in rpt_lines:
|
|
model, lid = report._parse_line_id(rln.get('id'))[-1][1:]
|
|
if model == 'account.tax':
|
|
tax_vals[lid] = {
|
|
'base_amount': rln['columns'][0]['no_format'],
|
|
'tax_amount': rln['columns'][1]['no_format'],
|
|
'tax_non_deductible': rln['columns'][2]['no_format'],
|
|
'tax_deductible': rln['columns'][3]['no_format'],
|
|
'tax_due': rln['columns'][4]['no_format'],
|
|
}
|
|
|
|
taxes = self.env['account.tax'].browse(tax_vals.keys())
|
|
grouped = {}
|
|
for tx in taxes:
|
|
grouped.setdefault(tx.country_id.name, []).append({
|
|
'base_amount': report._format_value(options, tax_vals[tx.id]['base_amount'], 'monetary'),
|
|
'tax_amount': report._format_value(options, tax_vals[tx.id]['tax_amount'], 'monetary'),
|
|
'tax_non_deductible': report._format_value(options, tax_vals[tx.id]['tax_non_deductible'], 'monetary'),
|
|
'tax_non_deductible_no_format': tax_vals[tx.id]['tax_non_deductible'],
|
|
'tax_deductible': report._format_value(options, tax_vals[tx.id]['tax_deductible'], 'monetary'),
|
|
'tax_deductible_no_format': tax_vals[tx.id]['tax_deductible'],
|
|
'tax_due': report._format_value(options, tax_vals[tx.id]['tax_due'], 'monetary'),
|
|
'tax_due_no_format': tax_vals[tx.id]['tax_due'],
|
|
'name': tx.name,
|
|
'line_id': report._get_generic_line_id('account.tax', tx.id),
|
|
})
|
|
return dict(sorted(grouped.items()))
|
|
|
|
# ================================================================
|
|
# ACTIONS
|
|
# ================================================================
|
|
|
|
def journal_report_tax_tag_template_open_aml(self, options, params=None):
|
|
tag_ids = params.get('tag_ids')
|
|
domain = (
|
|
self.env['account.report'].browse(options['report_id'])._get_options_domain(options, 'strict_range')
|
|
+ [('tax_tag_ids', 'in', [tag_ids])]
|
|
+ self.env['account.move.line']._get_tax_exigible_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,
|
|
}
|
|
|
|
def journal_report_action_dropdown_audit_default_tax_report(self, options, params):
|
|
return self.env['account.generic.tax.report.handler'].caret_option_audit_tax(options, params)
|
|
|
|
def journal_report_action_open_tax_journal_items(self, options, params):
|
|
ctx = {
|
|
'search_default_posted': 0 if options.get('all_entries') else 1,
|
|
'search_default_date_between': 1,
|
|
'date_from': params and params.get('date_from') or options.get('date', {}).get('date_from'),
|
|
'date_to': params and params.get('date_to') or options.get('date', {}).get('date_to'),
|
|
'search_default_journal_id': params.get('journal_id'),
|
|
'expand': 1,
|
|
}
|
|
if params and params.get('tax_type') == 'tag':
|
|
ctx.update({'search_default_group_by_tax_tags': 1, 'search_default_group_by_account': 2})
|
|
elif params and params.get('tax_type') == 'tax':
|
|
ctx.update({'search_default_group_by_taxes': 1, 'search_default_group_by_account': 2})
|
|
if params and 'journal_id' in params:
|
|
ctx['search_default_journal_id'] = [params['journal_id']]
|
|
if options and options.get('journals') and not ctx.get('search_default_journal_id'):
|
|
sel = [j['id'] for j in options['journals'] if j.get('selected') and j['model'] == 'account.journal']
|
|
if len(sel) == 1:
|
|
ctx['search_default_journal_id'] = sel
|
|
return {
|
|
'name': params.get('name'),
|
|
'view_mode': 'list,pivot,graph,kanban',
|
|
'res_model': 'account.move.line',
|
|
'views': [(self.env.ref('account.view_move_line_tree').id, 'list')],
|
|
'type': 'ir.actions.act_window',
|
|
'domain': [('display_type', 'not in', ('line_section', 'line_note'))],
|
|
'context': ctx,
|
|
}
|
|
|
|
def journal_report_action_open_account_move_lines_by_account(self, options, params):
|
|
report = self.env['account.report'].browse(options['report_id'])
|
|
jnl = self.env['account.journal'].browse(params['journal_id'])
|
|
acct = self.env['account.account'].browse(params['account_id'])
|
|
domain = [('journal_id.id', '=', jnl.id), ('account_id.id', '=', acct.id)]
|
|
domain += report._get_options_domain(options, 'strict_range')
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': _("%(journal)s - %(account)s", journal=jnl.name, account=acct.name),
|
|
'res_model': 'account.move.line',
|
|
'views': [[False, 'list']],
|
|
'domain': domain,
|
|
}
|
|
|
|
def journal_report_open_aml_by_move(self, options, params):
|
|
report = self.env['account.report'].browse(options['report_id'])
|
|
jnl = self.env['account.journal'].browse(params['journal_id'])
|
|
ctx_extra = {'search_default_group_by_account': 0, 'show_more_partner_info': 1}
|
|
if jnl.type in ('bank', 'credit'):
|
|
params['view_ref'] = 'fusion_accounting.view_journal_report_audit_bank_move_line_tree'
|
|
ctx_extra['search_default_exclude_bank_lines'] = 1
|
|
else:
|
|
params['view_ref'] = 'fusion_accounting.view_journal_report_audit_move_line_tree'
|
|
ctx_extra.update({'search_default_group_by_partner': 1, 'search_default_group_by_move': 2})
|
|
if jnl.type in ('sale', 'purchase'):
|
|
ctx_extra['search_default_invoices_lines'] = 1
|
|
action = report.open_journal_items(options=options, params=params)
|
|
action.get('context', {}).update(ctx_extra)
|
|
return action
|