From 5c44b9b7c83a531ba1eb80b4cee25b6f8ae23fc7 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 25 Aug 2024 18:50:55 +0200 Subject: [PATCH] feat: E Invoice Import (first draft) --- .../european_e_invoice/doctype/__init__.py | 0 .../doctype/e_invoice_import/__init__.py | 0 .../e_invoice_import/e_invoice_import.js | 23 ++ .../e_invoice_import/e_invoice_import.json | 290 ++++++++++++++++++ .../e_invoice_import/e_invoice_import.py | 155 ++++++++++ .../e_invoice_import/test_e_invoice_import.py | 9 + .../doctype/e_invoice_item/__init__.py | 0 .../e_invoice_item/e_invoice_item.json | 124 ++++++++ .../doctype/e_invoice_item/e_invoice_item.py | 9 + pyproject.toml | 4 +- 10 files changed, 612 insertions(+), 2 deletions(-) create mode 100644 eu_einvoice/european_e_invoice/doctype/__init__.py create mode 100644 eu_einvoice/european_e_invoice/doctype/e_invoice_import/__init__.py create mode 100644 eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.js create mode 100644 eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json create mode 100644 eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.py create mode 100644 eu_einvoice/european_e_invoice/doctype/e_invoice_import/test_e_invoice_import.py create mode 100644 eu_einvoice/european_e_invoice/doctype/e_invoice_item/__init__.py create mode 100644 eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json create mode 100644 eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.py diff --git a/eu_einvoice/european_e_invoice/doctype/__init__.py b/eu_einvoice/european_e_invoice/doctype/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eu_einvoice/european_e_invoice/doctype/e_invoice_import/__init__.py b/eu_einvoice/european_e_invoice/doctype/e_invoice_import/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.js b/eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.js new file mode 100644 index 0000000..68156ab --- /dev/null +++ b/eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.js @@ -0,0 +1,23 @@ +// Copyright (c) 2024, ALYF GmbH and contributors +// For license information, please see license.txt + +frappe.ui.form.on("E Invoice Import", { + setup(frm) { + frm.set_query("company", function () { + return { + filters: { + is_group: 0, + }, + }; + }); + + frm.set_query("supplier_address", function (doc) { + return { + filters: [ + ["Dynamic Link", "link_doctype", "=", "Supplier"], + ["Dynamic Link", "link_name", "=", doc.supplier], + ], + }; + }); + }, +}); diff --git a/eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json b/eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json new file mode 100644 index 0000000..0a467b3 --- /dev/null +++ b/eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.json @@ -0,0 +1,290 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "hash", + "creation": "2024-08-24 09:41:48.844623", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "file_section_section", + "einvoice", + "header_section", + "issue_date", + "id", + "section_break_z2sq", + "amended_from", + "trade_tab", + "agreement_section", + "seller_name", + "seller_tax_id", + "seller_address_line_1", + "seller_address_line_2", + "seller_city", + "seller_postcode", + "seller_country", + "supplier", + "create_supplier", + "supplier_address", + "create_supplier_address", + "column_break_reul", + "buyer_name", + "buyer_address_line_1", + "buyer_address_line_2", + "buyer_city", + "buyer_postcode", + "buyer_country", + "company", + "items_section", + "items", + "settlement_section", + "currency" + ], + "fields": [ + { + "fieldname": "section_break_z2sq", + "fieldtype": "Section Break" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "E Invoice Import", + "print_hide": 1, + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "file_section_section", + "fieldtype": "Section Break", + "label": "File Section" + }, + { + "fieldname": "einvoice", + "fieldtype": "Attach", + "label": "E-Invoice" + }, + { + "fieldname": "trade_tab", + "fieldtype": "Tab Break", + "label": "Trade" + }, + { + "fieldname": "agreement_section", + "fieldtype": "Section Break", + "label": "Agreement" + }, + { + "fieldname": "seller_name", + "fieldtype": "Data", + "label": "Seller Name", + "read_only": 1 + }, + { + "fieldname": "seller_address_line_1", + "fieldtype": "Data", + "label": "Seller Address Line 1", + "read_only": 1 + }, + { + "fieldname": "seller_address_line_2", + "fieldtype": "Data", + "label": "Seller Address Line 2", + "read_only": 1 + }, + { + "fieldname": "seller_city", + "fieldtype": "Data", + "label": "Seller City", + "read_only": 1 + }, + { + "fieldname": "seller_postcode", + "fieldtype": "Data", + "label": "Seller Postcode", + "read_only": 1 + }, + { + "fieldname": "seller_country", + "fieldtype": "Link", + "label": "Seller Country", + "options": "Country", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "create_supplier", + "fieldtype": "Check", + "label": "Create Supplier", + "read_only_depends_on": "supplier" + }, + { + "fieldname": "column_break_reul", + "fieldtype": "Column Break" + }, + { + "fieldname": "buyer_name", + "fieldtype": "Data", + "label": "Buyer Name", + "read_only": 1 + }, + { + "fieldname": "buyer_address_line_1", + "fieldtype": "Data", + "label": "Buyer Address Line 1", + "read_only": 1 + }, + { + "fieldname": "buyer_address_line_2", + "fieldtype": "Data", + "label": "Buyer Address Line 2", + "read_only": 1 + }, + { + "fieldname": "buyer_city", + "fieldtype": "Data", + "label": "Buyer City", + "read_only": 1 + }, + { + "fieldname": "buyer_postcode", + "fieldtype": "Data", + "label": "Buyer Postcode", + "read_only": 1 + }, + { + "fieldname": "buyer_country", + "fieldtype": "Link", + "label": "Buyer Country", + "options": "Country", + "read_only": 1 + }, + { + "fieldname": "items_section", + "fieldtype": "Section Break", + "label": "Items" + }, + { + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "options": "E Invoice Item" + }, + { + "fieldname": "supplier", + "fieldtype": "Link", + "label": "Supplier", + "options": "Supplier" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "fieldname": "supplier_address", + "fieldtype": "Link", + "label": "Supplier Address", + "options": "Address", + "read_only_depends_on": "eval: !doc.supplier" + }, + { + "default": "0", + "fieldname": "create_supplier_address", + "fieldtype": "Check", + "label": "Create Supplier Address", + "read_only_depends_on": "eval: doc.supplier_address || (!doc.supplier && !doc.create_supplier)" + }, + { + "fieldname": "seller_tax_id", + "fieldtype": "Data", + "label": "Seller Tax ID", + "read_only": 1 + }, + { + "fieldname": "header_section", + "fieldtype": "Section Break", + "label": "Header" + }, + { + "fieldname": "issue_date", + "fieldtype": "Date", + "label": "Issue Date", + "read_only": 1 + }, + { + "fieldname": "id", + "fieldtype": "Data", + "label": "ID", + "read_only": 1 + }, + { + "fieldname": "settlement_section", + "fieldtype": "Section Break", + "label": "Settlement" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency" + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2024-08-25 12:39:27.030108", + "modified_by": "Administrator", + "module": "European e-Invoice", + "name": "E Invoice Import", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase User", + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.py b/eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.py new file mode 100644 index 0000000..0f72a11 --- /dev/null +++ b/eu_einvoice/european_e_invoice/doctype/e_invoice_import/e_invoice_import.py @@ -0,0 +1,155 @@ +# Copyright (c) 2024, ALYF GmbH and contributors +# For license information, please see license.txt + +import json +from pathlib import Path + +import frappe +from drafthorse.models.document import Document as DrafthorseDocument +from erpnext import get_default_company +from frappe import _dict, get_site_path +from frappe.model.document import Document +from frappe.utils.data import today + + +class EInvoiceImport(Document): + def before_save(self): + if self.has_value_changed("einvoice"): + self.parse_einvoice() + + if not self.supplier and self.create_supplier: + self._create_supplier() + + if not self.supplier_address and self.create_supplier_address: + self._create_supplier_address() + + def before_submit(self): + if not self.supplier: + frappe.throw("Please create or select a supplier before submitting") + + if not self.company: + frappe.throw("Please select a company before submitting") + + if not self.items: + frappe.throw("No items found in the invoice") + + self.create_purchase_invoice() + + def parse_einvoice(self): + path_to_einvoice = Path(get_site_path(self.einvoice.lstrip("/"))).resolve() + xml = path_to_einvoice.read_bytes() + doc = DrafthorseDocument.parse(xml) + self.id = str(doc.header.id) + self.issue_date = str(doc.header.issue_date_time) + self.currency = str(doc.trade.settlement.currency_code) + self.parse_seller(doc.trade.agreement.seller) + self.parse_buyer(doc.trade.agreement.buyer) + + self.items = [] + for li in doc.trade.items.children: + self.parse_line_item(li) + + def parse_seller(self, seller): + self.seller_name = str(seller.name) + if frappe.db.exists("Supplier", self.seller_name): + self.supplier = self.seller_name + self.seller_tax_id = ( + seller.tax_registrations.children[0].id._text if seller.tax_registrations.children else None + ) + if self.seller_tax_id and not self.supplier: + self.supplier = frappe.db.get_value("Supplier", {"tax_id": self.seller_tax_id}, "name") + + self.parse_address(seller.address, "seller") + + def parse_buyer(self, buyer): + self.buyer_name = str(buyer.name) + if frappe.db.exists("Company", self.buyer_name): + self.company = self.buyer_name + else: + self.company = get_default_company() + + self.parse_address(buyer.address, "buyer") + + def parse_address(self, address, prefix: str) -> _dict: + country = frappe.db.get_value("Country", {"code": str(address.country_id).lower()}, "name") + + self.set(f"{prefix}_city", str(address.city_name)) + self.set(f"{prefix}_address_line_1", str(address.line_one)) + self.set(f"{prefix}_address_line_2", str(address.line_two)) + self.set(f"{prefix}_postcode", str(address.postcode)) + self.set(f"{prefix}_country", str(country)) + + def parse_line_item(self, li): + item = self.append("items") + supplier = None + + net_rate = float(li.agreement.net.amount._value) + basis_qty = float(li.agreement.net.basis_quantity._amount or "1") + rate = net_rate / basis_qty + + item.product_name = str(li.product.name) + item.product_description = str(li.product.description) + item.seller_product_id = str(li.product.seller_assigned_id) + item_code = str(li.product.buyer_assigned_id) + if item_code and not frappe.db.exists("Item", item_code): + item_code = None + + if item.seller_product_id and supplier and not item_code: + item_code = frappe.db.get_value( + "Item Supplier", {"supplier": supplier, "supplier_part_no": item.seller_product_id}, "parent" + ) + item.item = item_code + + item.billed_quantity = float(li.delivery.billed_quantity._amount) + item.unit_code = str(li.delivery.billed_quantity._unit_code) + item.uom = frappe.db.get_value("UOM", {"common_code": item.unit_code}, "name") + + item.net_rate = rate + item.tax_rate = float(li.settlement.trade_tax.rate_applicable_percent._value) + item.total_amount = float(li.settlement.monetary_summation.total_amount._value) + + def _create_supplier(self): + supplier = frappe.new_doc("Supplier") + supplier.supplier_name = self.seller_name + supplier.tax_id = self.seller_tax_id + supplier.insert() + + self.supplier = supplier.name + self.create_supplier = 0 + + def _create_supplier_address(self): + address = frappe.new_doc("Address") + address.address_line1 = self.seller_address_line_1 + address.address_line2 = self.seller_address_line_2 + address.city = self.seller_city + address.pincode = self.seller_postcode + address.country = self.seller_country + address.append("links", {"link_doctype": "Supplier", "link_name": self.supplier}) + address.insert() + + self.supplier_address = address.name + self.create_supplier_address = 0 + + def create_purchase_invoice(self): + pi = frappe.new_doc("Purchase Invoice") + pi.supplier = self.supplier + pi.company = self.company + pi.posting_date = today() + pi.bill_no = self.id + pi.bill_date = self.issue_date + pi.currency = self.currency + for item in self.items: + pi.append( + "items", + { + "item_code": item.item, + "qty": item.billed_quantity, + "uom": item.uom, + "rate": item.net_rate, + }, + ) + + # TODO: add back-link to Purchase Invoice + # pi.einvoice_import = self.name + pi.set_missing_values() + pi.insert(ignore_mandatory=True) diff --git a/eu_einvoice/european_e_invoice/doctype/e_invoice_import/test_e_invoice_import.py b/eu_einvoice/european_e_invoice/doctype/e_invoice_import/test_e_invoice_import.py new file mode 100644 index 0000000..5ccdaba --- /dev/null +++ b/eu_einvoice/european_e_invoice/doctype/e_invoice_import/test_e_invoice_import.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, ALYF GmbH and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestEInvoiceImport(FrappeTestCase): + pass diff --git a/eu_einvoice/european_e_invoice/doctype/e_invoice_item/__init__.py b/eu_einvoice/european_e_invoice/doctype/e_invoice_item/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json b/eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json new file mode 100644 index 0000000..dc8f87f --- /dev/null +++ b/eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.json @@ -0,0 +1,124 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-08-25 10:57:25.968745", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "product_section", + "product_name", + "seller_product_id", + "product_description", + "column_break_grml", + "item", + "delivery_section", + "billed_quantity", + "unit_code", + "column_break_pmsf", + "uom", + "agreement_section", + "net_rate", + "settlement_section", + "tax_rate", + "total_amount" + ], + "fields": [ + { + "fieldname": "product_section", + "fieldtype": "Section Break", + "label": "Product" + }, + { + "fieldname": "product_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Product Name" + }, + { + "fieldname": "product_description", + "fieldtype": "Small Text", + "label": "Product Description" + }, + { + "fieldname": "column_break_grml", + "fieldtype": "Column Break" + }, + { + "fieldname": "item", + "fieldtype": "Link", + "label": "Item", + "options": "Item" + }, + { + "fieldname": "delivery_section", + "fieldtype": "Section Break", + "label": "Delivery" + }, + { + "fieldname": "billed_quantity", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Billed Quantity" + }, + { + "fieldname": "unit_code", + "fieldtype": "Data", + "label": "Unit Code" + }, + { + "fieldname": "column_break_pmsf", + "fieldtype": "Column Break" + }, + { + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM" + }, + { + "fieldname": "agreement_section", + "fieldtype": "Section Break", + "label": "Agreement" + }, + { + "fieldname": "net_rate", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Net Rate" + }, + { + "fieldname": "settlement_section", + "fieldtype": "Section Break", + "label": "Settlement" + }, + { + "fieldname": "tax_rate", + "fieldtype": "Percent", + "label": "Tax Rate" + }, + { + "fieldname": "total_amount", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Total Amount" + }, + { + "fieldname": "seller_product_id", + "fieldtype": "Data", + "label": "Seller Product ID" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2024-08-25 11:33:39.214634", + "modified_by": "Administrator", + "module": "European e-Invoice", + "name": "E Invoice Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.py b/eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.py new file mode 100644 index 0000000..35eeb5a --- /dev/null +++ b/eu_einvoice/european_e_invoice/doctype/e_invoice_item/e_invoice_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, ALYF GmbH and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class EInvoiceItem(Document): + pass diff --git a/pyproject.toml b/pyproject.toml index ff92236..931a877 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,8 +9,8 @@ readme = "README.md" dynamic = ["version"] dependencies = [ # "frappe~=15.0.0" # Installed and managed by bench. - "factur-x~=3.1", - "drafthorse~=2.4.0" + # "factur-x~=3.1", + "drafthorse~=2.4.0", ] [build-system]