From f2077d0569f9cec3c43ff2fd5cdb58dccd4b2cd9 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 20 Apr 2022 01:51:13 +0200 Subject: [PATCH] feat: validate EU VAT ID (#2) --- README.md | 4 ++ docs/validate_vat_id.webm | Bin 0 -> 23265 bytes erpnext_germany/api.py | 20 ++++++++ erpnext_germany/hooks.py | 8 +-- erpnext_germany/public/js/customer.js | 5 ++ erpnext_germany/public/js/supplier.js | 5 ++ erpnext_germany/public/js/validate_vat_id.js | 51 +++++++++++++++++++ erpnext_germany/translations/de.csv | 4 ++ erpnext_germany/utils/eu_vat.py | 12 +++++ erpnext_germany/utils/test_utils.py | 21 ++++++++ 10 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 docs/validate_vat_id.webm create mode 100644 erpnext_germany/api.py create mode 100644 erpnext_germany/public/js/customer.js create mode 100644 erpnext_germany/public/js/supplier.js create mode 100644 erpnext_germany/public/js/validate_vat_id.js create mode 100644 erpnext_germany/utils/eu_vat.py create mode 100644 erpnext_germany/utils/test_utils.py diff --git a/README.md b/README.md index 4281e7b4..6ce3bbde 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,10 @@ App to hold regional code for Germany, built on top of ERPNext. ![Section with Register Information](docs/register_information.png) +- Validation of EU VAT IDs + + + ## Installation ### On Frappe Cloud diff --git a/docs/validate_vat_id.webm b/docs/validate_vat_id.webm new file mode 100644 index 0000000000000000000000000000000000000000..123682fc5410c90578f617b0f319ad662b5c0e7e GIT binary patch literal 23265 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 bool: + """Use the EU VAT checker to validate a VAT ID.""" + from .utils.eu_vat import is_valid_eu_vat_id + + result = frappe.cache().hget("eu_vat_validation", vat_id, shared=True) + if result is not None: + return result + + try: + result = is_valid_eu_vat_id(vat_id) + frappe.cache().hset("eu_vat_validation", vat_id, result, shared=True) + except Exception: + frappe.response["status_code"] = 501 + result = None + + return result diff --git a/erpnext_germany/hooks.py b/erpnext_germany/hooks.py index 0a967351..26e29676 100644 --- a/erpnext_germany/hooks.py +++ b/erpnext_germany/hooks.py @@ -1,7 +1,6 @@ from . import __version__ as app_version from .constants import REGISTER_COURTS - app_name = "erpnext_germany" app_title = "ERPNext Germany" app_publisher = "ALYF GmbH" @@ -17,7 +16,7 @@ # include js, css files in header of desk.html # app_include_css = "/assets/erpnext_germany/css/erpnext_germany.css" -# app_include_js = "/assets/erpnext_germany/js/erpnext_germany.js" +app_include_js = "/assets/erpnext_germany/js/validate_vat_id.js" # include js, css files in header of web template # web_include_css = "/assets/erpnext_germany/css/erpnext_germany.css" @@ -34,7 +33,10 @@ # page_js = {"page" : "public/js/file.js"} # include js in doctype views -# doctype_js = {"doctype" : "public/js/doctype.js"} +doctype_js = { + "Customer": "public/js/customer.js", + "Supplier": "public/js/supplier.js", +} # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} diff --git a/erpnext_germany/public/js/customer.js b/erpnext_germany/public/js/customer.js new file mode 100644 index 00000000..5a903a9b --- /dev/null +++ b/erpnext_germany/public/js/customer.js @@ -0,0 +1,5 @@ +frappe.ui.form.on("Customer", { + setup: function (frm) { + erpnext_germany.utils.setup_vat_id_validation_button(frm); + } +}); \ No newline at end of file diff --git a/erpnext_germany/public/js/supplier.js b/erpnext_germany/public/js/supplier.js new file mode 100644 index 00000000..9f77d6d5 --- /dev/null +++ b/erpnext_germany/public/js/supplier.js @@ -0,0 +1,5 @@ +frappe.ui.form.on("Supplier", { + setup: function (frm) { + erpnext_germany.utils.setup_vat_id_validation_button(frm); + } +}); \ No newline at end of file diff --git a/erpnext_germany/public/js/validate_vat_id.js b/erpnext_germany/public/js/validate_vat_id.js new file mode 100644 index 00000000..6930dde9 --- /dev/null +++ b/erpnext_germany/public/js/validate_vat_id.js @@ -0,0 +1,51 @@ + +frappe.provide("erpnext_germany.utils"); + +erpnext_germany.utils.setup_vat_id_validation_button = function (frm) { + const $wrapper = $(frm.fields_dict.tax_id.input_area); + const $link_button = $( + ` + + ${frappe.utils.icon("search", "sm")} + + ` + ); + + $($wrapper).append($link_button); + $link_button.toggle(true); + $link_button.on("click", "a", () => { + const vat_id = frm.doc.tax_id; + erpnext_germany.utils.check_vat_id(vat_id) + .then((is_valid) => { + if (is_valid === true) { + frappe.show_alert({ + message: __("Tax ID is a valid EU VAT ID"), + indicator: "green", + }); + } else if (is_valid === false) { + frappe.show_alert({ + message: __("Tax ID is not a valid EU VAT ID"), + indicator: "red", + }); + } else { + frappe.show_alert({ + message: __("Tax ID could not be validated"), + indicator: "grey", + }); + } + }); + }); +}; + +erpnext_germany.utils.check_vat_id = function (vat_id) { + return frappe.xcall( + "erpnext_germany.api.validate_vat_id", + { vat_id: vat_id }, + ); +}; diff --git a/erpnext_germany/translations/de.csv b/erpnext_germany/translations/de.csv index b0aae0ee..d9838eda 100644 --- a/erpnext_germany/translations/de.csv +++ b/erpnext_germany/translations/de.csv @@ -1,3 +1,7 @@ Register Type,Registerart Register Number,Registernummer Register Court,Registergericht +Validate EU VAT ID,Ust-IdNr. validieren +Tax ID is a valid EU VAT ID,Steuernummer ist gültige Ust-IdNr. +Tax ID is not a valid EU VAT ID,Steuernummer ist keine gültige Ust-IdNr. +Tax ID could not be validated,Steuernummer konnte nicht validiert werden. diff --git a/erpnext_germany/utils/eu_vat.py b/erpnext_germany/utils/eu_vat.py new file mode 100644 index 00000000..097f793a --- /dev/null +++ b/erpnext_germany/utils/eu_vat.py @@ -0,0 +1,12 @@ +import zeep + + +def is_valid_eu_vat_id(vat_id) -> bool: + """Use the EU VAT checker to validate a VAT ID.""" + country_code = vat_id[:2].upper() + vat_number = vat_id[2:].replace(" ", "") + + client = zeep.Client("https://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl") + result = client.service.checkVat(vatNumber=vat_number, countryCode=country_code) + + return result.valid diff --git a/erpnext_germany/utils/test_utils.py b/erpnext_germany/utils/test_utils.py new file mode 100644 index 00000000..019c8994 --- /dev/null +++ b/erpnext_germany/utils/test_utils.py @@ -0,0 +1,21 @@ +from unittest import TestCase +from .eu_vat import is_valid_vat_id + + +class TestUtils(TestCase): + def test_validate_vat_id(self): + valid_ids = [ + "DE329035522", # ALYF + "DE210157578", # SAP + ] + invalid_ids = [ + "ABC123", + "Test Test", + "DE1234567890", + ] + + for vat_id in valid_ids: + self.assertTrue(is_valid_vat_id(vat_id)) + + for vat_id in invalid_ids: + self.assertFalse(is_valid_vat_id(vat_id))