366 lines
17 KiB
Python
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",
|
|
}
|