Skip to content

Commit

Permalink
[ADD] Primera version QR Verifactu (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
ozono authored Nov 28, 2024
1 parent 61fb366 commit 477fd4f
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 0 deletions.
2 changes: 2 additions & 0 deletions l10n_es_aeat_verifactu/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"l10n_es_aeat",
"account_invoice_refund_link",
"queue_job",
"account",
],
"data": [
"data/aeat_verifactu_tax_agency_data.xml",
Expand All @@ -32,5 +33,6 @@
"views/aeat_verifactu_map_view.xml",
"views/aeat_verifactu_map_lines_view.xml",
"views/aeat_verifactu_registration_keys_view.xml",
"views/report_invoice.xml",
],
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@
<field
name="verifactu_wsdl_out_test_address"
>https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP</field>
<field name="verifactu_qr_base_url">https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR</field>
<field name="verifactu_qr_base_url_test_address">https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR</field>
</record>
</odoo>
14 changes: 14 additions & 0 deletions l10n_es_aeat_verifactu/models/account_move.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

import pytz
from collections import OrderedDict

from odoo import _, api, fields, models
from odoo.exceptions import UserError
Expand Down Expand Up @@ -377,5 +378,18 @@ def _get_receiver_dict(self):
}
}

def _get_verifactu_qr_values(self):
"""Get the QR values for the verifactu"""
self.ensure_one()
company_vat = self.company_id.partner_id._parse_aeat_vat_info()[2]
return OrderedDict(
[
("nif", company_vat),
("numserie", self.name),
("fecha", self.invoice_date.strftime("%d-%m-%Y")),
("importe", self.amount_total),
]
)

def cancel_verifactu(self):
raise NotImplementedError
2 changes: 2 additions & 0 deletions l10n_es_aeat_verifactu/models/aeat_tax_agency.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class AeatTaxAgency(models.Model):
verifactu_wsdl_out_test_address = fields.Char(
string="SuministroInformacion Test Address"
)
verifactu_qr_base_url = fields.Char(string="QR Base URL")
verifactu_qr_base_url_test_address = fields.Char(string="QR Base URL Test")

def _connect_params_verifactu(self, mapping_key, company):
self.ensure_one()
Expand Down
63 changes: 63 additions & 0 deletions l10n_es_aeat_verifactu/models/verifactu_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
# Copyright 2024 Aures TIC - Almudena de La Puente <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

import base64
import io
import json
import logging
from hashlib import sha256
from urllib.parse import urlencode

from requests import Session

Expand All @@ -15,6 +18,14 @@

from odoo.addons.l10n_es_aeat.models.aeat_mixin import round_by_keys

_logger = logging.getLogger(__name__)

try:
import qrcode
except (ImportError, IOError) as err:
qrcode = None
_logger.error(err)

###########################################
# revisar los imports que no hagan falta
# cuando funcione bien el _connect_aeat sin tener que poner
Expand Down Expand Up @@ -85,6 +96,8 @@ class VerifactuMixin(models.AbstractModel):
readonly=True,
string="Verifactu Code",
)
verifactu_qr_url = fields.Char("URL", compute="_compute_verifactu_qr_url")
verifactu_qr = fields.Binary(string="QR", compute="_compute_verifactu_qr")

def _compute_verifactu_enabled(self):
raise NotImplementedError
Expand All @@ -100,6 +113,56 @@ def _compute_verifactu_macrodata(self):
>= 0
)

def _compute_verifactu_qr_url(self):
"""Returns the URL to be used in the QR code. A sample URL would be (urlencoded):
https://prewww2.aeat.es/wlpl/TIKECONT/ValidarQR?nif=89890001K&numserie=12345678%26G33&fecha=01-01-2024&importe=241.4
"""
for record in self:
agency = self.env.ref("l10n_es_aeat.aeat_tax_agency_spain")
if record.company_id.verifactu_test:
qr_base_url = agency.verifactu_qr_base_url_test_address
else:
qr_base_url = agency.verifactu_qr_base_url

qr_values = record._get_verifactu_qr_values()

# Check all values are ASCII between 32 and 126
for value in qr_values.values():
try:
str(value).encode("ascii")
except UnicodeEncodeError:
raise UserError(_("QR URL value '{}' is not ASCII").format(value))

# Build QR URL
qr_url = "{}?{}".format(
qr_base_url,
urlencode(qr_values, encoding="utf-8"),
)

record.verifactu_qr_url = qr_url

def _compute_verifactu_qr(self):
# If qrcode module is not available, we can't generate QR codes
if not qrcode:
_logger.error("qrcode module is not available")
return
for record in self:
if record.state != "posted" or not record.verifactu_enabled:
record.verifactu_qr = False
continue
qr = qrcode.QRCode(
border=0, error_correction=qrcode.constants.ERROR_CORRECT_M
)
qr.add_data(record.verifactu_qr_url)
qr.make()
img = qr.make_image()
with io.BytesIO() as temp:
img.save(temp, format="PNG")
record.verifactu_qr = base64.b64encode(temp.getvalue())

