Files
enterprise-addons/l10n_dk_reports/wizard/tax_report_wizard.py
T

366 lines
17 KiB
Python

import requests
from dateutil.relativedelta import relativedelta
from enum import Enum
from lxml import etree
from uuid import uuid4
from odoo import _, api, fields, models
from odoo.exceptions import RedirectWarning, UserError
from odoo.tools import date_utils
from odoo.tools.xml_utils import find_xml_value
URN_NAMESPACE = {'urn': "urn:oio:skat:nemvirksomhed:ws:1.0.0"}
URN1_NAMESPACE = {'urn1': "urn:oio:skat:nemvirksomhed:1.0.0"}
NS_NAMESPACE = {'ns': "http://rep.oio.dk/skat.dk/TSE/angivelse/xml/schemas/2006/09/01/"}
class FrequencyCode(Enum):
NONE_FREQUENCY_CODE = '0'
IMMEDIATELY_FREQUENCY_CODE = '1'
DAILY_FREQUENCY_CODE = '2'
WEEKLY_FREQUENCY_CODE = '5'
TWO_WEEKS_FREQUENCY_CODE = '6'
MONTH_FREQUENCY_CODE = '7'
QUARTER_FREQUENCY_CODE = '8'
HALF_YEAR_FREQUENCY_CODE = '9'
YEAR_FREQUENCY_CODE = '10'
VARIES_FREQUENCY_CODE = '16'
OCCASIONALLY_FREQUENCY_CODE = '17'
class L10nDkTaxReportRSUCalendarWizard(models.TransientModel):
_name = 'l10n_dk_reports.tax.report.calendar.wizard'
_description = 'L10n DK Tax Report calendar service RSU'
date_from = fields.Date(string="Period Starting Date")
date_to = fields.Date(string="Period Ending Date")
description = fields.Char(help="A description of the declaration type", default='Moms')
report_id = fields.Many2one(comodel_name='account.report')
company_id = fields.Many2one(comodel_name='res.company', readonly=True, required=True)
def action_call_company_calendar(self):
"""
This function will send the first request to the web service (VirksomhedKalenderHent). This will have a
response containing the periods for which the legal entity must submit VAT Returns.
"""
self.ensure_one()
ReportHandler = self.env['l10n_dk.tax.report.handler']
# Check that the information of the company and the wizard are correct before creating the body and sending it
self._check_call_company_calendar()
# Creation - body of the request
body = self.env['ir.qweb']._render('l10n_dk_reports.calendarService', {
'transaction_id': uuid4(),
'transaction_time': fields.Datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
'company_registry': self.company_id.company_registry,
'description': self.description,
'date_from': self.date_from,
'date_to': self.date_to,
})
envelope = ReportHandler._create_envelope(body)
try:
# Send the request to the endpoint
response = requests.post(
url="https://b2b.skat.dk/B2BVirksomhedKalenderHentWSSProxyWEB/VirksomhedKalenderHentService",
data=envelope['data'],
headers={'content-type': 'text/xml'},
timeout=10,
)
response.raise_for_status()
except requests.exceptions.RequestException as e:
raise UserError(_("Sorry, something went wrong: %s", e))
response_tree = etree.fromstring(response.text)
# Deal with the error code
ReportHandler._error_code_handler(response_tree)
has_information = response_tree.xpath('.//urn:AngivelseFrekvensListe', namespaces=URN_NAMESPACE)
if has_information is None:
raise UserError(_("The information entered or your information's (Company registry) give an empty result"))
response_info = {
'start_date': find_xml_value('.//urn1:AngivelseFrekvensForholdGyldigFraDate', response_tree, namespaces=URN1_NAMESPACE),
'frequency_code': find_xml_value('.//urn1:AngivelseFrekvensTypeCode', response_tree, namespaces=URN1_NAMESPACE),
'deadline_date': response_tree.xpath('.//urn1:AngivelseFristKalenderFristDato', namespaces=URN1_NAMESPACE)[-1].text,
}
# Optional Field
end_date = find_xml_value('.//urn1:AngivelseFrekvensForholdGyldigTilDate', response_tree, namespaces=URN1_NAMESPACE)
if end_date is not None:
response_info['end_date'] = end_date
due_date = response_tree.xpath('.//urn1:AngivelseFristKalenderBetalingDato', namespaces=URN1_NAMESPACE)[-1]
if due_date is not None:
response_info['due_date'] = due_date
payment_date = response_tree.xpath('.//urn1:AngivelseFristKalenderBetalingDato', namespaces=URN1_NAMESPACE)[-1]
if payment_date is not None:
response_info['payment_date'] = payment_date.text
settlement_period_start, settlement_period_end = self._calculate_settlement_period(
response_info.get('frequency_code'),
fields.Date.to_date(response_info.get('deadline_date')),
)
wizard_info = self.env['l10n_dk_reports.tax.report.submit.draft.wizard'].create({
'frequency_code': response_info.get('frequency_code'),
'start_date': response_info.get('start_date'),
'deadline_date': response_info.get('deadline_date'),
'end_date': response_info.get('end_date'),
'due_date': response_info.get('due_date'),
'payment_date': response_info.get('payment_date'),
'settlement_period_start': settlement_period_start,
'settlement_period_end': settlement_period_end,
'report_id': self.report_id.id,
'company_id': self.company_id.id,
})
return wizard_info._get_records_action(
name=_("Tax Report RSU Submit Draft"),
target="new",
)
def _check_call_company_calendar(self):
if self.date_from == self.date_to:
raise UserError(_("Please use a Period starting date different than your Period ending date!"))
if not self.company_id.company_registry:
raise RedirectWarning(
_("No company registry associated with your company. Please define one."),
self.env.ref('base.action_res_company_form').id,
_("Company Settings"),
)
@api.model
def _calculate_settlement_period(self, frequency_code, deadline_date):
"""
Calculate the settlement period based on the given frequency code and deadline date.
:param frequency_code: Code representing the frequency.
:param deadline_date: The deadline date to calculate from.
:return: A tuple containing the start and end dates of the settlement period.
"""
def weekly(date):
return date_utils.start_of(date, 'week'), date_utils.end_of(date, 'week')
def bimonthly(date):
start_of_week, end_of_week = weekly(date)
return start_of_week - relativedelta(days=7), end_of_week
def half_yearly(date):
quarter_number = date_utils.get_quarter_number(date)
if quarter_number % 2 == 0:
return (
date_utils.start_of(date_utils.start_of(date, 'quarter') - relativedelta(days=1), 'quarter'),
date_utils.end_of(date, 'quarter')
)
return (
date_utils.start_of(date_utils.start_of(date, 'quarter'), 'quarter'),
date_utils.end_of(date_utils.end_of(date, 'quarter') + relativedelta(days=1), 'quarter')
)
match frequency_code:
case FrequencyCode.IMMEDIATELY_FREQUENCY_CODE:
return deadline_date, deadline_date
case FrequencyCode.DAILY_FREQUENCY_CODE:
return deadline_date - relativedelta(days=1), deadline_date
case FrequencyCode.WEEKLY_FREQUENCY_CODE:
return weekly(deadline_date)
case FrequencyCode.TWO_WEEKS_FREQUENCY_CODE:
return bimonthly(deadline_date)
case FrequencyCode.MONTH_FREQUENCY_CODE:
return date_utils.get_month(deadline_date)
case FrequencyCode.QUARTER_FREQUENCY_CODE:
return date_utils.get_quarter(deadline_date)
case FrequencyCode.HALF_YEAR_FREQUENCY_CODE:
return half_yearly(deadline_date)
case FrequencyCode.YEAR_FREQUENCY_CODE:
return date_utils.start_of(deadline_date, 'year'), date_utils.end_of(deadline_date, 'year')
case _:
return False, False
class L10nDkTaxReportRSUSubmitDraftWizard(models.TransientModel):
_name = 'l10n_dk_reports.tax.report.submit.draft.wizard'
_description = 'L10n DK Tax Report Submit Draft service RSU'
frequency_code = fields.Selection(
selection=[
('0', "None"),
('1', "Immediately"),
('2', "Daily"),
('5', "Weekly"),
('6', "Every 14th day"),
('7', "Monthly"),
('8', "Quarterly"),
('9', "Every 6 months"),
('10', "Yearly"),
('16', "Varies"),
('17', "Occasionally"),
],
readonly=True,
help="How often you have to send the tax report",
)
start_date = fields.Date(readonly=True, help="When the company was first registered for VAT")
deadline_date = fields.Date(readonly=True)
end_date = fields.Date(readonly=True)
due_date = fields.Date(readonly=True)
payment_date = fields.Date(readonly=True)
settlement_period_start = fields.Date()
settlement_period_end = fields.Date()
report_id = fields.Many2one(comodel_name='account.report')
company_id = fields.Many2one(comodel_name='res.company', readonly=True)
def action_submit_draft(self):
self.ensure_one()
lines = self.report_id._get_lines(self.report_id.get_options())
ReportHandler = self.env['l10n_dk.tax.report.handler']
def get_line_value_from_xmlid(lines, xmlid):
return self.env.company.currency_id.round(self.env['account.report']._get_line_from_xml_id(lines, xmlid)['columns'][0]['no_format'])
body = self.env['ir.qweb']._render('l10n_dk_reports.submitDraftService', {
'transaction_id': uuid4(),
'transaction_time': fields.Datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
'company_registry': self.company_id.company_registry,
'settlement_start': self.settlement_period_start,
'settlement_end': self.settlement_period_end,
'vat_declaration': get_line_value_from_xmlid(lines, 'l10n_dk.account_tax_report_line_vat_statement'),
'co2_tax': get_line_value_from_xmlid(lines, 'l10n_dk.account_tax_report_line_deduction_co2_tax'),
'categoryA_goods': get_line_value_from_xmlid(lines, 'l10n_dk.account_tax_report_line_section_a_products'),
'categoryB_eu_goods': get_line_value_from_xmlid(lines, 'l10n_dk.account_tax_report_line_section_b_product_eu'),
'categoryB_non_eu_goods': get_line_value_from_xmlid(lines, 'l10n_dk.account_tax_report_line_section_b_products_non_eu'),
'electricity': get_line_value_from_xmlid(lines, 'l10n_dk.account_tax_report_line_deduction_electrical_tax'),
'categoryC_other_goods_service': get_line_value_from_xmlid(lines, 'l10n_dk.account_tax_report_line_section_c'),
'natural_or_town_gas': get_line_value_from_xmlid(lines, 'l10n_dk.account_tax_report_line_deduction_gas_tax'),
'purchase_vat': get_line_value_from_xmlid(lines, 'l10n_dk.account_tax_report_line_deduction_purchase_tax'),
'coal': get_line_value_from_xmlid(lines, 'l10n_dk.account_tax_report_line_deduction_coal_tax'),
'vat_on_purchase_goods': get_line_value_from_xmlid(lines, 'l10n_dk.account_tax_report_line_international_purchase_products'),
'vat_on_purchase_services': get_line_value_from_xmlid(lines, 'l10n_dk.account_tax_report_line_international_purchase_services'),
'oil': get_line_value_from_xmlid(lines, 'l10n_dk.account_tax_report_line_deduction_oil_bottle_tax'),
'sale_vat': get_line_value_from_xmlid(lines, 'l10n_dk.account_tax_report_line_sales_tax'),
'water': get_line_value_from_xmlid(lines, 'l10n_dk.account_tax_report_line_deduction_water_tax'),
'categoryA_benefits': get_line_value_from_xmlid(lines, 'l10n_dk.account_tax_report_line_section_a_services'),
'categoryB_services': get_line_value_from_xmlid(lines, 'l10n_dk.account_tax_report_line_section_b_services'),
})
envelope = ReportHandler._create_envelope(body)
try:
# Send the request to the endpoint
response = requests.post(
url="https://b2b.skat.dk/B2BModtagMomsangivelseForeloebigWSSProxyWEB/ModtagMomsangivelseForeloebigService",
data=envelope['data'],
headers={'content-type': 'text/xml'},
timeout=10,
)
response.raise_for_status()
except requests.exceptions.RequestException as e:
raise UserError(_("Sorry, something went wrong: %s", e))
response_tree = etree.fromstring(response.text)
# Deal with the error code
ReportHandler._error_code_handler(response_tree)
transaction_identifier = response_tree.xpath('.//urn1:TransaktionIdentifier', namespaces=URN1_NAMESPACE)
if transaction_identifier is None:
raise UserError(_("The information entered give an empty result"))
wizard_info = self.env['l10n_dk_reports.tax.report.receipt.wizard'].create({
'transaction_identifier': transaction_identifier[-1].text,
'link': find_xml_value('.//urn1:UrlIndicator', response_tree, namespaces=URN1_NAMESPACE),
'company_id': self.company_id.id,
})
return wizard_info._get_records_action(
name=_("Tax Report RSU Receipt"),
target="new",
)
class L10nDkTaxReportRSUReceiptWizard(models.TransientModel):
_name = 'l10n_dk_reports.tax.report.receipt.wizard'
_description = 'L10n DK Tax Report receipt service RSU'
link = fields.Char()
transaction_identifier = fields.Char()
is_report_approved = fields.Boolean()
is_receipt_received = fields.Boolean()
ocr_card_type_code = fields.Char(readonly=True)
ocr_identification_number = fields.Char(readonly=True)
ocr_account_number = fields.Char(readonly=True)
payment_ref = fields.Char(readonly=True)
deadline_date = fields.Date(readonly=True)
attachment = fields.Binary(attachment=True)
company_id = fields.Many2one(comodel_name='res.company', readonly=True)
def action_receive_receipt(self):
self.ensure_one()
ReportHandler = self.env['l10n_dk.tax.report.handler']
body = self.env['ir.qweb']._render('l10n_dk_reports.receiptService', {
'transaction_id': uuid4(),
'transaction_time': fields.Datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
'received_transaction_identifier': self.transaction_identifier,
'company_registry': self.company_id.company_registry,
})
envelope = ReportHandler._create_envelope(body)
try:
# Send the request to the endpoint
response = requests.post(
url="https://b2b.skat.dk/B2BMomsangivelseKvitteringHentWSSProxyWEB/MomsangivelseKvitteringHentService",
data=envelope['data'],
headers={'content-type': 'text/xml'},
timeout=10,
)
response.raise_for_status()
except requests.exceptions.RequestException as e:
raise UserError(_("Sorry, something went wrong: %s", e))
response_tree = etree.fromstring(response.text)
# Deal with the error code
ReportHandler._error_code_handler(response_tree)
encoded_pdf = response_tree.xpath('.//urn1:DokumentFilIndholdData', namespaces=URN1_NAMESPACE)
if encoded_pdf is None:
raise UserError(_("The receipt has not been received."))
self.is_receipt_received = True
self.attachment = encoded_pdf[-1].text
self.ocr_card_type_code = find_xml_value('.//urn1:OCRKortTypeKode', response_tree, namespaces=URN1_NAMESPACE)
self.ocr_identification_number = find_xml_value('.//ns:OCRnummerIdentifikator', response_tree, namespaces=NS_NAMESPACE)
self.ocr_account_number = find_xml_value('.//urn1:OCRKontoNummerIdentifikator', response_tree, namespaces=URN1_NAMESPACE)
self.payment_ref = find_xml_value('.//urn1:BetalingIkkeBetaltBeloeb', response_tree, namespaces=URN1_NAMESPACE)
self.deadline_date = find_xml_value('.//urn1:AngivelseFristKalenderBetalingDato', response_tree, namespaces=URN1_NAMESPACE)
return self._get_records_action(
name=_("Tax Report RSU Receipt"),
target="new",
)
def action_approve_tax_report(self):
self.ensure_one()
self.is_report_approved = True
return {
"name": "Approve tax report",
"type": "ir.actions.act_url",
"url": self.link,
}
def action_download_pdf(self):
self.ensure_one()
attachment = self.env['ir.attachment'].search([
('res_model', '=', self._name),
('res_id', '=', self.id),
('res_field', '=', 'attachment'), # The binary field name
], limit=1)
return {
"type": "ir.actions.act_url",
"url": f"/web/content/{attachment.id}",
"target": "new",
}