From 2262a579cefbbf92ecf65a16883698c9f0a45cf2 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Thu, 8 Jun 2023 21:47:17 +0200 Subject: [PATCH 01/19] feat: FEC import --- .../doctype/journal_entry/journal_entry.py | 1 + erpnext/accounts/general_ledger.py | 4 +- .../__init__.py | 0 .../fec_accounting_journal_mapping.json | 41 ++++ .../fec_accounting_journal_mapping.py | 9 + .../regional/doctype/fec_import/__init__.py | 0 .../regional/doctype/fec_import/fec_import.js | 37 ++++ .../doctype/fec_import/fec_import.json | 73 +++++++ .../regional/doctype/fec_import/fec_import.py | 195 ++++++++++++++++++ .../doctype/fec_import/test_fec_import.py | 9 + .../doctype/fec_import_settings/__init__.py | 0 .../fec_import_settings.js | 8 + .../fec_import_settings.json | 89 ++++++++ .../fec_import_settings.py | 9 + .../test_fec_import_settings.py | 9 + 15 files changed, 483 insertions(+), 1 deletion(-) create mode 100644 erpnext/regional/doctype/fec_accounting_journal_mapping/__init__.py create mode 100644 erpnext/regional/doctype/fec_accounting_journal_mapping/fec_accounting_journal_mapping.json create mode 100644 erpnext/regional/doctype/fec_accounting_journal_mapping/fec_accounting_journal_mapping.py create mode 100644 erpnext/regional/doctype/fec_import/__init__.py create mode 100644 erpnext/regional/doctype/fec_import/fec_import.js create mode 100644 erpnext/regional/doctype/fec_import/fec_import.json create mode 100644 erpnext/regional/doctype/fec_import/fec_import.py create mode 100644 erpnext/regional/doctype/fec_import/test_fec_import.py create mode 100644 erpnext/regional/doctype/fec_import_settings/__init__.py create mode 100644 erpnext/regional/doctype/fec_import_settings/fec_import_settings.js create mode 100644 erpnext/regional/doctype/fec_import_settings/fec_import_settings.json create mode 100644 erpnext/regional/doctype/fec_import_settings/fec_import_settings.py create mode 100644 erpnext/regional/doctype/fec_import_settings/test_fec_import_settings.py diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index ff82488ce1..c2f4dfcf5e 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -932,6 +932,7 @@ class JournalEntry(AccountsController): "project": d.project, "finance_book": self.finance_book, "accounting_journal": d.accounting_journal, + "accounting_entry_number": self.flags.accounting_entry_number, }, item=d, ) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 2e9111099d..5517a21723 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -370,7 +370,9 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): if not entry.get("accounting_journal"): get_accounting_journal(entry) - entry["accounting_entry_number"] = accounting_number + if not entry.get("accounting_entry_number"): + entry["accounting_entry_number"] = accounting_number + make_entry(entry, adv_adj, update_outstanding, from_repost) diff --git a/erpnext/regional/doctype/fec_accounting_journal_mapping/__init__.py b/erpnext/regional/doctype/fec_accounting_journal_mapping/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/regional/doctype/fec_accounting_journal_mapping/fec_accounting_journal_mapping.json b/erpnext/regional/doctype/fec_accounting_journal_mapping/fec_accounting_journal_mapping.json new file mode 100644 index 0000000000..8209f96f62 --- /dev/null +++ b/erpnext/regional/doctype/fec_accounting_journal_mapping/fec_accounting_journal_mapping.json @@ -0,0 +1,41 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-06-08 18:04:30.601238", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "accounting_journal_in_fec", + "accounting_journal_in_dokos" + ], + "fields": [ + { + "fieldname": "accounting_journal_in_fec", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Accounting Journal in FEC", + "reqd": 1 + }, + { + "fieldname": "accounting_journal_in_dokos", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Accounting Journal in Dokos", + "options": "Accounting Journal", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-06-08 18:04:30.601238", + "modified_by": "Administrator", + "module": "Regional", + "name": "FEC Accounting Journal Mapping", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} diff --git a/erpnext/regional/doctype/fec_accounting_journal_mapping/fec_accounting_journal_mapping.py b/erpnext/regional/doctype/fec_accounting_journal_mapping/fec_accounting_journal_mapping.py new file mode 100644 index 0000000000..149bae2961 --- /dev/null +++ b/erpnext/regional/doctype/fec_accounting_journal_mapping/fec_accounting_journal_mapping.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Dokos SAS and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class FECAccountingJournalMapping(Document): + pass diff --git a/erpnext/regional/doctype/fec_import/__init__.py b/erpnext/regional/doctype/fec_import/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/regional/doctype/fec_import/fec_import.js b/erpnext/regional/doctype/fec_import/fec_import.js new file mode 100644 index 0000000000..53dc8e88b7 --- /dev/null +++ b/erpnext/regional/doctype/fec_import/fec_import.js @@ -0,0 +1,37 @@ +// Copyright (c) 2023, Dokos SAS and contributors +// For license information, please see license.txt + +frappe.ui.form.on('FEC Import', { + refresh(frm) { + frm.get_field("fec_file").df.options = { + restrictions: { + allowed_file_types: [".txt"], + }, + }; + + if (frm.doc.fec_file) { + frm.page.set_primary_action(__("Import FEC"), function() { + frappe.call({ + method: "upload_fec", + doc: frm.doc, + }).then(r => { + frappe.show_alert({ + message: __("Import in progress"), + indicator: "green" + }) + }) + }); + } + + }, + fec_file(frm) { + if (frm.doc.fec_file) { + frm.call({ + method: "get_company", + doc: frm.doc, + }).then(r => { + frm.set_value("company", r.message) + }) + } + }, +}); diff --git a/erpnext/regional/doctype/fec_import/fec_import.json b/erpnext/regional/doctype/fec_import/fec_import.json new file mode 100644 index 0000000000..8068be5a98 --- /dev/null +++ b/erpnext/regional/doctype/fec_import/fec_import.json @@ -0,0 +1,73 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "naming_series:", + "beta": 1, + "creation": "2023-06-08 16:51:07.044126", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "naming_series", + "fec_file", + "section_break_3", + "company" + ], + "fields": [ + { + "default": "FEC-.YYYY.-.#####", + "fieldname": "naming_series", + "fieldtype": "Select", + "hidden": 1, + "label": "Naming Series", + "options": "FEC-.YYYY.-.#####", + "reqd": 1 + }, + { + "fieldname": "fec_file", + "fieldtype": "Attach", + "in_list_view": 1, + "label": "FEC File", + "no_copy": 1 + }, + { + "depends_on": "eval:doc.fec_file", + "fieldname": "section_break_3", + "fieldtype": "Section Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-06-08 21:22:57.027237", + "modified_by": "Administrator", + "module": "Regional", + "name": "FEC Import", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "select": 1, + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} diff --git a/erpnext/regional/doctype/fec_import/fec_import.py b/erpnext/regional/doctype/fec_import/fec_import.py new file mode 100644 index 0000000000..0ec8fa4153 --- /dev/null +++ b/erpnext/regional/doctype/fec_import/fec_import.py @@ -0,0 +1,195 @@ +# Copyright (c) 2023, Dokos SAS and contributors +# For license information, please see license.txt + +import datetime +from collections import defaultdict + +import frappe +from frappe.model.document import Document +from frappe.utils import flt +from frappe.utils.csvutils import read_csv_content + + +class FECImport(Document): + @frappe.whitelist() + def get_company(self): + company = None + if self.fec_file: + file_name = frappe.db.get_value( + "File", + { + "file_url": self.fec_file, + "attached_to_doctype": self.doctype, + "attached_to_name": self.name, + }, + "file_name", + ) + try: + if siren := file_name.split("FEC")[0]: + return frappe.db.get_value("Company", dict(siren_number=siren)) + except Exception: + return company + + return company + + @frappe.whitelist() + def upload_fec(self): + fileid = frappe.db.get_value( + "File", + {"file_url": self.fec_file, "attached_to_doctype": self.doctype, "attached_to_name": self.name}, + ) + _file = frappe.get_doc("File", fileid) + fcontent = _file.get_content() + rows = read_csv_content(fcontent, delimiter="\t") + + header = rows[0] + data = rows[1:] + + output = list() + for d in data: + row = frappe._dict() + for count, head in enumerate(header): + if head: + row[head] = d[count] or "" + + output.append(row) + + FECImporter(settings=self, data=output) + + +class FECImporter: + def __init__(self, settings, data): + self.settings = settings + self.company_settings = {} + if frappe.db.exists("FEC Import Settings", dict(company=self.settings.company)): + self.company_settings = frappe.get_doc( + "FEC Import Settings", dict(company=self.settings.company) + ) + + self.data = data + + self.import_data() + + def import_data(self): + self.get_journals_mapping() + self.group_data() + self.get_accounts_data() + self.create_document() + self.insert_documents() + + def group_data(self): + initial_group = defaultdict(list) + self.grouped_data = defaultdict(lambda: defaultdict(list)) + + for d in self.data: + if not frappe.db.exists("GL Entry", dict(accounting_entry_number=d.EcritureNum)): + initial_group[d["EcritureNum"]].append(frappe._dict(d)) + + for ecriturenum in initial_group: + if [line.CompAuxNum for line in initial_group[ecriturenum]]: + journal_type = frappe.get_cached_value( + "Accounting Journal", initial_group[ecriturenum][0].JournalCode, "type" + ) + if journal_type == "Sales": + self.grouped_data["Sales Invoice"][ecriturenum] = initial_group[ecriturenum] + elif journal_type == "Purchase": + self.grouped_data["Purchase Invoice"][ecriturenum] = initial_group[ecriturenum] + + else: + self.grouped_data["Journal Entry"][ecriturenum] = initial_group[ecriturenum] + + def get_accounts_data(self): + self.accounts = { + x.account_number.strip(): x.name + for x in frappe.get_all( + "Account", + filters={"disabled": 0, "account_number": ("is", "set")}, + fields=["name", "account_number"], + ) + } + + def get_journals_mapping(self): + dokos_journals = { + x.journal_code: x.name + for x in frappe.get_all( + "Accounting Journal", filters={"disabled": 0}, fields=["journal_code", "name"] + ) + } + + self.journals = {} + mapped_journals = dokos_journals + for mapping in self.company_settings.get("accounting_journal_mapping", []): + for j in mapped_journals: + if j == mapping.accounting_journal_in_dokos: + self.journals[mapping.accounting_journal_in_fec] = mapped_journals[j] + continue + + self.journals[j] = mapped_journals[j] + + mapped_journals = self.journals + + def create_document(self): + self.journal_entries = [] + self.sales_invoices = [] + self.purchase_invoices = [] + + for ecriturenum in self.grouped_data["Journal Entry"]: + self.create_journal_entry(ecriturenum) + + for ecriturenum in self.grouped_data["Sales Invoice"]: + self.create_sales_invoice(ecriturenum) + + for ecriturenum in self.grouped_data["Purchase Invoice"]: + self.create_purchase_invoice(ecriturenum) + + def create_journal_entry(self, ecriturenum): + journal_entry = frappe.get_doc( + { + "doctype": "Journal Entry", + "company": self.settings.company, + "posting_date": datetime.datetime.strptime( + self.grouped_data[ecriturenum][0].EcritureDate, "%Y%m%d" + ).strftime("%Y-%m-%d"), + "cheque_no": ecriturenum, + } + ) + for line in self.grouped_data[ecriturenum]: + journal_code = self.journals.get(line["JournalCode"]) + + compte_aux = line.CompAuxNum + compte_aux_type = None + if compte_aux: + journal_type = frappe.get_cached_value("Accounting Journal", journal_code, "type") + if journal_type == "Sales": + compte_aux = frappe.db.exists("Customer", compte_aux) + compte_aux_type = "Customer" + elif journal_type == "Purchase": + compte_aux = frappe.db.exists("Supplier", compte_aux) + compte_aux_type = "Supplier" + + journal_entry.append( + "accounts", + { + "accounting_journal": journal_code, + "account": self.accounts.get(line["CompteNum"]), + "debit_in_account_currency": flt(line["Debit"].replace(",", ".")), + "credit_in_account_currency": flt(line["Credit"].replace(",", ".")), + "user_remark": line.EcritureLib, + "reference_name": line.PieceRef, + "party_type": compte_aux_type if compte_aux_type and compte_aux else None, + "party": compte_aux if compte_aux_type and compte_aux else None, + }, + ) + journal_entry.flags.accounting_entry_number = line.EcritureNum + + self.journal_entries.append(journal_entry) + + def create_sales_invoice(self, ecriturenum): + pass + + def create_purchase_invoice(self, ecriturenum): + pass + + def insert_documents(self): + print(self.journal_entries[0].as_dict()) + self.journal_entries[0].insert() diff --git a/erpnext/regional/doctype/fec_import/test_fec_import.py b/erpnext/regional/doctype/fec_import/test_fec_import.py new file mode 100644 index 0000000000..453ac4a29d --- /dev/null +++ b/erpnext/regional/doctype/fec_import/test_fec_import.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Dokos SAS and Contributors +# For license information, please see license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestFECImport(FrappeTestCase): + pass diff --git a/erpnext/regional/doctype/fec_import_settings/__init__.py b/erpnext/regional/doctype/fec_import_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.js b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.js new file mode 100644 index 0000000000..d34988bdd0 --- /dev/null +++ b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Dokos SAS and contributors +// For license information, please see license.txt + +frappe.ui.form.on('FEC Import Settings', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json new file mode 100644 index 0000000000..c217e91478 --- /dev/null +++ b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json @@ -0,0 +1,89 @@ +{ + "actions": [], + "autoname": "field:company", + "beta": 1, + "creation": "2023-06-08 18:04:51.693692", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "section_break_2", + "accounting_journal_mapping", + "sales_invoices_tab", + "sales_item", + "purchase_invoices_tab", + "purchase_item" + ], + "fields": [ + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "section_break_2", + "fieldtype": "Tab Break", + "label": "Accounting Journal" + }, + { + "fieldname": "accounting_journal_mapping", + "fieldtype": "Table", + "label": "Accounting Journal Mapping", + "options": "FEC Accounting Journal Mapping" + }, + { + "fieldname": "sales_invoices_tab", + "fieldtype": "Tab Break", + "label": "Sales Invoices" + }, + { + "fieldname": "sales_item", + "fieldtype": "Link", + "label": "Sales Item", + "options": "Item" + }, + { + "fieldname": "purchase_invoices_tab", + "fieldtype": "Tab Break", + "label": "Purchase Invoices" + }, + { + "fieldname": "purchase_item", + "fieldtype": "Link", + "label": "Purchase Item", + "options": "Item" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-06-08 21:47:00.988040", + "modified_by": "Administrator", + "module": "Regional", + "name": "FEC Import Settings", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "select": 1, + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} diff --git a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.py b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.py new file mode 100644 index 0000000000..ebd9df01a5 --- /dev/null +++ b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Dokos SAS and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class FECImportSettings(Document): + pass diff --git a/erpnext/regional/doctype/fec_import_settings/test_fec_import_settings.py b/erpnext/regional/doctype/fec_import_settings/test_fec_import_settings.py new file mode 100644 index 0000000000..9d8abcc47f --- /dev/null +++ b/erpnext/regional/doctype/fec_import_settings/test_fec_import_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Dokos SAS and Contributors +# For license information, please see license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestFECImportSettings(FrappeTestCase): + pass -- GitLab From 412e50ed64a954e14d96cd1de4bd7826f215450e Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Fri, 9 Jun 2023 15:37:39 +0200 Subject: [PATCH 02/19] refactor: rename FEC import into FEC import tool --- .../regional/doctype/fec_import/fec_import.py | 195 -------- .../fec_import_settings.json | 64 ++- .../__init__.py | 0 .../fec_import_tool.js} | 2 +- .../fec_import_tool.json} | 45 +- .../fec_import_tool/fec_import_tool.py | 463 ++++++++++++++++++ .../test_fec_import_tool.py} | 2 +- 7 files changed, 568 insertions(+), 203 deletions(-) delete mode 100644 erpnext/regional/doctype/fec_import/fec_import.py rename erpnext/regional/doctype/{fec_import => fec_import_tool}/__init__.py (100%) rename erpnext/regional/doctype/{fec_import/fec_import.js => fec_import_tool/fec_import_tool.js} (94%) rename erpnext/regional/doctype/{fec_import/fec_import.json => fec_import_tool/fec_import_tool.json} (59%) create mode 100644 erpnext/regional/doctype/fec_import_tool/fec_import_tool.py rename erpnext/regional/doctype/{fec_import/test_fec_import.py => fec_import_tool/test_fec_import_tool.py} (80%) diff --git a/erpnext/regional/doctype/fec_import/fec_import.py b/erpnext/regional/doctype/fec_import/fec_import.py deleted file mode 100644 index 0ec8fa4153..0000000000 --- a/erpnext/regional/doctype/fec_import/fec_import.py +++ /dev/null @@ -1,195 +0,0 @@ -# Copyright (c) 2023, Dokos SAS and contributors -# For license information, please see license.txt - -import datetime -from collections import defaultdict - -import frappe -from frappe.model.document import Document -from frappe.utils import flt -from frappe.utils.csvutils import read_csv_content - - -class FECImport(Document): - @frappe.whitelist() - def get_company(self): - company = None - if self.fec_file: - file_name = frappe.db.get_value( - "File", - { - "file_url": self.fec_file, - "attached_to_doctype": self.doctype, - "attached_to_name": self.name, - }, - "file_name", - ) - try: - if siren := file_name.split("FEC")[0]: - return frappe.db.get_value("Company", dict(siren_number=siren)) - except Exception: - return company - - return company - - @frappe.whitelist() - def upload_fec(self): - fileid = frappe.db.get_value( - "File", - {"file_url": self.fec_file, "attached_to_doctype": self.doctype, "attached_to_name": self.name}, - ) - _file = frappe.get_doc("File", fileid) - fcontent = _file.get_content() - rows = read_csv_content(fcontent, delimiter="\t") - - header = rows[0] - data = rows[1:] - - output = list() - for d in data: - row = frappe._dict() - for count, head in enumerate(header): - if head: - row[head] = d[count] or "" - - output.append(row) - - FECImporter(settings=self, data=output) - - -class FECImporter: - def __init__(self, settings, data): - self.settings = settings - self.company_settings = {} - if frappe.db.exists("FEC Import Settings", dict(company=self.settings.company)): - self.company_settings = frappe.get_doc( - "FEC Import Settings", dict(company=self.settings.company) - ) - - self.data = data - - self.import_data() - - def import_data(self): - self.get_journals_mapping() - self.group_data() - self.get_accounts_data() - self.create_document() - self.insert_documents() - - def group_data(self): - initial_group = defaultdict(list) - self.grouped_data = defaultdict(lambda: defaultdict(list)) - - for d in self.data: - if not frappe.db.exists("GL Entry", dict(accounting_entry_number=d.EcritureNum)): - initial_group[d["EcritureNum"]].append(frappe._dict(d)) - - for ecriturenum in initial_group: - if [line.CompAuxNum for line in initial_group[ecriturenum]]: - journal_type = frappe.get_cached_value( - "Accounting Journal", initial_group[ecriturenum][0].JournalCode, "type" - ) - if journal_type == "Sales": - self.grouped_data["Sales Invoice"][ecriturenum] = initial_group[ecriturenum] - elif journal_type == "Purchase": - self.grouped_data["Purchase Invoice"][ecriturenum] = initial_group[ecriturenum] - - else: - self.grouped_data["Journal Entry"][ecriturenum] = initial_group[ecriturenum] - - def get_accounts_data(self): - self.accounts = { - x.account_number.strip(): x.name - for x in frappe.get_all( - "Account", - filters={"disabled": 0, "account_number": ("is", "set")}, - fields=["name", "account_number"], - ) - } - - def get_journals_mapping(self): - dokos_journals = { - x.journal_code: x.name - for x in frappe.get_all( - "Accounting Journal", filters={"disabled": 0}, fields=["journal_code", "name"] - ) - } - - self.journals = {} - mapped_journals = dokos_journals - for mapping in self.company_settings.get("accounting_journal_mapping", []): - for j in mapped_journals: - if j == mapping.accounting_journal_in_dokos: - self.journals[mapping.accounting_journal_in_fec] = mapped_journals[j] - continue - - self.journals[j] = mapped_journals[j] - - mapped_journals = self.journals - - def create_document(self): - self.journal_entries = [] - self.sales_invoices = [] - self.purchase_invoices = [] - - for ecriturenum in self.grouped_data["Journal Entry"]: - self.create_journal_entry(ecriturenum) - - for ecriturenum in self.grouped_data["Sales Invoice"]: - self.create_sales_invoice(ecriturenum) - - for ecriturenum in self.grouped_data["Purchase Invoice"]: - self.create_purchase_invoice(ecriturenum) - - def create_journal_entry(self, ecriturenum): - journal_entry = frappe.get_doc( - { - "doctype": "Journal Entry", - "company": self.settings.company, - "posting_date": datetime.datetime.strptime( - self.grouped_data[ecriturenum][0].EcritureDate, "%Y%m%d" - ).strftime("%Y-%m-%d"), - "cheque_no": ecriturenum, - } - ) - for line in self.grouped_data[ecriturenum]: - journal_code = self.journals.get(line["JournalCode"]) - - compte_aux = line.CompAuxNum - compte_aux_type = None - if compte_aux: - journal_type = frappe.get_cached_value("Accounting Journal", journal_code, "type") - if journal_type == "Sales": - compte_aux = frappe.db.exists("Customer", compte_aux) - compte_aux_type = "Customer" - elif journal_type == "Purchase": - compte_aux = frappe.db.exists("Supplier", compte_aux) - compte_aux_type = "Supplier" - - journal_entry.append( - "accounts", - { - "accounting_journal": journal_code, - "account": self.accounts.get(line["CompteNum"]), - "debit_in_account_currency": flt(line["Debit"].replace(",", ".")), - "credit_in_account_currency": flt(line["Credit"].replace(",", ".")), - "user_remark": line.EcritureLib, - "reference_name": line.PieceRef, - "party_type": compte_aux_type if compte_aux_type and compte_aux else None, - "party": compte_aux if compte_aux_type and compte_aux else None, - }, - ) - journal_entry.flags.accounting_entry_number = line.EcritureNum - - self.journal_entries.append(journal_entry) - - def create_sales_invoice(self, ecriturenum): - pass - - def create_purchase_invoice(self, ecriturenum): - pass - - def insert_documents(self): - print(self.journal_entries[0].as_dict()) - self.journal_entries[0].insert() diff --git a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json index c217e91478..eb64bec49a 100644 --- a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json +++ b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json @@ -8,13 +8,20 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "company", "section_break_2", + "company", "accounting_journal_mapping", "sales_invoices_tab", + "create_sales_invoices", "sales_item", + "customer_group", + "territory", "purchase_invoices_tab", - "purchase_item" + "create_purchase_invoices", + "purchase_item", + "supplier_group", + "payment_entries_tab", + "create_payment_entries" ], "fields": [ { @@ -43,9 +50,11 @@ "label": "Sales Invoices" }, { + "depends_on": "eval:doc.create_sales_invoices", "fieldname": "sales_item", "fieldtype": "Link", "label": "Sales Item", + "mandatory_depends_on": "eval:doc.create_sales_invoices", "options": "Item" }, { @@ -54,15 +63,64 @@ "label": "Purchase Invoices" }, { + "depends_on": "eval:doc.create_purchase_invoices", "fieldname": "purchase_item", "fieldtype": "Link", "label": "Purchase Item", + "mandatory_depends_on": "eval:doc.create_purchase_invoices", "options": "Item" + }, + { + "default": "0", + "fieldname": "create_sales_invoices", + "fieldtype": "Check", + "label": "Create Sales Invoices" + }, + { + "default": "0", + "fieldname": "create_purchase_invoices", + "fieldtype": "Check", + "label": "Create Purchase Invoices" + }, + { + "depends_on": "eval:doc.create_sales_invoices", + "fieldname": "customer_group", + "fieldtype": "Link", + "label": "Default Customer Group", + "mandatory_depends_on": "eval:doc.create_sales_invoices", + "options": "Customer Group" + }, + { + "depends_on": "eval:doc.create_purchase_invoices", + "fieldname": "supplier_group", + "fieldtype": "Link", + "label": "Default Supplier Group", + "mandatory_depends_on": "eval:doc.create_purchase_invoices", + "options": "Supplier Group" + }, + { + "depends_on": "eval:doc.create_sales_invoices", + "fieldname": "territory", + "fieldtype": "Link", + "label": "Default Territory", + "mandatory_depends_on": "eval:doc.create_sales_invoices", + "options": "Territory" + }, + { + "fieldname": "payment_entries_tab", + "fieldtype": "Tab Break", + "label": "Payment Entries" + }, + { + "default": "0", + "fieldname": "create_payment_entries", + "fieldtype": "Check", + "label": "Create Payment Entries" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-06-08 21:47:00.988040", + "modified": "2023-06-09 14:24:27.745902", "modified_by": "Administrator", "module": "Regional", "name": "FEC Import Settings", diff --git a/erpnext/regional/doctype/fec_import/__init__.py b/erpnext/regional/doctype/fec_import_tool/__init__.py similarity index 100% rename from erpnext/regional/doctype/fec_import/__init__.py rename to erpnext/regional/doctype/fec_import_tool/__init__.py diff --git a/erpnext/regional/doctype/fec_import/fec_import.js b/erpnext/regional/doctype/fec_import_tool/fec_import_tool.js similarity index 94% rename from erpnext/regional/doctype/fec_import/fec_import.js rename to erpnext/regional/doctype/fec_import_tool/fec_import_tool.js index 53dc8e88b7..3f3fca4f2d 100644 --- a/erpnext/regional/doctype/fec_import/fec_import.js +++ b/erpnext/regional/doctype/fec_import_tool/fec_import_tool.js @@ -1,7 +1,7 @@ // Copyright (c) 2023, Dokos SAS and contributors // For license information, please see license.txt -frappe.ui.form.on('FEC Import', { +frappe.ui.form.on('FEC Import Tool', { refresh(frm) { frm.get_field("fec_file").df.options = { restrictions: { diff --git a/erpnext/regional/doctype/fec_import/fec_import.json b/erpnext/regional/doctype/fec_import_tool/fec_import_tool.json similarity index 59% rename from erpnext/regional/doctype/fec_import/fec_import.json rename to erpnext/regional/doctype/fec_import_tool/fec_import_tool.json index 8068be5a98..1d6d779011 100644 --- a/erpnext/regional/doctype/fec_import/fec_import.json +++ b/erpnext/regional/doctype/fec_import_tool/fec_import_tool.json @@ -12,7 +12,13 @@ "naming_series", "fec_file", "section_break_3", - "company" + "company", + "filters_section", + "from_date", + "column_break_7", + "to_date", + "section_break_9", + "import_journal" ], "fields": [ { @@ -42,14 +48,47 @@ "label": "Company", "options": "Company", "reqd": 1 + }, + { + "depends_on": "eval:doc.fec_file && doc.company", + "fieldname": "filters_section", + "fieldtype": "Section Break", + "hide_border": 1, + "label": "Filters" + }, + { + "fieldname": "from_date", + "fieldtype": "Date", + "label": "Import From Date" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "fieldname": "to_date", + "fieldtype": "Date", + "label": "Import To Date" + }, + { + "depends_on": "eval:doc.fec_file && doc.company", + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "import_journal", + "fieldtype": "Link", + "label": "Import From Journal", + "options": "Accounting Journal" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-06-08 21:22:57.027237", + "modified": "2023-06-09 09:44:56.202663", "modified_by": "Administrator", "module": "Regional", - "name": "FEC Import", + "name": "FEC Import Tool", "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ diff --git a/erpnext/regional/doctype/fec_import_tool/fec_import_tool.py b/erpnext/regional/doctype/fec_import_tool/fec_import_tool.py new file mode 100644 index 0000000000..237f2ce1c8 --- /dev/null +++ b/erpnext/regional/doctype/fec_import_tool/fec_import_tool.py @@ -0,0 +1,463 @@ +# Copyright (c) 2023, Dokos SAS and contributors +# For license information, please see license.txt + +import datetime +from collections import defaultdict + +import frappe +from frappe.model.document import Document +from frappe.utils import flt, getdate +from frappe.utils.csvutils import read_csv_content + + +class FECImportTool(Document): + @frappe.whitelist() + def get_company(self): + company = None + if self.fec_file: + file_name = frappe.db.get_value( + "File", + { + "file_url": self.fec_file, + "attached_to_doctype": self.doctype, + "attached_to_name": self.name, + }, + "file_name", + ) + try: + if siren := file_name.split("FEC")[0]: + return frappe.db.get_value("Company", dict(siren_number=siren)) + except Exception: + return company + + return company + + @frappe.whitelist() + def upload_fec(self): + fileid = frappe.db.get_value( + "File", + {"file_url": self.fec_file, "attached_to_doctype": self.doctype, "attached_to_name": self.name}, + ) + + _file = frappe.get_doc("File", fileid) + fcontent = _file.get_content() + rows = read_csv_content(fcontent, delimiter="\t") + + header = rows[0] + data = rows[1:] + + output = list() + for d in data: + row = frappe._dict() + for count, head in enumerate(header): + if head: + row[head] = d[count] or "" + + output.append(row) + + try: + print("START IMPORT") + import_in_progress = FECImport(settings=self, data=output) + import_in_progress.import_data() + except Exception: + print(frappe.get_traceback()) + + +class FECImport: + def __init__(self, settings, data): + self.settings = settings + self.company_settings = {} + if frappe.db.exists("FEC Import Settings", dict(company=self.settings.company)): + self.company_settings = frappe.get_doc( + "FEC Import Settings", dict(company=self.settings.company) + ) + + self.data = data + + def import_data(self): + self.get_journals_mapping() + self.parse_credit_debit() + self.group_data() + # self.get_accounts_data() + # self.create_document() + # self.insert_transactionnal_documents() + + def group_data(self): + initial_group = defaultdict(list) + self.grouped_data = defaultdict(lambda: defaultdict(list)) + self.payment_data = defaultdict(list) + + for d in self.data: + # if not frappe.db.exists("GL Entry", dict(accounting_entry_number=d.EcritureNum)): + initial_group[d["EcritureNum"]].append(frappe._dict(d)) + + for ecriturenum in initial_group: + if [line.CompAuxNum for line in initial_group[ecriturenum] if line.CompAuxNum]: + + # if not self.is_within_date_range(initial_group[ecriturenum][0]): + # continue + + journal_type = frappe.get_cached_value( + "Accounting Journal", initial_group[ecriturenum][0].JournalCode, "type" + ) + + if self.company_settings.create_sales_invoices and journal_type == "Sales": + self.grouped_data["Sales Invoice"][ecriturenum] = initial_group[ecriturenum] + elif self.company_settings.create_purchase_invoices and journal_type == "Purchase": + self.grouped_data["Purchase Invoice"][ecriturenum] = initial_group[ecriturenum] + elif self.company_settings.create_payment_entries and journal_type == "Bank": + self.grouped_data["Payment Entry"][ecriturenum] = initial_group[ecriturenum] + else: + self.grouped_data["Journal Entry"][ecriturenum] = initial_group[ecriturenum] + + else: + self.grouped_data["Journal Entry"][ecriturenum] = initial_group[ecriturenum] + + if ecriturelet := [line.EcritureLet for line in initial_group[ecriturenum] if line.EcritureLet]: + self.payment_data[ecriturelet[0]].extend(initial_group[ecriturenum]) + + # for a in self.grouped_data: + # print("GROUPS") + # print(a, len(self.grouped_data[a])) + + print(self.payment_data) + + def parse_credit_debit(self): + for d in self.data: + d["Debit"] = flt(d["Debit"].replace(",", ".")) + d["Credit"] = flt(d["Credit"].replace(",", ".")) + + def is_within_date_range(self, line): + posting_date = datetime.datetime.strptime(line.EcritureDate, "%Y%m%d").strftime("%Y-%m-%d") + + if self.settings.from_date and getdate(posting_date) < getdate(self.settings.from_date): + return False + + if self.settings.to_date and getdate(posting_date) > getdate(self.settings.to_date): + return False + + return True + + def get_accounts_data(self): + self.accounts = { + x.account_number.strip(): x.name + for x in frappe.get_all( + "Account", + filters={"disabled": 0, "account_number": ("is", "set")}, + fields=["name", "account_number"], + ) + } + + def get_journals_mapping(self): + dokos_journals = { + x.journal_code: x.name + for x in frappe.get_all( + "Accounting Journal", filters={"disabled": 0}, fields=["journal_code", "name"] + ) + } + + self.journals = {} + mapped_journals = dokos_journals + for mapping in self.company_settings.get("accounting_journal_mapping", []): + for j in mapped_journals: + if j == mapping.accounting_journal_in_dokos: + self.journals[mapping.accounting_journal_in_fec] = mapped_journals[j] + continue + + self.journals[j] = mapped_journals[j] + + mapped_journals = self.journals + + def create_document(self): + self.journal_entries = [] + self.sales_invoices = [] + self.purchase_invoices = [] + self.payment_entries = [] + + for ecriturenum in self.grouped_data["Sales Invoice"]: + self.create_sales_invoice(ecriturenum, self.grouped_data["Sales Invoice"][ecriturenum]) + + for ecriturenum in self.grouped_data["Purchase Invoice"]: + self.create_purchase_invoice(ecriturenum, self.grouped_data["Purchase Invoice"][ecriturenum]) + + for ecriturenum in self.grouped_data["Journal Entry"]: + self.create_journal_entry(ecriturenum, self.grouped_data["Journal Entry"][ecriturenum]) + + for ecriturenum in self.grouped_data["Payment Entry"]: + self.create_payment_entry(ecriturenum, self.grouped_data["Journal Entry"][ecriturenum]) + + def create_journal_entry(self, ecriturenum, rows): + posting_date = datetime.datetime.strptime(rows[0].EcritureDate, "%Y%m%d").strftime("%Y-%m-%d") + journal_entry = frappe.get_doc( + { + "doctype": "Journal Entry", + "company": self.settings.company, + "posting_date": posting_date, + "cheque_no": ecriturenum, + "cheque_date": posting_date, + } + ) + for line in rows: + journal_code = self.journals.get(line["JournalCode"]) + + if self.settings.import_journal and self.settings.import_journal != journal_code: + continue + + compte_aux = line.CompAuxNum + compte_aux_type = None + if compte_aux: + journal_type = frappe.get_cached_value("Accounting Journal", journal_code, "type") + account_type = frappe.get_cached_value( + "Account", self.accounts.get(line["CompteNum"]), "account_type" + ) + + if journal_type == "Sales" or account_type == "Receivable": + compte_aux = frappe.db.exists("Customer", compte_aux) + compte_aux_type = "Customer" + elif journal_type == "Purchase" or account_type == "Payable": + compte_aux = frappe.db.exists("Supplier", compte_aux) + compte_aux_type = "Supplier" + + if account_type in ["Receivable", "Payable"] and not (compte_aux and compte_aux_type): + if account_type == "Receivable": + compte_aux_type = "Customer" + compte_aux = self.create_customer(compte_aux, line.CompAuxLib) + else: + compte_aux_type = "Supplier" + compte_aux = self.create_supplier(compte_aux, line.CompAuxLib) + + journal_entry.append( + "accounts", + { + "accounting_journal": journal_code, + "account": self.accounts.get(line["CompteNum"]), + "debit_in_account_currency": line["Debit"], + "credit_in_account_currency": line["Credit"], + "user_remark": line.EcritureLib, + # "reference_name": line.PieceRef, + "party_type": compte_aux_type if compte_aux_type and compte_aux else None, + "party": compte_aux if compte_aux_type and compte_aux else None, + }, + ) + journal_entry.flags.accounting_entry_number = ecriturenum + journal_entry.flags.ecriturenum = ecriturenum + + if journal_entry.accounts: + self.journal_entries.append(journal_entry) + + def create_sales_invoice(self, ecriturenum, rows): + invoicing_details = self.get_invoicing_details(rows, "Sales Invoice") + posting_date = datetime.datetime.strptime(rows[0].EcritureDate, "%Y%m%d").strftime("%Y-%m-%d") + + sales_invoice = frappe.new_doc("Sales Invoice") + sales_invoice.flags.ignore_permissions = True + sales_invoice.flags.ecriturenum = ecriturenum + sales_invoice.update( + { + "company": self.settings.company, + "posting_date": posting_date, + "set_posting_time": 1, + "customer": invoicing_details.get("party_name"), + "debit_to": invoicing_details.get("party_account"), + "accounting_journal": self.journals.get(rows[0]["JournalCode"]), + "remarks": invoicing_details.get("party_line", {}).get("EcritureLib"), + "due_date": posting_date, + } + ) + + self.add_items(sales_invoice, invoicing_details.get("items"), self.company_settings.sales_item) + self.add_taxes(sales_invoice, invoicing_details.get("taxes")) + sales_invoice.set_missing_values() + + if sales_invoice.items: + self.sales_invoices.append(sales_invoice) + + else: + self.create_journal_entry(ecriturenum, rows) + + def create_purchase_invoice(self, ecriturenum, rows): + invoicing_details = self.get_invoicing_details(rows, "Purchase Invoice") + posting_date = datetime.datetime.strptime(rows[0].EcritureDate, "%Y%m%d").strftime("%Y-%m-%d") + + purchase_invoice = frappe.new_doc("Purchase Invoice") + purchase_invoice.flags.ignore_permissions = True + purchase_invoice.flags.ecriturenum = ecriturenum + purchase_invoice.update( + { + "company": self.settings.company, + "posting_date": posting_date, + "set_posting_time": 1, + "supplier": invoicing_details.get("party_name"), + "credit_to": invoicing_details.get("party_account"), + "accounting_journal": self.journals.get(rows[0]["JournalCode"]), + "remarks": invoicing_details.get("party_line", {}).get("EcritureLib"), + } + ) + + self.add_items( + purchase_invoice, invoicing_details.get("items"), self.company_settings.sales_item + ) + self.add_taxes(purchase_invoice, invoicing_details.get("taxes")) + purchase_invoice.set_missing_values() + + if purchase_invoice.items: + self.purchase_invoices.append(purchase_invoice) + + else: + self.create_journal_entry(ecriturenum, rows) + + def create_payment_entry(self, ecriturenum, rows): + # posting_date = datetime.datetime.strptime(rows[0].EcritureDate, "%Y%m%d").strftime("%Y-%m-%d") + # invoicing_details = self.get_invoicing_details(rows, "Payment Entry") + + # payment_entry = frappe.new_doc("Purchase Invoice") + # payment_entry.flags.ignore_permissions = True + # payment_entry.update({ + # "company": self.settings.company, + # "posting_date": posting_date, + # "set_posting_time": 1, + # "supplier": invoicing_details.get("party_name"), + # "credit_to": invoicing_details.get("party_account"), + # "accounting_journal": self.journals.get(rows[0]["JournalCode"]), + # "remarks": invoicing_details.get("party_line", {}).get("EcritureLib") + # } + # ) + + # self.add_items(payment_entry, invoicing_details.get("items"), self.company_settings.sales_item) + # self.add_taxes(payment_entry, invoicing_details.get("taxes")) + # payment_entry.set_missing_values() + + # if payment_entry.items: + # self.payment_entries.append(payment_entry) + + # else: + # self.create_journal_entry(ecriturenum, rows) + pass + + def get_invoicing_details(self, lines, invoice_type): + invoicing_details = { + "party_account": "", + "party_name": "", + "party_line": {}, + "taxes": [], + "items": [], + } + + party_type = "Supplier" if invoice_type == "Purchase Invoice" else "Customer" + + for line in lines: + if line.CompteNum.startswith("40") or line.CompteNum.startswith("41"): + if line.CompAuxNum: + invoicing_details["party_name"] = line["CompAuxNum"] + + invoicing_details["party_account"] = self.accounts.get(line["CompteNum"]) + invoicing_details["party_line"] = line + + if not invoicing_details["party_name"] or not frappe.db.exists( + party_type, invoicing_details["party_name"] + ): + invoicing_details["party_name"] = ( + self.create_supplier(line.CompAuxNum, line.CompAuxLib) + if invoice_type == "Purchase Invoice" + else self.create_customer(line.CompAuxNum, line.CompAuxLib) + ) + + elif ( + (line.CompteNum.startswith("6") and line.Debit > 0.0) + if invoice_type == "Purchase Invoice" + else (line.CompteNum.startswith("7") and line.Credit > 0.0) + ): + invoicing_details["items"].append(line) + + else: + invoicing_details["taxes"].append(line) + + return invoicing_details + + def add_items(self, invoice_document, item_lines, item): + for line in item_lines: + invoice_document.append( + "items", + { + "item_code": item, + "qty": 1, + "rate": line["Credit"] + if invoice_document.get("doctype") == "Sales Invoice" + else line["Debit"], + "income_account": self.accounts.get(line["CompteNum"]) + if invoice_document.get("doctype") == "Sales Invoice" + else None, + "expense_account": self.accounts.get(line["CompteNum"]) + if invoice_document.get("doctype") == "Purchase Invoice" + else None, + }, + ) + + def add_taxes(self, invoice_document, tax_lines): + for line in tax_lines: + amount = 0.0 + if invoice_document.get("doctype") == "Sales Invoice": + amount = line["Credit"] + if not amount: + amount = flt(line["Debit"]) * -1 + + elif invoice_document.get("doctype") == "Purchase Invoice": + amount = flt(line["Debit"]) + if not amount: + amount = flt(line["Credit"]) * -1 + + invoice_document.append( + "taxes", + { + "charge_type": "Actual", + "account_head": self.accounts.get(line["CompteNum"]), + "tax_amount": amount, + "description": line.EcritureLib, + }, + ) + + def create_customer(self, compauxnum, compauxlib): + customer = frappe.new_doc("Customer") + customer.__newname = compauxnum + customer.customer_name = compauxlib + customer.customer_group = self.company_settings.customer_group or frappe.db.get_single_value( + "Selling Settings", "customer_group" + ) + customer.territory = self.company_settings.territory or frappe.db.get_single_value( + "Selling Settings", "territory" + ) + customer.insert() + + return customer.name + + def create_supplier(self, compauxnum, compauxlib): + supplier = frappe.new_doc("Supplier") + supplier.__newname = compauxnum + supplier.supplier_name = compauxlib + supplier.supplier_group = self.company_settings.supplier_group or frappe.db.get_single_value( + "Buying Settings", "supplier_group" + ) + supplier.insert() + + return supplier.name + + def insert_transactionnal_documents(self): + print("journal_entries", len(self.journal_entries)) + print("sales_invoices", len(self.sales_invoices)) + print("purchase_invoices", len(self.purchase_invoices)) + + self.ecriturenum_map = {} + + for journal_entry in self.journal_entries[:2]: + journal_entry.insert() + self.ecriturenum_map[journal_entry.flags.ecriturenum] = journal_entry.name + + for sales_invoice in self.sales_invoices[:2]: + sales_invoice.insert() + self.ecriturenum_map[sales_invoice.flags.ecriturenum] = sales_invoice.name + + for purchase_invoice in self.purchase_invoices[:2]: + purchase_invoice.insert() + self.ecriturenum_map[purchase_invoice.flags.ecriturenum] = purchase_invoice.name + + print(self.ecriturenum_map) diff --git a/erpnext/regional/doctype/fec_import/test_fec_import.py b/erpnext/regional/doctype/fec_import_tool/test_fec_import_tool.py similarity index 80% rename from erpnext/regional/doctype/fec_import/test_fec_import.py rename to erpnext/regional/doctype/fec_import_tool/test_fec_import_tool.py index 453ac4a29d..1633eea4f8 100644 --- a/erpnext/regional/doctype/fec_import/test_fec_import.py +++ b/erpnext/regional/doctype/fec_import_tool/test_fec_import_tool.py @@ -5,5 +5,5 @@ from frappe.tests.utils import FrappeTestCase -class TestFECImport(FrappeTestCase): +class TestFECImportTool(FrappeTestCase): pass -- GitLab From 400ecff4b86c738796ea8497ddf6cb529d04de51 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Tue, 13 Jun 2023 10:38:00 +0200 Subject: [PATCH 03/19] wip: FEC import tool --- .../fec_import_tool/fec_import_tool.py | 123 +++++++++--------- 1 file changed, 61 insertions(+), 62 deletions(-) diff --git a/erpnext/regional/doctype/fec_import_tool/fec_import_tool.py b/erpnext/regional/doctype/fec_import_tool/fec_import_tool.py index 237f2ce1c8..65dc795941 100644 --- a/erpnext/regional/doctype/fec_import_tool/fec_import_tool.py +++ b/erpnext/regional/doctype/fec_import_tool/fec_import_tool.py @@ -78,14 +78,14 @@ class FECImport: self.get_journals_mapping() self.parse_credit_debit() self.group_data() - # self.get_accounts_data() - # self.create_document() - # self.insert_transactionnal_documents() + self.get_accounts_data() + self.create_document() + # self.insert_transactional_documents() def group_data(self): initial_group = defaultdict(list) self.grouped_data = defaultdict(lambda: defaultdict(list)) - self.payment_data = defaultdict(list) + self.payment_data = defaultdict(tuple) for d in self.data: # if not frappe.db.exists("GL Entry", dict(accounting_entry_number=d.EcritureNum)): @@ -113,15 +113,10 @@ class FECImport: else: self.grouped_data["Journal Entry"][ecriturenum] = initial_group[ecriturenum] - if ecriturelet := [line.EcritureLet for line in initial_group[ecriturenum] if line.EcritureLet]: - self.payment_data[ecriturelet[0]].extend(initial_group[ecriturenum]) - # for a in self.grouped_data: # print("GROUPS") # print(a, len(self.grouped_data[a])) - print(self.payment_data) - def parse_credit_debit(self): for d in self.data: d["Debit"] = flt(d["Debit"].replace(",", ".")) @@ -184,10 +179,13 @@ class FECImport: self.create_journal_entry(ecriturenum, self.grouped_data["Journal Entry"][ecriturenum]) for ecriturenum in self.grouped_data["Payment Entry"]: - self.create_payment_entry(ecriturenum, self.grouped_data["Journal Entry"][ecriturenum]) + self.create_journal_entry(ecriturenum, self.grouped_data["Journal Entry"][ecriturenum]) + + print("payment_data", self.payment_data) def create_journal_entry(self, ecriturenum, rows): posting_date = datetime.datetime.strptime(rows[0].EcritureDate, "%Y%m%d").strftime("%Y-%m-%d") + add_to_payment_data = None journal_entry = frappe.get_doc( { "doctype": "Journal Entry", @@ -203,6 +201,8 @@ class FECImport: if self.settings.import_journal and self.settings.import_journal != journal_code: continue + reference_type, reference_name = None, None + compte_aux = line.CompAuxNum compte_aux_type = None if compte_aux: @@ -211,13 +211,21 @@ class FECImport: "Account", self.accounts.get(line["CompteNum"]), "account_type" ) - if journal_type == "Sales" or account_type == "Receivable": + if journal_type in ("Sales", "Bank") or account_type == "Receivable": compte_aux = frappe.db.exists("Customer", compte_aux) compte_aux_type = "Customer" - elif journal_type == "Purchase" or account_type == "Payable": + elif journal_type in ("Purchase", "Bank") or account_type == "Payable": compte_aux = frappe.db.exists("Supplier", compte_aux) compte_aux_type = "Supplier" + if account_type in ["Receivable", "Payable"] and not (compte_aux and compte_aux_type): + if line.EcritureLet and (reference := self.payment_data.get(line.EcritureLet)): + reference_type = reference[0] + reference_name = reference[1] + + elif line.EcritureLet: + add_to_payment_data = line.EcritureLet + if account_type in ["Receivable", "Payable"] and not (compte_aux and compte_aux_type): if account_type == "Receivable": compte_aux_type = "Customer" @@ -233,8 +241,9 @@ class FECImport: "account": self.accounts.get(line["CompteNum"]), "debit_in_account_currency": line["Debit"], "credit_in_account_currency": line["Credit"], - "user_remark": line.EcritureLib, - # "reference_name": line.PieceRef, + "user_remark": f"{line.EcritureLib}
{line.PieceRef}", + "reference_type": reference_type, + "reference_name": reference_name, "party_type": compte_aux_type if compte_aux_type and compte_aux else None, "party": compte_aux if compte_aux_type and compte_aux else None, }, @@ -243,7 +252,11 @@ class FECImport: journal_entry.flags.ecriturenum = ecriturenum if journal_entry.accounts: - self.journal_entries.append(journal_entry) + journal_entry.insert() + journal_entry.submit() + + if add_to_payment_data: + self.payment_data[add_to_payment_data] = (journal_entry.doctype, journal_entry.name) def create_sales_invoice(self, ecriturenum, rows): invoicing_details = self.get_invoicing_details(rows, "Sales Invoice") @@ -269,8 +282,13 @@ class FECImport: self.add_taxes(sales_invoice, invoicing_details.get("taxes")) sales_invoice.set_missing_values() - if sales_invoice.items: - self.sales_invoices.append(sales_invoice) + if sales_invoice.customer and sales_invoice.items: + # self.sales_invoices.append(sales_invoice) + sales_invoice.insert() + sales_invoice.submit() + + for ref in list(invoicing_details.get("references")): + self.payment_data[ref] = (sales_invoice.doctype, sales_invoice.name) else: self.create_journal_entry(ecriturenum, rows) @@ -300,40 +318,17 @@ class FECImport: self.add_taxes(purchase_invoice, invoicing_details.get("taxes")) purchase_invoice.set_missing_values() - if purchase_invoice.items: - self.purchase_invoices.append(purchase_invoice) + if purchase_invoice.supplier and purchase_invoice.items: + # self.purchase_invoices.append(purchase_invoice) + purchase_invoice.insert() + purchase_invoice.submit() + + for ref in list(invoicing_details.get("references")): + self.payment_data[ref] = (purchase_invoice.doctype, purchase_invoice.name) else: self.create_journal_entry(ecriturenum, rows) - def create_payment_entry(self, ecriturenum, rows): - # posting_date = datetime.datetime.strptime(rows[0].EcritureDate, "%Y%m%d").strftime("%Y-%m-%d") - # invoicing_details = self.get_invoicing_details(rows, "Payment Entry") - - # payment_entry = frappe.new_doc("Purchase Invoice") - # payment_entry.flags.ignore_permissions = True - # payment_entry.update({ - # "company": self.settings.company, - # "posting_date": posting_date, - # "set_posting_time": 1, - # "supplier": invoicing_details.get("party_name"), - # "credit_to": invoicing_details.get("party_account"), - # "accounting_journal": self.journals.get(rows[0]["JournalCode"]), - # "remarks": invoicing_details.get("party_line", {}).get("EcritureLib") - # } - # ) - - # self.add_items(payment_entry, invoicing_details.get("items"), self.company_settings.sales_item) - # self.add_taxes(payment_entry, invoicing_details.get("taxes")) - # payment_entry.set_missing_values() - - # if payment_entry.items: - # self.payment_entries.append(payment_entry) - - # else: - # self.create_journal_entry(ecriturenum, rows) - pass - def get_invoicing_details(self, lines, invoice_type): invoicing_details = { "party_account": "", @@ -341,6 +336,7 @@ class FECImport: "party_line": {}, "taxes": [], "items": [], + "references": set(), } party_type = "Supplier" if invoice_type == "Purchase Invoice" else "Customer" @@ -372,6 +368,9 @@ class FECImport: else: invoicing_details["taxes"].append(line) + if line.EcritureLet: + invoicing_details["references"].add(line.EcritureLet) + return invoicing_details def add_items(self, invoice_document, item_lines, item): @@ -441,23 +440,23 @@ class FECImport: return supplier.name - def insert_transactionnal_documents(self): - print("journal_entries", len(self.journal_entries)) - print("sales_invoices", len(self.sales_invoices)) - print("purchase_invoices", len(self.purchase_invoices)) + # def insert_transactional_documents(self): + # print("journal_entries", len(self.journal_entries)) + # print("sales_invoices", len(self.sales_invoices)) + # print("purchase_invoices", len(self.purchase_invoices)) - self.ecriturenum_map = {} + # self.ecriturenum_map = {} - for journal_entry in self.journal_entries[:2]: - journal_entry.insert() - self.ecriturenum_map[journal_entry.flags.ecriturenum] = journal_entry.name + # for journal_entry in self.journal_entries[:2]: + # journal_entry.insert() + # self.ecriturenum_map[journal_entry.flags.ecriturenum] = journal_entry.name - for sales_invoice in self.sales_invoices[:2]: - sales_invoice.insert() - self.ecriturenum_map[sales_invoice.flags.ecriturenum] = sales_invoice.name + # for sales_invoice in self.sales_invoices[:2]: + # sales_invoice.insert() + # self.ecriturenum_map[sales_invoice.flags.ecriturenum] = sales_invoice.name - for purchase_invoice in self.purchase_invoices[:2]: - purchase_invoice.insert() - self.ecriturenum_map[purchase_invoice.flags.ecriturenum] = purchase_invoice.name + # for purchase_invoice in self.purchase_invoices[:2]: + # purchase_invoice.insert() + # self.ecriturenum_map[purchase_invoice.flags.ecriturenum] = purchase_invoice.name - print(self.ecriturenum_map) + # print(self.ecriturenum_map) -- GitLab From 83883a2bef72b9a4fa0a62de83978dc6ea08ebdd Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Wed, 14 Jun 2023 16:14:52 +0200 Subject: [PATCH 04/19] refactor: FEC import tool --- .../accounting_journal.json | 7 +- .../doctype/fec_import_document/__init__.py | 0 .../fec_import_document.js | 22 + .../fec_import_document.json | 156 ++++++ .../fec_import_document.py | 427 +++++++++++++++ .../fec_import_document_list.js | 20 + .../test_fec_import_document.py | 9 + .../doctype/fec_import_line/__init__.py | 0 .../fec_import_line/fec_import_line.json | 240 +++++++++ .../fec_import_line/fec_import_line.py | 8 + .../fec_import_settings.json | 17 +- .../fec_import_tool/fec_import_tool.js | 24 + .../fec_import_tool/fec_import_tool.py | 503 +++++------------- 13 files changed, 1068 insertions(+), 365 deletions(-) create mode 100644 erpnext/regional/doctype/fec_import_document/__init__.py create mode 100644 erpnext/regional/doctype/fec_import_document/fec_import_document.js create mode 100644 erpnext/regional/doctype/fec_import_document/fec_import_document.json create mode 100644 erpnext/regional/doctype/fec_import_document/fec_import_document.py create mode 100644 erpnext/regional/doctype/fec_import_document/fec_import_document_list.js create mode 100644 erpnext/regional/doctype/fec_import_document/test_fec_import_document.py create mode 100644 erpnext/regional/doctype/fec_import_line/__init__.py create mode 100644 erpnext/regional/doctype/fec_import_line/fec_import_line.json create mode 100644 erpnext/regional/doctype/fec_import_line/fec_import_line.py diff --git a/erpnext/accounts/doctype/accounting_journal/accounting_journal.json b/erpnext/accounts/doctype/accounting_journal/accounting_journal.json index 5eb50288e3..3cef00d12e 100644 --- a/erpnext/accounts/doctype/accounting_journal/accounting_journal.json +++ b/erpnext/accounts/doctype/accounting_journal/accounting_journal.json @@ -23,8 +23,7 @@ "fieldtype": "Data", "in_list_view": 1, "label": "Journal Name", - "reqd": 1, - "unique": 1 + "reqd": 1 }, { "fieldname": "type", @@ -79,10 +78,11 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2022-03-15 09:29:59.117824", + "modified": "2023-06-14 12:35:23.944466", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting Journal", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -119,6 +119,7 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "journal_name", "track_changes": 1 } diff --git a/erpnext/regional/doctype/fec_import_document/__init__.py b/erpnext/regional/doctype/fec_import_document/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/regional/doctype/fec_import_document/fec_import_document.js b/erpnext/regional/doctype/fec_import_document/fec_import_document.js new file mode 100644 index 0000000000..f7b1c75a3f --- /dev/null +++ b/erpnext/regional/doctype/fec_import_document/fec_import_document.js @@ -0,0 +1,22 @@ +// Copyright (c) 2023, Dokos SAS and contributors +// For license information, please see license.txt + +frappe.ui.form.on('FEC Import Document', { + refresh: function(frm) { + if (frm.doc.status != "Completed") { + frm.add_custom_button(`${__("Retry an integration")}`, () => + frappe.call({ + method: "create_linked_document", + doc: frm.doc + }).then(r => { + frappe.show_alert({ + message: __("Retry in progress"), + indicator: "orange" + }) + + frm.refresh() + }) + ); + } + } +}); diff --git a/erpnext/regional/doctype/fec_import_document/fec_import_document.json b/erpnext/regional/doctype/fec_import_document/fec_import_document.json new file mode 100644 index 0000000000..8b9a5232eb --- /dev/null +++ b/erpnext/regional/doctype/fec_import_document/fec_import_document.json @@ -0,0 +1,156 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-06-14 09:33:57.672790", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "fec_import", + "status", + "error", + "column_break_4", + "settings", + "company", + "data_section", + "gl_entries_date", + "column_break_8", + "gl_entry_reference", + "section_break_10", + "gl_entries", + "section_break_13", + "linked_document_type", + "column_break_15", + "linked_document" + ], + "fields": [ + { + "fieldname": "fec_import", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "FEC Import", + "options": "FEC Import Tool", + "reqd": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Pending\nCompleted\nError", + "reqd": 1 + }, + { + "depends_on": "eval:doc.status==\"Error\"", + "fieldname": "error", + "fieldtype": "Small Text", + "label": "Error", + "read_only": 1 + }, + { + "fieldname": "data_section", + "fieldtype": "Section Break", + "hide_border": 1, + "label": "FEC Data" + }, + { + "fieldname": "gl_entries", + "fieldtype": "Table", + "label": "GL Entries", + "options": "FEC Import Line" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "settings", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Settings", + "options": "FEC Import Settings", + "reqd": 1 + }, + { + "fieldname": "gl_entries_date", + "fieldtype": "Date", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "GL Entries Date" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "fieldname": "gl_entry_reference", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "GL Entry Reference" + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, + { + "fetch_from": "settings.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "section_break_13", + "fieldtype": "Section Break" + }, + { + "fieldname": "linked_document_type", + "fieldtype": "Link", + "label": "Linked Document Type", + "options": "DocType" + }, + { + "fieldname": "column_break_15", + "fieldtype": "Column Break" + }, + { + "fieldname": "linked_document", + "fieldtype": "Dynamic Link", + "label": "Linked Document", + "options": "linked_document_type" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-06-14 12:03:06.586896", + "modified_by": "Administrator", + "module": "Regional", + "name": "FEC Import Document", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "select": 1, + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "gl_entry_reference" +} diff --git a/erpnext/regional/doctype/fec_import_document/fec_import_document.py b/erpnext/regional/doctype/fec_import_document/fec_import_document.py new file mode 100644 index 0000000000..439fdae76c --- /dev/null +++ b/erpnext/regional/doctype/fec_import_document/fec_import_document.py @@ -0,0 +1,427 @@ +# Copyright (c) 2023, Dokos SAS and contributors +# For license information, please see license.txt + +import datetime + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import flt, get_year_ending, get_year_start, getdate + +from erpnext.accounts.utils import FiscalYearError, get_fiscal_years + + +class FECImportDocument(Document): + def validate(self): + if self.status != "Completed": + for row in self.gl_entries: + self.get_accounting_journal(row) + self.get_gl_account(row) + self.get_party(row) + self.parse_dates(row) + + def check_fiscal_year(self): + try: + posting_date = getdate(self.gl_entries_date) + get_fiscal_years(posting_date) + except FiscalYearError: + # TODO: Create Fiscal Year based on FEC file name + frappe.clear_messages() + doc = frappe.get_doc( + { + "doctype": "Fiscal Year", + "year": posting_date.year, + "year_start_date": get_year_start(posting_date), + "year_end_date": get_year_ending(posting_date), + } + ) + doc.insert(ignore_if_duplicate=True) + + def get_accounting_journal(self, row): + if row.accounting_journal: + return + + journals = self.get_accounting_journals_mapping() + row.accounting_journal = journals.get(row.journalcode) + + def get_accounting_journals_mapping(self): + dokos_journals = { + x.journal_code: x.name + for x in frappe.get_all( + "Accounting Journal", filters={"disabled": 0}, fields=["journal_code", "name"] + ) + } + + company_settings = frappe.get_doc("FEC Import Settings", self.settings) + + journals = {} + mapped_journals = dokos_journals + for mapping in company_settings.get("accounting_journal_mapping", []): + for j in mapped_journals: + if j == mapping.accounting_journal_in_dokos: + journals[mapping.accounting_journal_in_fec] = mapped_journals[j] + continue + + journals[j] = mapped_journals[j] + + mapped_journals = journals + + return journals or dokos_journals + + def get_gl_account(self, row): + if row.account: + return + + accounts = { + x.account_number.strip(): x.name + for x in frappe.get_all( + "Account", + filters={"disabled": 0, "account_number": ("is", "set")}, + fields=["name", "account_number"], + ) + } + + row.account = accounts.get(row.comptenum) + + def get_party(self, row): + if not row.compauxnum or (row.party_type and row.party): + return + + journal_type = frappe.get_cached_value("Accounting Journal", row.accounting_journal, "type") + account_type = frappe.get_cached_value("Account", row.account, "account_type") + + compte_aux = None + compte_aux_type = None + if journal_type in ("Sales", "Bank") or account_type == "Receivable": + if compte_aux := frappe.db.exists("Customer", row.compauxnum): + compte_aux_type = "Customer" + elif journal_type in ("Purchase", "Bank") or account_type == "Payable": + if compte_aux := frappe.db.exists("Supplier", row.compauxnum): + compte_aux_type = "Supplier" + + if account_type in ["Receivable", "Payable"] and not (compte_aux and compte_aux_type): + if account_type == "Receivable": + compte_aux_type = "Customer" + compte_aux = self.create_customer(compte_aux, row.compauxlib) + else: + compte_aux_type = "Supplier" + compte_aux = self.create_supplier(compte_aux, row.compauxlib) + + row.party_type = compte_aux_type + row.party = compte_aux + + def create_customer(self, compauxnum, compauxlib): + company_settings = frappe.get_doc("FEC Import Settings", self.settings) + + customer = frappe.new_doc("Customer") + customer.name = compauxnum + customer.customer_name = compauxlib + customer.customer_group = company_settings.customer_group or frappe.db.get_single_value( + "Selling Settings", "customer_group" + ) + customer.territory = company_settings.territory or frappe.db.get_single_value( + "Selling Settings", "territory" + ) + + frappe.flags.in_import = True + customer.flags.name_set = True + customer.insert(ignore_if_duplicate=True) + frappe.flags.in_import = False + + return customer.name + + def create_supplier(self, compauxnum, compauxlib): + company_settings = frappe.get_doc("FEC Import Settings", self.settings) + + supplier = frappe.new_doc("Supplier") + supplier.name = compauxnum + supplier.supplier_name = compauxlib + supplier.supplier_group = company_settings.supplier_group or frappe.db.get_single_value( + "Buying Settings", "supplier_group" + ) + + frappe.flags.in_import = True + supplier.flags.name_set = True + supplier.insert(ignore_if_duplicate=True) + frappe.flags.in_import = False + + return supplier.name + + def parse_dates(self, row): + row.posting_date = parse_date(row.ecrituredate) + row.transaction_date = parse_date(row.piecedate) + row.validation_date = parse_date(row.validdate) + row.reconciliation_date = parse_date(row.datelet) + + @frappe.whitelist() + def create_linked_document(self): + fields = ["accounting_journal", "account"] + for line in self.gl_entries: + for field in fields: + if not line.get(field): + frappe.throw( + _( + "Row #{0}: Data for field {1} could not be found automatically. Please select it manually" + ).format(line.idx, frappe.unscrub(field)) + ) + + return self.create_references() + + def process_document_in_background(self): + frappe.enqueue_doc(self.doctype, self.name, "create_references", timeout=1000) + + def create_references(self): + self.db_set("status", "Pending") + self.db_set("error", None) + try: + self.check_fiscal_year() + + party = [l.party for l in self.gl_entries if l.party] + party_type = [l.party_type for l in self.gl_entries if l.party_type] + if len(party) == 1 and len(party_type) == 1: + if not [ + l.account + for l in self.gl_entries + if frappe.get_cached_value("Account", l.account, "account_type") in ["Bank", "Cash"] + ]: + if party_type[0] == "Customer": + self.create_sales_invoice() + elif party_type[0] == "Supplier": + self.create_purchase_invoice() + else: + self.create_journal_entry() + else: + self.create_journal_entry() + else: + self.create_journal_entry() + except Exception: + self.db_set("status", "Error") + self.db_set("error", frappe.get_traceback()) + + def create_journal_entry(self): + journal_entry = frappe.get_doc( + { + "doctype": "Journal Entry", + "company": self.company, + "posting_date": self.gl_entries_date, + "cheque_no": self.gl_entry_reference, + "cheque_date": self.gl_entries_date, + } + ) + for line in self.gl_entries: + self.check_account_is_not_a_group(line.account) + journal_entry.append( + "accounts", + { + "accounting_journal": line.accounting_journal, + "account": line.account, + "debit_in_account_currency": line.debit, + "debit": line.debit, + "credit_in_account_currency": line.credit, + "credit": line.credit, + "user_remark": f"{line.ecriturelib}
{line.pieceref}", + # "reference_type": reference_type, + # "reference_name": reference_name, + "party_type": line.party_type, + "party": line.party, + }, + ) + + if journal_entry.accounts: + journal_entry.insert() + journal_entry.submit() + + self.db_set("linked_document_type", "Journal Entry") + self.db_set("linked_document", journal_entry.name) + self.db_set("status", "Completed") + + def create_sales_invoice(self): + customer, debit_to, remark = self.get_party_and_party_account() + + sales_invoice = frappe.new_doc("Sales Invoice") + sales_invoice.flags.ignore_permissions = True + sales_invoice.update( + { + "company": self.company, + "posting_date": self.gl_entries_date, + "set_posting_time": 1, + "customer": customer, + "debit_to": debit_to, + "accounting_journal": self.gl_entries[0].accounting_journal, + "remarks": remark, + "due_date": self.gl_entries_date, + } + ) + + self.add_items(sales_invoice) + self.add_taxes(sales_invoice) + sales_invoice.set_missing_values() + + if sales_invoice.customer and sales_invoice.items: + # self.sales_invoices.append(sales_invoice) + try: + if self.gl_entry_reference: + sales_invoice.name = self.gl_entry_reference + sales_invoice.flags.draft_name_set = True + sales_invoice.insert() + + if self.gl_entry_reference: + sales_invoice.flags.name_set = True + + sales_invoice.flags.ignore_version = True + sales_invoice.submit() + + self.db_set("linked_document_type", "Sales Invoice") + self.db_set("linked_document", sales_invoice.name) + self.db_set("status", "Completed") + except frappe.DuplicateEntryError: + print("Duplicate Invoice", sales_invoice.name) + self.db_set("linked_document_type", "Sales Invoice") + self.db_set("linked_document", sales_invoice.name) + self.db_set("status", "Completed") + + else: + self.create_journal_entry() + + def create_purchase_invoice(self): + supplier, credit_to, remark = self.get_party_and_party_account() + + purchase_invoice = frappe.new_doc("Purchase Invoice") + purchase_invoice.flags.ignore_permissions = True + purchase_invoice.update( + { + "company": self.company, + "posting_date": self.gl_entries_date, + "set_posting_time": 1, + "supplier": supplier, + "credit_to": credit_to, + "accounting_journal": self.gl_entries[0].accounting_journal, + "remarks": remark, + } + ) + + self.add_items(purchase_invoice) + self.add_taxes(purchase_invoice) + purchase_invoice.set_missing_values() + + if purchase_invoice.supplier and purchase_invoice.items: + # self.purchase_invoices.append(purchase_invoice) + try: + if self.gl_entry_reference: + purchase_invoice.name = self.gl_entry_reference + purchase_invoice.flags.draft_name_set = True + purchase_invoice.insert() + + if self.gl_entry_reference: + purchase_invoice.flags.name_set = True + + purchase_invoice.flags.ignore_version = True + purchase_invoice.submit() + + self.db_set("linked_document_type", "Purchase Invoice") + self.db_set("linked_document", purchase_invoice.name) + self.db_set("status", "Completed") + except frappe.DuplicateEntryError: + print("Duplicate Invoice", purchase_invoice.name) + self.db_set("linked_document_type", "Purchase Invoice") + self.db_set("linked_document", purchase_invoice.name) + self.db_set("status", "Completed") + + else: + self.create_journal_entry() + + def get_party_and_party_account(self): + party_name = None + party_account = None + party_remark = None + for line in self.gl_entries: + if line.comptenum.startswith("40") or line.comptenum.startswith("41"): + if line.compauxnum: + party_name = line.party + + party_account = line.account + party_remark = line.ecriturelib + + break + + return party_name, party_account, party_remark + + def add_items(self, transaction): + for line in self.gl_entries: + if ( + (line.comptenum.startswith("6") and line.debit > 0.0) + if transaction.doctype == "Purchase Invoice" + else (line.comptenum.startswith("7") and line.credit > 0.0) + ): + transaction.append( + "items", + { + "item_code": frappe.db.get_value( + "FEC Import Settings", + self.settings, + "purchase_item" if transaction.doctype == "Purchase Invoice" else "sales_item", + ), + "qty": 1, + "rate": line.credit if transaction.doctype == "Sales Invoice" else line.debit, + "income_account": line.account if transaction.doctype == "Sales Invoice" else None, + "expense_account": line.account if transaction.doctype == "Purchase Invoice" else None, + "remarks": line.ecriturelib, + }, + ) + + self.check_account_is_not_a_group(line.account) + + def add_taxes(self, transaction): + for line in self.gl_entries: + if ( + (line.comptenum.startswith("6") and line.debit > 0.0) + if transaction.doctype == "Purchase Invoice" + else (line.comptenum.startswith("7") and line.credit > 0.0) + ): + continue + + if line.comptenum.startswith("40") or line.comptenum.startswith("41"): + continue + + amount = 0.0 + if transaction.doctype == "Sales Invoice": + amount = line.credit + if not amount: + amount = flt(line.debit) * -1 + + elif transaction.doctype == "Purchase Invoice": + amount = flt(line.debit) + if not amount: + amount = flt(line.credit) * -1 + + transaction.append( + "taxes", + { + "charge_type": "Actual", + "account_head": line.account, + "tax_amount": amount, + "description": line.ecriturelib, + }, + ) + + self.check_account_is_not_a_group(line.account) + + def check_account_is_not_a_group(self, account): + if frappe.db.get_value("Account", account, "is_group"): + frappe.db.set_value("Account", account, "is_group", 0) + + +def parse_date(date): + if not date: + return + + return datetime.datetime.strptime(date, "%Y%m%d").strftime("%Y-%m-%d") + + +@frappe.whitelist() +def bulk_process(docnames): + docnames = frappe.parse_json(docnames) + for docname in docnames: + doc = frappe.get_doc("FEC Import Document", docname) + if doc.status != "Completed": + doc.run_method("process_document_in_background") diff --git a/erpnext/regional/doctype/fec_import_document/fec_import_document_list.js b/erpnext/regional/doctype/fec_import_document/fec_import_document_list.js new file mode 100644 index 0000000000..5b9fda2c27 --- /dev/null +++ b/erpnext/regional/doctype/fec_import_document/fec_import_document_list.js @@ -0,0 +1,20 @@ +frappe.listview_settings["FEC Import Document"] = { + onload: function (doclist) { + const action = () => { + const selected_docs = doclist.get_checked_items(); + if (selected_docs.length > 0) { + let docnames = selected_docs.map((doc) => doc.name); + frappe.call({ + method: "erpnext.regional.doctype.fec_import_document.fec_import_document.bulk_process", + args: { docnames }, + }).then(() => { + frappe.show_alert({ + message: __("Bulk processing in progress"), + indicator: "orange" + }) + }) + } + }; + doclist.page.add_actions_menu_item(__("Bulk Process"), action, false); + }, +}; diff --git a/erpnext/regional/doctype/fec_import_document/test_fec_import_document.py b/erpnext/regional/doctype/fec_import_document/test_fec_import_document.py new file mode 100644 index 0000000000..642c02a7b1 --- /dev/null +++ b/erpnext/regional/doctype/fec_import_document/test_fec_import_document.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Dokos SAS and Contributors +# For license information, please see license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestFECImportDocument(FrappeTestCase): + pass diff --git a/erpnext/regional/doctype/fec_import_line/__init__.py b/erpnext/regional/doctype/fec_import_line/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/regional/doctype/fec_import_line/fec_import_line.json b/erpnext/regional/doctype/fec_import_line/fec_import_line.json new file mode 100644 index 0000000000..bfd76003a8 --- /dev/null +++ b/erpnext/regional/doctype/fec_import_line/fec_import_line.json @@ -0,0 +1,240 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-06-14 09:55:52.985432", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "fec_data_section", + "journalcode", + "journallib", + "ecriturenum", + "ecrituredate", + "ecriturelib", + "column_break_7", + "comptenum", + "comptelib", + "compauxnum", + "compauxlib", + "pieceref", + "piecedate", + "column_break_14", + "debit", + "credit", + "ecriturelet", + "datelet", + "validdate", + "montantdevise", + "idevise", + "dokos_data_section", + "accounting_journal", + "account", + "column_break_25", + "party_type", + "party", + "dokos_dates_section", + "posting_date", + "transaction_date", + "column_break_31", + "validation_date", + "reconciliation_date", + "section_break_22", + "hashed_data" + ], + "fields": [ + { + "fieldname": "journalcode", + "fieldtype": "Data", + "in_list_view": 1, + "label": "JournalCode" + }, + { + "fieldname": "journallib", + "fieldtype": "Small Text", + "label": "JournalLib" + }, + { + "fieldname": "ecriturenum", + "fieldtype": "Data", + "label": "EcritureNum" + }, + { + "fieldname": "ecrituredate", + "fieldtype": "Data", + "label": "EcritureDate" + }, + { + "fieldname": "comptenum", + "fieldtype": "Data", + "in_list_view": 1, + "label": "CompteNum" + }, + { + "fieldname": "comptelib", + "fieldtype": "Small Text", + "label": "CompteLib" + }, + { + "fieldname": "compauxnum", + "fieldtype": "Data", + "in_list_view": 1, + "label": "CompAuxNum" + }, + { + "fieldname": "compauxlib", + "fieldtype": "Small Text", + "label": "CompAuxLib" + }, + { + "fieldname": "pieceref", + "fieldtype": "Data", + "label": "PieceRef" + }, + { + "fieldname": "piecedate", + "fieldtype": "Data", + "label": "PieceDate" + }, + { + "fieldname": "ecriturelib", + "fieldtype": "Small Text", + "label": "EcritureLib" + }, + { + "fieldname": "debit", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Debit" + }, + { + "fieldname": "credit", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Credit" + }, + { + "fieldname": "ecriturelet", + "fieldtype": "Data", + "label": "EcritureLet" + }, + { + "fieldname": "datelet", + "fieldtype": "Data", + "label": "DateLet" + }, + { + "fieldname": "validdate", + "fieldtype": "Data", + "label": "ValidDate" + }, + { + "fieldname": "montantdevise", + "fieldtype": "Float", + "label": "Montantdevise" + }, + { + "fieldname": "idevise", + "fieldtype": "Data", + "label": "Idevise" + }, + { + "fieldname": "fec_data_section", + "fieldtype": "Section Break", + "label": "FEC Data" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_22", + "fieldtype": "Section Break" + }, + { + "fieldname": "dokos_data_section", + "fieldtype": "Section Break", + "hide_border": 1, + "label": "Dokos Data" + }, + { + "fieldname": "accounting_journal", + "fieldtype": "Link", + "label": "Accounting Journal", + "options": "Accounting Journal" + }, + { + "fieldname": "account", + "fieldtype": "Link", + "label": "Account", + "options": "Account" + }, + { + "fieldname": "column_break_25", + "fieldtype": "Column Break" + }, + { + "fieldname": "party_type", + "fieldtype": "Link", + "label": "Party Type", + "options": "DocType" + }, + { + "fieldname": "party", + "fieldtype": "Dynamic Link", + "label": "Party", + "options": "party_type" + }, + { + "fieldname": "dokos_dates_section", + "fieldtype": "Section Break", + "label": "Dokos Dates" + }, + { + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date" + }, + { + "fieldname": "transaction_date", + "fieldtype": "Date", + "label": "Transaction Date" + }, + { + "fieldname": "column_break_31", + "fieldtype": "Column Break" + }, + { + "fieldname": "validation_date", + "fieldtype": "Data", + "label": "Validation Date" + }, + { + "fieldname": "reconciliation_date", + "fieldtype": "Data", + "label": "Reconciliation Date" + }, + { + "fieldname": "hashed_data", + "fieldtype": "Data", + "label": "Hashed Data", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-06-14 10:54:03.274088", + "modified_by": "Administrator", + "module": "Regional", + "name": "FEC Import Line", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} diff --git a/erpnext/regional/doctype/fec_import_line/fec_import_line.py b/erpnext/regional/doctype/fec_import_line/fec_import_line.py new file mode 100644 index 0000000000..d48e0eec9c --- /dev/null +++ b/erpnext/regional/doctype/fec_import_line/fec_import_line.py @@ -0,0 +1,8 @@ +# Copyright (c) 2023, Dokos SAS and contributors +# For license information, please see license.txt + +from frappe.model.document import Document + + +class FECImportLine(Document): + pass diff --git a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json index eb64bec49a..c781593b2b 100644 --- a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json +++ b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json @@ -8,6 +8,7 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "links_tab", "section_break_2", "company", "accounting_journal_mapping", @@ -116,11 +117,23 @@ "fieldname": "create_payment_entries", "fieldtype": "Check", "label": "Create Payment Entries" + }, + { + "fieldname": "links_tab", + "fieldtype": "Tab Break", + "label": "Links", + "show_dashboard": 1 } ], "index_web_pages_for_search": 1, - "links": [], - "modified": "2023-06-09 14:24:27.745902", + "links": [ + { + "group": "Imported Documents", + "link_doctype": "FEC Import Document", + "link_fieldname": "settings" + } + ], + "modified": "2023-06-14 13:45:32.232701", "modified_by": "Administrator", "module": "Regional", "name": "FEC Import Settings", diff --git a/erpnext/regional/doctype/fec_import_tool/fec_import_tool.js b/erpnext/regional/doctype/fec_import_tool/fec_import_tool.js index 3f3fca4f2d..229af4df02 100644 --- a/erpnext/regional/doctype/fec_import_tool/fec_import_tool.js +++ b/erpnext/regional/doctype/fec_import_tool/fec_import_tool.js @@ -21,6 +21,30 @@ frappe.ui.form.on('FEC Import Tool', { }) }) }); + + frm.add_custom_button(__("Create Journals"), function() { + frappe.call({ + method: "create_journals", + doc: frm.doc, + }).then(r => { + frappe.show_alert({ + message: __("Accounting journals created.
Please setup a correct journal type for each."), + indicator: "green" + }) + }) + }, __("Actions")); + + frm.add_custom_button(__("Create Accounts"), function() { + frappe.call({ + method: "create_accounts", + doc: frm.doc, + }).then(r => { + frappe.show_alert({ + message: __("Accounts created.
Please setup a correct account type for each."), + indicator: "green" + }) + }) + }, __("Actions")); } }, diff --git a/erpnext/regional/doctype/fec_import_tool/fec_import_tool.py b/erpnext/regional/doctype/fec_import_tool/fec_import_tool.py index 65dc795941..515277ccce 100644 --- a/erpnext/regional/doctype/fec_import_tool/fec_import_tool.py +++ b/erpnext/regional/doctype/fec_import_tool/fec_import_tool.py @@ -2,9 +2,11 @@ # For license information, please see license.txt import datetime +import hashlib from collections import defaultdict import frappe +from frappe import _ from frappe.model.document import Document from frappe.utils import flt, getdate from frappe.utils.csvutils import read_csv_content @@ -34,6 +36,15 @@ class FECImportTool(Document): @frappe.whitelist() def upload_fec(self): + data = self.get_data() + + try: + import_in_progress = FECImportDocumentCreator(settings=self, data=data) + import_in_progress.import_data() + except Exception: + print(frappe.get_traceback()) + + def get_data(self): fileid = frappe.db.get_value( "File", {"file_url": self.fec_file, "attached_to_doctype": self.doctype, "attached_to_name": self.name}, @@ -55,72 +66,142 @@ class FECImportTool(Document): output.append(row) - try: - print("START IMPORT") - import_in_progress = FECImport(settings=self, data=output) - import_in_progress.import_data() - except Exception: - print(frappe.get_traceback()) + return output + @frappe.whitelist() + def create_journals(self): + journals = {l["JournalCode"]: l["JournalLib"] for l in self.get_data()} -class FECImport: + for journal in journals: + doc = frappe.new_doc("Accounting Journal") + doc.journal_code = journal + doc.journal_name = journals[journal] + doc.insert(ignore_if_duplicate=True) + + @frappe.whitelist() + def create_accounts(self): + accounts = {l["CompteNum"]: l["CompteLib"] for l in self.get_data()} + account_groups = frappe.get_all( + "Account", + filters={ + "disabled": 0, + "is_group": 1, + "account_number": ("is", "set"), + "company": self.company, + "do_not_show_account_number": 0, + }, + fields=["name", "parent_account", "account_number"], + ) + accounts_with_number_list = [x.name for x in account_groups] + + account_groups_with_numbers = {} + for group in account_groups: + if not group.parent_account in accounts_with_number_list: + account_groups_with_numbers.update({group.account_number[:-1]: group.parent_account}) + account_groups_with_numbers.update({group.account_number: group.name}) + + for account in accounts: + if not frappe.db.exists("Account", dict(account_number=account)): + doc = frappe.new_doc("Account") + doc.account_name = accounts[account] + doc.account_number = account + doc.parent_account = self.get_parent_account(account_groups_with_numbers, account) + doc.insert(ignore_if_duplicate=True) + + def get_parent_account(self, account_groups, account): + if frappe.db.exists( + "Account", dict(account_number=str(account)[:-1], disabled=0, company=self.company) + ) and account_groups.get(account[:-1]): + return account_groups.get(account[:-1]) + + account_numbers = [key for key, value in account_groups.items()] + for idx, acc in enumerate(account): + sub_number = account[:idx] + if sub_number in account_numbers: + continue + elif account_groups.get(account[: idx - 1]): + return account_groups[account[: idx - 1]] + + root_mapping = { + "1": "Equity", + "2": "Asset", + "3": "Asset", + "4": "Liability", + "5": "Equity", + "6": "Expense", + "7": "Income", + } + + return frappe.db.get_value( + "Account", + dict( + disabled=0, + root_type=root_mapping.get(str(account)[0]), + company=self.company, + parent_account=("is", "not set"), + ), + ) + + +class FECImportDocumentCreator: def __init__(self, settings, data): self.settings = settings - self.company_settings = {} - if frappe.db.exists("FEC Import Settings", dict(company=self.settings.company)): - self.company_settings = frappe.get_doc( - "FEC Import Settings", dict(company=self.settings.company) + self.company_settings = frappe.db.get_value( + "FEC Import Settings", dict(company=self.settings.company) + ) + if not self.company_settings: + frappe.throw( + _("Please configure a FEC Import Settings document for company {0}").format( + self.settings.company + ) ) self.data = data def import_data(self): - self.get_journals_mapping() - self.parse_credit_debit() self.group_data() - self.get_accounts_data() - self.create_document() - # self.insert_transactional_documents() + self.create_fec_import_documents() def group_data(self): - initial_group = defaultdict(list) self.grouped_data = defaultdict(lambda: defaultdict(list)) - self.payment_data = defaultdict(tuple) + accounting_journals = self.get_accounting_journals_mapping() for d in self.data: - # if not frappe.db.exists("GL Entry", dict(accounting_entry_number=d.EcritureNum)): - initial_group[d["EcritureNum"]].append(frappe._dict(d)) - - for ecriturenum in initial_group: - if [line.CompAuxNum for line in initial_group[ecriturenum] if line.CompAuxNum]: - - # if not self.is_within_date_range(initial_group[ecriturenum][0]): - # continue - - journal_type = frappe.get_cached_value( - "Accounting Journal", initial_group[ecriturenum][0].JournalCode, "type" - ) - - if self.company_settings.create_sales_invoices and journal_type == "Sales": - self.grouped_data["Sales Invoice"][ecriturenum] = initial_group[ecriturenum] - elif self.company_settings.create_purchase_invoices and journal_type == "Purchase": - self.grouped_data["Purchase Invoice"][ecriturenum] = initial_group[ecriturenum] - elif self.company_settings.create_payment_entries and journal_type == "Bank": - self.grouped_data["Payment Entry"][ecriturenum] = initial_group[ecriturenum] - else: - self.grouped_data["Journal Entry"][ecriturenum] = initial_group[ecriturenum] - - else: - self.grouped_data["Journal Entry"][ecriturenum] = initial_group[ecriturenum] + if not self.is_within_date_range(d): + continue - # for a in self.grouped_data: - # print("GROUPS") - # print(a, len(self.grouped_data[a])) + if ( + self.settings.import_journal + and not accounting_journals.get(d.get("JournalCode")) == self.settings.import_journal + ): + continue - def parse_credit_debit(self): - for d in self.data: - d["Debit"] = flt(d["Debit"].replace(",", ".")) - d["Credit"] = flt(d["Credit"].replace(",", ".")) + self.grouped_data[d["EcritureDate"]][d["PieceRef"]].append(frappe._dict(d)) + + @staticmethod + def parse_credit_debit(d): + d["Debit"] = flt(d["Debit"].replace(",", ".")) + d["Credit"] = flt(d["Credit"].replace(",", ".")) + d["Montantdevise"] = flt(d["Montantdevise"].replace(",", ".")) + + def create_fec_import_documents(self): + for date in self.grouped_data: + for piece in self.grouped_data[date]: + doc = frappe.new_doc("FEC Import Document") + doc.fec_import = self.settings.name + doc.settings = self.company_settings + doc.gl_entries_date = datetime.datetime.strptime(date, "%Y%m%d").strftime("%Y-%m-%d") + doc.gl_entry_reference = piece + + for line in self.grouped_data[date][piece]: + concatenated_data = "".join([value for key, value in line.items()]) + self.parse_credit_debit(line) + row = {frappe.scrub(key): value for key, value in line.items()} + row["hashed_data"] = hash_line(concatenated_data) + doc.append("gl_entries", row) + + doc.insert() + doc.run_method("process_document_in_background") def is_within_date_range(self, line): posting_date = datetime.datetime.strptime(line.EcritureDate, "%Y%m%d").strftime("%Y-%m-%d") @@ -133,17 +214,7 @@ class FECImport: return True - def get_accounts_data(self): - self.accounts = { - x.account_number.strip(): x.name - for x in frappe.get_all( - "Account", - filters={"disabled": 0, "account_number": ("is", "set")}, - fields=["name", "account_number"], - ) - } - - def get_journals_mapping(self): + def get_accounting_journals_mapping(self): dokos_journals = { x.journal_code: x.name for x in frappe.get_all( @@ -151,312 +222,24 @@ class FECImport: ) } - self.journals = {} + company_settings = frappe.get_doc("FEC Import Settings", self.company_settings) + + journals = {} mapped_journals = dokos_journals - for mapping in self.company_settings.get("accounting_journal_mapping", []): + for mapping in company_settings.get("accounting_journal_mapping", []): for j in mapped_journals: if j == mapping.accounting_journal_in_dokos: - self.journals[mapping.accounting_journal_in_fec] = mapped_journals[j] + journals[mapping.accounting_journal_in_fec] = mapped_journals[j] continue - self.journals[j] = mapped_journals[j] - - mapped_journals = self.journals - - def create_document(self): - self.journal_entries = [] - self.sales_invoices = [] - self.purchase_invoices = [] - self.payment_entries = [] - - for ecriturenum in self.grouped_data["Sales Invoice"]: - self.create_sales_invoice(ecriturenum, self.grouped_data["Sales Invoice"][ecriturenum]) - - for ecriturenum in self.grouped_data["Purchase Invoice"]: - self.create_purchase_invoice(ecriturenum, self.grouped_data["Purchase Invoice"][ecriturenum]) - - for ecriturenum in self.grouped_data["Journal Entry"]: - self.create_journal_entry(ecriturenum, self.grouped_data["Journal Entry"][ecriturenum]) - - for ecriturenum in self.grouped_data["Payment Entry"]: - self.create_journal_entry(ecriturenum, self.grouped_data["Journal Entry"][ecriturenum]) - - print("payment_data", self.payment_data) - - def create_journal_entry(self, ecriturenum, rows): - posting_date = datetime.datetime.strptime(rows[0].EcritureDate, "%Y%m%d").strftime("%Y-%m-%d") - add_to_payment_data = None - journal_entry = frappe.get_doc( - { - "doctype": "Journal Entry", - "company": self.settings.company, - "posting_date": posting_date, - "cheque_no": ecriturenum, - "cheque_date": posting_date, - } - ) - for line in rows: - journal_code = self.journals.get(line["JournalCode"]) - - if self.settings.import_journal and self.settings.import_journal != journal_code: - continue - - reference_type, reference_name = None, None - - compte_aux = line.CompAuxNum - compte_aux_type = None - if compte_aux: - journal_type = frappe.get_cached_value("Accounting Journal", journal_code, "type") - account_type = frappe.get_cached_value( - "Account", self.accounts.get(line["CompteNum"]), "account_type" - ) - - if journal_type in ("Sales", "Bank") or account_type == "Receivable": - compte_aux = frappe.db.exists("Customer", compte_aux) - compte_aux_type = "Customer" - elif journal_type in ("Purchase", "Bank") or account_type == "Payable": - compte_aux = frappe.db.exists("Supplier", compte_aux) - compte_aux_type = "Supplier" - - if account_type in ["Receivable", "Payable"] and not (compte_aux and compte_aux_type): - if line.EcritureLet and (reference := self.payment_data.get(line.EcritureLet)): - reference_type = reference[0] - reference_name = reference[1] - - elif line.EcritureLet: - add_to_payment_data = line.EcritureLet - - if account_type in ["Receivable", "Payable"] and not (compte_aux and compte_aux_type): - if account_type == "Receivable": - compte_aux_type = "Customer" - compte_aux = self.create_customer(compte_aux, line.CompAuxLib) - else: - compte_aux_type = "Supplier" - compte_aux = self.create_supplier(compte_aux, line.CompAuxLib) - - journal_entry.append( - "accounts", - { - "accounting_journal": journal_code, - "account": self.accounts.get(line["CompteNum"]), - "debit_in_account_currency": line["Debit"], - "credit_in_account_currency": line["Credit"], - "user_remark": f"{line.EcritureLib}
{line.PieceRef}", - "reference_type": reference_type, - "reference_name": reference_name, - "party_type": compte_aux_type if compte_aux_type and compte_aux else None, - "party": compte_aux if compte_aux_type and compte_aux else None, - }, - ) - journal_entry.flags.accounting_entry_number = ecriturenum - journal_entry.flags.ecriturenum = ecriturenum - - if journal_entry.accounts: - journal_entry.insert() - journal_entry.submit() - - if add_to_payment_data: - self.payment_data[add_to_payment_data] = (journal_entry.doctype, journal_entry.name) - - def create_sales_invoice(self, ecriturenum, rows): - invoicing_details = self.get_invoicing_details(rows, "Sales Invoice") - posting_date = datetime.datetime.strptime(rows[0].EcritureDate, "%Y%m%d").strftime("%Y-%m-%d") - - sales_invoice = frappe.new_doc("Sales Invoice") - sales_invoice.flags.ignore_permissions = True - sales_invoice.flags.ecriturenum = ecriturenum - sales_invoice.update( - { - "company": self.settings.company, - "posting_date": posting_date, - "set_posting_time": 1, - "customer": invoicing_details.get("party_name"), - "debit_to": invoicing_details.get("party_account"), - "accounting_journal": self.journals.get(rows[0]["JournalCode"]), - "remarks": invoicing_details.get("party_line", {}).get("EcritureLib"), - "due_date": posting_date, - } - ) - - self.add_items(sales_invoice, invoicing_details.get("items"), self.company_settings.sales_item) - self.add_taxes(sales_invoice, invoicing_details.get("taxes")) - sales_invoice.set_missing_values() - - if sales_invoice.customer and sales_invoice.items: - # self.sales_invoices.append(sales_invoice) - sales_invoice.insert() - sales_invoice.submit() - - for ref in list(invoicing_details.get("references")): - self.payment_data[ref] = (sales_invoice.doctype, sales_invoice.name) - - else: - self.create_journal_entry(ecriturenum, rows) - - def create_purchase_invoice(self, ecriturenum, rows): - invoicing_details = self.get_invoicing_details(rows, "Purchase Invoice") - posting_date = datetime.datetime.strptime(rows[0].EcritureDate, "%Y%m%d").strftime("%Y-%m-%d") - - purchase_invoice = frappe.new_doc("Purchase Invoice") - purchase_invoice.flags.ignore_permissions = True - purchase_invoice.flags.ecriturenum = ecriturenum - purchase_invoice.update( - { - "company": self.settings.company, - "posting_date": posting_date, - "set_posting_time": 1, - "supplier": invoicing_details.get("party_name"), - "credit_to": invoicing_details.get("party_account"), - "accounting_journal": self.journals.get(rows[0]["JournalCode"]), - "remarks": invoicing_details.get("party_line", {}).get("EcritureLib"), - } - ) - - self.add_items( - purchase_invoice, invoicing_details.get("items"), self.company_settings.sales_item - ) - self.add_taxes(purchase_invoice, invoicing_details.get("taxes")) - purchase_invoice.set_missing_values() - - if purchase_invoice.supplier and purchase_invoice.items: - # self.purchase_invoices.append(purchase_invoice) - purchase_invoice.insert() - purchase_invoice.submit() - - for ref in list(invoicing_details.get("references")): - self.payment_data[ref] = (purchase_invoice.doctype, purchase_invoice.name) - - else: - self.create_journal_entry(ecriturenum, rows) - - def get_invoicing_details(self, lines, invoice_type): - invoicing_details = { - "party_account": "", - "party_name": "", - "party_line": {}, - "taxes": [], - "items": [], - "references": set(), - } - - party_type = "Supplier" if invoice_type == "Purchase Invoice" else "Customer" - - for line in lines: - if line.CompteNum.startswith("40") or line.CompteNum.startswith("41"): - if line.CompAuxNum: - invoicing_details["party_name"] = line["CompAuxNum"] - - invoicing_details["party_account"] = self.accounts.get(line["CompteNum"]) - invoicing_details["party_line"] = line - - if not invoicing_details["party_name"] or not frappe.db.exists( - party_type, invoicing_details["party_name"] - ): - invoicing_details["party_name"] = ( - self.create_supplier(line.CompAuxNum, line.CompAuxLib) - if invoice_type == "Purchase Invoice" - else self.create_customer(line.CompAuxNum, line.CompAuxLib) - ) - - elif ( - (line.CompteNum.startswith("6") and line.Debit > 0.0) - if invoice_type == "Purchase Invoice" - else (line.CompteNum.startswith("7") and line.Credit > 0.0) - ): - invoicing_details["items"].append(line) - - else: - invoicing_details["taxes"].append(line) - - if line.EcritureLet: - invoicing_details["references"].add(line.EcritureLet) - - return invoicing_details - - def add_items(self, invoice_document, item_lines, item): - for line in item_lines: - invoice_document.append( - "items", - { - "item_code": item, - "qty": 1, - "rate": line["Credit"] - if invoice_document.get("doctype") == "Sales Invoice" - else line["Debit"], - "income_account": self.accounts.get(line["CompteNum"]) - if invoice_document.get("doctype") == "Sales Invoice" - else None, - "expense_account": self.accounts.get(line["CompteNum"]) - if invoice_document.get("doctype") == "Purchase Invoice" - else None, - }, - ) - - def add_taxes(self, invoice_document, tax_lines): - for line in tax_lines: - amount = 0.0 - if invoice_document.get("doctype") == "Sales Invoice": - amount = line["Credit"] - if not amount: - amount = flt(line["Debit"]) * -1 - - elif invoice_document.get("doctype") == "Purchase Invoice": - amount = flt(line["Debit"]) - if not amount: - amount = flt(line["Credit"]) * -1 - - invoice_document.append( - "taxes", - { - "charge_type": "Actual", - "account_head": self.accounts.get(line["CompteNum"]), - "tax_amount": amount, - "description": line.EcritureLib, - }, - ) - - def create_customer(self, compauxnum, compauxlib): - customer = frappe.new_doc("Customer") - customer.__newname = compauxnum - customer.customer_name = compauxlib - customer.customer_group = self.company_settings.customer_group or frappe.db.get_single_value( - "Selling Settings", "customer_group" - ) - customer.territory = self.company_settings.territory or frappe.db.get_single_value( - "Selling Settings", "territory" - ) - customer.insert() - - return customer.name - - def create_supplier(self, compauxnum, compauxlib): - supplier = frappe.new_doc("Supplier") - supplier.__newname = compauxnum - supplier.supplier_name = compauxlib - supplier.supplier_group = self.company_settings.supplier_group or frappe.db.get_single_value( - "Buying Settings", "supplier_group" - ) - supplier.insert() - - return supplier.name - - # def insert_transactional_documents(self): - # print("journal_entries", len(self.journal_entries)) - # print("sales_invoices", len(self.sales_invoices)) - # print("purchase_invoices", len(self.purchase_invoices)) - - # self.ecriturenum_map = {} + journals[j] = mapped_journals[j] - # for journal_entry in self.journal_entries[:2]: - # journal_entry.insert() - # self.ecriturenum_map[journal_entry.flags.ecriturenum] = journal_entry.name + mapped_journals = journals - # for sales_invoice in self.sales_invoices[:2]: - # sales_invoice.insert() - # self.ecriturenum_map[sales_invoice.flags.ecriturenum] = sales_invoice.name + return journals or dokos_journals - # for purchase_invoice in self.purchase_invoices[:2]: - # purchase_invoice.insert() - # self.ecriturenum_map[purchase_invoice.flags.ecriturenum] = purchase_invoice.name - # print(self.ecriturenum_map) +def hash_line(data): + sha = hashlib.sha256() + sha.update(frappe.safe_encode(str(data))) + return sha.hexdigest() -- GitLab From f3d3ee86ecc0be944ae575dfc81450ddf535b2e1 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Wed, 14 Jun 2023 19:00:08 +0200 Subject: [PATCH 05/19] chore: retry failed entries --- .../fec_import_document.json | 12 ++- .../fec_import_document.py | 84 +++++++++++++++---- .../fec_import_settings.json | 17 +--- .../fec_import_tool/fec_import_tool.json | 10 ++- .../fec_import_tool/fec_import_tool.py | 26 +++++- 5 files changed, 116 insertions(+), 33 deletions(-) diff --git a/erpnext/regional/doctype/fec_import_document/fec_import_document.json b/erpnext/regional/doctype/fec_import_document/fec_import_document.json index 8b9a5232eb..f988f8ee92 100644 --- a/erpnext/regional/doctype/fec_import_document/fec_import_document.json +++ b/erpnext/regional/doctype/fec_import_document/fec_import_document.json @@ -9,6 +9,7 @@ "field_order": [ "fec_import", "status", + "import_type", "error", "column_break_4", "settings", @@ -125,11 +126,20 @@ "fieldtype": "Dynamic Link", "label": "Linked Document", "options": "linked_document_type" + }, + { + "default": "Miscellaneous", + "fieldname": "import_type", + "fieldtype": "Select", + "hidden": 1, + "label": "Import Type", + "options": "Miscellaneous\nTransaction\nPayment", + "reqd": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-06-14 12:03:06.586896", + "modified": "2023-06-14 17:30:17.701398", "modified_by": "Administrator", "module": "Regional", "name": "FEC Import Document", diff --git a/erpnext/regional/doctype/fec_import_document/fec_import_document.py b/erpnext/regional/doctype/fec_import_document/fec_import_document.py index 439fdae76c..de8ccdb672 100644 --- a/erpnext/regional/doctype/fec_import_document/fec_import_document.py +++ b/erpnext/regional/doctype/fec_import_document/fec_import_document.py @@ -7,10 +7,15 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import flt, get_year_ending, get_year_start, getdate +from tenacity import retry, retry_if_result, stop_after_attempt from erpnext.accounts.utils import FiscalYearError, get_fiscal_years +def value_is_true(value): + return value is True + + class FECImportDocument(Document): def validate(self): if self.status != "Completed": @@ -20,6 +25,17 @@ class FECImportDocument(Document): self.get_party(row) self.parse_dates(row) + def before_insert(self): + self.set_import_type() + + def set_import_type(self): + if self.is_payment_entry(): + self.import_type = "Payment" + elif [l.party for l in self.gl_entries if l.party]: + self.import_type = "Transaction" + else: + self.import_type = "Miscellaneous" + def check_fiscal_year(self): try: posting_date = getdate(self.gl_entries_date) @@ -167,38 +183,41 @@ class FECImportDocument(Document): return self.create_references() - def process_document_in_background(self): - frappe.enqueue_doc(self.doctype, self.name, "create_references", timeout=1000) + def process_document_in_background(self, defer_payments=False): + frappe.enqueue_doc( + self.doctype, self.name, "create_references", defer_payments=defer_payments, timeout=1000 + ) - def create_references(self): + @retry(stop=stop_after_attempt(5), retry=retry_if_result(value_is_true)) + def create_references(self, defer_payments=False): self.db_set("status", "Pending") self.db_set("error", None) try: self.check_fiscal_year() + company_settings = frappe.get_doc("FEC Import Settings", self.settings) party = [l.party for l in self.gl_entries if l.party] party_type = [l.party_type for l in self.gl_entries if l.party_type] if len(party) == 1 and len(party_type) == 1: - if not [ - l.account - for l in self.gl_entries - if frappe.get_cached_value("Account", l.account, "account_type") in ["Bank", "Cash"] - ]: - if party_type[0] == "Customer": + if not self.is_payment_entry(): + if company_settings.create_sales_invoices and party_type[0] == "Customer": self.create_sales_invoice() - elif party_type[0] == "Supplier": + elif company_settings.create_sales_invoices and party_type[0] == "Supplier": self.create_purchase_invoice() else: self.create_journal_entry() else: - self.create_journal_entry() + self.create_journal_entry(True if defer_payments else False) else: self.create_journal_entry() except Exception: self.db_set("status", "Error") self.db_set("error", frappe.get_traceback()) - def create_journal_entry(self): + if defer_payments: + return True + + def create_journal_entry(self, payment_entry=False): journal_entry = frappe.get_doc( { "doctype": "Journal Entry", @@ -210,6 +229,7 @@ class FECImportDocument(Document): ) for line in self.gl_entries: self.check_account_is_not_a_group(line.account) + reference_type, reference_name = self.get_payment_references(line) journal_entry.append( "accounts", { @@ -220,14 +240,20 @@ class FECImportDocument(Document): "credit_in_account_currency": line.credit, "credit": line.credit, "user_remark": f"{line.ecriturelib}
{line.pieceref}", - # "reference_type": reference_type, - # "reference_name": reference_name, + "reference_type": reference_type, + "reference_name": reference_name, "party_type": line.party_type, "party": line.party, }, ) + if payment_entry and not reference_type and not reference_name: + frappe.throw(_("Payment references could not be found")) + if journal_entry.accounts: + if self.gl_entry_reference: + journal_entry.name = self.gl_entry_reference + journal_entry.flags.draft_name_set = True journal_entry.insert() journal_entry.submit() @@ -235,6 +261,36 @@ class FECImportDocument(Document): self.db_set("linked_document", journal_entry.name) self.db_set("status", "Completed") + def is_payment_entry(self): + return [line.ecriturelet for line in self.gl_entries if line.ecriturelet] and [ + l.account + for l in self.gl_entries + if frappe.get_cached_value("Account", l.account, "account_type") in ["Bank", "Cash"] + ] + + def get_payment_references(self, line): + reference_type, reference_name = None, None + if line.ecriturelet: + filters = dict( + name=("!=", line.name), + ecriturelet=line.ecriturelet, + comptenum=line.comptenum, + compauxnum=line.compauxnum, + datelet=line.datelet, + ) + + if flt(line.credit) > 0.0: + filters["debit"] = (">", 0.0) + else: + filters["credit"] = (">", 0.0) + + for doc in frappe.get_all("FEC Import Line", filters=filters, pluck="parent"): + reference_type, reference_name = frappe.db.get_value( + "FEC Import Document", doc, ["linked_document_type", "linked_document"] + ) + + return reference_type, reference_name + def create_sales_invoice(self): customer, debit_to, remark = self.get_party_and_party_account() diff --git a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json index c781593b2b..1afacd895a 100644 --- a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json +++ b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json @@ -20,9 +20,7 @@ "purchase_invoices_tab", "create_purchase_invoices", "purchase_item", - "supplier_group", - "payment_entries_tab", - "create_payment_entries" + "supplier_group" ], "fields": [ { @@ -107,17 +105,6 @@ "mandatory_depends_on": "eval:doc.create_sales_invoices", "options": "Territory" }, - { - "fieldname": "payment_entries_tab", - "fieldtype": "Tab Break", - "label": "Payment Entries" - }, - { - "default": "0", - "fieldname": "create_payment_entries", - "fieldtype": "Check", - "label": "Create Payment Entries" - }, { "fieldname": "links_tab", "fieldtype": "Tab Break", @@ -133,7 +120,7 @@ "link_fieldname": "settings" } ], - "modified": "2023-06-14 13:45:32.232701", + "modified": "2023-06-14 16:53:32.807483", "modified_by": "Administrator", "module": "Regional", "name": "FEC Import Settings", diff --git a/erpnext/regional/doctype/fec_import_tool/fec_import_tool.json b/erpnext/regional/doctype/fec_import_tool/fec_import_tool.json index 1d6d779011..f30270f1b9 100644 --- a/erpnext/regional/doctype/fec_import_tool/fec_import_tool.json +++ b/erpnext/regional/doctype/fec_import_tool/fec_import_tool.json @@ -84,8 +84,14 @@ } ], "index_web_pages_for_search": 1, - "links": [], - "modified": "2023-06-09 09:44:56.202663", + "links": [ + { + "group": "Import Details", + "link_doctype": "FEC Import Document", + "link_fieldname": "fec_import" + } + ], + "modified": "2023-06-14 16:48:28.995377", "modified_by": "Administrator", "module": "Regional", "name": "FEC Import Tool", diff --git a/erpnext/regional/doctype/fec_import_tool/fec_import_tool.py b/erpnext/regional/doctype/fec_import_tool/fec_import_tool.py index 515277ccce..473255d1e1 100644 --- a/erpnext/regional/doctype/fec_import_tool/fec_import_tool.py +++ b/erpnext/regional/doctype/fec_import_tool/fec_import_tool.py @@ -161,6 +161,7 @@ class FECImportDocumentCreator: def import_data(self): self.group_data() self.create_fec_import_documents() + self.process_fec_import_documents() def group_data(self): self.grouped_data = defaultdict(lambda: defaultdict(list)) @@ -187,6 +188,7 @@ class FECImportDocumentCreator: def create_fec_import_documents(self): for date in self.grouped_data: for piece in self.grouped_data[date]: + iter_next = False doc = frappe.new_doc("FEC Import Document") doc.fec_import = self.settings.name doc.settings = self.company_settings @@ -198,10 +200,32 @@ class FECImportDocumentCreator: self.parse_credit_debit(line) row = {frappe.scrub(key): value for key, value in line.items()} row["hashed_data"] = hash_line(concatenated_data) + + if frappe.db.exists("FEC Import Line", dict(hashed_data=row["hashed_data"])): + iter_next = True + break + doc.append("gl_entries", row) + if iter_next: + continue + doc.insert() - doc.run_method("process_document_in_background") + + def process_fec_import_documents(self): + groups = {"Transaction": [], "Payment": [], "Miscellaneous": []} + + for doc in frappe.get_all( + "FEC Import Document", + filters={"status": "Pending"}, + fields=["name", "import_type"], + order_by="gl_entries_date", + ): + groups[doc.import_type].append(doc.name) + + for group in ["Transaction", "Miscellaneous", "Payment"]: + for d in groups[group]: + frappe.get_doc("FEC Import Document", d).run_method("process_document_in_background") def is_within_date_range(self, line): posting_date = datetime.datetime.strptime(line.EcritureDate, "%Y%m%d").strftime("%Y-%m-%d") -- GitLab From a9097db781c3ef3473270417892c5377b0e89453 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Wed, 14 Jun 2023 21:25:06 +0200 Subject: [PATCH 06/19] feat: handle bytes order mark in data --- erpnext/regional/doctype/fec_import_tool/fec_import_tool.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/regional/doctype/fec_import_tool/fec_import_tool.py b/erpnext/regional/doctype/fec_import_tool/fec_import_tool.py index 473255d1e1..b3be2fe29f 100644 --- a/erpnext/regional/doctype/fec_import_tool/fec_import_tool.py +++ b/erpnext/regional/doctype/fec_import_tool/fec_import_tool.py @@ -1,6 +1,7 @@ # Copyright (c) 2023, Dokos SAS and contributors # For license information, please see license.txt +import codecs import datetime import hashlib from collections import defaultdict @@ -62,6 +63,9 @@ class FECImportTool(Document): row = frappe._dict() for count, head in enumerate(header): if head: + if head.__contains__(codecs.BOM_UTF8.decode("utf-8")): + # A Byte Order Mark is present + head = head.strip(codecs.BOM_UTF8.decode("utf-8")) row[head] = d[count] or "" output.append(row) -- GitLab From cc65dff8986d85f38535a73df3bb3ca8ea3cf75f Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Wed, 14 Jun 2023 21:25:33 +0200 Subject: [PATCH 07/19] feat: Add options for submitting documents --- .../fec_import_document.py | 22 +++++++------ .../fec_import_settings.json | 31 ++++++++++++++++++- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/erpnext/regional/doctype/fec_import_document/fec_import_document.py b/erpnext/regional/doctype/fec_import_document/fec_import_document.py index de8ccdb672..b15974287e 100644 --- a/erpnext/regional/doctype/fec_import_document/fec_import_document.py +++ b/erpnext/regional/doctype/fec_import_document/fec_import_document.py @@ -255,7 +255,9 @@ class FECImportDocument(Document): journal_entry.name = self.gl_entry_reference journal_entry.flags.draft_name_set = True journal_entry.insert() - journal_entry.submit() + + if frappe.db.get_value("FEC Import Settings", self.settings, "submit_journal_entries"): + journal_entry.submit() self.db_set("linked_document_type", "Journal Entry") self.db_set("linked_document", journal_entry.name) @@ -321,11 +323,12 @@ class FECImportDocument(Document): sales_invoice.flags.draft_name_set = True sales_invoice.insert() - if self.gl_entry_reference: - sales_invoice.flags.name_set = True + if frappe.db.get_value("FEC Import Settings", self.settings, "submit_sales_invoices"): + if self.gl_entry_reference: + sales_invoice.flags.name_set = True - sales_invoice.flags.ignore_version = True - sales_invoice.submit() + sales_invoice.flags.ignore_version = True + sales_invoice.submit() self.db_set("linked_document_type", "Sales Invoice") self.db_set("linked_document", sales_invoice.name) @@ -368,11 +371,12 @@ class FECImportDocument(Document): purchase_invoice.flags.draft_name_set = True purchase_invoice.insert() - if self.gl_entry_reference: - purchase_invoice.flags.name_set = True + if frappe.db.get_value("FEC Import Settings", self.settings, "submit_sales_invoices"): + if self.gl_entry_reference: + purchase_invoice.flags.name_set = True - purchase_invoice.flags.ignore_version = True - purchase_invoice.submit() + purchase_invoice.flags.ignore_version = True + purchase_invoice.submit() self.db_set("linked_document_type", "Purchase Invoice") self.db_set("linked_document", purchase_invoice.name) diff --git a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json index 1afacd895a..6e2b1b6436 100644 --- a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json +++ b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json @@ -12,13 +12,17 @@ "section_break_2", "company", "accounting_journal_mapping", + "journal_entries_tab", + "submit_journal_entries", "sales_invoices_tab", "create_sales_invoices", + "submit_sales_invoices", "sales_item", "customer_group", "territory", "purchase_invoices_tab", "create_purchase_invoices", + "submit_purchase_invoices", "purchase_item", "supplier_group" ], @@ -110,6 +114,31 @@ "fieldtype": "Tab Break", "label": "Links", "show_dashboard": 1 + }, + { + "default": "1", + "depends_on": "eval:doc.create_sales_invoices", + "fieldname": "submit_sales_invoices", + "fieldtype": "Check", + "label": "Automatically Submit Sales Invoices" + }, + { + "default": "1", + "depends_on": "eval:doc.create_purchase_invoices", + "fieldname": "submit_purchase_invoices", + "fieldtype": "Check", + "label": "Automatically Submit Purchase Invoices" + }, + { + "fieldname": "journal_entries_tab", + "fieldtype": "Tab Break", + "label": "Journal Entries" + }, + { + "default": "1", + "fieldname": "submit_journal_entries", + "fieldtype": "Check", + "label": "Automatically Submit Journal Entries" } ], "index_web_pages_for_search": 1, @@ -120,7 +149,7 @@ "link_fieldname": "settings" } ], - "modified": "2023-06-14 16:53:32.807483", + "modified": "2023-06-14 21:20:02.438054", "modified_by": "Administrator", "module": "Regional", "name": "FEC Import Settings", -- GitLab From 3e6724cb2288e4322d4fc655977a1482ecfb9577 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Wed, 14 Jun 2023 21:34:53 +0200 Subject: [PATCH 08/19] refactor: Rename FEC Import Tool to FEC Import --- .../doctype/{fec_import_tool => fec_import}/__init__.py | 0 .../fec_import_tool.js => fec_import/fec_import.js} | 2 +- .../fec_import_tool.json => fec_import/fec_import.json} | 2 +- .../fec_import_tool.py => fec_import/fec_import.py} | 2 +- .../test_fec_import_tool.py => fec_import/test_fec_import.py} | 2 +- .../doctype/fec_import_document/fec_import_document.json | 4 ++-- 6 files changed, 6 insertions(+), 6 deletions(-) rename erpnext/regional/doctype/{fec_import_tool => fec_import}/__init__.py (100%) rename erpnext/regional/doctype/{fec_import_tool/fec_import_tool.js => fec_import/fec_import.js} (97%) rename erpnext/regional/doctype/{fec_import_tool/fec_import_tool.json => fec_import/fec_import.json} (98%) rename erpnext/regional/doctype/{fec_import_tool/fec_import_tool.py => fec_import/fec_import.py} (99%) rename erpnext/regional/doctype/{fec_import_tool/test_fec_import_tool.py => fec_import/test_fec_import.py} (80%) diff --git a/erpnext/regional/doctype/fec_import_tool/__init__.py b/erpnext/regional/doctype/fec_import/__init__.py similarity index 100% rename from erpnext/regional/doctype/fec_import_tool/__init__.py rename to erpnext/regional/doctype/fec_import/__init__.py diff --git a/erpnext/regional/doctype/fec_import_tool/fec_import_tool.js b/erpnext/regional/doctype/fec_import/fec_import.js similarity index 97% rename from erpnext/regional/doctype/fec_import_tool/fec_import_tool.js rename to erpnext/regional/doctype/fec_import/fec_import.js index 229af4df02..16cf9752a4 100644 --- a/erpnext/regional/doctype/fec_import_tool/fec_import_tool.js +++ b/erpnext/regional/doctype/fec_import/fec_import.js @@ -1,7 +1,7 @@ // Copyright (c) 2023, Dokos SAS and contributors // For license information, please see license.txt -frappe.ui.form.on('FEC Import Tool', { +frappe.ui.form.on('FEC Import', { refresh(frm) { frm.get_field("fec_file").df.options = { restrictions: { diff --git a/erpnext/regional/doctype/fec_import_tool/fec_import_tool.json b/erpnext/regional/doctype/fec_import/fec_import.json similarity index 98% rename from erpnext/regional/doctype/fec_import_tool/fec_import_tool.json rename to erpnext/regional/doctype/fec_import/fec_import.json index f30270f1b9..34053160ba 100644 --- a/erpnext/regional/doctype/fec_import_tool/fec_import_tool.json +++ b/erpnext/regional/doctype/fec_import/fec_import.json @@ -94,7 +94,7 @@ "modified": "2023-06-14 16:48:28.995377", "modified_by": "Administrator", "module": "Regional", - "name": "FEC Import Tool", + "name": "FEC Import", "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ diff --git a/erpnext/regional/doctype/fec_import_tool/fec_import_tool.py b/erpnext/regional/doctype/fec_import/fec_import.py similarity index 99% rename from erpnext/regional/doctype/fec_import_tool/fec_import_tool.py rename to erpnext/regional/doctype/fec_import/fec_import.py index b3be2fe29f..f17a6e10d9 100644 --- a/erpnext/regional/doctype/fec_import_tool/fec_import_tool.py +++ b/erpnext/regional/doctype/fec_import/fec_import.py @@ -13,7 +13,7 @@ from frappe.utils import flt, getdate from frappe.utils.csvutils import read_csv_content -class FECImportTool(Document): +class FECImport(Document): @frappe.whitelist() def get_company(self): company = None diff --git a/erpnext/regional/doctype/fec_import_tool/test_fec_import_tool.py b/erpnext/regional/doctype/fec_import/test_fec_import.py similarity index 80% rename from erpnext/regional/doctype/fec_import_tool/test_fec_import_tool.py rename to erpnext/regional/doctype/fec_import/test_fec_import.py index 1633eea4f8..453ac4a29d 100644 --- a/erpnext/regional/doctype/fec_import_tool/test_fec_import_tool.py +++ b/erpnext/regional/doctype/fec_import/test_fec_import.py @@ -5,5 +5,5 @@ from frappe.tests.utils import FrappeTestCase -class TestFECImportTool(FrappeTestCase): +class TestFECImport(FrappeTestCase): pass diff --git a/erpnext/regional/doctype/fec_import_document/fec_import_document.json b/erpnext/regional/doctype/fec_import_document/fec_import_document.json index f988f8ee92..e99b139530 100644 --- a/erpnext/regional/doctype/fec_import_document/fec_import_document.json +++ b/erpnext/regional/doctype/fec_import_document/fec_import_document.json @@ -32,7 +32,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "FEC Import", - "options": "FEC Import Tool", + "options": "FEC Import", "reqd": 1 }, { @@ -139,7 +139,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-06-14 17:30:17.701398", + "modified": "2023-06-14 21:33:17.387750", "modified_by": "Administrator", "module": "Regional", "name": "FEC Import Document", -- GitLab From b91fd50b5bf053c7841ebf83e0eb6cda5e3649ed Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Wed, 21 Jun 2023 16:40:43 +0200 Subject: [PATCH 09/19] fix: Import document UX + small fixes --- .../regional/doctype/fec_import/fec_import.js | 10 +++- .../fec_import_document.js | 4 +- .../fec_import_document.json | 8 ++- .../fec_import_document.py | 2 + .../fec_import_line/fec_import_line.json | 60 ++++++++++++------- .../fec_import_settings.js | 8 ++- 6 files changed, 62 insertions(+), 30 deletions(-) diff --git a/erpnext/regional/doctype/fec_import/fec_import.js b/erpnext/regional/doctype/fec_import/fec_import.js index 16cf9752a4..f9517e7594 100644 --- a/erpnext/regional/doctype/fec_import/fec_import.js +++ b/erpnext/regional/doctype/fec_import/fec_import.js @@ -5,7 +5,7 @@ frappe.ui.form.on('FEC Import', { refresh(frm) { frm.get_field("fec_file").df.options = { restrictions: { - allowed_file_types: [".txt"], + allowed_file_types: [".txt", ".csv"], }, }; @@ -45,6 +45,14 @@ frappe.ui.form.on('FEC Import', { }) }) }, __("Actions")); + + frm.add_custom_button(__("View Journals"), function() { + frappe.set_route("List", "Accounting Journal") + }, __("View")); + + frm.add_custom_button(__("View Accounts"), function() { + frappe.set_route("Tree", "Account") + }, __("View")); } }, diff --git a/erpnext/regional/doctype/fec_import_document/fec_import_document.js b/erpnext/regional/doctype/fec_import_document/fec_import_document.js index f7b1c75a3f..7c506c1653 100644 --- a/erpnext/regional/doctype/fec_import_document/fec_import_document.js +++ b/erpnext/regional/doctype/fec_import_document/fec_import_document.js @@ -4,7 +4,7 @@ frappe.ui.form.on('FEC Import Document', { refresh: function(frm) { if (frm.doc.status != "Completed") { - frm.add_custom_button(`${__("Retry an integration")}`, () => + frm.add_custom_button(`${__("Retry an integration")}`, () => { frappe.call({ method: "create_linked_document", doc: frm.doc @@ -16,7 +16,7 @@ frappe.ui.form.on('FEC Import Document', { frm.refresh() }) - ); + }); } } }); diff --git a/erpnext/regional/doctype/fec_import_document/fec_import_document.json b/erpnext/regional/doctype/fec_import_document/fec_import_document.json index e99b139530..4f31618b71 100644 --- a/erpnext/regional/doctype/fec_import_document/fec_import_document.json +++ b/erpnext/regional/doctype/fec_import_document/fec_import_document.json @@ -81,7 +81,8 @@ "fieldtype": "Date", "in_list_view": 1, "in_standard_filter": 1, - "label": "GL Entries Date" + "label": "GL Entries Date", + "read_only": 1 }, { "fieldname": "column_break_8", @@ -92,7 +93,8 @@ "fieldtype": "Data", "in_list_view": 1, "in_standard_filter": 1, - "label": "GL Entry Reference" + "label": "GL Entry Reference", + "read_only": 1 }, { "fieldname": "section_break_10", @@ -139,7 +141,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-06-14 21:33:17.387750", + "modified": "2023-06-21 16:33:02.991895", "modified_by": "Administrator", "module": "Regional", "name": "FEC Import Document", diff --git a/erpnext/regional/doctype/fec_import_document/fec_import_document.py b/erpnext/regional/doctype/fec_import_document/fec_import_document.py index b15974287e..6a947ef1ca 100644 --- a/erpnext/regional/doctype/fec_import_document/fec_import_document.py +++ b/erpnext/regional/doctype/fec_import_document/fec_import_document.py @@ -171,6 +171,8 @@ class FECImportDocument(Document): @frappe.whitelist() def create_linked_document(self): + self.run_method("validate") + fields = ["accounting_journal", "account"] for line in self.gl_entries: for field in fields: diff --git a/erpnext/regional/doctype/fec_import_line/fec_import_line.json b/erpnext/regional/doctype/fec_import_line/fec_import_line.json index bfd76003a8..6c66431d5e 100644 --- a/erpnext/regional/doctype/fec_import_line/fec_import_line.json +++ b/erpnext/regional/doctype/fec_import_line/fec_import_line.json @@ -47,96 +47,114 @@ "fieldname": "journalcode", "fieldtype": "Data", "in_list_view": 1, - "label": "JournalCode" + "label": "JournalCode", + "read_only": 1 }, { "fieldname": "journallib", "fieldtype": "Small Text", - "label": "JournalLib" + "label": "JournalLib", + "read_only": 1 }, { "fieldname": "ecriturenum", "fieldtype": "Data", - "label": "EcritureNum" + "label": "EcritureNum", + "read_only": 1 }, { "fieldname": "ecrituredate", "fieldtype": "Data", - "label": "EcritureDate" + "label": "EcritureDate", + "read_only": 1 }, { "fieldname": "comptenum", "fieldtype": "Data", "in_list_view": 1, - "label": "CompteNum" + "label": "CompteNum", + "read_only": 1 }, { "fieldname": "comptelib", "fieldtype": "Small Text", - "label": "CompteLib" + "label": "CompteLib", + "read_only": 1 }, { "fieldname": "compauxnum", "fieldtype": "Data", "in_list_view": 1, - "label": "CompAuxNum" + "label": "CompAuxNum", + "read_only": 1 }, { "fieldname": "compauxlib", "fieldtype": "Small Text", - "label": "CompAuxLib" + "label": "CompAuxLib", + "read_only": 1 }, { "fieldname": "pieceref", "fieldtype": "Data", - "label": "PieceRef" + "label": "PieceRef", + "read_only": 1 }, { "fieldname": "piecedate", "fieldtype": "Data", - "label": "PieceDate" + "label": "PieceDate", + "read_only": 1 }, { "fieldname": "ecriturelib", "fieldtype": "Small Text", - "label": "EcritureLib" + "label": "EcritureLib", + "read_only": 1 }, { "fieldname": "debit", "fieldtype": "Float", "in_list_view": 1, - "label": "Debit" + "label": "Debit", + "read_only": 1 }, { "fieldname": "credit", "fieldtype": "Float", "in_list_view": 1, - "label": "Credit" + "label": "Credit", + "read_only": 1 }, { "fieldname": "ecriturelet", "fieldtype": "Data", - "label": "EcritureLet" + "label": "EcritureLet", + "read_only": 1 }, { "fieldname": "datelet", "fieldtype": "Data", - "label": "DateLet" + "label": "DateLet", + "read_only": 1 }, { "fieldname": "validdate", "fieldtype": "Data", - "label": "ValidDate" + "label": "ValidDate", + "read_only": 1 }, { "fieldname": "montantdevise", "fieldtype": "Float", - "label": "Montantdevise" + "label": "Montantdevise", + "read_only": 1 }, { "fieldname": "idevise", "fieldtype": "Data", - "label": "Idevise" + "label": "Idevise", + "read_only": 1 }, { "fieldname": "fec_data_section", @@ -210,12 +228,12 @@ }, { "fieldname": "validation_date", - "fieldtype": "Data", + "fieldtype": "Date", "label": "Validation Date" }, { "fieldname": "reconciliation_date", - "fieldtype": "Data", + "fieldtype": "Date", "label": "Reconciliation Date" }, { @@ -228,7 +246,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-06-14 10:54:03.274088", + "modified": "2023-06-21 16:32:37.859603", "modified_by": "Administrator", "module": "Regional", "name": "FEC Import Line", diff --git a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.js b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.js index d34988bdd0..85ba4dcf96 100644 --- a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.js +++ b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.js @@ -2,7 +2,9 @@ // For license information, please see license.txt frappe.ui.form.on('FEC Import Settings', { - // refresh: function(frm) { - - // } + refresh: function(frm) { + frm.add_custom_button(__("Import a FEC"), () => { + frappe.new_doc('FEC Import', {company: frm.doc.company}); + }) + } }); -- GitLab From a4335a9c8ca4cb2c3fefb606fb7a3df082e85648 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Thu, 22 Jun 2023 08:02:13 +0200 Subject: [PATCH 10/19] fix: Improve UX --- .../doctype/journal_entry/journal_entry.py | 4 +- .../regional/doctype/fec_import/fec_import.js | 55 +++++++++++++---- .../doctype/fec_import/fec_import.json | 8 ++- .../regional/doctype/fec_import/fec_import.py | 61 ++++++++++++++++++- .../fec_import_document.py | 10 ++- .../fec_import_settings.js | 8 ++- erpnext/translations/fr.csv | 4 +- 7 files changed, 129 insertions(+), 21 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index c2f4dfcf5e..4d7011f98c 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -485,13 +485,13 @@ class JournalEntry(AccountsController): if account_root_type == "Asset" and flt(d.debit) > 0: frappe.throw( _( - "Row #{0}: For {1}, you can select reference document only if account gets credited" + "Row #{0}: For {1}, you can select a reference document only if account gets credited" ).format(d.idx, d.account) ) elif account_root_type == "Liability" and flt(d.credit) > 0: frappe.throw( _( - "Row #{0}: For {1}, you can select reference document only if account gets debited" + "Row #{0}: For {1}, you can select a reference document only if account gets debited" ).format(d.idx, d.account) ) diff --git a/erpnext/regional/doctype/fec_import/fec_import.js b/erpnext/regional/doctype/fec_import/fec_import.js index f9517e7594..f89d7ff242 100644 --- a/erpnext/regional/doctype/fec_import/fec_import.js +++ b/erpnext/regional/doctype/fec_import/fec_import.js @@ -9,6 +9,11 @@ frappe.ui.form.on('FEC Import', { }, }; + frm.trigger("add_description"); + frm.trigger("add_actions"); + }, + + add_actions(frm) { if (frm.doc.fec_file) { frm.page.set_primary_action(__("Import FEC"), function() { frappe.call({ @@ -22,40 +27,40 @@ frappe.ui.form.on('FEC Import', { }) }); - frm.add_custom_button(__("Create Journals"), function() { + frm.add_custom_button(__("Create Accounts"), function() { frappe.call({ - method: "create_journals", + method: "create_accounts", doc: frm.doc, }).then(r => { frappe.show_alert({ - message: __("Accounting journals created.
Please setup a correct journal type for each."), + message: __("Accounts created.
Please setup a correct account type for each."), indicator: "green" }) }) }, __("Actions")); - frm.add_custom_button(__("Create Accounts"), function() { + frm.add_custom_button(__("Create Journals"), function() { frappe.call({ - method: "create_accounts", + method: "create_journals", doc: frm.doc, }).then(r => { frappe.show_alert({ - message: __("Accounts created.
Please setup a correct account type for each."), + message: __("Accounting journals created.
Please setup a correct journal type for each."), indicator: "green" }) }) }, __("Actions")); - frm.add_custom_button(__("View Journals"), function() { - frappe.set_route("List", "Accounting Journal") - }, __("View")); - frm.add_custom_button(__("View Accounts"), function() { frappe.set_route("Tree", "Account") }, __("View")); - } + frm.add_custom_button(__("View Journals"), function() { + frappe.set_route("List", "Accounting Journal") + }, __("View")); + } }, + fec_file(frm) { if (frm.doc.fec_file) { frm.call({ @@ -66,4 +71,32 @@ frappe.ui.form.on('FEC Import', { }) } }, + + add_description(frm) { + let description = __("1. Import your FEC file") + if (!frm.is_new()) { + description += "
" + description += __("2. Import your accounts.") + description += "
" + description += "" + description += __("This operation will not erase your existing accounts. Dokos will try to append them to their corresponding parents in the tree.") + description += "" + description += "
" + description += __("3. Configure your accounts properly by adding the correct account type, especially for receivable and payable accounts (Classe 4).") + description += "
" + description += __("4. Import your accounting journals.") + description += "
" + description += "" + description += __("This operation will not erase your existing accounting journals.") + description += "" + + description += "
" + description += __("5. Configure your journals properly, especially the Bank and Cash journals.") + } + + description += "
" + + frm.get_field("description").$wrapper.html(description) + frm.get_field("description").$wrapper.addClass("mb-4") + } }); diff --git a/erpnext/regional/doctype/fec_import/fec_import.json b/erpnext/regional/doctype/fec_import/fec_import.json index 34053160ba..9e7151fcf2 100644 --- a/erpnext/regional/doctype/fec_import/fec_import.json +++ b/erpnext/regional/doctype/fec_import/fec_import.json @@ -10,6 +10,7 @@ "engine": "InnoDB", "field_order": [ "naming_series", + "description", "fec_file", "section_break_3", "company", @@ -81,6 +82,11 @@ "fieldtype": "Link", "label": "Import From Journal", "options": "Accounting Journal" + }, + { + "fieldname": "description", + "fieldtype": "HTML", + "label": "Description" } ], "index_web_pages_for_search": 1, @@ -91,7 +97,7 @@ "link_fieldname": "fec_import" } ], - "modified": "2023-06-14 16:48:28.995377", + "modified": "2023-06-21 16:48:13.545416", "modified_by": "Administrator", "module": "Regional", "name": "FEC Import", diff --git a/erpnext/regional/doctype/fec_import/fec_import.py b/erpnext/regional/doctype/fec_import/fec_import.py index f17a6e10d9..037fe65c3e 100644 --- a/erpnext/regional/doctype/fec_import/fec_import.py +++ b/erpnext/regional/doctype/fec_import/fec_import.py @@ -9,7 +9,7 @@ from collections import defaultdict import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import flt, getdate +from frappe.utils import cint, flt, getdate from frappe.utils.csvutils import read_csv_content @@ -75,11 +75,53 @@ class FECImport(Document): @frappe.whitelist() def create_journals(self): journals = {l["JournalCode"]: l["JournalLib"] for l in self.get_data()} + bank_journals = list( + {l["JournalCode"] for l in self.get_data() if l["CompteNum"].startswith("512")} + ) + cash_journals = list( + {l["JournalCode"] for l in self.get_data() if l["CompteNum"].startswith("53")} + ) + sales_journals = list( + {l["JournalCode"] for l in self.get_data() if l["CompteNum"].startswith("7")} + ) + purchase_journals = list( + {l["JournalCode"] for l in self.get_data() if l["CompteNum"].startswith("6")} + ) for journal in journals: + journal_type = "Miscellaneous" + account = None + + if journal in bank_journals: + journal_type = "Bank" + account_number = list( + { + l["CompteNum"] + for l in self.get_data() + if l["JournalCode"] == journal and l["CompteNum"].startswith("512") + } + )[0] + account = frappe.get_value("Account", dict(account_number=account_number)) + elif journal in cash_journals: + journal_type = "Cash" + account_number = list( + { + l["CompteNum"] + for l in self.get_data() + if l["JournalCode"] == journal and l["CompteNum"].startswith("53") + } + )[0] + account = frappe.get_value("Account", dict(account_number=account_number)) + elif journal in sales_journals: + journal_type = "Sales" + elif journal in purchase_journals: + journal_type = "Purchase" + doc = frappe.new_doc("Accounting Journal") doc.journal_code = journal doc.journal_name = journals[journal] + doc.type = journal_type + doc.account = account doc.insert(ignore_if_duplicate=True) @frappe.whitelist() @@ -110,6 +152,7 @@ class FECImport(Document): doc.account_name = accounts[account] doc.account_number = account doc.parent_account = self.get_parent_account(account_groups_with_numbers, account) + doc.account_type = self.get_account_type(account) doc.insert(ignore_if_duplicate=True) def get_parent_account(self, account_groups, account): @@ -146,6 +189,22 @@ class FECImport(Document): ), ) + def get_account_type(self, account_number): + if cint(account_number[:3]) in range(400, 409): + return "Payable" + + elif cint(account_number[:3]) in range(410, 419): + return "Receivable" + + elif cint(account_number[:3]) == 512: + return "Bank" + + elif cint(account_number[:2]) == 53: + return "Cash" + + elif cint(account_number[:2]) == 10: + return "Equity" + class FECImportDocumentCreator: def __init__(self, settings, data): diff --git a/erpnext/regional/doctype/fec_import_document/fec_import_document.py b/erpnext/regional/doctype/fec_import_document/fec_import_document.py index 6a947ef1ca..8cc2605e33 100644 --- a/erpnext/regional/doctype/fec_import_document/fec_import_document.py +++ b/erpnext/regional/doctype/fec_import_document/fec_import_document.py @@ -225,7 +225,7 @@ class FECImportDocument(Document): "doctype": "Journal Entry", "company": self.company, "posting_date": self.gl_entries_date, - "cheque_no": self.gl_entry_reference, + "cheque_no": self.gl_entry_reference or "N/A", "cheque_date": self.gl_entries_date, } ) @@ -275,6 +275,12 @@ class FECImportDocument(Document): def get_payment_references(self, line): reference_type, reference_name = None, None if line.ecriturelet: + account_root_type = frappe.get_cached_value("Account", line.account, "root_type") + if account_root_type == "Asset" and flt(line.debit) > 0: + return None, None + elif account_root_type == "Liability" and flt(line.credit) > 0: + return None, None + filters = dict( name=("!=", line.name), ecriturelet=line.ecriturelet, @@ -293,6 +299,8 @@ class FECImportDocument(Document): "FEC Import Document", doc, ["linked_document_type", "linked_document"] ) + # TODO: Handle manual reconciliation + return reference_type, reference_name def create_sales_invoice(self): diff --git a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.js b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.js index 85ba4dcf96..d26d80c42c 100644 --- a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.js +++ b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.js @@ -3,8 +3,10 @@ frappe.ui.form.on('FEC Import Settings', { refresh: function(frm) { - frm.add_custom_button(__("Import a FEC"), () => { - frappe.new_doc('FEC Import', {company: frm.doc.company}); - }) + if (!frm.is_new()) { + frm.add_custom_button(__("Import a FEC"), () => { + frappe.new_doc('FEC Import', {company: frm.doc.company}); + }) + } } }); diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index 8b8f55649b..a147a282a3 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -9148,8 +9148,8 @@ Row #{0}: Finished Good Item Qty is not specified for service item {0},Ligne #{0 Row #{0}: Finished Good Item is not specified for service item {1},Ligne #{0}: L'article de produit fini n'est pas spécifié pour l'article de service {1}, Row #{0}: Finished Good Item {1} must be a sub-contracted item for service item {2},Ligne #{0}: L'article de produit fini {1} doit être un article sous-traité pour l'article de service {2}, Row #{0}: Finished Good Item {1} must be a sub-contracted item,Ligne #{0}: L'article produit fini {1} doit être un article de sous-traitance, -"Row #{0}: For {1}, you can select reference document only if account gets credited","Ligne #{0}: Pour {1}, vous pouvez sélectionner un document de référence seulement si le compte est crédité", -"Row #{0}: For {1}, you can select reference document only if account gets debited","Ligne #{0}: Pour {1}, vous pouvez sélectionner un document de référence seulement si le compte est débité", +"Row #{0}: For {1}, you can select a reference document only if account gets credited","Ligne #{0}: Pour {1}, vous pouvez sélectionner un document de référence seulement si le compte est crédité", +"Row #{0}: For {1}, you can select a reference document only if account gets debited","Ligne #{0}: Pour {1}, vous pouvez sélectionner un document de référence seulement si le compte est débité", Row #{0}: From Date cannot be before To Date,Ligne #{0} : La date de début ne peut pas être antérieure à la date de fin, Row #{0}: Item added,Ligne #{0}: Article ajouté, Row #{0}: Item {1} does not exist,Ligne #{0} : L'élément {1} n'existe pas, -- GitLab From 4a6329a85cf85a608de199a943af98639a6e48cc Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Thu, 24 Aug 2023 09:17:58 +0200 Subject: [PATCH 11/19] fix: translations and setup wizard --- erpnext/regional/france/setup.py | 2 +- erpnext/translations/fr.csv | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/regional/france/setup.py b/erpnext/regional/france/setup.py index a22238892a..d4df0714ea 100644 --- a/erpnext/regional/france/setup.py +++ b/erpnext/regional/france/setup.py @@ -143,7 +143,7 @@ def default_accounts_mapping(accounts, company): "capital_work_in_progress_account": 231, "asset_received_but_not_billed": 722, "default_advance_received_account": 4191, - "default_down_payment_payable_account": 4091, + "default_advance_paid_account": 4091, } return { diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index a147a282a3..5674f7277c 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -3241,7 +3241,7 @@ Default Payroll Payable Account,Compte de paie par défaut, Default Price List,Liste de prix par défaut, Default Priority,Priorité par défaut, Default Provisional Account,Compte provisionnel par défaut, -Default Purchase Unit of Measure,Unité de Mesure par défaut à l'Achat, +Default Purchase Unit of Measure,Unité de Mesure par défaut à l'achat, Default Quotation Validity Days,N° de jours de validité par défaut pour les devis, Default Receivable Account,Compte client par défaut, Default Receivable Accounts,Comptes débiteurs par défaut, @@ -3256,7 +3256,7 @@ Default Service Level Agreement,Accord de niveau de service par défaut, Default Shift,Quart par défaut, Default Shipping Account,Compte d'expédition par défaut, Default Source Warehouse,Entrepôt source par défaut, -Default Stock UOM,Unité de mesure par Défaut des Articles, +Default Stock UOM,Unité de mesure par défaut des articles, Default Supplier Group,Groupe de fournisseurs par défaut, Default Supplier,Fournisseur par défaut, Default Target Warehouse,Entrepôt cible par défaut, @@ -3266,8 +3266,8 @@ Default Territory,Région par défaut, Default UOM,Unité de mesure par défaut, Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You need to either cancel the linked documents or create a new Item.,"L'unité de mesure par défaut de l'élément {0} ne peut pas être modifiée directement car vous avez déjà effectué une ou plusieurs transactions avec une autre UOM. Vous devez soit annuler les documents liés, soit créer un nouvel élément.", Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.,L'Unité de Mesure par Défaut pour l'Article {0} ne peut pas être modifiée directement parce que vous avez déjà fait une (des) transaction (s) avec une autre unité de mesure. Vous devez créer un nouvel article pour utiliser une Unité de mesure par défaut différente., -Default Unit of Measure for Variant '{0}' must be same as in Template '{1}',L'Unité de mesure par défaut pour la variante '{0}' doit être la même que dans le Modèle '{1}', -Default Unit of Measure,Unité de Mesure par Défaut, +Default Unit of Measure for Variant '{0}' must be same as in Template '{1}',L'unité de mesure par défaut pour la variante '{0}' doit être la même que dans le Modèle '{1}', +Default Unit of Measure,Unité de mesure par défaut, Default Valuation Method,Méthode de Valorisation par Défaut, Default Value,Valeur par défaut, Default Values,Valeurs Par Défaut, -- GitLab From bfaaf027df6d04c4b1a24c3544b101a38270a0f6 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Thu, 24 Aug 2023 10:15:08 +0200 Subject: [PATCH 12/19] fix: Add explicit link to settings and rollback on error --- .../regional/doctype/fec_import/fec_import.js | 15 ++++++++++- .../doctype/fec_import/fec_import.json | 15 ++++++++++- .../regional/doctype/fec_import/fec_import.py | 27 ++++++++++++++----- .../fec_import_document.py | 3 +++ .../fec_import_settings.json | 7 ++++- .../fec_import_settings.py | 25 +++++++++++++++++ 6 files changed, 83 insertions(+), 9 deletions(-) diff --git a/erpnext/regional/doctype/fec_import/fec_import.js b/erpnext/regional/doctype/fec_import/fec_import.js index f89d7ff242..aa730f9987 100644 --- a/erpnext/regional/doctype/fec_import/fec_import.js +++ b/erpnext/regional/doctype/fec_import/fec_import.js @@ -2,6 +2,15 @@ // For license information, please see license.txt frappe.ui.form.on('FEC Import', { + setup(frm) { + frm.set_query("import_settings", function(doc) { + return { + filters: { + "company": doc.company + } + } + }) + }, refresh(frm) { frm.get_field("fec_file").df.options = { restrictions: { @@ -16,12 +25,16 @@ frappe.ui.form.on('FEC Import', { add_actions(frm) { if (frm.doc.fec_file) { frm.page.set_primary_action(__("Import FEC"), function() { + frappe.show_alert({ + message: __("Import started"), + indicator: "orange" + }) frappe.call({ method: "upload_fec", doc: frm.doc, }).then(r => { frappe.show_alert({ - message: __("Import in progress"), + message: __("Import finished. Please check the imported documents."), indicator: "green" }) }) diff --git a/erpnext/regional/doctype/fec_import/fec_import.json b/erpnext/regional/doctype/fec_import/fec_import.json index 9e7151fcf2..77aebcb602 100644 --- a/erpnext/regional/doctype/fec_import/fec_import.json +++ b/erpnext/regional/doctype/fec_import/fec_import.json @@ -14,6 +14,8 @@ "fec_file", "section_break_3", "company", + "column_break_lduy", + "import_settings", "filters_section", "from_date", "column_break_7", @@ -87,6 +89,17 @@ "fieldname": "description", "fieldtype": "HTML", "label": "Description" + }, + { + "fieldname": "column_break_lduy", + "fieldtype": "Column Break" + }, + { + "fieldname": "import_settings", + "fieldtype": "Link", + "label": "Import Setttings", + "options": "FEC Import Settings", + "reqd": 1 } ], "index_web_pages_for_search": 1, @@ -97,7 +110,7 @@ "link_fieldname": "fec_import" } ], - "modified": "2023-06-21 16:48:13.545416", + "modified": "2023-08-24 09:38:33.331065", "modified_by": "Administrator", "module": "Regional", "name": "FEC Import", diff --git a/erpnext/regional/doctype/fec_import/fec_import.py b/erpnext/regional/doctype/fec_import/fec_import.py index 037fe65c3e..05c23c8150 100644 --- a/erpnext/regional/doctype/fec_import/fec_import.py +++ b/erpnext/regional/doctype/fec_import/fec_import.py @@ -14,6 +14,22 @@ from frappe.utils.csvutils import read_csv_content class FECImport(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + company: DF.Link + fec_file: DF.Attach | None + from_date: DF.Date | None + import_journal: DF.Link | None + import_settings: DF.Link + naming_series: DF.Literal["FEC-.YYYY.-.#####"] + to_date: DF.Date | None + # end: auto-generated types @frappe.whitelist() def get_company(self): company = None @@ -118,6 +134,7 @@ class FECImport(Document): journal_type = "Purchase" doc = frappe.new_doc("Accounting Journal") + doc.company = self.company doc.journal_code = journal doc.journal_name = journals[journal] doc.type = journal_type @@ -149,6 +166,7 @@ class FECImport(Document): for account in accounts: if not frappe.db.exists("Account", dict(account_number=account)): doc = frappe.new_doc("Account") + doc.company = self.company doc.account_name = accounts[account] doc.account_number = account doc.parent_account = self.get_parent_account(account_groups_with_numbers, account) @@ -209,10 +227,7 @@ class FECImport(Document): class FECImportDocumentCreator: def __init__(self, settings, data): self.settings = settings - self.company_settings = frappe.db.get_value( - "FEC Import Settings", dict(company=self.settings.company) - ) - if not self.company_settings: + if not self.settings.import_settings: frappe.throw( _("Please configure a FEC Import Settings document for company {0}").format( self.settings.company @@ -254,7 +269,7 @@ class FECImportDocumentCreator: iter_next = False doc = frappe.new_doc("FEC Import Document") doc.fec_import = self.settings.name - doc.settings = self.company_settings + doc.settings = self.settings.import_settings doc.gl_entries_date = datetime.datetime.strptime(date, "%Y%m%d").strftime("%Y-%m-%d") doc.gl_entry_reference = piece @@ -309,7 +324,7 @@ class FECImportDocumentCreator: ) } - company_settings = frappe.get_doc("FEC Import Settings", self.company_settings) + company_settings = frappe.get_doc("FEC Import Settings", self.settings.import_settings) journals = {} mapped_journals = dokos_journals diff --git a/erpnext/regional/doctype/fec_import_document/fec_import_document.py b/erpnext/regional/doctype/fec_import_document/fec_import_document.py index 8cc2605e33..35658f4a18 100644 --- a/erpnext/regional/doctype/fec_import_document/fec_import_document.py +++ b/erpnext/regional/doctype/fec_import_document/fec_import_document.py @@ -213,6 +213,7 @@ class FECImportDocument(Document): else: self.create_journal_entry() except Exception: + frappe.db.rollback() self.db_set("status", "Error") self.db_set("error", frappe.get_traceback()) @@ -246,6 +247,7 @@ class FECImportDocument(Document): "reference_name": reference_name, "party_type": line.party_type, "party": line.party, + "cost_center": frappe.get_cached_value("Company", self.company, "cost_center"), }, ) @@ -256,6 +258,7 @@ class FECImportDocument(Document): if self.gl_entry_reference: journal_entry.name = self.gl_entry_reference journal_entry.flags.draft_name_set = True + journal_entry.insert() if frappe.db.get_value("FEC Import Settings", self.settings, "submit_journal_entries"): diff --git a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json index 6e2b1b6436..283086e942 100644 --- a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json +++ b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json @@ -143,13 +143,18 @@ ], "index_web_pages_for_search": 1, "links": [ + { + "group": "Import Configuration", + "link_doctype": "FEC Import", + "link_fieldname": "import_settings" + }, { "group": "Imported Documents", "link_doctype": "FEC Import Document", "link_fieldname": "settings" } ], - "modified": "2023-06-14 21:20:02.438054", + "modified": "2023-08-24 09:42:45.726044", "modified_by": "Administrator", "module": "Regional", "name": "FEC Import Settings", diff --git a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.py b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.py index ebd9df01a5..d0e4e3305f 100644 --- a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.py +++ b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.py @@ -6,4 +6,29 @@ from frappe.model.document import Document class FECImportSettings(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.regional.doctype.fec_accounting_journal_mapping.fec_accounting_journal_mapping import ( + FECAccountingJournalMapping, + ) + + accounting_journal_mapping: DF.Table[FECAccountingJournalMapping] + company: DF.Link + create_purchase_invoices: DF.Check + create_sales_invoices: DF.Check + customer_group: DF.Link | None + purchase_item: DF.Link | None + sales_item: DF.Link | None + submit_journal_entries: DF.Check + submit_purchase_invoices: DF.Check + submit_sales_invoices: DF.Check + supplier_group: DF.Link | None + territory: DF.Link | None + # end: auto-generated types pass -- GitLab From 8251fead559c86bcd4bc8ccdff22f79cdc38326c Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Thu, 24 Aug 2023 14:44:45 +0200 Subject: [PATCH 13/19] fix: Multiple fixes and auto reconciliation tool --- .../doctype/journal_entry/journal_entry.json | 4 +- .../doctype/journal_entry/journal_entry.py | 80 ++++++++++++++++++- .../regional/doctype/fec_import/fec_import.js | 32 ++++++++ .../doctype/fec_import/fec_import.json | 15 +++- .../regional/doctype/fec_import/fec_import.py | 50 +++++++++++- .../fec_import_document.json | 22 ++++- .../fec_import_document.py | 70 ++++++++++++---- .../fec_import_settings.json | 15 +++- erpnext/translations/fr.csv | 2 +- 9 files changed, 264 insertions(+), 26 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 47a8becb08..a17b53e4a4 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -102,7 +102,7 @@ "no_copy": 1, "oldfieldname": "naming_series", "oldfieldtype": "Select", - "options": "ACC-JV-.YYYY.-", + "options": "ACC-JV-.FY.-", "print_hide": 1, "reqd": 1, "set_only_once": 1 @@ -570,7 +570,7 @@ "idx": 176, "is_submittable": 1, "links": [], - "modified": "2023-08-10 14:32:22.366895", + "modified": "2023-08-24 14:41:28.097349", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 4d7011f98c..bf2a7fb1ae 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -35,6 +35,80 @@ class StockAccountInvalidTransaction(frappe.ValidationError): class JournalEntry(AccountsController): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.accounts.doctype.journal_entry_account.journal_entry_account import ( + JournalEntryAccount, + ) + + accounting_journal: DF.Link | None + accounts: DF.Table[JournalEntryAccount] + amended_from: DF.Link | None + apply_tds: DF.Check + auto_repeat: DF.Link | None + bill_date: DF.Date | None + bill_no: DF.Data | None + cheque_date: DF.Date | None + cheque_no: DF.Data | None + clearance_date: DF.Date | None + company: DF.Link + difference: DF.Currency + due_date: DF.Date | None + finance_book: DF.Link | None + from_template: DF.Link | None + inter_company_journal_entry_reference: DF.Link | None + is_opening: DF.Literal["No", "Yes"] + is_system_generated: DF.Check + letter_head: DF.Link | None + mode_of_payment: DF.Link | None + multi_currency: DF.Check + naming_series: DF.Literal["ACC-JV-.FY.-"] + paid_loan: DF.Data | None + pay_to_recd_from: DF.Data | None + payment_order: DF.Link | None + posting_date: DF.Date + process_deferred_accounting: DF.Link | None + remark: DF.SmallText | None + reversal_of: DF.Link | None + select_print_heading: DF.Link | None + stock_entry: DF.Link | None + tax_withholding_category: DF.Link | None + title: DF.Data | None + total_amount: DF.Currency + total_amount_currency: DF.Link | None + total_amount_in_words: DF.Data | None + total_credit: DF.Currency + total_debit: DF.Currency + unreconciled_amount: DF.Currency + user_remark: DF.SmallText | None + voucher_type: DF.Literal[ + "Journal Entry", + "Inter Company Journal Entry", + "Bank Entry", + "Cash Entry", + "Credit Card Entry", + "Debit Note", + "Credit Note", + "Contra Entry", + "Excise Entry", + "Write Off Entry", + "Opening Entry", + "Depreciation Entry", + "Exchange Rate Revaluation", + "Exchange Gain Or Loss", + "Deferred Revenue", + "Deferred Expense", + ] + write_off_amount: DF.Currency + write_off_based_on: DF.Literal["Accounts Receivable", "Accounts Payable"] + # end: auto-generated types + def __init__(self, *args, **kwargs): super(JournalEntry, self).__init__(*args, **kwargs) @@ -602,9 +676,9 @@ class JournalEntry(AccountsController): frappe.throw( _("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}").format( d.idx, - field_dict.get(d.reference_type)[0], - field_dict.get(d.reference_type)[1], - d.reference_type, + _(field_dict.get(d.reference_type)[0]), + _(field_dict.get(d.reference_type)[1]), + _(d.reference_type), d.reference_name, ) ) diff --git a/erpnext/regional/doctype/fec_import/fec_import.js b/erpnext/regional/doctype/fec_import/fec_import.js index aa730f9987..170a55c83d 100644 --- a/erpnext/regional/doctype/fec_import/fec_import.js +++ b/erpnext/regional/doctype/fec_import/fec_import.js @@ -41,6 +41,10 @@ frappe.ui.form.on('FEC Import', { }); frm.add_custom_button(__("Create Accounts"), function() { + frappe.show_alert({ + message: __("Accounts creation started"), + indicator: "orange" + }) frappe.call({ method: "create_accounts", doc: frm.doc, @@ -53,6 +57,10 @@ frappe.ui.form.on('FEC Import', { }, __("Actions")); frm.add_custom_button(__("Create Journals"), function() { + frappe.show_alert({ + message: __("Journals creation started"), + indicator: "orange" + }) frappe.call({ method: "create_journals", doc: frm.doc, @@ -72,6 +80,30 @@ frappe.ui.form.on('FEC Import', { frappe.set_route("List", "Accounting Journal") }, __("View")); } + + frappe.model.with_doctype("FEC Import Document", () => { + frappe.db.get_list("FEC Import Document", { + filters: { + fec_import: frm.doc.name, + } + }).then(r => { + frm.add_custom_button(__("Auto reconcile open entries"), function() { + frappe.show_alert({ + message: __("Reconciliation started"), + indicator: "orange" + }) + frappe.call({ + method: "auto_reconcile_entries", + doc: frm.doc, + }).then(r => { + frappe.show_alert({ + message: __("Reconciliation completed."), + indicator: "green" + }) + }) + }, __("Actions")); + }) + }) }, fec_file(frm) { diff --git a/erpnext/regional/doctype/fec_import/fec_import.json b/erpnext/regional/doctype/fec_import/fec_import.json index 77aebcb602..4849cd9d5c 100644 --- a/erpnext/regional/doctype/fec_import/fec_import.json +++ b/erpnext/regional/doctype/fec_import/fec_import.json @@ -110,7 +110,7 @@ "link_fieldname": "fec_import" } ], - "modified": "2023-08-24 09:38:33.331065", + "modified": "2023-08-24 14:42:00.643554", "modified_by": "Administrator", "module": "Regional", "name": "FEC Import", @@ -129,6 +129,19 @@ "select": 1, "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "select": 1, + "share": 1, + "write": 1 } ], "sort_field": "modified", diff --git a/erpnext/regional/doctype/fec_import/fec_import.py b/erpnext/regional/doctype/fec_import/fec_import.py index 05c23c8150..0454c86d18 100644 --- a/erpnext/regional/doctype/fec_import/fec_import.py +++ b/erpnext/regional/doctype/fec_import/fec_import.py @@ -30,6 +30,7 @@ class FECImport(Document): naming_series: DF.Literal["FEC-.YYYY.-.#####"] to_date: DF.Date | None # end: auto-generated types + @frappe.whitelist() def get_company(self): company = None @@ -223,6 +224,51 @@ class FECImport(Document): elif cint(account_number[:2]) == 10: return "Equity" + @frappe.whitelist() + def auto_reconcile_entries(self): + from erpnext.accounts.report.accounts_receivable.accounts_receivable import ( + ReceivablePayableReport, + ) + + for party_type in ["Customer", "Supplier"]: + args = { + "account_type": "Payable" if party_type == "Supplier" else "Receivable", + "naming_by": ["Buying Settings", "supp_master_name"] + if party_type == "Supplier" + else ["Selling Settings", "cust_master_name"], + } + + filters = { + "company": self.company, + "ageing_based_on": "Due Date", + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + result = ReceivablePayableReport(filters).run(args) + parties = set( + (d.get("party"), d.get("party_account")) for d in result[1] if d.get("party") != "Total" + ) + + for data in parties: + pr = self.create_payment_reconciliation(party_type, data[0], data[1]) + pr.get_unreconciled_entries() + invoices = [x.as_dict() for x in pr.get("invoices")] + payments = [x.as_dict() for x in pr.get("payments")] + if invoices and payments: + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + + def create_payment_reconciliation(self, party_type, party, party_account): + pr = frappe.new_doc("Payment Reconciliation") + pr.company = self.company + pr.party_type = party_type + pr.party = party + pr.receivable_payable_account = party_account + return pr + class FECImportDocumentCreator: def __init__(self, settings, data): @@ -255,7 +301,9 @@ class FECImportDocumentCreator: ): continue - self.grouped_data[d["EcritureDate"]][d["PieceRef"]].append(frappe._dict(d)) + self.grouped_data[d["EcritureDate"]][f'{d["JournalCode"]} | {d["EcritureNum"]}'].append( + frappe._dict(d) + ) @staticmethod def parse_credit_debit(d): diff --git a/erpnext/regional/doctype/fec_import_document/fec_import_document.json b/erpnext/regional/doctype/fec_import_document/fec_import_document.json index 4f31618b71..79aada1c5f 100644 --- a/erpnext/regional/doctype/fec_import_document/fec_import_document.json +++ b/erpnext/regional/doctype/fec_import_document/fec_import_document.json @@ -18,6 +18,7 @@ "gl_entries_date", "column_break_8", "gl_entry_reference", + "pieceref", "section_break_10", "gl_entries", "section_break_13", @@ -137,11 +138,17 @@ "label": "Import Type", "options": "Miscellaneous\nTransaction\nPayment", "reqd": 1 + }, + { + "fieldname": "pieceref", + "fieldtype": "Data", + "label": "PieceRef", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-06-21 16:33:02.991895", + "modified": "2023-08-24 14:42:15.131156", "modified_by": "Administrator", "module": "Regional", "name": "FEC Import Document", @@ -159,6 +166,19 @@ "select": 1, "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "select": 1, + "share": 1, + "write": 1 } ], "sort_field": "modified", diff --git a/erpnext/regional/doctype/fec_import_document/fec_import_document.py b/erpnext/regional/doctype/fec_import_document/fec_import_document.py index 35658f4a18..84d0773240 100644 --- a/erpnext/regional/doctype/fec_import_document/fec_import_document.py +++ b/erpnext/regional/doctype/fec_import_document/fec_import_document.py @@ -17,9 +17,34 @@ def value_is_true(value): class FECImportDocument(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.regional.doctype.fec_import_line.fec_import_line import FECImportLine + + company: DF.Link + error: DF.SmallText | None + fec_import: DF.Link + gl_entries: DF.Table[FECImportLine] + gl_entries_date: DF.Date | None + gl_entry_reference: DF.Data | None + import_type: DF.Literal["Miscellaneous", "Transaction", "Payment"] + linked_document: DF.DynamicLink | None + linked_document_type: DF.Link | None + pieceref: DF.Data | None + settings: DF.Link + status: DF.Literal["Pending", "Completed", "Error"] + # end: auto-generated types + def validate(self): if self.status != "Completed": for row in self.gl_entries: + self.set_pieceref() self.get_accounting_journal(row) self.get_gl_account(row) self.get_party(row) @@ -53,6 +78,11 @@ class FECImportDocument(Document): ) doc.insert(ignore_if_duplicate=True) + def set_pieceref(self): + references = set(line.pieceref for line in self.gl_entries if line.pieceref) + if len(references) == 1: + self.pieceref = list(references)[0] + def get_accounting_journal(self, row): if row.accounting_journal: return @@ -226,7 +256,7 @@ class FECImportDocument(Document): "doctype": "Journal Entry", "company": self.company, "posting_date": self.gl_entries_date, - "cheque_no": self.gl_entry_reference or "N/A", + "cheque_no": self.pieceref or "N/A", "cheque_date": self.gl_entries_date, } ) @@ -255,8 +285,8 @@ class FECImportDocument(Document): frappe.throw(_("Payment references could not be found")) if journal_entry.accounts: - if self.gl_entry_reference: - journal_entry.name = self.gl_entry_reference + if self.pieceref: + journal_entry.name = self.pieceref journal_entry.flags.draft_name_set = True journal_entry.insert() @@ -269,7 +299,7 @@ class FECImportDocument(Document): self.db_set("status", "Completed") def is_payment_entry(self): - return [line.ecriturelet for line in self.gl_entries if line.ecriturelet] and [ + return [ l.account for l in self.gl_entries if frappe.get_cached_value("Account", l.account, "account_type") in ["Bank", "Cash"] @@ -290,19 +320,27 @@ class FECImportDocument(Document): comptenum=line.comptenum, compauxnum=line.compauxnum, datelet=line.datelet, + validdate=line.validdate, ) if flt(line.credit) > 0.0: - filters["debit"] = (">", 0.0) + filters["debit"] = line.credit else: - filters["credit"] = (">", 0.0) + filters["credit"] = line.debit - for doc in frappe.get_all("FEC Import Line", filters=filters, pluck="parent"): - reference_type, reference_name = frappe.db.get_value( - "FEC Import Document", doc, ["linked_document_type", "linked_document"] + for doc in frappe.get_all("FEC Import Line", filters=filters, fields=["parent"]): + linked_document_type, linked_document = frappe.db.get_value( + "FEC Import Document", doc.parent, ["linked_document_type", "linked_document"] ) - # TODO: Handle manual reconciliation + if not (linked_document_type and linked_document): + continue + + meta = frappe.get_meta(linked_document_type) + if meta.has_field("outstanding_amount"): + outstanding = frappe.db.get_value(linked_document_type, linked_document, "outstanding_amount") + if outstanding >= abs(line.debit - line.credit): + reference_type, reference_name = linked_document_type, linked_document return reference_type, reference_name @@ -331,13 +369,13 @@ class FECImportDocument(Document): if sales_invoice.customer and sales_invoice.items: # self.sales_invoices.append(sales_invoice) try: - if self.gl_entry_reference: - sales_invoice.name = self.gl_entry_reference + if self.pieceref: + sales_invoice.name = self.pieceref sales_invoice.flags.draft_name_set = True sales_invoice.insert() if frappe.db.get_value("FEC Import Settings", self.settings, "submit_sales_invoices"): - if self.gl_entry_reference: + if self.pieceref: sales_invoice.flags.name_set = True sales_invoice.flags.ignore_version = True @@ -379,13 +417,13 @@ class FECImportDocument(Document): if purchase_invoice.supplier and purchase_invoice.items: # self.purchase_invoices.append(purchase_invoice) try: - if self.gl_entry_reference: - purchase_invoice.name = self.gl_entry_reference + if self.pieceref: + purchase_invoice.name = self.pieceref purchase_invoice.flags.draft_name_set = True purchase_invoice.insert() if frappe.db.get_value("FEC Import Settings", self.settings, "submit_sales_invoices"): - if self.gl_entry_reference: + if self.pieceref: purchase_invoice.flags.name_set = True purchase_invoice.flags.ignore_version = True diff --git a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json index 283086e942..f3cf024423 100644 --- a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json +++ b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json @@ -154,7 +154,7 @@ "link_fieldname": "settings" } ], - "modified": "2023-08-24 09:42:45.726044", + "modified": "2023-08-24 14:41:50.237923", "modified_by": "Administrator", "module": "Regional", "name": "FEC Import Settings", @@ -173,6 +173,19 @@ "select": 1, "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "select": 1, + "share": 1, + "write": 1 } ], "sort_field": "modified", diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index 5674f7277c..30e7e6b53a 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -9294,7 +9294,7 @@ Row {0}: Packed Qty must be equal to {1} Qty.,Ligne {0}: La qté emballée doit Row {0}: Packing Slip is already created for Item {1}.,Ligne {0}: le bordereau de colis est déjà créé pour l'article {1}, Row {0}: Paid amount {1} is greater than pending accrued amount {2} against loan {3},Ligne {0}: Le montant payé {1} est supérieur au au montant accumulé restant {2} pour le prêt {3}, Row {0}: Paid amount {1} is greater than pending accrued amount {2}against loan {3},Ligne {0}: Le montant payé {1} est supérieur au montant accumulé en attente {2} pour le prêt {3}, -Row {0}: Party / Account does not match with {1} / {2} in {3} {4},Ligne {0} : Tiers / Compte ne correspond pas à {1} / {2} en {3} {4}, +Row {0}: Party / Account does not match with {1} / {2} in {3} {4},Ligne {0} : Le tiers / compte ne correspond pas à {1} / {2} pour le·la {3} {4}, Row {0}: Party Type and Party is required for Receivable / Payable account {1},Ligne {0} : Le Type de tiers et le Tiers sont requis pour le compte Débiteur / Créditeur {1}, Row {0}: Payment against Sales/Purchase Order should always be marked as advance,Ligne {0} : Les paiements contre des commandes client / fournisseur doivent toujours être marqués comme des avances, Row {0}: Please check 'Is Advance' against Account {1} if this is an advance entry.,Ligne {0} : Veuillez vérifier 'Est Avance' sur le compte {1} si c'est une avance., -- GitLab From f457fc83971bf8aedd91d588d411d401087b4ae8 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Thu, 24 Aug 2023 15:15:45 +0200 Subject: [PATCH 14/19] fix: Rearrange fields and fallback when there is no entry number (intermediary FEC) --- .../regional/doctype/fec_import/fec_import.py | 6 +-- .../fec_import_settings.json | 46 +++++++++++++------ .../fec_import_settings.py | 6 +-- 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/erpnext/regional/doctype/fec_import/fec_import.py b/erpnext/regional/doctype/fec_import/fec_import.py index 0454c86d18..219d6f1816 100644 --- a/erpnext/regional/doctype/fec_import/fec_import.py +++ b/erpnext/regional/doctype/fec_import/fec_import.py @@ -301,9 +301,9 @@ class FECImportDocumentCreator: ): continue - self.grouped_data[d["EcritureDate"]][f'{d["JournalCode"]} | {d["EcritureNum"]}'].append( - frappe._dict(d) - ) + self.grouped_data[d["EcritureDate"]][ + f'{d["JournalCode"]} | {d["EcritureNum"] or d["PieceRef"]}' + ].append(frappe._dict(d)) @staticmethod def parse_credit_debit(d): diff --git a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json index f3cf024423..9c109fb49c 100644 --- a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json +++ b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.json @@ -18,13 +18,17 @@ "create_sales_invoices", "submit_sales_invoices", "sales_item", - "customer_group", - "territory", "purchase_invoices_tab", "create_purchase_invoices", "submit_purchase_invoices", "purchase_item", - "supplier_group" + "parties_tab", + "suppliers_section", + "supplier_group", + "section_break_nquh", + "customers_column", + "customer_group", + "territory" ], "fields": [ { @@ -86,28 +90,25 @@ "label": "Create Purchase Invoices" }, { - "depends_on": "eval:doc.create_sales_invoices", "fieldname": "customer_group", "fieldtype": "Link", "label": "Default Customer Group", - "mandatory_depends_on": "eval:doc.create_sales_invoices", - "options": "Customer Group" + "options": "Customer Group", + "reqd": 1 }, { - "depends_on": "eval:doc.create_purchase_invoices", "fieldname": "supplier_group", "fieldtype": "Link", "label": "Default Supplier Group", - "mandatory_depends_on": "eval:doc.create_purchase_invoices", - "options": "Supplier Group" + "options": "Supplier Group", + "reqd": 1 }, { - "depends_on": "eval:doc.create_sales_invoices", "fieldname": "territory", "fieldtype": "Link", "label": "Default Territory", - "mandatory_depends_on": "eval:doc.create_sales_invoices", - "options": "Territory" + "options": "Territory", + "reqd": 1 }, { "fieldname": "links_tab", @@ -139,6 +140,25 @@ "fieldname": "submit_journal_entries", "fieldtype": "Check", "label": "Automatically Submit Journal Entries" + }, + { + "fieldname": "parties_tab", + "fieldtype": "Tab Break", + "label": "Parties" + }, + { + "fieldname": "suppliers_section", + "fieldtype": "Section Break", + "label": "Suppliers" + }, + { + "fieldname": "section_break_nquh", + "fieldtype": "Section Break" + }, + { + "fieldname": "customers_column", + "fieldtype": "Column Break", + "label": "Customers" } ], "index_web_pages_for_search": 1, @@ -154,7 +174,7 @@ "link_fieldname": "settings" } ], - "modified": "2023-08-24 14:41:50.237923", + "modified": "2023-08-24 15:03:46.413291", "modified_by": "Administrator", "module": "Regional", "name": "FEC Import Settings", diff --git a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.py b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.py index d0e4e3305f..9bfc28df8b 100644 --- a/erpnext/regional/doctype/fec_import_settings/fec_import_settings.py +++ b/erpnext/regional/doctype/fec_import_settings/fec_import_settings.py @@ -22,13 +22,13 @@ class FECImportSettings(Document): company: DF.Link create_purchase_invoices: DF.Check create_sales_invoices: DF.Check - customer_group: DF.Link | None + customer_group: DF.Link purchase_item: DF.Link | None sales_item: DF.Link | None submit_journal_entries: DF.Check submit_purchase_invoices: DF.Check submit_sales_invoices: DF.Check - supplier_group: DF.Link | None - territory: DF.Link | None + supplier_group: DF.Link + territory: DF.Link # end: auto-generated types pass -- GitLab From bf9a1532a74fff3cdb3baa8086f41aac99131e20 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 24 Aug 2023 11:06:51 +0530 Subject: [PATCH 15/19] Merge pull request #36786 from s-aga-r/SCR-QI feat: `Quality Inspection` for Subcontracting Receipt -- GitLab From b2b7778295f4e996842e40a46d8a591408f98437 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Aug 2023 18:33:49 +0530 Subject: [PATCH 16/19] Merge pull request #36596 from Nihantra-Patel/pos_receipt_mail fix: POS Invoice Email Receipt Mail -- GitLab From 5139ccabdaee1752f6a34ea8f7f94d4f537ed941 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Aug 2023 19:30:54 +0530 Subject: [PATCH 17/19] Merge pull request #36799 from deepeshgarg007/tds_on_debit_note fix: Tax withholding reversal on Debit Notes -- GitLab From 18d707ebdfe2f70fb65c04a7d1feb24ea8ccdae6 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Mon, 28 Aug 2023 09:33:10 +0200 Subject: [PATCH 18/19] fix: Don't rename journal entries --- .../doctype/fec_import_document/fec_import_document.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/regional/doctype/fec_import_document/fec_import_document.py b/erpnext/regional/doctype/fec_import_document/fec_import_document.py index 84d0773240..b9db1f90d8 100644 --- a/erpnext/regional/doctype/fec_import_document/fec_import_document.py +++ b/erpnext/regional/doctype/fec_import_document/fec_import_document.py @@ -285,10 +285,6 @@ class FECImportDocument(Document): frappe.throw(_("Payment references could not be found")) if journal_entry.accounts: - if self.pieceref: - journal_entry.name = self.pieceref - journal_entry.flags.draft_name_set = True - journal_entry.insert() if frappe.db.get_value("FEC Import Settings", self.settings, "submit_journal_entries"): -- GitLab From 904c45432fb10a7ee9a8bd3c1cd0dd092bd12eef Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Mon, 28 Aug 2023 14:52:52 +0200 Subject: [PATCH 19/19] UX: Improve form global UX --- .../regional/doctype/fec_import/fec_import.js | 19 ++-- .../doctype/fec_import/fec_import.json | 8 +- .../regional/doctype/fec_import/fec_import.py | 105 ++++++++++++++++-- 3 files changed, 104 insertions(+), 28 deletions(-) diff --git a/erpnext/regional/doctype/fec_import/fec_import.js b/erpnext/regional/doctype/fec_import/fec_import.js index 170a55c83d..4365669585 100644 --- a/erpnext/regional/doctype/fec_import/fec_import.js +++ b/erpnext/regional/doctype/fec_import/fec_import.js @@ -10,6 +10,14 @@ frappe.ui.form.on('FEC Import', { } } }) + + frappe.realtime.on('fec_doc_update', data => { + if (data.fec_import !== frm.doc.name) return; + const message = __('{0} of {1} {2}', [data.current, data.total, data.type]); + const percent = Math.floor((data.current * 100) / data.total); + const title = __("{0} creation", [data.type]) + frm.dashboard.show_progress(title, percent, message); + }) }, refresh(frm) { frm.get_field("fec_file").df.options = { @@ -106,17 +114,6 @@ frappe.ui.form.on('FEC Import', { }) }, - fec_file(frm) { - if (frm.doc.fec_file) { - frm.call({ - method: "get_company", - doc: frm.doc, - }).then(r => { - frm.set_value("company", r.message) - }) - } - }, - add_description(frm) { let description = __("1. Import your FEC file") if (!frm.is_new()) { diff --git a/erpnext/regional/doctype/fec_import/fec_import.json b/erpnext/regional/doctype/fec_import/fec_import.json index 4849cd9d5c..3341281c20 100644 --- a/erpnext/regional/doctype/fec_import/fec_import.json +++ b/erpnext/regional/doctype/fec_import/fec_import.json @@ -49,8 +49,7 @@ "fieldname": "company", "fieldtype": "Link", "label": "Company", - "options": "Company", - "reqd": 1 + "options": "Company" }, { "depends_on": "eval:doc.fec_file && doc.company", @@ -98,8 +97,7 @@ "fieldname": "import_settings", "fieldtype": "Link", "label": "Import Setttings", - "options": "FEC Import Settings", - "reqd": 1 + "options": "FEC Import Settings" } ], "index_web_pages_for_search": 1, @@ -110,7 +108,7 @@ "link_fieldname": "fec_import" } ], - "modified": "2023-08-24 14:42:00.643554", + "modified": "2023-08-28 14:27:25.146981", "modified_by": "Administrator", "module": "Regional", "name": "FEC Import", diff --git a/erpnext/regional/doctype/fec_import/fec_import.py b/erpnext/regional/doctype/fec_import/fec_import.py index 219d6f1816..020bf90b67 100644 --- a/erpnext/regional/doctype/fec_import/fec_import.py +++ b/erpnext/regional/doctype/fec_import/fec_import.py @@ -9,7 +9,7 @@ from collections import defaultdict import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cint, flt, getdate +from frappe.utils import cint, flt, get_year_start, getdate from frappe.utils.csvutils import read_csv_content @@ -22,18 +22,38 @@ class FECImport(Document): if TYPE_CHECKING: from frappe.types import DF - company: DF.Link + company: DF.Link | None fec_file: DF.Attach | None from_date: DF.Date | None import_journal: DF.Link | None - import_settings: DF.Link + import_settings: DF.Link | None naming_series: DF.Literal["FEC-.YYYY.-.#####"] to_date: DF.Date | None # end: auto-generated types + def validate(self): + company_and_dates = self.get_company_and_dates() + if not self.company: + self.company = company_and_dates.company + + if not self.from_date: + self.from_date = company_and_dates.from_date + + if not self.to_date: + self.to_date = company_and_dates.to_date + + if self.company and not self.import_settings: + if ( + settings := frappe.get_all( + "FEC Import Settings", filters={"company": self.company}, pluck="name" + ) + ) and len(settings) == 1: + self.import_settings = settings[0] + @frappe.whitelist() - def get_company(self): - company = None + def get_company_and_dates(self): + company_and_dates = frappe._dict(company=None, from_date=None, to_date=None) + if self.fec_file: file_name = frappe.db.get_value( "File", @@ -45,15 +65,28 @@ class FECImport(Document): "file_name", ) try: + if closing_date := file_name.split("FEC")[1]: + closing_date = closing_date.split(".txt")[0] + company_and_dates["to_date"] = datetime.datetime.strptime(closing_date, "%Y%m%d").strftime( + "%Y-%m-%d" + ) + company_and_dates["from_date"] = get_year_start(closing_date, as_str=True) + if siren := file_name.split("FEC")[0]: - return frappe.db.get_value("Company", dict(siren_number=siren)) + company_and_dates["company"] = frappe.db.get_value("Company", dict(siren_number=siren)) except Exception: - return company + company_and_dates - return company + return company_and_dates @frappe.whitelist() def upload_fec(self): + if not self.company: + frappe.throw(_("Please select a company")) + + if not self.import_settings: + frappe.throw(_("Please select an import settings document")) + data = self.get_data() try: @@ -105,7 +138,16 @@ class FECImport(Document): {l["JournalCode"] for l in self.get_data() if l["CompteNum"].startswith("6")} ) - for journal in journals: + for index, journal in enumerate(journals): + frappe.publish_realtime( + "fec_doc_update", + { + "fec_import": self.name, + "current": index + 1, + "total": len(journals), + "type": _("accounting journals created"), + }, + ) journal_type = "Miscellaneous" account = None @@ -164,7 +206,16 @@ class FECImport(Document): account_groups_with_numbers.update({group.account_number[:-1]: group.parent_account}) account_groups_with_numbers.update({group.account_number: group.name}) - for account in accounts: + for index, account in enumerate(accounts): + frappe.publish_realtime( + "fec_doc_update", + { + "fec_import": self.name, + "current": index + 1, + "total": len(accounts), + "type": _("accounts created"), + }, + ) if not frappe.db.exists("Account", dict(account_number=account)): doc = frappe.new_doc("Account") doc.company = self.company @@ -252,7 +303,16 @@ class FECImport(Document): (d.get("party"), d.get("party_account")) for d in result[1] if d.get("party") != "Total" ) - for data in parties: + for index, data in enumerate(parties): + frappe.publish_realtime( + "fec_doc_update", + { + "fec_import": self.name, + "current": index + 1, + "total": len(parties), + "type": _("parties reconciled"), + }, + ) pr = self.create_payment_reconciliation(party_type, data[0], data[1]) pr.get_unreconciled_entries() invoices = [x.as_dict() for x in pr.get("invoices")] @@ -291,7 +351,16 @@ class FECImportDocumentCreator: self.grouped_data = defaultdict(lambda: defaultdict(list)) accounting_journals = self.get_accounting_journals_mapping() - for d in self.data: + for index, d in enumerate(self.data): + frappe.publish_realtime( + "fec_doc_update", + { + "fec_import": self.settings.name, + "current": index + 1, + "total": len(self.data), + "type": _("line processed"), + }, + ) if not self.is_within_date_range(d): continue @@ -312,8 +381,20 @@ class FECImportDocumentCreator: d["Montantdevise"] = flt(d["Montantdevise"].replace(",", ".")) def create_fec_import_documents(self): + current_index = 0 + total_elements = sum(len(self.grouped_data[date]) for date in self.grouped_data) for date in self.grouped_data: for piece in self.grouped_data[date]: + current_index += 1 + frappe.publish_realtime( + "fec_doc_update", + { + "fec_import": self.settings.name, + "current": current_index, + "total": total_elements, + "type": _("voucher creation initiated"), + }, + ) iter_next = False doc = frappe.new_doc("FEC Import Document") doc.fec_import = self.settings.name -- GitLab