191 lines
8.8 KiB
Python
191 lines
8.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
import logging
|
|
|
|
from lxml import etree
|
|
|
|
from odoo import fields, models, release, _
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools import date_utils, SQL
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class L10n_NoTaxReportHandler(models.AbstractModel):
|
|
_name = 'l10n_no.tax.report.handler'
|
|
_inherit = ['account.tax.report.handler']
|
|
_description = "Norvegian Tax Report Custom Handler"
|
|
|
|
def _custom_options_initializer(self, report, options, previous_options):
|
|
super()._custom_options_initializer(report, options, previous_options=previous_options)
|
|
|
|
options.setdefault('buttons', []).append({
|
|
'name': _('XML'),
|
|
'sequence': 3,
|
|
'action': 'export_file',
|
|
'action_param': 'print_norwegian_report_xml',
|
|
'file_export_type': _('XML'),
|
|
})
|
|
|
|
def _l10n_no_set_time_interval(self, options):
|
|
"""Set the correct time interval based on the dates of the report.
|
|
|
|
A time interval is a string in Norwegian that represents the starting and ending months of the report
|
|
(example: "januar-februar").
|
|
|
|
If the report is for only one month or for the whole year, the time interval is different
|
|
(example: "januar" for a single month or "aarlig" for a full year).
|
|
|
|
:rtype: dict
|
|
:returns: a dictionary with values useful to set a time interval
|
|
"""
|
|
date_from = fields.Date.to_date(options['date']['date_from'])
|
|
date_to = fields.Date.to_date(options['date']['date_to'])
|
|
|
|
if date_from.day != 1 or date_to.day != date_utils.end_of(date_to, 'month').day:
|
|
raise UserError(_("The chosen date interval is invalid, it should be from the start of one month to the end of the other month"))
|
|
|
|
month_from = date_from.month
|
|
month_to = date_to.month
|
|
year_from = date_from.year
|
|
year_to = date_to.year
|
|
|
|
number_of_months = (year_to - year_from) * 12 + (month_to - month_from) + 1 # +1, it's the end of the month
|
|
|
|
if number_of_months not in (1, 2, 3, 6, 12) or month_to % number_of_months != 0:
|
|
raise UserError(_("The chosen dates do not correspond to a valid time interval for the tax return XML"))
|
|
|
|
norwegian_months = {
|
|
0: 'aarlig',
|
|
1: 'januar',
|
|
2: 'februar',
|
|
3: 'mars',
|
|
4: 'april',
|
|
5: 'mai',
|
|
6: 'juni',
|
|
7: 'juli',
|
|
8: 'august',
|
|
9: 'september',
|
|
10: 'oktober',
|
|
11: 'november',
|
|
12: 'desember',
|
|
}
|
|
if number_of_months in (1, 12):
|
|
months_str_terms = [norwegian_months[month_to % 12]]
|
|
else:
|
|
months_str_terms = [norwegian_months[month_from], norwegian_months[month_to]]
|
|
|
|
time_period = '-'.join(months_str_terms)
|
|
return {
|
|
'date_from': options['date']['date_from'],
|
|
'date_to': options['date']['date_to'],
|
|
'time_period': time_period,
|
|
'year': year_from,
|
|
'number_of_months': number_of_months,
|
|
}
|
|
|
|
def _l10n_no_query_taxes(self, report, options):
|
|
"""Fetch taxes to fill the xml file.
|
|
|
|
Taxes can either be input or output taxes. Input taxes have a negative tax amount while output taxes have a
|
|
positive one. Inside the xml file, input taxes don't need to display a base amount, only a tax amount.
|
|
|
|
Most (standard) taxes are grouped by their codes (norwegian tax codes), in order to have for each tax the total
|
|
base amount, as well as the total tax amount.
|
|
|
|
:rtype: dict
|
|
:returns: a dictionary with:
|
|
(1) the total amount to be paid to the authorities
|
|
(2) a list of taxes (dictionaries) to be displayed in the XML
|
|
"""
|
|
|
|
query = report._get_report_query(options, 'strict_range')
|
|
tax_details_query = self.env['account.move.line']._get_query_tax_details(query.from_clause, query.where_clause)
|
|
|
|
# The following tax details query will group taxes with their tax repartition lines in order to have one result
|
|
# per standard tax and two results per deductible taxes.
|
|
acc_tag_name = self.env['account.account.tag'].with_context(lang='en_US')._field_to_sql('tag', 'name')
|
|
tax_name = self.env['account.tax']._field_to_sql('tax', 'name')
|
|
|
|
self.env.cr.execute(SQL(
|
|
'''
|
|
SELECT
|
|
/* -Take the opposite to reflect amounts due to and owed by the tax authorities.
|
|
-`tax_amount` rounding down to a clean whole number for tax authority rules.
|
|
-Negative repartition lines cause the tax and base amounts to have different signs,
|
|
use the absolute `base_amount` to align them for the XML.
|
|
-Format `rate` for Norwegian rules: drop useless trailing zeros and change the decimal point to a comma.
|
|
*/
|
|
(FLOOR(SUM(-tdr.tax_amount)))::integer AS tax_amount,
|
|
(ABS(SUM(tdr.base_amount)))::integer AS base_amount,
|
|
REPLACE((ROUND(tax.amount, 2)::float)::text, '.', ',') AS rate,
|
|
%(tax_name)s AS name,
|
|
SUBSTRING(report_line.code, '[0-9]+') AS tax_code
|
|
FROM (%(tax_details_query)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
|
|
JOIN account_account_tag_account_tax_repartition_line_rel repartition_rel ON repartition_rel.account_tax_repartition_line_id = trl.id
|
|
JOIN account_account_tag tag ON
|
|
tag.id = repartition_rel.account_account_tag_id
|
|
AND tag.applicability = 'taxes'
|
|
AND tag.country_id = %(base_no_id)s
|
|
JOIN account_report_expression expression ON expression.engine = 'tax_tags' AND LTRIM(expression.formula, '-') = %(acc_tag_name)s
|
|
JOIN account_report_line report_line ON expression.report_line_id = report_line.id
|
|
JOIN res_country tax_country ON tax_country.id = tax.country_id
|
|
WHERE tdr.tax_exigible
|
|
GROUP BY tdr.tax_repartition_line_id, tax.id, report_line.code
|
|
''',
|
|
tax_name=tax_name,
|
|
tax_details_query=tax_details_query,
|
|
base_no_id=self.env.ref('base.no').id,
|
|
acc_tag_name=acc_tag_name,
|
|
))
|
|
|
|
tax_details_list = self.env.cr.dictfetchall()
|
|
tax_total = sum(row['tax_amount'] for row in tax_details_list)
|
|
return {
|
|
'tax_total': tax_total,
|
|
'tax_details_list': tax_details_list,
|
|
}
|
|
|
|
def print_norwegian_report_xml(self, options):
|
|
report = self.env['account.report'].browse(options['report_id'])
|
|
sender_company = report._get_sender_company_for_export(options)
|
|
if not sender_company.l10n_no_bronnoysund_number:
|
|
raise UserError(_("No number for the Register of Legal Entities, please set one up for company %s", sender_company.name))
|
|
|
|
# Number used for bill payment in Norway to identify the customer and the invoice, abbreviated KID
|
|
# (KID = kundeidentifikasjon)
|
|
if not sender_company.vat:
|
|
raise UserError(_("No KID number, please set one up for company %s", sender_company.name))
|
|
|
|
time_interval_dict = self._l10n_no_set_time_interval(options)
|
|
tax_details_dict = self._l10n_no_query_taxes(report, options)
|
|
|
|
data = {
|
|
'xmlns': 'no:skatteetaten:fastsetting:avgift:mva:skattemeldingformerverdiavgift:v1.0',
|
|
'submission_ref': 1, # Always 1 - no online submission (yet)
|
|
'odoo_version': release.version,
|
|
'company_kid': sender_company.vat.upper().removeprefix('NO').removesuffix('MVA'),
|
|
'company_bronnoysund_number': sender_company.l10n_no_bronnoysund_number,
|
|
'company_name': sender_company.name,
|
|
'category': "alminnelig",
|
|
'note': f"Mva-melding for {sender_company.name}",
|
|
'sender_company': sender_company,
|
|
**time_interval_dict,
|
|
**tax_details_dict,
|
|
}
|
|
xml_content = self.env['ir.qweb']._render('l10n_no_reports.no_evat_template', values=data)
|
|
tree = etree.fromstring(xml_content, parser=etree.XMLParser(remove_blank_text=True))
|
|
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',
|
|
}
|