687 lines
35 KiB
Python
687 lines
35 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
from lxml import etree
|
|
import warnings
|
|
|
|
from odoo import _, fields, models
|
|
from odoo.exceptions import RedirectWarning, UserError
|
|
from odoo.tools import SQL
|
|
from odoo.tools.misc import format_date
|
|
from odoo.tools.float_utils import float_repr
|
|
|
|
|
|
|
|
class L10n_EeTaxReportHandler(models.AbstractModel):
|
|
_name = 'l10n_ee.tax.report.handler'
|
|
_inherit = ['account.tax.report.handler']
|
|
_description = 'Estonian Tax Report Custom Handler'
|
|
|
|
def _custom_options_initializer(self, report, options, previous_options):
|
|
super()._custom_options_initializer(report, options, previous_options=previous_options)
|
|
|
|
# Add XML export button
|
|
options.setdefault('buttons', []).append({
|
|
'name': _('XML'),
|
|
'sequence': 30,
|
|
'action': 'export_file',
|
|
'action_param': 'export_to_xml',
|
|
'file_export_type': _('XML'),
|
|
})
|
|
|
|
def action_audit_cell(self, options, params):
|
|
# OVERRIDES 'account_reports'
|
|
""" The lines of the Estonian VAT report are rounded to the unit and use the
|
|
aggregation engine for the computation. To facilitate the auditing of tax
|
|
lines, 'balance_from_tags' is added to the report, using the tax_tags engine.
|
|
"""
|
|
report_line = self.env['account.report.line'].browse(params['report_line_id'])
|
|
|
|
if set(report_line.expression_ids.mapped('label')) == {'balance', 'balance_from_tags'}:
|
|
params['expression_label'] = 'balance_from_tags'
|
|
|
|
return report_line.report_id.action_audit_cell(options, params)
|
|
|
|
####################################################
|
|
# EXPORT
|
|
####################################################
|
|
|
|
def _compute_xml_version(self, options):
|
|
""" Compute the version of the XML file to export.
|
|
"""
|
|
date_to = fields.Date.from_string(options['date'].get('date_to'))
|
|
if date_to >= fields.Date.from_string("2025-01-01") and date_to < fields.Date.from_string("2025-07-01"):
|
|
return "KMD5"
|
|
elif date_to >= fields.Date.from_string("2025-07-01"):
|
|
return "KMD6"
|
|
return "KMD4"
|
|
|
|
def export_to_xml(self, options):
|
|
""" Create export of the Normal period filling of the VAT return forms KMD
|
|
and KMD INF. Requires the sender company's company registry number to be set.
|
|
"""
|
|
report = self.env['account.report'].browse(options['report_id'])
|
|
date_to = fields.Date.from_string(options['date'].get('date_to'))
|
|
|
|
if options['date']['period_type'] != 'month' and options['return_periodicity']['periodicity'] != 'monthly':
|
|
raise UserError(_('Choose a month to export the VAT Report'))
|
|
|
|
sender_company = report._get_sender_company_for_export(options)
|
|
if not sender_company.company_registry:
|
|
raise RedirectWarning(
|
|
message=_("No company registry number associated with your company. Please define one."),
|
|
action={
|
|
'name': _("Company: %s", sender_company.name),
|
|
'type': 'ir.actions.act_window',
|
|
'view_mode': 'form',
|
|
'res_model': 'res.company',
|
|
'views': [[False, 'form']],
|
|
'target': 'new',
|
|
'res_id': sender_company.id,
|
|
'context': {'create': False},
|
|
},
|
|
button_text=_("Go to Company"),
|
|
)
|
|
|
|
xml_data = {
|
|
'tax_payer_reg_code': sender_company.company_registry,
|
|
# Not needed when uploaded personally, since you logged in at the Estonian government platform with your ID
|
|
'submitter_person_code': '',
|
|
'year': date_to.year,
|
|
'month': date_to.month,
|
|
'version': self._compute_xml_version(options),
|
|
'declaration_type': 1, # Normal period
|
|
'sale_lines': [],
|
|
'purchase_lines': [],
|
|
}
|
|
|
|
# Tax report (KMD report)
|
|
kmd_lines = report._get_lines(options)
|
|
tax_line_prefix = 'l10n_ee.tax_report_line_'
|
|
tax_line_numbers = ('1', '1_24', '1_1', '1_2', '2', '2_1', '2_2', '3', '3_1', '3_1_1', '3_2', '3_2_1',
|
|
'5', '5_1', '5_2', '5_3', '5_3_cars', '5_4', '5_4_cars',
|
|
'6', '6_1', '7', '7_1', '8', '9', '10', '11')
|
|
tax_line_mapping = {
|
|
tax_line.id : f'line_{line_number}'
|
|
for line_number in tax_line_numbers
|
|
if (tax_line := self.env.ref(tax_line_prefix + line_number, raise_if_not_found=False))
|
|
}
|
|
colexpr_to_idx = {col['expression_label']: idx for idx, col in enumerate(options.get('columns', []))}
|
|
currency = self.env.company.currency_id
|
|
|
|
for line in kmd_lines:
|
|
if 'report_line_id' not in line['columns'][0]:
|
|
continue
|
|
line_id = line['columns'][0]['report_line_id']
|
|
if line_id not in tax_line_mapping:
|
|
continue
|
|
xml_variable = tax_line_mapping[line_id]
|
|
balance = line['columns'][colexpr_to_idx['balance']]['no_format']
|
|
if balance and xml_variable.endswith('_cars'): # line indicating number of cars
|
|
xml_data[xml_variable] = f"{balance:.0f}"
|
|
elif balance and xml_variable in ('line_1', 'line_1_2'):
|
|
total_balance = balance + float(xml_data.get('line_1', '0.0'))
|
|
xml_data['line_1'] = f'{total_balance:.2f}'
|
|
elif balance:
|
|
xml_data[xml_variable] = float_repr(currency.round(balance), currency.decimal_places)
|
|
|
|
# Loop through Part A and then Part B of the KMD INF, adding the lines to the sales or purchase subsection
|
|
report_export_mapping = {
|
|
'kmd_inf_report_part_a': 'sale_lines',
|
|
'kmd_inf_report_part_b': 'purchase_lines'
|
|
}
|
|
for kmd_inf_part, export_section in report_export_mapping.items():
|
|
kmd_inf_report = self.env.ref(f'l10n_ee_reports.{kmd_inf_part}', raise_if_not_found=False)
|
|
kmd_inf_report_options = kmd_inf_report.get_options(previous_options={**options, 'unfold_all': True})
|
|
kmd_inf_lines = kmd_inf_report._get_lines(kmd_inf_report_options)
|
|
|
|
for line in kmd_inf_lines:
|
|
# The grouping lines do not need to be included in the KMD INF, since they do not represent invoice values.
|
|
if not line['groupby']:
|
|
annex_line = {}
|
|
for col in line['columns']:
|
|
label = col['expression_label']
|
|
value = col['no_format']
|
|
if col['figure_type'] == 'monetary':
|
|
if value:
|
|
value = float_repr(currency.round(value), currency.decimal_places)
|
|
elif col['figure_type'] == 'date':
|
|
value = format_date(self.env, value, date_format='yyyy-MM-dd')
|
|
annex_line[label] = value
|
|
xml_data[export_section].append(annex_line)
|
|
|
|
xml_content = self.env['ir.qweb']._render('l10n_ee_reports.vat_report_xml', values=xml_data)
|
|
tree = etree.fromstring(xml_content)
|
|
formatted_xml = etree.tostring(tree, pretty_print=True, xml_declaration=True, encoding='UTF-8')
|
|
|
|
return {
|
|
'file_name': report.get_default_report_filename(options, 'xml'),
|
|
'file_content': formatted_xml,
|
|
'file_type': 'xml',
|
|
}
|
|
|
|
|
|
class L10n_EeKmdInfReportHandler(models.AbstractModel):
|
|
_name = 'l10n_ee.kmd.inf.report.handler'
|
|
_inherit = ['account.tax.report.handler']
|
|
_description = 'Estonian KMD INF Report Custom Handler'
|
|
|
|
def _custom_options_initializer(self, report, options, previous_options=None):
|
|
super()._custom_options_initializer(report, options, previous_options=previous_options)
|
|
|
|
if report == self.env.ref('l10n_ee_reports.kmd_inf_report_part_a', raise_if_not_found=False):
|
|
# Part A grouped lines display no information, hence part A should be unfolded by default
|
|
options['unfold_all'] = options['export_mode'] == 'print' or previous_options.get('unfold_all', True) if previous_options else True
|
|
|
|
def _custom_unfold_all_batch_data_generator(self, report, options, lines_to_expand_by_function):
|
|
"""
|
|
VAT-specific batch data generator.
|
|
"""
|
|
|
|
rslt = {}
|
|
|
|
if report == self.env.ref('l10n_ee_reports.kmd_inf_report_part_b', raise_if_not_found=False):
|
|
return rslt
|
|
|
|
for expand_function_name, lines_to_expand in lines_to_expand_by_function.items():
|
|
for line_to_expand in lines_to_expand:
|
|
if expand_function_name == '_report_expand_unfoldable_line_with_groupby':
|
|
report_line_id = report._get_res_id_from_line_id(line_to_expand['id'], 'account.report.line')
|
|
expressions_to_evaluate = report.line_ids.expression_ids.filtered(
|
|
lambda x: x.report_line_id.id == report_line_id and x.engine == 'custom'
|
|
)
|
|
if not expressions_to_evaluate:
|
|
continue
|
|
|
|
group_by = line_to_expand['groupby'].split(',')
|
|
groupby_to_expand = group_by[1] if len(group_by) > 1 else line_to_expand['groupby']
|
|
|
|
for column_group_key, column_group_options in report._split_options_per_column_group(options).items():
|
|
all_column_groups_expression_totals = report._compute_expression_totals_for_each_column_group(
|
|
expressions_to_evaluate,
|
|
column_group_options,
|
|
groupby_to_expand=groupby_to_expand,
|
|
)
|
|
|
|
all_line_ids = {
|
|
grouping_key
|
|
for grouping_key, _value
|
|
in all_column_groups_expression_totals[column_group_key][expressions_to_evaluate[0]]['value']
|
|
}
|
|
|
|
# Batch fetch move_id for these lines
|
|
move_lines = self.env['account.move.line'].browse(all_line_ids)
|
|
line_to_move_map = {l.id: l.move_id.id for l in move_lines}
|
|
|
|
for expression in expressions_to_evaluate:
|
|
for grouping_key, result in all_column_groups_expression_totals[column_group_key][expression]['value']:
|
|
move_id = line_to_move_map[grouping_key]
|
|
|
|
# Build the same key to be used later
|
|
full_key = f"[{report_line_id}]move_id:{move_id}=>id"
|
|
rslt.setdefault(full_key, {}).setdefault(column_group_key, {}).setdefault(expression, {'value': [], 'sublines_info': set()})
|
|
|
|
rslt[full_key][column_group_key][expression]['value'].append((grouping_key, result))
|
|
rslt[full_key][column_group_key][expression]['sublines_info'].add(grouping_key)
|
|
return rslt
|
|
|
|
def _custom_line_postprocessor(self, report, options, lines):
|
|
""" Change label for grouping lines in the result of the report's _get_lines(),
|
|
indicating the VAT rate and the special procedure of the line.
|
|
"""
|
|
lines = super()._custom_line_postprocessor(report, options, lines)
|
|
if report == self.env.ref('l10n_ee_reports.kmd_inf_report_part_a', raise_if_not_found=False):
|
|
for line in lines:
|
|
if not line['groupby']:
|
|
row_names = {
|
|
'5': 'VAT 5%',
|
|
'5erikord': 'VAT 5% special procedure §41/42',
|
|
'9': 'VAT 9%',
|
|
'9erikord': 'VAT 9% special procedure §41/42',
|
|
'13': 'VAT 13%',
|
|
'13erikord': 'VAT 13% special procedure §41/42',
|
|
'20': 'VAT 20%',
|
|
'20erikord': 'VAT 20% special procedure §41/42',
|
|
'22': 'VAT 22%',
|
|
'22erikord': 'VAT 22% special procedure §41/42',
|
|
'24': 'VAT 24%',
|
|
'24erikord': 'VAT 24% special procedure §41/42'
|
|
}
|
|
line['name'] = row_names[line['columns'][5]['no_format']] # column 'tax_rate'
|
|
comments = line['columns'][7]['no_format'] # column 'comments'
|
|
line['name'] += ' special procedure §41^1' if '2' in comments else ''
|
|
return lines
|
|
|
|
def _customize_warnings(self, report, options, all_column_groups_expression_totals, warnings):
|
|
warning_partners = self._get_warning_partners(report, options)
|
|
if warning_partners:
|
|
warnings['l10n_ee_reports.kmd_inf_listing_missing_partners_warning'] = {
|
|
'alert_type': 'warning',
|
|
'count': len(warning_partners),
|
|
'ids': warning_partners,
|
|
}
|
|
|
|
####################################################
|
|
# WARNING
|
|
####################################################
|
|
|
|
def action_warning_partners(self, options, params):
|
|
view_id = self.env.ref('l10n_ee_reports.res_partner_kmd_inf_warning_view_tree', raise_if_not_found=False).id
|
|
return {
|
|
'name': _('Missing partners'),
|
|
'res_model': 'res.partner',
|
|
'views': [(view_id, 'list')],
|
|
'domain': [('id', 'in', params['ids'])],
|
|
'type': 'ir.actions.act_window',
|
|
}
|
|
|
|
def _get_warning_partners(self, report, options):
|
|
"""
|
|
Returns a list of partner IDs that should potentially have been included in the report. Those are partners
|
|
with a move with taxable supply at 24%, 22%, 20%, 13%, 9% or 5% in the selected period and no VAT, if:
|
|
- no country and no VAT
|
|
- no country and VAT not starting with EE
|
|
- no country and VAT is "/"
|
|
"""
|
|
query = report._get_report_query(options, 'strict_range')
|
|
|
|
if report == self.env.ref('l10n_ee_reports.kmd_inf_report_part_a', raise_if_not_found=False):
|
|
move_type = "('out_invoice', 'out_refund')"
|
|
else:
|
|
move_type = "('in_invoice', 'in_refund')"
|
|
|
|
add_where_clause = "AND res_partner.country_id IS NULL AND (res_partner.vat IS NULL OR res_partner.vat NOT ILIKE 'EE%%')"
|
|
|
|
tax_group_xmlids = ['tax_group_vat_24', 'tax_group_vat_22', 'tax_group_vat_20', 'tax_group_vat_13', 'tax_group_vat_9', 'tax_group_vat_5']
|
|
tax_group_ids = [tax_group.id for xmlid in tax_group_xmlids
|
|
if (tax_group := self.env.ref(f"account.{self.env.company.id}_{xmlid}", raise_if_not_found=False))]
|
|
|
|
sql_query = SQL("""
|
|
SELECT DISTINCT account_move_line__move_id.commercial_partner_id
|
|
FROM %(table_references)s
|
|
LEFT JOIN res_partner ON res_partner.id = account_move_line__move_id.commercial_partner_id
|
|
LEFT JOIN res_country ON res_country.id = res_partner.country_id
|
|
LEFT JOIN account_tax ON account_move_line.tax_line_id = account_tax.id
|
|
LEFT JOIN account_tax_group ON account_tax_group.id = account_tax.tax_group_id
|
|
WHERE %(search_condition)s
|
|
AND account_tax_group.id = ANY(%(tax_group_ids)s)
|
|
AND account_move_line__move_id.move_type IN %(move_type)s
|
|
%(add_where_clause)s
|
|
""",
|
|
table_references=query.from_clause,
|
|
search_condition=query.where_clause,
|
|
tax_group_ids=tax_group_ids,
|
|
move_type=SQL(move_type),
|
|
add_where_clause=SQL(add_where_clause),
|
|
)
|
|
self.env.cr.execute(sql_query)
|
|
return [r[0] for r in self.env.cr.fetchall()]
|
|
|
|
####################################################
|
|
# RETURN RESULT FUNCTION
|
|
####################################################
|
|
|
|
def build_result_dict_kmd_inf_a(self, query_res_lines, current_groupby):
|
|
if current_groupby == 'id':
|
|
res = query_res_lines[0]
|
|
return {
|
|
'buyer_reg_code': res['buyer_reg_code'],
|
|
'buyer_name': res['buyer_name'],
|
|
'invoice_number': res['invoice_number'],
|
|
'invoice_date': res['invoice_date'],
|
|
'invoice_sum': res['invoice_sum'],
|
|
'tax_rate': res['tax_rate'],
|
|
'sum_for_rate_in_period': res['sum_rate_period'],
|
|
'comments': res['comments'],
|
|
'has_sublines': True,
|
|
}
|
|
return {
|
|
'buyer_reg_code': None,
|
|
'buyer_name': None,
|
|
'invoice_number': None,
|
|
'invoice_date': None,
|
|
'invoice_sum': 0,
|
|
'tax_rate': 0,
|
|
'sum_for_rate_in_period': 0,
|
|
'comments': None,
|
|
'has_sublines': True,
|
|
}
|
|
|
|
def build_result_dict_kmd_inf_b(self, query_res_lines, current_groupby):
|
|
if current_groupby:
|
|
res = query_res_lines[0]
|
|
return {
|
|
'seller_reg_code': res['seller_reg_code'],
|
|
'seller_name': res['seller_name'],
|
|
'invoice_number': res['invoice_number'],
|
|
'invoice_date': res['invoice_date'],
|
|
'invoice_sum_vat': res['invoice_sum_vat'],
|
|
'vat_in_period': res['vat_in_period'],
|
|
'comments': res['comments'],
|
|
'has_sublines': True,
|
|
}
|
|
return {
|
|
'seller_reg_code': None,
|
|
'seller_name': None,
|
|
'invoice_number': None,
|
|
'invoice_date': None,
|
|
'invoice_sum_vat': 0,
|
|
'vat_in_period': 0,
|
|
'comments': None,
|
|
'has_sublines': True,
|
|
}
|
|
|
|
####################################################
|
|
# CUSTOM ENGINES
|
|
####################################################
|
|
|
|
def _report_custom_engine_kmd_inf_a(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
|
|
""" Part A contains information about invoices issued.
|
|
|
|
Only invoices containing supply taxable at the rate of 24%, 22%, 20%, 13%, 9% or 5% are included. Different lines are
|
|
used for supply taxable under the general procedure, the special procedure provided for in § 41 and § 42 of
|
|
the Value-Added Tax Act (special code 1) and the special procedure provided for in § 41^1 (special code 2).
|
|
Furthermore, if the invoice contains the supply taxable at the rate of 0% or tax-exempt supply or if the
|
|
invoice contains the supply that is taxable at two or more different tax rates, special code 3 is used.
|
|
"""
|
|
return self._report_custom_engine_kmd_inf(options, current_groupby, next_groupby, 'a')
|
|
|
|
def _report_custom_engine_kmd_inf_b(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
|
|
""" Part B contains information about invoices received.
|
|
|
|
Only invoices containing supply declared in field 5 of the Form KMD are included. Each line corresponds to
|
|
one invoice. Special code 11 is used for partial deduction of input value added tax according to § 29 (4),
|
|
§ 30 or § 32 of the VAT Act. Special code 12 is used for the acquisition of goods subject to VAT under the
|
|
special arrangements provided for in § 41^1 of the VAT Act.
|
|
"""
|
|
return self._report_custom_engine_kmd_inf(options, current_groupby, next_groupby, 'b')
|
|
|
|
def _convert_threshold_to_company_currency(self, threshold, options):
|
|
""" Returns a EUR threshold to company currency, using the options' date_to for conversion """
|
|
threshold_currency = self.env.ref('base.EUR')
|
|
|
|
if not threshold_currency.active:
|
|
raise UserError(_("Currency %s, used for a threshold in this report, is either nonexistent or inactive. Please create or activate it.", threshold_currency.name))
|
|
|
|
company_currency = self.env.company.currency_id
|
|
return threshold_currency._convert(threshold, company_currency, self.env.company, options['date']['date_to'])
|
|
|
|
def _report_custom_engine_kmd_inf(self, options, current_groupby, next_groupby, kmd_inf_part):
|
|
""" Builds the query and dictionary necessary to display the report lines for the KMD INF.
|
|
|
|
The query for part A and part B are built separately, as they have different columns and group by.
|
|
In both cases, only invoices containing a 24%, 22%, 13%, 9% or 5% tax (and 20% until end of 2025) are displayed.
|
|
Invoices from/to foreign companies not having a register code in Estonia are excluded. Both parts also
|
|
include a column displaying the company registry number of the partner as well as their name, number of
|
|
invoice, date of invoice, and the special code providing comments in the form of one or more code.
|
|
"""
|
|
# Build parameters for query
|
|
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 []))
|
|
partners_to_exclude = []
|
|
|
|
# First get all the partners that match the domain but don't reach the threshold. We'll have to exclude them
|
|
# Skip when current_groupby == 'id' because when you expand an invoice to see its tax details,
|
|
# Odoo's report engine automatically filters the query to show only that one invoice's lines.
|
|
# This would wrongly exclude the partner if that single invoice is below €1,000, even when
|
|
# the partner's total for the month is above €1,000.
|
|
if current_groupby != 'id':
|
|
domain = [
|
|
('partner_id', '!=', False),
|
|
('account_type', 'not in', ('asset_receivable', 'liability_payable')),
|
|
]
|
|
domain += [('move_id.move_type', 'in', ('in_invoice', 'in_refund'))] if kmd_inf_part == 'b' else [('move_id.move_type', 'in', ('out_invoice', 'out_refund'))]
|
|
query = report._get_report_query(options, 'strict_range', domain)
|
|
threshold_value = self._convert_threshold_to_company_currency(1000, options)
|
|
|
|
if kmd_inf_part == 'a':
|
|
extra_where = SQL("""
|
|
AND res_partner.is_company
|
|
AND EXISTS (
|
|
SELECT 1
|
|
FROM account_move_line_account_tax_rel rel
|
|
JOIN account_tax ON account_tax.id = rel.account_tax_id
|
|
WHERE rel.account_move_line_id = account_move_line.id
|
|
AND account_tax.amount != 0
|
|
)
|
|
""")
|
|
else:
|
|
xmlids = ['tax_report_line_5_tag', 'tax_report_line_5_1_tag', 'tax_report_line_5_2_tag', 'tax_report_line_5_3_tag', 'tax_report_line_5_4_tag']
|
|
tag_ids = self._get_tag_ids_from_report_lines(xmlids)
|
|
|
|
extra_where = SQL("""
|
|
AND EXISTS (
|
|
SELECT 1
|
|
FROM account_move_line aml
|
|
LEFT JOIN account_account_tag_account_move_line_rel AS tag_rel ON tag_rel.account_move_line_id = aml.id
|
|
WHERE aml.move_id = account_move_line__move_id.id
|
|
AND aml.display_type = 'tax'
|
|
AND tag_rel.account_account_tag_id = ANY(%(tag_ids)s)
|
|
GROUP BY aml.move_id
|
|
HAVING SUM(aml.balance) != 0
|
|
)
|
|
""", tag_ids=tag_ids)
|
|
|
|
partners_to_exclude_query = SQL("""
|
|
SELECT account_move_line.partner_id
|
|
FROM %(table_references)s
|
|
%(ct_query)s
|
|
LEFT JOIN res_partner ON res_partner.id = account_move_line__move_id.commercial_partner_id
|
|
LEFT JOIN res_country ON res_country.id = res_partner.country_id
|
|
WHERE %(search_condition)s
|
|
AND account_move_line.partner_id IS NOT NULL
|
|
AND (res_country.code = 'EE' OR res_partner.vat ILIKE %(country_code)s)
|
|
%(extra_where)s
|
|
GROUP BY account_move_line.partner_id
|
|
HAVING
|
|
COALESCE(
|
|
ABS(SUM(%(balance_select)s)
|
|
FILTER (WHERE account_move_line__move_id.move_type IN ('out_invoice', 'in_invoice') AND account_move_line.tax_line_id IS NULL)),
|
|
0
|
|
) < %(threshold_value)s
|
|
AND
|
|
COALESCE(
|
|
ABS(SUM(%(balance_select)s)
|
|
FILTER (WHERE account_move_line__move_id.move_type IN ('out_refund', 'in_refund') AND account_move_line.tax_line_id IS NULL)),
|
|
0
|
|
) < %(threshold_value)s
|
|
""",
|
|
table_references=query.from_clause,
|
|
ct_query=report._currency_table_aml_join(options),
|
|
search_condition=query.where_clause,
|
|
country_code='EE%',
|
|
extra_where=extra_where,
|
|
balance_select=report._currency_table_apply_rate(SQL('account_move_line.balance')),
|
|
threshold_value=threshold_value,
|
|
)
|
|
self.env.cr.execute(partners_to_exclude_query)
|
|
partners_to_exclude = [row[0] for row in self.env.cr.fetchall()]
|
|
|
|
query = report._get_report_query(options, 'strict_range', [('partner_id', 'not in', partners_to_exclude)])
|
|
|
|
# Additional lines in the query depending on the group by
|
|
if current_groupby == 'id':
|
|
# At the lowest level, we want to group invoice lines by the tax (excluding 0%) and by whether it
|
|
# is under one of the two types of special procedure. So we combine the lines of the same tax and type
|
|
# under the same move_line_id as a grouping key.
|
|
select_from_groupby = SQL("MIN(%s) AS grouping_key,", SQL.identifier("account_move_line", "id"))
|
|
groupby_clause = SQL()
|
|
elif current_groupby:
|
|
groupby_field_sql = self.env['account.move.line']._field_to_sql('account_move_line', current_groupby, query)
|
|
select_from_groupby = SQL("%s AS grouping_key,", groupby_field_sql)
|
|
groupby_clause = SQL(", %s", groupby_field_sql)
|
|
else:
|
|
select_from_groupby = SQL()
|
|
groupby_clause = SQL()
|
|
|
|
if kmd_inf_part == 'a':
|
|
# In part A, the invoice total without VAT and the taxable supply presented in the fields 1, 1^1,
|
|
# 2 and 2^1 of Form KMD (VAT report) are displayed. We exclude partners whose Tax ID is / and exclude
|
|
# foreigners who have a Tax ID not starting by EE.
|
|
xmlids = [
|
|
'tax_report_line_1_tag_sale',
|
|
'tax_report_line_1_tag_purchase',
|
|
'tax_report_line_1_1_tag_sale',
|
|
'tax_report_line_1_1_tag_purchase',
|
|
'tax_report_line_1_24_tag_sale',
|
|
'tax_report_line_1_24_tag_purchase',
|
|
'tax_report_line_1_2_tag',
|
|
'tax_report_line_2_tag',
|
|
'tax_report_line_2_1_tag',
|
|
'tax_report_line_2_2_tag',
|
|
]
|
|
|
|
sql_query = SQL("""
|
|
WITH multiple_tax_moves AS (
|
|
SELECT account_move_line.move_id
|
|
FROM account_move_line
|
|
LEFT JOIN account_move_line_account_tax_rel AS move_tax_rel ON move_tax_rel.account_move_line_id = account_move_line.id
|
|
LEFT JOIN account_tax ON account_tax.id = move_tax_rel.account_tax_id
|
|
WHERE account_move_line.display_type = 'product'
|
|
GROUP BY account_move_line.move_id
|
|
HAVING COUNT(DISTINCT account_tax.amount) > 1
|
|
OR SUM(CASE WHEN account_tax.amount = 0 OR account_tax.amount IS NULL THEN 1 ELSE 0 END) > 0
|
|
)
|
|
|
|
SELECT
|
|
%(select_from_groupby)s
|
|
res_partner.company_registry AS buyer_reg_code,
|
|
res_partner.name AS buyer_name,
|
|
account_move_line__move_id.name AS invoice_number,
|
|
account_move_line__move_id.invoice_date AS invoice_date,
|
|
account_move_line__move_id.amount_untaxed_signed AS invoice_sum,
|
|
-- the balance of outgoing moves are inverted, so multiply sum by -1
|
|
-SUM(
|
|
-- if special code is 2, column sum_rate_period should not be filled
|
|
CASE WHEN tag.id = ANY(%(tag_ids)s) AND (base_tax.l10n_ee_kmd_inf_code <> '2' OR base_tax.l10n_ee_kmd_inf_code IS NULL)
|
|
THEN account_move_line.balance
|
|
ELSE 0 END
|
|
) AS sum_rate_period,
|
|
-- special code depends on the tax's l10n_ee_kmd_inf_code and on number of distinct tax rates in the move
|
|
CONCAT_WS(
|
|
', ',
|
|
STRING_AGG(DISTINCT base_tax.l10n_ee_kmd_inf_code, ', '),
|
|
CASE WHEN account_move_line__move_id.id IN (SELECT move_id FROM multiple_tax_moves) THEN '3' END
|
|
) AS comments,
|
|
base_tax.tax_rate
|
|
|
|
FROM %(table_references)s
|
|
|
|
LEFT JOIN res_partner ON res_partner.id = account_move_line__move_id.commercial_partner_id
|
|
LEFT JOIN res_country ON res_country.id = res_partner.country_id
|
|
LEFT JOIN account_account_tag_account_move_line_rel AS tag_move_line_rel ON tag_move_line_rel.account_move_line_id = account_move_line.id
|
|
LEFT JOIN account_account_tag AS tag ON tag.id = tag_move_line_rel.account_account_tag_id
|
|
LEFT JOIN account_move_line_account_tax_rel AS move_tax_rel ON account_move_line.id = move_tax_rel.account_move_line_id
|
|
LEFT JOIN (
|
|
SELECT
|
|
tax.id,
|
|
tax.l10n_ee_kmd_inf_code,
|
|
CASE WHEN tax.l10n_ee_kmd_inf_code = '1' THEN CONCAT(tax.amount::INTEGER, 'erikord') ELSE (tax.amount::INTEGER)::VARCHAR END AS tax_rate
|
|
FROM account_tax as tax
|
|
LEFT JOIN account_tax_group AS tax_group ON tax_group.id = tax.tax_group_id
|
|
) base_tax ON move_tax_rel.account_tax_id = base_tax.id
|
|
|
|
WHERE
|
|
%(search_condition)s
|
|
AND account_move_line__move_id.move_type IN ('out_invoice', 'out_refund')
|
|
AND tax_rate != '0'
|
|
AND res_partner.is_company
|
|
AND (res_country.code = 'EE' OR res_partner.vat ILIKE %(country_code)s)
|
|
|
|
GROUP BY
|
|
res_partner.id,
|
|
account_move_line__move_id.id,
|
|
tax_rate,
|
|
l10n_ee_kmd_inf_code
|
|
%(groupby_clause)s
|
|
""",
|
|
select_from_groupby=select_from_groupby,
|
|
tag_ids=self._get_tag_ids_from_report_lines(xmlids),
|
|
table_references=query.from_clause,
|
|
search_condition=query.where_clause,
|
|
country_code='EE%',
|
|
groupby_clause=groupby_clause,
|
|
)
|
|
else:
|
|
# In part B, the invoice total with VAT and the input VAT amount presented in the field 5 of Form KMD are
|
|
# displayed. We exclude only partners whose Tax ID does not start by EE.
|
|
xmlids = ['tax_report_line_5_tag', 'tax_report_line_5_1_tag', 'tax_report_line_5_2_tag', 'tax_report_line_5_3_tag', 'tax_report_line_5_4_tag']
|
|
|
|
sql_query = SQL("""
|
|
SELECT
|
|
%(select_from_groupby)s
|
|
res_partner.company_registry AS seller_reg_code,
|
|
res_partner.name AS seller_name,
|
|
COALESCE(account_move_line__move_id.ref, account_move_line__move_id.name) AS invoice_number,
|
|
account_move_line__move_id.invoice_date AS invoice_date,
|
|
-- invoice_sum_vat needs to be positive for bills (negative for refunds) and include reverse charge taxes (remove amount from negative repartition lines)
|
|
-account_move_line__move_id.amount_total_signed - SUM(
|
|
CASE WHEN account_tax.l10n_ee_kmd_inf_code = '12' AND tax_repartition.factor_percent < 0 THEN account_move_line.balance ELSE 0 END
|
|
) AS invoice_sum_vat,
|
|
-- input vat in period only includes lines with tax tags corresponding to field 5 of KMD form
|
|
SUM(CASE WHEN account_move_line.display_type = 'tax' AND tag.id = ANY(%(tag_ids)s) THEN account_move_line.balance ELSE 0 END) AS vat_in_period,
|
|
STRING_AGG(DISTINCT account_tax.l10n_ee_kmd_inf_code, ', ') AS comments
|
|
|
|
FROM %(table_references)s
|
|
|
|
LEFT JOIN res_partner ON res_partner.id = account_move_line__move_id.commercial_partner_id
|
|
LEFT JOIN res_country ON res_country.id = res_partner.country_id
|
|
LEFT JOIN account_account_tag_account_move_line_rel AS tag_move_line_rel ON tag_move_line_rel.account_move_line_id = account_move_line.id
|
|
LEFT JOIN account_account_tag AS tag ON tag.id = tag_move_line_rel.account_account_tag_id
|
|
LEFT JOIN account_tax ON account_move_line.tax_line_id = account_tax.id
|
|
LEFT JOIN account_tax_repartition_line AS tax_repartition ON account_move_line.tax_repartition_line_id = tax_repartition.id
|
|
|
|
WHERE
|
|
%(search_condition)s
|
|
AND account_move_line__move_id.move_type IN ('in_invoice', 'in_refund')
|
|
AND (res_country.code = 'EE' OR res_partner.vat ILIKE %(country_code)s)
|
|
|
|
GROUP BY
|
|
res_partner.id,
|
|
account_move_line__move_id.id
|
|
%(groupby_clause)s
|
|
|
|
HAVING
|
|
SUM(CASE WHEN account_move_line.display_type = 'tax' AND tag.id = ANY(%(tag_ids)s) THEN account_move_line.balance ELSE 0 END) != 0
|
|
""",
|
|
select_from_groupby=select_from_groupby,
|
|
tag_ids=self._get_tag_ids_from_report_lines(xmlids),
|
|
table_references=query.from_clause,
|
|
search_condition=query.where_clause,
|
|
country_code='EE%',
|
|
groupby_clause=groupby_clause,
|
|
)
|
|
|
|
self.env.cr.execute(sql_query)
|
|
query_res_lines = self.env.cr.dictfetchall()
|
|
|
|
build_functions = {
|
|
'a': self.build_result_dict_kmd_inf_a,
|
|
'b': self.build_result_dict_kmd_inf_b
|
|
}
|
|
# Group result per grouping key and call function to build result dictionaries
|
|
if not current_groupby:
|
|
return build_functions[kmd_inf_part](query_res_lines, current_groupby)
|
|
|
|
result = []
|
|
all_res_per_grouping_key = {}
|
|
for query_res in query_res_lines:
|
|
grouping_key = query_res['grouping_key']
|
|
all_res_per_grouping_key.setdefault(grouping_key, []).append(query_res)
|
|
|
|
for grouping_key, query_res_lines in all_res_per_grouping_key.items():
|
|
result.append((grouping_key, build_functions[kmd_inf_part](query_res_lines, current_groupby)))
|
|
|
|
return result
|
|
|
|
####################################################
|
|
# HELPER FUNCTIONS
|
|
####################################################
|
|
|
|
def _get_tag_ids_from_report_lines(self, report_lines_xmlids):
|
|
return list({
|
|
tag_id
|
|
for xmlid in report_lines_xmlids
|
|
for ref in [self.env.ref(f'l10n_ee.{xmlid}', raise_if_not_found=False)]
|
|
if ref
|
|
for tag_id in ref._get_matching_tags().ids
|
|
})
|