def _get_verifactu_qr_values(self):
raise NotImplementedError

@api.model
def _get_verifactu_tax_keys(self):
return self.env["account.fiscal.position"]._get_verifactu_tax_keys()
Expand Down
1 change: 1 addition & 0 deletions l10n_es_aeat_verifactu/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
* Almudena de La Puente <[email protected]>
* Laura Cazorla <[email protected]>
* Andreu Orensanz <[email protected]>
* Iván Antón <[email protected]>
101 changes: 101 additions & 0 deletions l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import json
from hashlib import sha256
from urllib.parse import urlparse, parse_qs

from odoo.modules.module import get_resource_path

Expand Down Expand Up @@ -196,3 +197,103 @@ def test_get_verifactu_invoice_data(self):
for inv_type, lines, extra_vals in mapping:
self._create_and_test_invoice_verifactu_dict(inv_type, lines, extra_vals)
return


class TestL10nEsAeatVerifactuQR(TestL10nEsAeatVerifactuBase):
@classmethod
def setUpClass(cls):
super().setUpClass()

def _get_required_qr_params(self):
"""Helper to generate the required QR code parameters."""
return {
"nif": self.invoice.company_id.partner_id._parse_aeat_vat_info()[2],
"numserie": self.invoice.name,
"fecha": self.invoice._change_date_format(self.invoice.invoice_date),
"importe": self.invoice.amount_total,
}

def test_verifactu_qr_generation(self):
"""
Test the generation of the QR code image for the invoice.
"""
self.invoice.action_post()
qr_code = self.invoice.verifactu_qr

self.assertTrue(qr_code, "QR code should be generated for the invoice.")
self.assertIsInstance(qr_code, bytes, "QR code should be in bytes format.")

def test_verifactu_qr_url_format(self):
"""
Test the format of the generated QR URL to ensure it meets expected criteria.
"""
self.invoice.action_post()
qr_url = self.invoice.verifactu_qr_url

self.assertTrue(qr_url, "QR URL should be generated for the invoice.")

test_url = self.env.ref(
"l10n_es_aeat.aeat_tax_agency_spain"
).verifactu_qr_base_url_test_address
self.assertTrue(test_url, "Test URL should not be empty.")

parsed_url = urlparse(qr_url)
actual_params = parse_qs(parsed_url.query)

expected_params = self._get_required_qr_params()
for key, expected_value in expected_params.items():
self.assertIn(
key, actual_params, f"QR URL should contain the parameter: {key}"
)
self.assertEqual(
actual_params[key][0],
str(expected_value),
f"QR URL parameter '{key}' should have value '{expected_value}', got '{actual_params[key][0]}' instead.",
)

def test_verifactu_qr_code_generation_on_draft(self):
"""
Ensure that the QR code is not generated for invoices in draft state.
"""
qr_code = self.invoice.verifactu_qr
self.assertFalse(qr_code, "QR code should not be generated for draft invoices.")

def test_verifactu_qr_code_after_update(self):
"""
Test that the QR code is regenerated if the invoice details are updated.
"""
self.invoice.action_post()
original_qr_code = self.invoice.verifactu_qr

self.invoice.button_cancel()

self.invoice.button_draft()

self.invoice.write(
{
"invoice_line_ids": [
(
0,
0,
{
"product_id": self.product.id,
"account_id": self.account_expense.id,
"name": "Updated line",
"price_unit": 200,
"quantity": 1,
},
)
]
}
)
self.invoice.action_post()

self.invoice.invalidate_model(["verifactu_qr_url", "verifactu_qr"])

updated_qr_code = self.invoice.verifactu_qr

self.assertNotEqual(
original_qr_code,
updated_qr_code,
"QR code should be regenerated after invoice update.",
)
7 changes: 7 additions & 0 deletions l10n_es_aeat_verifactu/views/aeat_tax_agency_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
/>
</group>
</group>
<group string="Validar QR">
<field name="verifactu_qr_base_url" string="Base URL" />
<field
name="verifactu_qr_base_url_test_address"
string="Test Base URL"
/>
</group>
</page>
</notebook>
</field>
Expand Down
17 changes: 17 additions & 0 deletions l10n_es_aeat_verifactu/views/report_invoice.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<template id="report_invoice_document" inherit_id="account.report_invoice_document"
priority="1000">
<div class="page" position="inside">
<div id="verifactu" style="padding-top:50px;page-break-inside:avoid;"
class="text-center" t-if="o.verifactu_enabled and o.verifactu_qr">
<ul class="list-inline mb4">QR Tributario</ul>
<ul id="verifactu_qr" class="list-inline mb4">
<img t-attf-src="data:image/png;base64,{{o.verifactu_qr}}"
style="min-width: 30mm; max-width: 40mm" />
</ul>
<ul class="list-inline mb4">VERI*FACTU</ul>
</div>
</div>
</template>
</odoo>

0 comments on commit 477fd4f

Please sign in to comment.