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 00000000..123682fc
Binary files /dev/null and b/docs/validate_vat_id.webm differ
diff --git a/erpnext_germany/api.py b/erpnext_germany/api.py
new file mode 100644
index 00000000..eb07091f
--- /dev/null
+++ b/erpnext_germany/api.py
@@ -0,0 +1,20 @@
+import frappe
+
+
+@frappe.whitelist()
+def validate_vat_id(vat_id: str) -> 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))