diff --git a/l10n_es_aeat_verifactu/__manifest__.py b/l10n_es_aeat_verifactu/__manifest__.py
index fbabed4eb9f..509982f39e8 100644
--- a/l10n_es_aeat_verifactu/__manifest__.py
+++ b/l10n_es_aeat_verifactu/__manifest__.py
@@ -17,6 +17,7 @@
"l10n_es_aeat",
"account_invoice_refund_link",
"queue_job",
+ "account",
],
"data": [
"data/aeat_verifactu_tax_agency_data.xml",
@@ -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",
],
}
diff --git a/l10n_es_aeat_verifactu/data/aeat_verifactu_tax_agency_data.xml b/l10n_es_aeat_verifactu/data/aeat_verifactu_tax_agency_data.xml
index c63852a39f6..b3ecf8e7b81 100644
--- a/l10n_es_aeat_verifactu/data/aeat_verifactu_tax_agency_data.xml
+++ b/l10n_es_aeat_verifactu/data/aeat_verifactu_tax_agency_data.xml
@@ -9,5 +9,7 @@
https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP
+ https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR
+ https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR
diff --git a/l10n_es_aeat_verifactu/models/account_move.py b/l10n_es_aeat_verifactu/models/account_move.py
index 0e1cb0bca18..a4da671077d 100644
--- a/l10n_es_aeat_verifactu/models/account_move.py
+++ b/l10n_es_aeat_verifactu/models/account_move.py
@@ -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
@@ -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
diff --git a/l10n_es_aeat_verifactu/models/aeat_tax_agency.py b/l10n_es_aeat_verifactu/models/aeat_tax_agency.py
index adbc1b91a0b..dbf95e6bcc1 100644
--- a/l10n_es_aeat_verifactu/models/aeat_tax_agency.py
+++ b/l10n_es_aeat_verifactu/models/aeat_tax_agency.py
@@ -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()
diff --git a/l10n_es_aeat_verifactu/models/verifactu_mixin.py b/l10n_es_aeat_verifactu/models/verifactu_mixin.py
index dfa921a4b68..3520c8e5334 100644
--- a/l10n_es_aeat_verifactu/models/verifactu_mixin.py
+++ b/l10n_es_aeat_verifactu/models/verifactu_mixin.py
@@ -2,9 +2,12 @@
# Copyright 2024 Aures TIC - Almudena de La Puente
# 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
@@ -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
@@ -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
@@ -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()
diff --git a/l10n_es_aeat_verifactu/readme/CONTRIBUTORS.rst b/l10n_es_aeat_verifactu/readme/CONTRIBUTORS.rst
index 29280226b91..13ba0bcd75c 100644
--- a/l10n_es_aeat_verifactu/readme/CONTRIBUTORS.rst
+++ b/l10n_es_aeat_verifactu/readme/CONTRIBUTORS.rst
@@ -2,3 +2,4 @@
* Almudena de La Puente
* Laura Cazorla
* Andreu Orensanz
+* Iván Antón
diff --git a/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py b/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py
index e0797cf8316..a0b50a61ee6 100644
--- a/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py
+++ b/l10n_es_aeat_verifactu/tests/test_10n_es_aeat_verifactu.py
@@ -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
@@ -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.",
+ )
diff --git a/l10n_es_aeat_verifactu/views/aeat_tax_agency_view.xml b/l10n_es_aeat_verifactu/views/aeat_tax_agency_view.xml
index 1f790731475..2c0ca0e5416 100644
--- a/l10n_es_aeat_verifactu/views/aeat_tax_agency_view.xml
+++ b/l10n_es_aeat_verifactu/views/aeat_tax_agency_view.xml
@@ -17,6 +17,13 @@
/>
+
+
+
+
diff --git a/l10n_es_aeat_verifactu/views/report_invoice.xml b/l10n_es_aeat_verifactu/views/report_invoice.xml
new file mode 100644
index 00000000000..910cd124107
--- /dev/null
+++ b/l10n_es_aeat_verifactu/views/report_invoice.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+