diff --git a/README.md b/README.md index d406c900..7dd29da7 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,9 @@ App to hold regional code for Germany, built on top of ERPNext. - Validation of EU VAT IDs - ![Validate EU VAT ID](docs/validate_vat_id.gif) + Automatically checks the validity of EU VAT IDs of all your customers every three months, or manually whenever you want. + + ![Validate EU VAT ID](docs/vat_check.png) ### HR diff --git a/docs/validate_vat_id.gif b/docs/validate_vat_id.gif deleted file mode 100644 index 278ac712..00000000 Binary files a/docs/validate_vat_id.gif and /dev/null differ diff --git a/docs/vat_check.png b/docs/vat_check.png new file mode 100644 index 00000000..e2a5c889 Binary files /dev/null and b/docs/vat_check.png differ diff --git a/erpnext_germany/api.py b/erpnext_germany/api.py index eb07091f..61535d58 100644 --- a/erpnext_germany/api.py +++ b/erpnext_germany/api.py @@ -1,20 +1,21 @@ import frappe +from .utils.eu_vat import check_vat, parse_vat_id @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 + is_valid = frappe.cache().hget("eu_vat_validation", vat_id, shared=True) + if is_valid is not None: + return is_valid try: - result = is_valid_eu_vat_id(vat_id) - frappe.cache().hset("eu_vat_validation", vat_id, result, shared=True) + country_code, vat_number = parse_vat_id(vat_id) + result = check_vat(country_code, vat_number) + is_valid = result.valid + frappe.cache().hset("eu_vat_validation", vat_id, is_valid, shared=True) except Exception: frappe.response["status_code"] = 501 - result = None + is_valid = None - return result + return is_valid diff --git a/erpnext_germany/erpnext_germany/doctype/vat_id_check/__init__.py b/erpnext_germany/erpnext_germany/doctype/vat_id_check/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/erpnext_germany/erpnext_germany/doctype/vat_id_check/test_vat_id_check.py b/erpnext_germany/erpnext_germany/doctype/vat_id_check/test_vat_id_check.py new file mode 100644 index 00000000..a5b4e4a5 --- /dev/null +++ b/erpnext_germany/erpnext_germany/doctype/vat_id_check/test_vat_id_check.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, ALYF GmbH and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestVATIDCheck(FrappeTestCase): + pass diff --git a/erpnext_germany/erpnext_germany/doctype/vat_id_check/vat_id_check.js b/erpnext_germany/erpnext_germany/doctype/vat_id_check/vat_id_check.js new file mode 100644 index 00000000..1c272d65 --- /dev/null +++ b/erpnext_germany/erpnext_germany/doctype/vat_id_check/vat_id_check.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, ALYF GmbH and contributors +// For license information, please see license.txt + +frappe.ui.form.on('VAT ID Check', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext_germany/erpnext_germany/doctype/vat_id_check/vat_id_check.json b/erpnext_germany/erpnext_germany/doctype/vat_id_check/vat_id_check.json new file mode 100644 index 00000000..242c1645 --- /dev/null +++ b/erpnext_germany/erpnext_germany/doctype/vat_id_check/vat_id_check.json @@ -0,0 +1,270 @@ +{ + "actions": [], + "creation": "2023-03-26 18:28:14.770174", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "customer", + "customer_vat_id", + "customer_address", + "column_break_hmgxr", + "status", + "section_break_6ctcl", + "trader_name", + "trader_street", + "trader_postcode", + "trader_city", + "column_break_cxvts", + "trader_name_match", + "trader_street_match", + "trader_postcode_match", + "trader_city_match", + "column_break_cjvyk", + "actual_trader_name", + "actual_trader_address", + "section_break_xljfy", + "company", + "column_break_scjlu", + "requester_vat_id", + "section_break_dowxn", + "is_valid", + "request_id" + ], + "fields": [ + { + "fieldname": "customer", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Customer", + "options": "Customer", + "set_only_once": 1 + }, + { + "fieldname": "column_break_hmgxr", + "fieldtype": "Column Break" + }, + { + "fieldname": "request_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Request ID", + "no_copy": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_valid", + "fieldtype": "Check", + "label": "Is Valid", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "set_only_once": 1 + }, + { + "fieldname": "section_break_xljfy", + "fieldtype": "Section Break", + "label": "Requester" + }, + { + "fieldname": "column_break_scjlu", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "section_break_dowxn", + "fieldtype": "Section Break", + "label": "Result" + }, + { + "fetch_from": "customer.tax_id", + "fetch_if_empty": 1, + "fieldname": "customer_vat_id", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Customer VAT ID", + "reqd": 1, + "set_only_once": 1 + }, + { + "fetch_from": "company.tax_id", + "fetch_if_empty": 1, + "fieldname": "requester_vat_id", + "fieldtype": "Data", + "label": "Requester VAT ID", + "set_only_once": 1 + }, + { + "default": "Planned", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Planned\nRunning\nCompleted\nService Unavailable\nInvalid Input\nError", + "read_only": 1 + }, + { + "fieldname": "customer_address", + "fieldtype": "Link", + "label": "Customer Address", + "options": "Address", + "set_only_once": 1 + }, + { + "fetch_from": "customer.customer_name", + "fetch_if_empty": 1, + "fieldname": "trader_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Trader Name", + "set_only_once": 1 + }, + { + "fetch_from": "customer_address.address_line1", + "fetch_if_empty": 1, + "fieldname": "trader_street", + "fieldtype": "Data", + "label": "Trader Street", + "set_only_once": 1 + }, + { + "fetch_from": "customer_address.pincode", + "fetch_if_empty": 1, + "fieldname": "trader_postcode", + "fieldtype": "Data", + "label": "Trader Postcode", + "set_only_once": 1 + }, + { + "fetch_from": "customer_address.city", + "fetch_if_empty": 1, + "fieldname": "trader_city", + "fieldtype": "Data", + "label": "Trader City", + "set_only_once": 1 + }, + { + "default": "0", + "fieldname": "trader_name_match", + "fieldtype": "Check", + "label": "Trader Name Match", + "no_copy": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "trader_street_match", + "fieldtype": "Check", + "label": "Trader Street Match", + "no_copy": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "trader_postcode_match", + "fieldtype": "Check", + "label": "Trader Postcode Match", + "no_copy": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "trader_city_match", + "fieldtype": "Check", + "label": "Trader City Match", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "actual_trader_name", + "fieldtype": "Data", + "label": "Actual Trader Name", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "actual_trader_address", + "fieldtype": "Small Text", + "label": "Actual Trader Address", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "section_break_6ctcl", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_cxvts", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_cjvyk", + "fieldtype": "Column Break" + } + ], + "links": [], + "modified": "2023-07-14 19:27:17.103880", + "modified_by": "Administrator", + "module": "ERPNext Germany", + "name": "VAT ID Check", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Master Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User", + "write": 1 + } + ], + "sort_field": "creation", + "sort_order": "DESC", + "states": [], + "title_field": "customer_vat_id" +} \ No newline at end of file diff --git a/erpnext_germany/erpnext_germany/doctype/vat_id_check/vat_id_check.py b/erpnext_germany/erpnext_germany/doctype/vat_id_check/vat_id_check.py new file mode 100644 index 00000000..17822f81 --- /dev/null +++ b/erpnext_germany/erpnext_germany/doctype/vat_id_check/vat_id_check.py @@ -0,0 +1,83 @@ +# Copyright (c) 2023, ALYF GmbH and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from erpnext_germany.utils.eu_vat import check_vat_approx, parse_vat_id + +from tenacity import RetryError + + +class VATIDCheck(Document): + def before_insert(self): + if self.requester_vat_id: + requester_country_code, requester_vat_number = parse_vat_id( + self.requester_vat_id + ) + self.requester_vat_id = f"{requester_country_code}{requester_vat_number}" + + country_code, vat_number = parse_vat_id(self.customer_vat_id) + self.customer_vat_id = f"{country_code}{vat_number}" + + def after_insert(self): + frappe.enqueue( + run_check, + doc=self, + queue="long", + now=frappe.conf.developer_mode or frappe.flags.in_test, + ) + + +def run_check(doc: VATIDCheck): + doc.db_set("status", "Running", notify=True) + requester_country_code, requester_vat_number = None, None + if doc.requester_vat_id: + try: + requester_country_code, requester_vat_number = parse_vat_id(doc.requester_vat_id) + except ValueError: + doc.db_set({"status": "Invalid Input", "is_valid": False}, notify=True) + return + + try: + country_code, vat_number = parse_vat_id(doc.customer_vat_id) + except ValueError: + doc.db_set({"status": "Invalid Input", "is_valid": False}, notify=True) + return + + try: + result = check_vat_approx( + country_code=country_code, + vat_number=vat_number, + trader_name=doc.trader_name, + trader_street=doc.trader_street, + trader_postcode=doc.trader_postcode, + trader_city=doc.trader_city, + requester_country_code=requester_country_code, + requester_vat_number=requester_vat_number + ) + except RetryError: + doc.db_set("status", "Service Unavailable", notify=True) + return + except Exception as e: + if e.message.upper() == "INVALID_INPUT": + doc.db_set({"status": "Invalid Input", "is_valid": False}, notify=True) + else: + doc.db_set({"status": "Error"}, notify=True) + frappe.log_error("VAT ID Check Error", frappe.get_traceback()) + + return + + doc.db_set( + { + "status": "Completed", + "is_valid": result.valid, + "trader_name_match": bool(result.traderNameMatch), + "trader_street_match": bool(result.traderStreetMatch), + "trader_postcode_match": bool(result.traderPostcodeMatch), + "trader_city_match": bool(result.traderCityMatch), + "request_id": result.requestIdentifier, + "actual_trader_name": result.traderName, + "actual_trader_address": result.traderAddress, + }, + notify=True, + ) diff --git a/erpnext_germany/erpnext_germany/doctype/vat_id_check/vat_id_check_list.js b/erpnext_germany/erpnext_germany/doctype/vat_id_check/vat_id_check_list.js new file mode 100644 index 00000000..8a5ef26c --- /dev/null +++ b/erpnext_germany/erpnext_germany/doctype/vat_id_check/vat_id_check_list.js @@ -0,0 +1,17 @@ +frappe.listview_settings["VAT ID Check"] = { + add_fields: ["is_valid"], + hide_name_column: true, + get_indicator: function (doc) { + if (doc.status === "Completed") { + return doc.is_valid === 1 + ? [__("Valid"), "green", "is_valid,=,Yes"] + : [__("Invalid"), "red", "is_valid,=,No"]; + } else if (doc.status === "Planned") { + return [__("Planned"), "blue", "status,=,Planned"]; + } else if (doc.status === "Running") { + return [__("Running"), "yellow", "status,=,Running"]; + } else if (["Service Unavailable", "Invalid Input"].includes(doc.status)) { + return [__(doc.status), "gray", `status,=,${doc.status}`]; + } + }, +}; diff --git a/erpnext_germany/hooks.py b/erpnext_germany/hooks.py index 5e4c28cb..abb596e4 100644 --- a/erpnext_germany/hooks.py +++ b/erpnext_germany/hooks.py @@ -16,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/validate_vat_id.js" +# app_include_js = "/assets/erpnext_germany/js/erpnext_germany.js" # include js, css files in header of web template # web_include_css = "/assets/erpnext_germany/css/erpnext_germany.css" @@ -33,10 +33,7 @@ # page_js = {"page" : "public/js/file.js"} # include js in doctype views -doctype_js = { - "Customer": "public/js/customer.js", - "Supplier": "public/js/supplier.js", -} +# doctype_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"} @@ -122,10 +119,10 @@ # Scheduled Tasks # --------------- -# scheduler_events = { -# "all": [ -# "erpnext_germany.tasks.all" -# ], +scheduler_events = { + "all": [ + "erpnext_germany.tasks.all" + ], # "daily": [ # "erpnext_germany.tasks.daily" # ], @@ -138,7 +135,7 @@ # "monthly": [ # "erpnext_germany.tasks.monthly" # ], -# } +} # Testing # ------- @@ -374,3 +371,16 @@ def get_register_fields(insert_after: str): ("current_accommodation_type", "hidden", 1, "Check"), ], } + +germany_custom_records = [ + { + "doctype": "DocType Link", + "parent": "Customer", + "parentfield": "links", + "parenttype": "Customize Form", + "group": "Pre Sales", + "link_doctype": "VAT ID Check", + "link_fieldname": "customer", + "custom": 1, + }, +] diff --git a/erpnext_germany/install.py b/erpnext_germany/install.py index ff20275a..aedc8eda 100644 --- a/erpnext_germany/install.py +++ b/erpnext_germany/install.py @@ -10,6 +10,7 @@ def after_install(): create_custom_fields(custom_fields) make_property_setters() import_data() + insert_custom_records() def import_data(): @@ -46,3 +47,15 @@ def make_property_setters(): for property_setter in property_setters: for_doctype = not property_setter[0] make_property_setter(doctype, *property_setter, for_doctype) + + +def insert_custom_records(): + for custom_record in frappe.get_hooks("germany_custom_records"): + filters = custom_record.copy() + # Clean up filters. They need to be a plain dict without nested dicts or lists. + for key, value in custom_record.items(): + if isinstance(value, (list, dict)): + del filters[key] + + if not frappe.db.exists(filters): + frappe.get_doc(custom_record).insert(ignore_if_duplicate=True) diff --git a/erpnext_germany/patches.txt b/erpnext_germany/patches.txt index 126d8c66..95ccad41 100644 --- a/erpnext_germany/patches.txt +++ b/erpnext_germany/patches.txt @@ -1,2 +1,3 @@ execute:from erpnext_germany.install import after_install; after_install() # 7 erpnext_germany.patches.add_tax_exemption_reason_fields +execute:from erpnext_germany.install import insert_custom_records; insert_custom_records() diff --git a/erpnext_germany/public/js/customer.js b/erpnext_germany/public/js/customer.js deleted file mode 100644 index 5a903a9b..00000000 --- a/erpnext_germany/public/js/customer.js +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 9f77d6d5..00000000 --- a/erpnext_germany/public/js/supplier.js +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 6930dde9..00000000 --- a/erpnext_germany/public/js/validate_vat_id.js +++ /dev/null @@ -1,51 +0,0 @@ - -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/tasks.py b/erpnext_germany/tasks.py new file mode 100644 index 00000000..db9341ac --- /dev/null +++ b/erpnext_germany/tasks.py @@ -0,0 +1,67 @@ +from erpnext import get_default_company +import frappe + + +def all(): + check_some_customers() + + +def get_customers(batch_size=4): + """Return a list of n customers who didn't have their VAT ID checked in the last 3 months.""" + from pypika import functions as fn, Interval + from frappe.query_builder import DocType + + customers = DocType("Customer") + vat_id_checks = DocType("VAT ID Check") + last_check = ( + frappe.qb.from_(vat_id_checks) + .select( + vat_id_checks.customer, + fn.Max(vat_id_checks.creation).as_("creation"), + ) + .groupby(vat_id_checks.customer) + ) + + return ( + frappe.qb.from_(customers) + .left_join(last_check) + .on(customers.name == last_check.customer) + .select( + customers.name, + customers.customer_name, + customers.customer_primary_address, + customers.tax_id + ) + .where( + customers.tax_id.notnull() + & (customers.disabled == 0) + & ( + last_check.creation.isnull() + | (last_check.creation < fn.Now() - Interval(months=3)) + ) + ) + .limit(batch_size) + .run() + ) + + +def check_some_customers(): + """Check VAT IDs of customers who didn't have their VAT ID checked in the last 3 months.""" + requester_vat_id = None + if company := get_default_company(): + requester_vat_id = frappe.get_cached_value("Company", company, "tax_id") + + for customer, customer_name, primary_address, vat_id in get_customers(): + doc = frappe.new_doc("VAT ID Check") + doc.customer = customer + doc.trader_name = customer_name + doc.customer_vat_id = vat_id + doc.company = company + doc.requester_vat_id = requester_vat_id + if primary_address: + address = frappe.get_doc("Address", primary_address) + doc.trader_street = address.address_line1 + doc.trader_postcode = address.pincode + doc.trader_city = address.city + + doc.insert(ignore_permissions=True) diff --git a/erpnext_germany/translations/de.csv b/erpnext_germany/translations/de.csv index 0772e634..ff7e2060 100644 --- a/erpnext_germany/translations/de.csv +++ b/erpnext_germany/translations/de.csv @@ -16,3 +16,12 @@ Has Children,Hat Kinder Highest School Qualification,Höchster Schulabschluss Has Other Employments,Hat Nebenbeschäftigungen Tax Exemption Reason,Steuerbefreiungsgrund, +VAT ID Check,USt-IdNr Prüfung +Request ID,Anfrage ID +Is Valid,Ist gültig +Valid,Gültig +Invalid,Ungültig +Requester VAT ID,USt-IdNr. des Anfragenden +Customer VAT ID,USt-IdNr. des Kunden +Result,Ergebnis +Requester,Anfragender diff --git a/erpnext_germany/uninstall.py b/erpnext_germany/uninstall.py index dbbe8eb9..6ba5a730 100644 --- a/erpnext_germany/uninstall.py +++ b/erpnext_germany/uninstall.py @@ -4,6 +4,7 @@ def before_uninstall(): remove_custom_fields() remove_property_setters() + remove_custom_records() def remove_custom_fields(): @@ -35,3 +36,10 @@ def remove_property_setters(): "value": ps[-2] } ) + + +def remove_custom_records(): + print("* removing custom records...") + for record in frappe.get_hooks("germany_custom_records"): + doctype = record.pop("doctype") + frappe.db.delete(doctype, record) diff --git a/erpnext_germany/utils/eu_vat.py b/erpnext_germany/utils/eu_vat.py index 097f793a..6e8e5a3d 100644 --- a/erpnext_germany/utils/eu_vat.py +++ b/erpnext_germany/utils/eu_vat.py @@ -1,12 +1,68 @@ -import zeep +import re +from tenacity import ( + retry, + retry_any, + retry_if_exception_message, + stop_after_attempt, + wait_exponential, +) +from zeep import Client -def is_valid_eu_vat_id(vat_id) -> bool: - """Use the EU VAT checker to validate a VAT ID.""" +WSDL_URL = "https://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl" +COUNTRY_CODE_REGEX = r"^[A-Z]{2}$" +VAT_NUMBER_REGEX = r"^[0-9A-Za-z\+\*\.]{2,12}$" + + +def parse_vat_id(vat_id: str) -> tuple[str, str]: 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) + # check vat_number and country_code with regex + if not re.match(COUNTRY_CODE_REGEX, country_code): + raise ValueError("Invalid country code") + + if not re.match(VAT_NUMBER_REGEX, vat_number): + raise ValueError("Invalid VAT number") + + return country_code, vat_number + + +def check_vat(country_code: str, vat_number: str): + """Use the EU VAT checker to validate a VAT ID.""" + return Client(WSDL_URL).service.checkVat( + vatNumber=vat_number, countryCode=country_code + ) + - return result.valid +@retry( + retry=retry_any( + retry_if_exception_message(message="GLOBAL_MAX_CONCURRENT_REQ"), + retry_if_exception_message(message="MS_MAX_CONCURRENT_REQ"), + retry_if_exception_message(message="SERVICE_UNAVAILABLE"), + retry_if_exception_message(message="MS_UNAVAILABLE"), + retry_if_exception_message(message="TIMEOUT"), + ), + stop=stop_after_attempt(3), + wait=wait_exponential(multiplier=1, min=2, max=64), +) +def check_vat_approx( + country_code: str, + vat_number: str, + trader_name: str | None = None, + trader_street: str | None = None, + trader_postcode: str | None = None, + trader_city: str | None = None, + requester_country_code: str | None = None, + requester_vat_number: str | None = None, +): + return Client(WSDL_URL).service.checkVatApprox( + countryCode=country_code, + vatNumber=vat_number, + traderName=trader_name, + traderStreet=trader_street, + traderPostcode=trader_postcode, + traderCity=trader_city, + requesterCountryCode=requester_country_code, + requesterVatNumber=requester_vat_number, + ) diff --git a/requirements.txt b/requirements.txt index 1243fdf9..b4bb2dbd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ # frappe -- https://github.com/frappe/frappe is installed via 'bench init' -zeep \ No newline at end of file +zeep +tenacity