diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..b72767b9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# Root editor config file +root = true + +# Common settings +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +# python, js indentation settings +[{*.py,*.js,*.vue,*.css,*.scss,*.html}] +indent_style = tab +indent_size = 2 +max_line_length = 99 diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..16f799f3 --- /dev/null +++ b/.flake8 @@ -0,0 +1,71 @@ +[flake8] +ignore = + B001, + B007, + B009, + B010, + B950, + E101, + E111, + E114, + E116, + E117, + E121, + E122, + E123, + E124, + E125, + E126, + E127, + E128, + E131, + E201, + E202, + E203, + E211, + E221, + E222, + E223, + E224, + E225, + E226, + E228, + E231, + E241, + E242, + E251, + E261, + E262, + E265, + E266, + E271, + E272, + E273, + E274, + E301, + E302, + E303, + E305, + E306, + E402, + E501, + E502, + E701, + E702, + E703, + E741, + W191, + W291, + W292, + W293, + W391, + W503, + W504, + E711, + E129, + F841, + E713, + E712, + +max-line-length = 200 +exclude=,test_*.py diff --git a/check_run/check_run/doctype/check_run/check_run.js b/check_run/check_run/doctype/check_run/check_run.js index f37e95c9..ccf06e3d 100644 --- a/check_run/check_run/doctype/check_run/check_run.js +++ b/check_run/check_run/doctype/check_run/check_run.js @@ -62,6 +62,20 @@ frappe.ui.form.on('Check Run', { $(frm.wrapper).on('dirty', () => { frm.trigger('update_primary_action') }) + if (frm.doc.__onload && frm.doc.__onload.check_run_submitting == frm.doc.name) { + frm.doc.status = 'Submitting' + frm.page.set_indicator(__('Submitting'), 'orange') + frm.disable_form() + cur_frm.$check_run.$children[0].state.status = 'Submitting' + } else if (frm.doc.__onload && frm.doc.__onload.check_run_submitting) { + frm.set_intro( + __( + `Check Run ${frm.doc.__onload.check_run_submitting} is processing. This Check Run cannot be processed until it completes.` + ), + 'red' + ) + cur_frm.$check_run.$children[0].state.status = 'Submitting' + } }, end_date: frm => { get_entries(frm) @@ -97,14 +111,17 @@ frappe.ui.form.on('Check Run', { frm.doc.status = 'Submitting' frm.page.set_indicator(__('Submitting'), 'orange') frm.disable_form() - $(frm.$check_run).css({ 'pointer-events': 'none' }) frappe.xcall('check_run.check_run.doctype.check_run.check_run.process_check_run', { docname: frm.doc.name }) + cur_frm.$check_run.$children[0].state.status = frm.doc.status }, update_primary_action: frm => { frm.disable_save() if (frm.is_dirty()) { frm.enable_save() - } else if (frm.doc.status === 'Draft') { + } else if ((frm.doc.__onload && frm.doc.__onload.check_run_submitting) || frm.doc.status == 'Submitting') { + frm.disable_save() + frm.disable_form() + } else if (frm.doc.status == 'Draft' && !(frm.doc.__onload && frm.doc.__onload.check_run_submitting)) { frm.page.set_primary_action(__('Process Check Run'), () => frm.trigger('process_check_run')) } }, @@ -141,7 +158,7 @@ function get_entries(frm) { check_run.mount_table(frm) if (!frappe.user.has_role(['Accounts Manager'])) { frm.disable_form() - frm.$check_run.css({ 'pointer-events': 'none' }) + cur_frm.$check_run.$children[0].state.status = frm.doc.status } }) } diff --git a/check_run/check_run/doctype/check_run/check_run.json b/check_run/check_run/doctype/check_run/check_run.json index 4cbd6886..3181fabc 100644 --- a/check_run/check_run/doctype/check_run/check_run.json +++ b/check_run/check_run/doctype/check_run/check_run.json @@ -11,6 +11,7 @@ "end_date", "posting_date", "beg_balance", + "company_discretionary_data", "column_break_3", "initial_check_number", "final_check_number", @@ -19,10 +20,10 @@ "company", "bank_account", "pay_to_account", + "amended_from", "section_break_9", "check_run_table", "transactions", - "amended_from", "print_count", "status" ], @@ -95,6 +96,14 @@ "remember_last_selected_value": 1, "reqd": 1 }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.docstatus==1", + "fieldname": "company_discretionary_data", + "fieldtype": "Data", + "label": "Company Discretionary Data", + "length": 20 + }, { "fieldname": "section_break_9", "fieldtype": "Section Break" @@ -135,7 +144,7 @@ "fieldname": "status", "fieldtype": "Select", "hidden": 1, - "options": "Draft\nSubmitted\nSubmitting\nReady to Print\nConfirm Print\nPrinted" + "options": "Draft\nSubmitting\nSubmitted\nReady to Print\nConfirm Print\nPrinted" }, { "fieldname": "check_run_table", @@ -151,11 +160,10 @@ ], "is_submittable": 1, "links": [], - "modified": "2023-02-28 16:55:18.369852", + "modified": "2023-03-10 12:28:36.910004", "modified_by": "Administrator", "module": "Check Run", "name": "Check Run", - "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -182,7 +190,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", - "states": [], "track_changes": 1, "track_seen": 1, "track_views": 1 diff --git a/check_run/check_run/doctype/check_run/check_run.py b/check_run/check_run/doctype/check_run/check_run.py index 90048b71..a41678e0 100644 --- a/check_run/check_run/doctype/check_run/check_run.py +++ b/check_run/check_run/doctype/check_run/check_run.py @@ -1,29 +1,26 @@ # Copyright (c) 2022, AgriTheory and contributors # For license information, please see license.txt -from __future__ import unicode_literals import datetime import json from itertools import groupby, zip_longest from io import StringIO -import types from PyPDF2 import PdfFileWriter import frappe from frappe.model.document import Document from frappe.utils.data import flt -from frappe.utils.data import date_diff, add_days, nowdate, getdate, now, get_datetime +from frappe.utils.data import nowdate, getdate, now, get_datetime from frappe.utils.print_format import read_multi_pdf from frappe.permissions import has_permission -from frappe.utils.file_manager import save_file, remove_all, download_file +from frappe.utils.file_manager import save_file, remove_all from frappe.utils.password import get_decrypted_password -from frappe.contacts.doctype.address.address import get_default_address +from frappe.contacts.doctype.address.address import get_default_address from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.functions import Coalesce from erpnext.accounts.utils import get_balance_on -from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions from atnacha import ACHEntry, ACHBatch, NACHAFile from check_run.check_run.doctype.check_run_settings.check_run_settings import create @@ -35,14 +32,19 @@ def onload(self): return settings = get_check_run_settings(self) if not settings: - self.set_onload('settings_missing', True) - errors = frappe.get_all('Error Log', {'method': ['like', f"%{self.name}%"]}) + self.set_onload("settings_missing", True) + errors = frappe.get_all("Error Log", {"method": ["like", f"%{self.name}%"]}) if errors and self.docstatus == 0: - self.set_onload('errors', True) + self.set_onload("errors", True) + check_run_submitting = frappe.defaults.get_global_default("check_run_submitting") + if check_run_submitting: + self.set_onload("check_run_submitting", check_run_submitting) + else: + self.set_onload("check_run_submitting", False) def validate(self): self.set_status() - gl_account = frappe.get_value('Bank Account', self.bank_account, 'account') + gl_account = frappe.get_value("Bank Account", self.bank_account, "account") if not gl_account: frappe.throw(frappe._("This Bank Account is not associated with a General Ledger Account.")) self.beg_balance = get_balance_on(gl_account, self.posting_date) @@ -57,39 +59,39 @@ def validate(self): def on_cancel(self): settings = get_check_run_settings(self) if not settings.allow_cancellation: - frappe.throw(frappe._('The settings for this Check Run do not allow cancellation')) + frappe.throw(frappe._("The settings for this Check Run do not allow cancellation")) if settings.allow_cancellation and settings.cascade_cancellation: # cancel all PEs linked to this check run - pes = frappe.get_all('Payment Entry', {'check_run': self.name, 'docstatus': 1}) + pes = frappe.get_all("Payment Entry", {"check_run": self.name, "docstatus": 1}) for pe in pes: - frappe.get_doc('Payment Entry', pe).cancel() + frappe.get_doc("Payment Entry", pe).cancel() if settings.allow_cancellation and not settings.cascade_cancellation: # unlink all PE's linked to this check run - pes = frappe.get_all('Payment Entry', {'check_run': self.name, 'docstatus': 1}) + pes = frappe.get_all("Payment Entry", {"check_run": self.name, "docstatus": 1}) for pe in pes: - frappe.db.set_value('Payment Entry', pe, 'check_run', '') + frappe.db.set_value("Payment Entry", pe, "check_run", "") def set_status(self, status=None): if status: self.status = status - elif self.status == 'Confirm Print': + elif self.status == "Confirm Print": pass elif self.docstatus == 0: - self.status = 'Draft' + self.status = "Draft" elif self.docstatus == 1 and self.print_count > 0: - self.status = 'Printed' + self.status = "Printed" elif self.docstatus == 1: - self.status = 'Submitted' + self.status = "Submitted" def set_last_check_number(self): if self.ach_only().ach_only: return - check_number = frappe.get_value('Bank Account', self.bank_account, "check_number") + check_number = frappe.get_value("Bank Account", self.bank_account, "check_number") self.initial_check_number = int(check_number or 0) + 1 def set_default_payable_account(self): if not self.pay_to_account: - self.pay_to_account = frappe.get_value('Company', self.company, "default_payable_account") + self.pay_to_account = frappe.get_value("Company", self.company, "default_payable_account") def set_default_dates(self): if not self.posting_date: @@ -98,28 +100,40 @@ def set_default_dates(self): self.end_date = getdate() def validate_transactions(self): - if not self.get('transactions'): + if not self.get("transactions"): return - selected = [txn for txn in json.loads(self.get('transactions')) if txn['pay']] + selected = [txn for txn in json.loads(self.get("transactions")) if txn["pay"]] wrong_status = [] for t in selected: - if not t['mode_of_payment']: + if not t["mode_of_payment"]: frappe.throw(frappe._(f"Mode of Payment Required: {t['party_name']} {t['ref_number']}")) - filters = {'name': t['name'] if t['doctype'] != 'Journal Entry' else t['ref_number']} - if frappe.get_value(t['doctype'], filters, 'docstatus') != 1: - wrong_status.append({'party_name': t['party_name'], 'ref_number': t['ref_number'] or '', 'name': t['name']}) + filters = {"name": t["name"] if t["doctype"] != "Journal Entry" else t["ref_number"]} + if frappe.get_value(t["doctype"], filters, "docstatus") != 1: + wrong_status.append( + {"party_name": t["party_name"], "ref_number": t["ref_number"] or "", "name": t["name"]} + ) if len(wrong_status) < 1: return - invalid_records = '' + invalid_records = "" for invalid_record in wrong_status: - invalid_records += ' '.join(invalid_record.values()) + '
' - frappe.throw(frappe._( - f"The following document(s) have been cancelled, please remove them from Check Run to continue:
{invalid_records}" - )) + invalid_records += " ".join(invalid_record.values()) + "
" + frappe.throw( + frappe._( + f"The following document(s) have been cancelled, please remove them from Check Run to continue:
{invalid_records}" + ) + ) @frappe.whitelist() def process_check_run(self): - self.status = 'Submitting' + # check_run_submitting = frappe.defaults.get_global_default("check_run_submitting") + # if check_run_submitting: + # frappe.throw( + # frappe._( + # f"""Check run {check_run_submitting} is in process. No other check runs can be submitted until it completes. Click here for details.""" + # ) + # ) + # return + self.status = "Submitting" transactions = self.transactions transactions = json.loads(transactions) if len(transactions) < 1: @@ -128,51 +142,65 @@ def process_check_run(self): if self.ach_only().ach_only: self.initial_check_number = "" self.final_check_number = "" - frappe.enqueue_doc(self.doctype, self.name, "_process_check_run", save=True, queue="short", timeout=3600) + frappe.enqueue_doc( + self.doctype, self.name, "_process_check_run", save=True, queue="short", timeout=3600, now=True + ) def _process_check_run(self, save=False): - savepoint = "process_check_run" - frappe.db.savepoint(savepoint) + frappe.defaults.set_global_default("check_run_submitting", self.name) + frappe.db.sql("SAVEPOINT process_check_run") try: transactions = self.transactions transactions = json.loads(transactions) - transactions = sorted([frappe._dict(item) for item in transactions if item.get("pay")], key=lambda x: x.party) + transactions = sorted( + (frappe._dict(item) for item in transactions if item.get("pay")), key=lambda x: x.party + ) _transactions = self.create_payment_entries(transactions) except Exception as e: - frappe.db.rollback(savepoint="process_check_run") + try: + frappe.db.rollback(save_point="process_check_run") + except Exception as _e: + pass + frappe.defaults.clear_default("check_run_submitting") raise e + frappe.defaults.clear_default("check_run_submitting") self.transactions = json.dumps(_transactions) - self.set_status('Submitted') + self.set_status("Submitted") self.save() self.submit() - if self.final_check_number: - frappe.db.set_value('Bank Account', self.bank_account, 'check_number', self.final_check_number) - frappe.publish_realtime('reload', '{}', doctype=self.doctype, docname=self.name) + frappe.db.sql("RELEASE SAVEPOINT process_check_run") + frappe.publish_realtime("reload", "{}", doctype=self.doctype, docname=self.name) def build_nacha_file(self, settings=None): - electronic_mop = frappe.get_all('Mode of Payment', {'type': 'Electronic', 'enabled': 1}, 'name', pluck="name") - ach_payment_entries = list(set( - [e.get('payment_entry') for e in json.loads(self.transactions) if e.get('mode_of_payment') in electronic_mop] - )) - payment_entries = [frappe.get_doc('Payment Entry', pe) for pe in ach_payment_entries] - nacha_file = build_nacha_file_from_payment_entries(self, payment_entries, settings) - ach_file = StringIO(nacha_file()) - ach_file.seek(0) - return ach_file + electronic_mop = frappe.get_all( + "Mode of Payment", {"type": "Electronic", "enabled": 1}, "name", pluck="name" + ) + ach_payment_entries = list( + { + e.get("payment_entry") + for e in json.loads(self.transactions) + if e.get("mode_of_payment") in electronic_mop + } + ) + payment_entries = [frappe.get_doc("Payment Entry", pe) for pe in ach_payment_entries] + return build_nacha_file_from_payment_entries(self, payment_entries, settings) @frappe.whitelist() def ach_only(self): transactions = json.loads(self.transactions) if self.transactions else [] - ach_only = frappe._dict({'ach_only': True, 'print_checks_only': True}) + ach_only = frappe._dict({"ach_only": True, "print_checks_only": True}) if not self.transactions: ach_only.ach_only = False ach_only.print_checks_only = False return ach_only - eft_mapping = {mop.name: mop.type for mop in frappe.get_all("Mode of Payment", {'enabled': True}, ['name', 'type'])} - if any([eft_mapping.get(t.get('mode_of_payment')) == 'Bank' for t in transactions]): + eft_mapping = { + mop.name: mop.type + for mop in frappe.get_all("Mode of Payment", {"enabled": True}, ["name", "type"]) + } + if any([eft_mapping.get(t.get("mode_of_payment")) == "Bank" for t in transactions]): ach_only.ach_only = False - if any([eft_mapping.get(t.get('mode_of_payment')) == 'Electronic' for t in transactions]): + if any([eft_mapping.get(t.get("mode_of_payment")) == "Electronic" for t in transactions]): ach_only.print_checks_only = False return ach_only @@ -183,30 +211,33 @@ def create_payment_entries(self, transactions): split = settings.number_of_invoices_per_voucher check_count = 0 _transactions = [] - gl_account = frappe.get_value('Bank Account', self.bank_account, 'account') - key_lookup = lambda x: x.party + gl_account = frappe.get_value("Bank Account", self.bank_account, "account") + key_lookup = lambda x: x.party # noqa: E731 if settings and settings.split_by_address: - key_lookup = lambda x: (x.get('party'), x.get('address')) + key_lookup = lambda x: (x.get("party"), x.get("address")) # noqa: E731 for transaction in transactions: - transaction['address'] = get_address( - transaction.get('party'), transaction.get('party_type'), transaction.get('doctype'), transaction.get('name') + transaction["address"] = get_address( + transaction.get("party"), + transaction.get("party_type"), + transaction.get("doctype"), + transaction.get("name"), ) - for party, _group in groupby(transactions, key=key_lookup): - _group = list(_group) - if frappe.db.get_value('Mode of Payment', _group[0].mode_of_payment, 'type') == 'Bank': - groups = list(zip_longest(*[iter(_group)] * split)) + for party, __group in groupby(transactions, key=key_lookup): + _group = list(__group) + if frappe.db.get_value("Mode of Payment", _group[0].mode_of_payment, "type") == "Bank": + groups = list(zip_longest(*[iter(_group)] * split)) else: groups = [_group] if not groups: continue for group in groups: _references = [] - if group[0].doctype == 'Purchase Invoice': - party = frappe.db.get_value('Purchase Invoice', group[0].name, 'supplier') - elif group[0].doctype == 'Expense Claim': - party = frappe.db.get_value('Expense Claim', group[0].name, 'employee') - elif group[0].doctype == 'Journal Entry': - party = frappe.db.get_value('Journal Entry Account', group[0].name, 'party') + if group[0].doctype == "Purchase Invoice": + party = frappe.db.get_value("Purchase Invoice", group[0].name, "supplier") + elif group[0].doctype == "Expense Claim": + party = frappe.db.get_value("Expense Claim", group[0].name, "employee") + elif group[0].doctype == "Journal Entry": + party = frappe.db.get_value("Journal Entry Account", group[0].name, "party") pe = frappe.new_doc("Payment Entry") pe.payment_type = "Pay" pe.posting_date = nowdate() @@ -215,38 +246,47 @@ def create_payment_entries(self, transactions): pe.bank_account = self.bank_account pe.paid_from = gl_account pe.paid_to = self.pay_to_account - pe.paid_to_account_currency = frappe.db.get_value("Account", self.bank_account, "account_currency") + pe.paid_to_account_currency = frappe.db.get_value( + "Account", self.bank_account, "account_currency" + ) pe.paid_from_account_currency = pe.paid_to_account_currency pe.reference_date = self.posting_date pe.party_type = group[0].party_type - pe.party = group[0].party + pe.party = party pe.check_run = self.name total_amount = 0 - if frappe.db.get_value('Mode of Payment', _group[0].mode_of_payment, 'type') == 'Bank': + if frappe.db.get_value("Mode of Payment", _group[0].mode_of_payment, "type") == "Bank": pe.reference_no = int(self.initial_check_number) + check_count check_count += 1 self.final_check_number = pe.reference_no else: - pe.reference_no = frappe._(f"via {_group[0].mode_of_payment} {self.get_formatted('posting_date')}") - + pe.reference_no = frappe._( + f"via {_group[0].mode_of_payment} {self.get_formatted('posting_date')}" + ) + for reference in group: if not reference: continue - if settings.automatically_release_on_hold_invoices and reference.doctype == 'Purchase Invoice': - if frappe.get_value(reference.doctype, reference.name, 'on_hold'): - frappe.db.set_value(reference.doctype, reference.name, 'on_hold', 0) - if reference.doctype == 'Journal Entry': + if ( + settings.automatically_release_on_hold_invoices and reference.doctype == "Purchase Invoice" + ): + if frappe.get_value(reference.doctype, reference.name, "on_hold"): + frappe.db.set_value(reference.doctype, reference.name, "on_hold", 0) + if reference.doctype == "Journal Entry": reference_name = reference.ref_number else: reference_name = reference.name or reference.ref_number - pe.append('references', { + pe.append( + "references", + { "reference_doctype": reference.doctype, "reference_name": reference_name, "due_date": reference.get("due_date"), "outstanding_amount": flt(reference.amount), "allocated_amount": flt(reference.amount), "total_amount": flt(reference.amount), - }) + }, + ) total_amount += reference.amount reference.check_number = pe.reference_no _references.append(reference) @@ -261,55 +301,63 @@ def create_payment_entries(self, transactions): except Exception as e: frappe.db.rollback() frappe.log_error(title=f"{self.name} Check Run Error", message=e) - frappe.publish_realtime('reload', '{}', doctype=self.doctype, docname=self.name) + frappe.publish_realtime("reload", "{}", doctype=self.doctype, docname=self.name) raise e for reference in _references: reference.payment_entry = pe.name _transactions.append(reference) return _transactions - @frappe.whitelist() def increment_print_count(self, reprint_check_number=None): - print('render checks') - frappe.enqueue_doc(self.doctype, self.name, 'render_check_pdf', reprint_check_number=reprint_check_number, queue='short', now=True) - + frappe.enqueue_doc( + self.doctype, + self.name, + "render_check_pdf", + reprint_check_number=reprint_check_number, + queue="short", + now=True, + ) @frappe.whitelist() def render_check_pdf(self, reprint_check_number=None): self.print_count = self.print_count + 1 - self.set_status('Submitted') - if not frappe.db.exists('File', 'Home/Check Run'): - try: + self.set_status("Submitted") + if not frappe.db.exists("File", "Home/Check Run"): + try: cr_folder = frappe.new_doc("File") - cr_folder.update({"file_name":"Check Run", "is_folder": True, "folder":"Home"}) + cr_folder.update({"file_name": "Check Run", "is_folder": True, "folder": "Home"}) cr_folder.save() except Exception as e: pass settings = get_check_run_settings(self) initial_check_number = int(self.initial_check_number) - if reprint_check_number and reprint_check_number != 'undefined': + if reprint_check_number and reprint_check_number != "undefined": self.initial_check_number = int(reprint_check_number) output = PdfFileWriter() transactions = json.loads(self.transactions) check_increment = 0 _transactions = [] - for pe, group in groupby(transactions, key=lambda x: x.get('payment_entry')): - group = list(group) - mode_of_payment, docstatus = frappe.db.get_value('Payment Entry', pe, ['mode_of_payment', 'docstatus']) or (None, None) - if docstatus == 1 and frappe.db.get_value('Mode of Payment', mode_of_payment, 'type') == 'Bank': + for pe, _group in groupby(transactions, key=lambda x: x.get("payment_entry")): + group = list(_group) + mode_of_payment, docstatus = frappe.db.get_value( + "Payment Entry", pe, ["mode_of_payment", "docstatus"] + ) or (None, None) + if docstatus == 1 and frappe.db.get_value("Mode of Payment", mode_of_payment, "type") == "Bank": output = frappe.get_print( - 'Payment Entry', + "Payment Entry", pe, - settings.print_format or frappe.get_meta('Payment Entry').default_print_format, + settings.print_format or frappe.get_meta("Payment Entry").default_print_format, as_pdf=True, output=output, no_letterhead=0, ) if initial_check_number != reprint_check_number: - frappe.db.set_value('Payment Entry', pe, 'reference_no', self.initial_check_number + check_increment) + frappe.db.set_value( + "Payment Entry", pe, "reference_no", self.initial_check_number + check_increment + ) for ref in group: - ref['check_number'] = self.initial_check_number + check_increment + ref["check_number"] = self.initial_check_number + check_increment _transactions.append(ref) check_increment += 1 elif docstatus == 1: @@ -317,30 +365,33 @@ def render_check_pdf(self, reprint_check_number=None): _transactions.append(ref) if _transactions and reprint_check_number: - self.db_set('transactions', json.dumps(_transactions)) - self.db_set('final_check_number', self.initial_check_number + (check_increment - 1)) - self.db_set('status', 'Ready to Print') - self.db_set('print_count', self.print_count) - frappe.db.set_value('Bank Account', self.bank_account, 'check_number', self.final_check_number) - save_file(f"{self.name}.pdf", read_multi_pdf(output), 'Check Run', self.name, 'Home/Check Run', False, 0) + self.db_set("transactions", json.dumps(_transactions)) + self.db_set("initial_check_number", self.initial_check_number) + self.db_set("final_check_number", self.initial_check_number + (check_increment - 1)) + self.db_set("status", "Ready to Print") + self.db_set("print_count", self.print_count) + frappe.db.set_value("Bank Account", self.bank_account, "check_number", self.final_check_number) + save_file( + f"{self.name}.pdf", read_multi_pdf(output), "Check Run", self.name, "Home/Check Run", False, 0 + ) frappe.db.commit() - frappe.publish_realtime('reload', '{}', doctype=self.doctype, docname=self.name) + frappe.publish_realtime("reload", "{}", doctype=self.doctype, docname=self.name) @frappe.whitelist() def check_for_draft_check_run(company, bank_account, payable_account): existing = frappe.get_value( - 'Check Run', { - 'company': company, - 'bank_account': bank_account, - 'pay_to_account': payable_account, - 'status': ['in', ['Draft', 'Submitted']], - 'initial_check_number': ['!=', 0] - } + "Check Run", + { + "company": company, + "bank_account": bank_account, + "pay_to_account": payable_account, + "docstatus": 0, + }, ) if existing: return existing - cr = frappe.new_doc('Check Run') + cr = frappe.new_doc("Check Run") cr.company = company cr.bank_account = bank_account cr.pay_to_account = payable_account @@ -351,129 +402,141 @@ def check_for_draft_check_run(company, bank_account, payable_account): @frappe.whitelist() def confirm_print(docname): # Remove PDF file(s) - remove_all('Check Run', docname, from_delete=False, delete_permanently=False) - + remove_all("Check Run", docname, from_delete=False, delete_permanently=False) + # Reset status - return frappe.db.set_value('Check Run', docname, 'status', 'Printed') + return frappe.db.set_value("Check Run", docname, "status", "Printed") @frappe.whitelist() def get_entries(doc): doc = frappe._dict(json.loads(doc)) if isinstance(doc, str) else doc if isinstance(doc.end_date, str): - doc.end_date = getdate(doc.end_date) - doc.posting_date = getdate(doc.posting_date) - modes_of_payment = frappe.get_all('Mode of Payment', order_by='name') - if frappe.db.exists('Check Run Settings', {'bank_account': doc.bank_account, 'pay_to_account': doc.pay_to_account}): - settings = frappe.get_doc('Check Run Settings', {'bank_account': doc.bank_account, 'pay_to_account': doc.pay_to_account}) + doc.end_date = getdate(doc.end_date) + doc.posting_date = getdate(doc.posting_date) + modes_of_payment = frappe.get_all("Mode of Payment", order_by="name") + if frappe.db.exists( + "Check Run Settings", {"bank_account": doc.bank_account, "pay_to_account": doc.pay_to_account} + ): + settings = frappe.get_doc( + "Check Run Settings", {"bank_account": doc.bank_account, "pay_to_account": doc.pay_to_account} + ) else: settings = None - if frappe.db.exists('Check Run', doc.name): - db_doc = frappe.get_doc('Check Run', doc.name) + if frappe.db.exists("Check Run", doc.name): + db_doc = frappe.get_doc("Check Run", doc.name) if doc.end_date == db_doc.end_date and db_doc.transactions: - return {'transactions': json.loads(db_doc.transactions), 'modes_of_payment': modes_of_payment} + return {"transactions": json.loads(db_doc.transactions), "modes_of_payment": modes_of_payment} company = doc.company pay_to_account = doc.pay_to_account end_date = doc.end_date # Build purchase invoices query - purchase_invoices = frappe.qb.DocType('Purchase Invoice') - suppliers = frappe.qb.DocType('Supplier') + purchase_invoices = frappe.qb.DocType("Purchase Invoice") + suppliers = frappe.qb.DocType("Supplier") pi_qb = ( frappe.qb.from_(purchase_invoices) - .inner_join(suppliers) - .on(purchase_invoices.supplier == suppliers.name) - .select( - ConstantColumn('Purchase Invoice').as_('doctype'), - ConstantColumn('Supplier').as_('party_type'), - purchase_invoices.name, - (purchase_invoices.bill_no).as_('ref_number'), - (purchase_invoices.supplier_name).as_('party'), - (suppliers.supplier_name).as_('party_name'), - (purchase_invoices.outstanding_amount).as_('amount'), - purchase_invoices.due_date, - purchase_invoices.posting_date, - Coalesce(purchase_invoices.supplier_default_mode_of_payment, suppliers.supplier_default_mode_of_payment, '\n').as_('mode_of_payment') - ) - .where(purchase_invoices.outstanding_amount > 0) - .where(purchase_invoices.company == company) - .where(purchase_invoices.docstatus == 1) - .where(purchase_invoices.credit_to == pay_to_account) - .where(purchase_invoices.due_date <= end_date) - .where(Coalesce(purchase_invoices.release_date, datetime.date(1900, 1, 1)) < end_date) + .inner_join(suppliers) + .on(purchase_invoices.supplier == suppliers.name) + .select( + ConstantColumn("Purchase Invoice").as_("doctype"), + ConstantColumn("Supplier").as_("party_type"), + purchase_invoices.name, + (purchase_invoices.bill_no).as_("ref_number"), + (purchase_invoices.supplier_name).as_("party"), + (suppliers.supplier_name).as_("party_name"), + (purchase_invoices.outstanding_amount).as_("amount"), + purchase_invoices.due_date, + purchase_invoices.posting_date, + Coalesce( + purchase_invoices.supplier_default_mode_of_payment, + suppliers.supplier_default_mode_of_payment, + "\n", + ).as_("mode_of_payment"), + ) + .where(purchase_invoices.outstanding_amount > 0) + .where(purchase_invoices.company == company) + .where(purchase_invoices.docstatus == 1) + .where(purchase_invoices.credit_to == pay_to_account) + .where(purchase_invoices.due_date <= end_date) + .where(Coalesce(purchase_invoices.release_date, datetime.date(1900, 1, 1)) < end_date) ) # Build expense claims query - exp_claims = frappe.qb.DocType('Expense Claim') - employees = frappe.qb.DocType('Employee') + exp_claims = frappe.qb.DocType("Expense Claim") + employees = frappe.qb.DocType("Employee") ec_qb = ( frappe.qb.from_(exp_claims) - .inner_join(employees) - .on(exp_claims.employee == employees.name) - .select( - ConstantColumn('Expense Claim').as_('doctype'), - ConstantColumn('Employee').as_('party_type'), - exp_claims.name, - (exp_claims.name).as_('ref_number'), - (exp_claims.employee).as_('party'), - (employees.employee_name).as_('party_name'), - (exp_claims.grand_total).as_('amount'), - (exp_claims.posting_date).as_('due_date'), - exp_claims.posting_date, - Coalesce(exp_claims.mode_of_payment, employees.mode_of_payment, '\n').as_('mode_of_payment') - ) - .where(exp_claims.grand_total > exp_claims.total_amount_reimbursed) - .where(exp_claims.company == company) - .where(exp_claims.docstatus == 1) - .where(exp_claims.payable_account == pay_to_account) - .where(exp_claims.posting_date <= end_date) + .inner_join(employees) + .on(exp_claims.employee == employees.name) + .select( + ConstantColumn("Expense Claim").as_("doctype"), + ConstantColumn("Employee").as_("party_type"), + exp_claims.name, + (exp_claims.name).as_("ref_number"), + (exp_claims.employee).as_("party"), + (employees.employee_name).as_("party_name"), + (exp_claims.grand_total).as_("amount"), + (exp_claims.posting_date).as_("due_date"), + exp_claims.posting_date, + Coalesce(exp_claims.mode_of_payment, employees.mode_of_payment, "\n").as_("mode_of_payment"), + ) + .where(exp_claims.grand_total > exp_claims.total_amount_reimbursed) + .where(exp_claims.company == company) + .where(exp_claims.docstatus == 1) + .where(exp_claims.payable_account == pay_to_account) + .where(exp_claims.posting_date <= end_date) ) # Build journal entries query - journal_entries = frappe.qb.DocType('Journal Entry') - je_accounts = frappe.qb.DocType('Journal Entry Account') - payment_entries = frappe.qb.DocType('Payment Entry') - pe_ref = frappe.qb.DocType('Payment Entry Reference') - + journal_entries = frappe.qb.DocType("Journal Entry") + je_accounts = frappe.qb.DocType("Journal Entry Account") + payment_entries = frappe.qb.DocType("Payment Entry") + pe_ref = frappe.qb.DocType("Payment Entry Reference") + sub_q = ( frappe.qb.from_(payment_entries) - .inner_join(pe_ref) - .on(payment_entries.name == pe_ref.parent) - .select(pe_ref.reference_name) - .where(pe_ref.reference_doctype == 'Journal Entry') - .where(payment_entries.party == je_accounts.party) - .where(payment_entries.docstatus == 1) + .inner_join(pe_ref) + .on(payment_entries.name == pe_ref.parent) + .select(pe_ref.reference_name) + .where(pe_ref.reference_doctype == "Journal Entry") + .where(payment_entries.party == je_accounts.party) + .where(payment_entries.docstatus == 1) ) je_qb = ( frappe.qb.from_(journal_entries) - .inner_join(je_accounts) - .on(journal_entries.name == je_accounts.parent) - .select( - ConstantColumn('Journal Entry').as_('doctype'), - je_accounts.party_type, - je_accounts.name, - (journal_entries.name).as_('ref_number'), - je_accounts.party, - (je_accounts.party).as_('party_name'), - (je_accounts.credit_in_account_currency).as_('amount'), - journal_entries.due_date, - journal_entries.posting_date, - Coalesce(journal_entries.mode_of_payment, '\n').as_('mode_of_payment') - ) - .where(journal_entries.company == company) - .where(journal_entries.docstatus == 1) - .where(je_accounts.account == pay_to_account) - .where(journal_entries.due_date <= end_date) - .where((journal_entries.name).notin(sub_q)) + .inner_join(je_accounts) + .on(journal_entries.name == je_accounts.parent) + .select( + ConstantColumn("Journal Entry").as_("doctype"), + je_accounts.party_type, + je_accounts.name, + (journal_entries.name).as_("ref_number"), + je_accounts.party, + (je_accounts.party).as_("party_name"), + (je_accounts.credit_in_account_currency).as_("amount"), + journal_entries.due_date, + journal_entries.posting_date, + Coalesce(journal_entries.mode_of_payment, "\n").as_("mode_of_payment"), + ) + .where(journal_entries.company == company) + .where(journal_entries.docstatus == 1) + .where(je_accounts.account == pay_to_account) + .where(journal_entries.due_date <= end_date) + .where((journal_entries.name).notin(sub_q)) ) - + if not settings: query = pi_qb.union(ec_qb).union(je_qb) else: query = "" - flags = (settings.include_purchase_invoices, settings.include_expense_claims, settings.include_journal_entries) + flags = ( + settings.include_purchase_invoices, + settings.include_expense_claims, + settings.include_journal_entries, + ) for flag, qb in zip(flags, (pi_qb, ec_qb, je_qb)): if not flag: continue @@ -482,24 +545,28 @@ def get_entries(doc): else: query = query.union(qb) if query: - query = query.orderby('due_date', 'name').get_sql() + query = query.orderby("due_date", "name").get_sql() - transactions = frappe.db.sql(query, { - 'company': company, 'pay_to_account': pay_to_account, 'end_date': end_date - }, as_dict=True) + transactions = frappe.db.sql( + query, {"company": company, "pay_to_account": pay_to_account, "end_date": end_date}, as_dict=True + ) for transaction in transactions: if settings and settings.pre_check_overdue_items: if transaction.due_date < doc.posting_date: transaction.pay = 1 - if transaction.doctype == 'Journal Entry': - if transaction.party_type == 'Supplier': - transaction.party_name = frappe.get_value('Supplier', transaction.party, 'supplier_name') - transaction.mode_of_payment = frappe.get_value('Supplier', transaction.party, 'supplier_default_mode_of_payment') - if transaction.party_type == 'Employee': - transaction.party_name = frappe.get_value('Employee', transaction.party, 'employee_name') - transaction.mode_of_payment = frappe.get_value('Employee', transaction.party, 'mode_of_payment') + if transaction.doctype == "Journal Entry": + if transaction.party_type == "Supplier": + transaction.party_name = frappe.get_value("Supplier", transaction.party, "supplier_name") + transaction.mode_of_payment = frappe.get_value( + "Supplier", transaction.party, "supplier_default_mode_of_payment" + ) + if transaction.party_type == "Employee": + transaction.party_name = frappe.get_value("Employee", transaction.party, "employee_name") + transaction.mode_of_payment = frappe.get_value( + "Employee", transaction.party, "mode_of_payment" + ) - return {'transactions': transactions, 'modes_of_payment': modes_of_payment} + return {"transactions": transactions, "modes_of_payment": modes_of_payment} @frappe.whitelist() @@ -507,32 +574,42 @@ def get_balance(doc): doc = frappe._dict(json.loads(doc)) if isinstance(doc, str) else doc if not doc.bank_account or not doc.posting_date: return - gl_account = frappe.get_value('Bank Account', doc.bank_account, 'account') + gl_account = frappe.get_value("Bank Account", doc.bank_account, "account") return get_balance_on(gl_account, doc.posting_date) @frappe.whitelist() def download_checks(docname): - has_permission('Payment Entry', ptype="print", verbose=False, user=frappe.session.user, raise_exception=True) - file_name = frappe.get_value('File', {'attached_to_name': docname}) - frappe.db.set_value('Check Run', docname, 'status', "Confirm Print") - return frappe.get_value('File', file_name, 'file_url') + has_permission( + "Payment Entry", ptype="print", verbose=False, user=frappe.session.user, raise_exception=True + ) + file_name = frappe.get_value("File", {"attached_to_name": docname}) + frappe.db.set_value("Check Run", docname, "status", "Confirm Print") + return frappe.get_value("File", file_name, "file_url") @frappe.whitelist() def download_nacha(docname): - has_permission('Payment Entry', ptype="print", verbose=False, user=frappe.session.user, raise_exception=True) - doc = frappe.get_doc('Check Run', docname) + has_permission( + "Payment Entry", ptype="print", verbose=False, user=frappe.session.user, raise_exception=True + ) + doc = frappe.get_doc("Check Run", docname) settings = get_check_run_settings(doc) ach_file = doc.build_nacha_file(settings) + if settings.custom_post_processing_hook: + ach_file = frappe.call(settings.custom_post_processing_hook, doc, settings, ach_file) + else: + ach_file = ach_file() + ach_file = StringIO(ach_file) + ach_file.seek(0) file_ext = settings.ach_file_extension if settings and settings.ach_file_extension else "ach" frappe.local.response.filename = f'{docname.replace(" ", "-").replace("/", "-")}.{file_ext}' frappe.local.response.type = "download" frappe.local.response.filecontent = ach_file.read() - comment = frappe.new_doc('Comment') + comment = frappe.new_doc("Comment") comment.owner = "Administrator" - comment.comment_type = 'Info' - comment.reference_doctype = 'Check Run' + comment.comment_type = "Info" + comment.reference_doctype = "Check Run" comment.reference_name = doc.name comment.published = 1 comment.content = f"{frappe.session.user} created a NACHA file on {now()}" @@ -544,58 +621,67 @@ def download_nacha(docname): def build_nacha_file_from_payment_entries(doc, payment_entries, settings): ach_entries = [] exceptions = [] - company_bank = frappe.db.get_value('Bank Account', doc.bank_account, 'bank') + company_bank = frappe.db.get_value("Bank Account", doc.bank_account, "bank") if not company_bank: - exceptions.append(f'Company Bank missing for {doc.company}') + exceptions.append(f"Company Bank missing for {doc.company}") if company_bank: - company_bank_aba_number = frappe.db.get_value('Bank', company_bank, 'aba_number') - company_bank_account_no = frappe.db.get_value('Bank Account', doc.bank_account, 'bank_account_no') - company_ach_id = frappe.db.get_value('Bank Account', doc.bank_account, 'company_ach_id') + company_bank_aba_number = frappe.db.get_value("Bank", company_bank, "aba_number") + company_bank_account_no = frappe.db.get_value( + "Bank Account", doc.bank_account, "bank_account_no" + ) + company_ach_id = frappe.db.get_value("Bank Account", doc.bank_account, "company_ach_id") if company_bank and not company_bank_aba_number: - exceptions.append(f'Company Bank ABA Number missing for {doc.bank_account}') + exceptions.append(f"Company Bank ABA Number missing for {doc.bank_account}") if company_bank and not company_bank_account_no: - exceptions.append(f'Company Bank Account Number missing for {doc.bank_account}') + exceptions.append(f"Company Bank Account Number missing for {doc.bank_account}") if company_bank and not company_ach_id: - exceptions.append(f'Company Bank ACH ID missing for {doc.bank_account}') + exceptions.append(f"Company Bank ACH ID missing for {doc.bank_account}") for pe in payment_entries: - party_bank_account = get_decrypted_password(pe.party_type, pe.party, fieldname='bank_account', raise_exception=False) + party_bank_account = get_decrypted_password( + pe.party_type, pe.party, fieldname="bank_account", raise_exception=False + ) if not party_bank_account: - exceptions.append(f'{pe.party_type} Bank Account missing for {pe.party_name}') - party_bank = frappe.db.get_value(pe.party_type, pe.party, 'bank') + exceptions.append(f"{pe.party_type} Bank Account missing for {pe.party_name}") + party_bank = frappe.db.get_value(pe.party_type, pe.party, "bank") if not party_bank: - exceptions.append(f'{pe.party_type} Bank missing for {pe.party_name}') + exceptions.append(f"{pe.party_type} Bank missing for {pe.party_name}") if party_bank: - party_bank_routing_number = frappe.db.get_value('Bank', party_bank, 'aba_number') + party_bank_routing_number = frappe.db.get_value("Bank", party_bank, "aba_number") if not party_bank_routing_number: - exceptions.append(f'{pe.party_type} Bank Routing Number missing for {pe.party_name}') + exceptions.append(f"{pe.party_type} Bank Routing Number missing for {pe.party_name}") ach_entry = ACHEntry( - transaction_code=22, # checking account + transaction_code=22, # checking account receiving_dfi_identification=party_bank_routing_number, dfi_account_number=party_bank_account, amount=int(pe.paid_amount * 100), - individual_id_number='', + individual_id_number="", individual_name=pe.party_name, - discretionary_data='', + discretionary_data="", addenda_record_indicator=0, ) ach_entries.append(ach_entry) - - if exceptions: - frappe.throw('
'.join(e for e in exceptions)) + if exceptions: + frappe.throw("
".join(e for e in exceptions)) + company_discretionary_data = ( + doc.get("company_discretionary_data") + if doc.get("company_discretionary_data") + else (settings.get("company_discretionary_data") or "") + ) + ach_description = settings.get("ach_description") or "" batch = ACHBatch( service_class_code=settings.ach_service_class_code, - company_name=doc.get('company'), - company_discretionary_data='', + company_name=doc.get("company"), + company_discretionary_data=company_discretionary_data[:20], company_id=company_ach_id, standard_class_code=settings.ach_standard_class_code, - company_entry_description=settings.ach_description or "", + company_entry_description=ach_description[:10] or "", company_descriptive_date=None, - effective_entry_date=getdate(), + effective_entry_date=doc.posting_date, settlement_date=None, originator_status_code=1, originating_dfi_id=company_bank_account_no, - entries=ach_entries + entries=ach_entries, ) nacha_file = NACHAFile( priority_code=1, @@ -603,13 +689,13 @@ def build_nacha_file_from_payment_entries(doc, payment_entries, settings): immediate_origin=settings.immediate_origin or "", file_creation_date=getdate(), file_creation_time=get_datetime(), - file_id_modifier='0', + file_id_modifier="0", blocking_factor=10, format_code=1, immediate_destination_name=company_bank, immediate_origin_name=doc.company, - reference_code='', - batches=[batch] + reference_code="", + batches=[batch], ) return nacha_file @@ -617,33 +703,37 @@ def build_nacha_file_from_payment_entries(doc, payment_entries, settings): @frappe.whitelist() def get_check_run_settings(doc): doc = frappe._dict(json.loads(doc)) if isinstance(doc, str) else doc - if frappe.db.exists('Check Run Settings', {'bank_account': doc.bank_account, 'pay_to_account': doc.pay_to_account}): - return frappe.get_doc('Check Run Settings', {'bank_account': doc.bank_account, 'pay_to_account': doc.pay_to_account}) + if frappe.db.exists( + "Check Run Settings", {"bank_account": doc.bank_account, "pay_to_account": doc.pay_to_account} + ): + return frappe.get_doc( + "Check Run Settings", {"bank_account": doc.bank_account, "pay_to_account": doc.pay_to_account} + ) else: return create(doc.company, doc.bank_account, doc.pay_to_account) - + def get_address(party, party_type, doctype, name): - if doctype == 'Purchase Invoice': - return frappe.get_value('Purchase Invoice', name, 'supplier_address') - elif doctype == 'Expense Claim': - return frappe.get_value('Employee', name, 'permanent_address') - elif doctype == 'Journal Entry': - if party_type == 'Supplier': - return get_default_address('Supplier', party) - elif party_type == 'Employee': - return frappe.get_value('Employee', name, 'permanent_address') + if doctype == "Purchase Invoice": + return frappe.get_value("Purchase Invoice", name, "supplier_address") + elif doctype == "Expense Claim": + return frappe.get_value("Employee", name, "permanent_address") + elif doctype == "Journal Entry": + if party_type == "Supplier": + return get_default_address("Supplier", party) + elif party_type == "Employee": + return frappe.get_value("Employee", name, "permanent_address") @frappe.whitelist() def ach_only(docname): - if not frappe.db.exists('Check Run', docname): - return {'ach_only': False, 'checks_only': False} - cr = frappe.get_doc('Check Run', docname) + if not frappe.db.exists("Check Run", docname): + return {"ach_only": False, "checks_only": False} + cr = frappe.get_doc("Check Run", docname) return cr.ach_only() @frappe.whitelist() def process_check_run(docname): - doc = frappe.get_doc('Check Run', docname) + doc = frappe.get_doc("Check Run", docname) doc.process_check_run() diff --git a/check_run/check_run/doctype/check_run_settings/check_run_settings.json b/check_run/check_run/doctype/check_run_settings/check_run_settings.json index 84910163..1b097f9e 100644 --- a/check_run/check_run/doctype/check_run_settings/check_run_settings.json +++ b/check_run/check_run/doctype/check_run_settings/check_run_settings.json @@ -28,7 +28,9 @@ "ach_standard_class_code", "ach_description", "column_break_21", - "immediate_origin" + "immediate_origin", + "company_discretionary_data", + "custom_post_processing_hook" ], "fields": [ { @@ -130,7 +132,8 @@ { "fieldname": "ach_description", "fieldtype": "Data", - "label": "ACH Description" + "label": "ACH Description", + "length": 10 }, { "fieldname": "print_format", @@ -163,10 +166,22 @@ "fieldname": "immediate_origin", "fieldtype": "Data", "label": "Immediate Origin" + }, + { + "fieldname": "company_discretionary_data", + "fieldtype": "Data", + "label": "Company Discretionary Data", + "length": 20 + }, + { + "fieldname": "custom_post_processing_hook", + "fieldtype": "Data", + "label": "Custom Post Processing Hook", + "read_only": 1 } ], "links": [], - "modified": "2023-03-09 07:34:52.062230", + "modified": "2023-03-17 13:43:25.837467", "modified_by": "Administrator", "module": "Check Run", "name": "Check Run Settings", @@ -201,4 +216,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/check_run/hooks.py b/check_run/hooks.py index 69c28529..a97b2f77 100644 --- a/check_run/hooks.py +++ b/check_run/hooks.py @@ -36,6 +36,7 @@ # include js in doctype views doctype_js = { 'Employee': 'public/js/custom/employee_custom.js', + "Payment Entry": 'public/js/custom/payment_entry_custom.js', "Supplier": 'public/js/custom/supplier_custom.js', } # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} @@ -102,13 +103,11 @@ # --------------- # Hook on document methods and events -# doc_events = { -# "*": { -# "on_update": "method", -# "on_cancel": "method", -# "on_trash": "method" -# } -# } +doc_events = { + "Payment Entry": { + "on_submit": "check_run.overrides.payment_entry.update_check_number", + } +} # Scheduled Tasks # --------------- diff --git a/check_run/overrides/payment_entry.py b/check_run/overrides/payment_entry.py new file mode 100644 index 00000000..ff6a4de5 --- /dev/null +++ b/check_run/overrides/payment_entry.py @@ -0,0 +1,7 @@ +import frappe + +@frappe.whitelist() +def update_check_number(doc, method=None): + mode_of_payment_type = frappe.db.get_value('Mode of Payment', doc.mode_of_payment, 'type') + if doc.bank_account and mode_of_payment_type == 'Bank': + frappe.db.set_value('Bank Account', doc.bank_account, 'check_number', doc.reference_no) diff --git a/check_run/public/js/check_run/CheckRun.vue b/check_run/public/js/check_run/CheckRun.vue index 3777a888..1a5eb56e 100644 --- a/check_run/public/js/check_run/CheckRun.vue +++ b/check_run/public/js/check_run/CheckRun.vue @@ -41,7 +41,7 @@ >⬍ - + @@ -83,7 +83,7 @@ {{ format_currency(item.amount, 'USD', 2) }} {{ moment(item.due_date).format('MM/DD/YY') }} - + = 1 || !this.transactions.length) { + if (this.state.status == 'Draft' || !this.transactions.length) { return } this.transactions[this.state.selectedRow].pay = !this.transactions[this.state.selectedRow].pay diff --git a/check_run/public/js/check_run/check_run.js b/check_run/public/js/check_run/check_run.js index 00a6f942..edc18b03 100644 --- a/check_run/public/js/check_run/check_run.js +++ b/check_run/public/js/check_run/check_run.js @@ -12,7 +12,7 @@ check_run.mount_table = frm => { frm.check_run_state = Vue.observable({ transactions: frm.transactions, party_filter: '', - docstatus: frm.doc.docstatus, + status: frm.doc.status, modes_of_payment: frm.modes_of_payment, show_party_filter: false, check_run_total: function () { @@ -35,7 +35,7 @@ check_run.mount_table = frm => { props: { transactions: frm.check_run_state.transactions, //list of transtactions modes_of_payment: frm.check_run_state.modes_of_payment, // populate modes_of_payment select. doesn't get updated - docstatus: frm.check_run_state.docstatus, // used to conditionally render column inputs based on submission status. doesn't get updated + status: frm.check_run_state.status, // used to conditionally render column inputs based on submission status state: frm.check_run_state, }, }), diff --git a/check_run/public/js/custom/payment_entry_custom.js b/check_run/public/js/custom/payment_entry_custom.js new file mode 100644 index 00000000..93109291 --- /dev/null +++ b/check_run/public/js/custom/payment_entry_custom.js @@ -0,0 +1,21 @@ +frappe.ui.form.on('Payment Entry', { + mode_of_payment: frm => { + get_next_check_number(frm) + }, + bank_account: frm => { + get_next_check_number(frm) + }, +}) + +function get_next_check_number(frm) { + if (!frm.doc.bank_account) { + return + } + if (!frm.doc.mode_of_payment) { + return + } + frappe.db.get_value('Bank Account', frm.doc.bank_account, 'check_number').then(r => { + let check_number = Number(r.message.check_number) + 1 + frm.set_value('reference_no', check_number) + }) +} diff --git a/check_run/test_setup.py b/check_run/test_setup.py index 94bb615c..ca920ef0 100644 --- a/check_run/test_setup.py +++ b/check_run/test_setup.py @@ -6,205 +6,288 @@ from erpnext.setup.utils import enable_all_roles_and_domains, set_defaults_for_tests from erpnext.accounts.doctype.account.account import update_account_number + def before_test(): frappe.clear_cache() today = frappe.utils.getdate() - setup_complete({ - "currency": "USD", - "full_name": "Administrator", - "company_name": "Chelsea Fruit Co", - "timezone": "America/New_York", - "company_abbr": "CFC", - "domains": ["Distribution"], - "country": "United States", - "fy_start_date": today.replace(month=1, day=1).isoformat(), - "fy_end_date": today.replace(month=12, day=31).isoformat(), - "language": "english", - "company_tagline": "Chelsea Fruit Co", - "email": "support@agritheory.dev", - "password": "admin", - "chart_of_accounts": "Standard with Numbers", - "bank_account": "Primary Checking" - }) + setup_complete( + { + "currency": "USD", + "full_name": "Administrator", + "company_name": "Chelsea Fruit Co", + "timezone": "America/New_York", + "company_abbr": "CFC", + "domains": ["Distribution"], + "country": "United States", + "fy_start_date": today.replace(month=1, day=1).isoformat(), + "fy_end_date": today.replace(month=12, day=31).isoformat(), + "language": "english", + "company_tagline": "Chelsea Fruit Co", + "email": "support@agritheory.dev", + "password": "admin", + "chart_of_accounts": "Standard with Numbers", + "bank_account": "Primary Checking", + } + ) enable_all_roles_and_domains() set_defaults_for_tests() frappe.db.commit() create_test_data() - for modu in frappe.get_all('Module Onboarding'): - frappe.db.set_value('Module Onboarding', modu, 'is_complete', 1) - frappe.set_value('Website Settings', 'Website Settings', 'home_page', 'login') + for modu in frappe.get_all("Module Onboarding"): + frappe.db.set_value("Module Onboarding", modu, "is_complete", 1) + frappe.set_value("Website Settings", "Website Settings", "home_page", "login") frappe.db.commit() + suppliers = [ - ("Exceptional Grid", "Electricity", "ACH/EFT", 150.00, "Net 14", { - 'address_line1': '2 Cosmo Point', - 'city': 'Summerville', - 'state': 'MA', - 'country': 'United States', - 'pincode': '34791' - }), - ("Liu & Loewen Accountants LLP", "Accounting Services", "ACH/EFT", 500.00, "Net 30", { - 'address_line1': '138 Wanda Square', - 'city': 'Chino', - 'state': 'ME', - 'country': 'United States', - 'pincode': '90953' - }), - ("Mare Digitalis", "Cloud Services", "Credit Card", 200.00, "Due on Receipt", { - 'address_line1': '1000 Toll Plaza Tunnel Alley', - 'city': 'Joplin', - 'state': 'CT', - 'country': 'United States', - 'pincode': '51485' - }), - ("AgriTheory", "ERPNext Consulting", "Check", 1000.00, "Net 14", { - 'address_line1': '1293 Bannan Road', - 'city': 'New Brighton', - 'state': 'NH', - 'country': 'United States', - 'pincode': '55932' - }), - ("HIJ Telecom, Inc", "Internet Services", "Check", 150.00, "Net 30", { - 'address_line1': '955 Winding Highway', - 'city': 'Glassboro', - 'state': 'NY', - 'country': 'United States', - 'pincode': '28026' - }), - ("Sphere Cellular", "Phone Services", "ACH/EFT", 250.00, "Net 30", { - 'address_line1': '1198 Carpenter Road', - 'city': 'Rolla', - 'state': 'VT', - 'country': 'United States', - 'pincode': '94286' - }), - ("Cooperative Ag Finance", "Financial Services", "Bank Draft", 5000.00, "Net 30", { - 'address_line1': '629 Loyola Landing', - 'city': 'Warner Robins', - 'state': 'CT', - 'country': 'United States', - 'pincode': '28989' - }), + ( + "Exceptional Grid", + "Electricity", + "ACH/EFT", + 150.00, + "Net 14", + { + "address_line1": "2 Cosmo Point", + "city": "Summerville", + "state": "MA", + "country": "United States", + "pincode": "34791", + }, + ), + ( + "Liu & Loewen Accountants LLP", + "Accounting Services", + "ACH/EFT", + 500.00, + "Net 30", + { + "address_line1": "138 Wanda Square", + "city": "Chino", + "state": "ME", + "country": "United States", + "pincode": "90953", + }, + ), + ( + "Mare Digitalis", + "Cloud Services", + "Credit Card", + 200.00, + "Due on Receipt", + { + "address_line1": "1000 Toll Plaza Tunnel Alley", + "city": "Joplin", + "state": "CT", + "country": "United States", + "pincode": "51485", + }, + ), + ( + "AgriTheory", + "ERPNext Consulting", + "Check", + 1000.00, + "Net 14", + { + "address_line1": "1293 Bannan Road", + "city": "New Brighton", + "state": "NH", + "country": "United States", + "pincode": "55932", + }, + ), + ( + "HIJ Telecom, Inc", + "Internet Services", + "Check", + 150.00, + "Net 30", + { + "address_line1": "955 Winding Highway", + "city": "Glassboro", + "state": "NY", + "country": "United States", + "pincode": "28026", + }, + ), + ( + "Sphere Cellular", + "Phone Services", + "ACH/EFT", + 250.00, + "Net 30", + { + "address_line1": "1198 Carpenter Road", + "city": "Rolla", + "state": "VT", + "country": "United States", + "pincode": "94286", + }, + ), + ( + "Cooperative Ag Finance", + "Financial Services", + "Bank Draft", + 5000.00, + "Net 30", + { + "address_line1": "629 Loyola Landing", + "city": "Warner Robins", + "state": "CT", + "country": "United States", + "pincode": "28989", + }, + ), ] tax_authority = [ - ("Local Tax Authority", "Payroll Taxes", "Check", 0.00, "Due on Receipt", { - 'address_line1': '18 Spooner Stravenue', - 'city': 'Danbury', - 'state': 'RI', - 'country': 'United States', - 'pincode': '07165' - }), + ( + "Local Tax Authority", + "Payroll Taxes", + "Check", + 0.00, + "Due on Receipt", + { + "address_line1": "18 Spooner Stravenue", + "city": "Danbury", + "state": "RI", + "country": "United States", + "pincode": "07165", + }, + ), ] employees = [ - ('Wilmer Larson', - 'Male', - '1977-03-06', - '2019-04-12', - '20 Gaven Path', - 'Spokane', - 'NV', - '66308'), - ('Shanel Finley', - 'Female', - '1984-04-23', - '2019-07-04', - '1070 Ulloa Green', - 'DeKalb', - 'PA', - '30474'), - ('Camellia Phelps', - 'Female', - '1980-07-06', - '2019-07-28', - '787 Sotelo Arcade', - 'Stockton', - 'CO', - '14860'), - ('Michale Mitchell', - 'Male', - '1984-06-29', - '2020-01-12', - '773 Icehouse Road', - 'West Sacramento', - 'VT', - '24355'), - ('Sharilyn Romero', - 'Female', - '1998-04-22', - '2020-03-20', - '432 Dudley Ranch', - 'Clovis', - 'WA', - '97159'), - ('Doug Buckley', - 'Male', - '1979-06-18', - '2020-09-08', - '771 Battery Caulfield Motorway', - 'Yonkers', - 'VT', - '38125'), - ('Margarito Wallace', - 'Male', - '1991-08-17', - '2020-11-01', - '639 Brook Park', - 'Terre Haute', - 'OR', - '41704'), - ('Mckenzie Ashley', - 'Female', - '1997-09-13', - '2021-02-22', - '1119 Hunter Glen', - 'Ormond Beach', - 'MD', - '30864'), - ('Merrie Oliver', - 'Other', - '1979-11-08', - '2021-03-11', - '267 Vega Freeway', - 'West Palm Beach', - 'FL', - '24411'), - ('Naoma Blake', - 'Female', - '1987-07-10', - '2021-06-21', - '649 Conrad Road', - 'Thousand Oaks', - 'CT', - '97929'), - ('Donnell Fry', - 'Male', - '1994-07-27', - '2021-06-24', - '504 Starr King Canyon', - 'Norwalk', - 'OR', - '46845'), - ('Shalanda Peterson', - 'Female', - '1999-10-04', - '2021-08-01', - '109 Seventh Parkway', - 'Urbana', - 'DE', - '55975') + ("Wilmer Larson", "Male", "1977-03-06", "2019-04-12", "20 Gaven Path", "Spokane", "NV", "66308"), + ( + "Shanel Finley", + "Female", + "1984-04-23", + "2019-07-04", + "1070 Ulloa Green", + "DeKalb", + "PA", + "30474", + ), + ( + "Camellia Phelps", + "Female", + "1980-07-06", + "2019-07-28", + "787 Sotelo Arcade", + "Stockton", + "CO", + "14860", + ), + ( + "Michale Mitchell", + "Male", + "1984-06-29", + "2020-01-12", + "773 Icehouse Road", + "West Sacramento", + "VT", + "24355", + ), + ( + "Sharilyn Romero", + "Female", + "1998-04-22", + "2020-03-20", + "432 Dudley Ranch", + "Clovis", + "WA", + "97159", + ), + ( + "Doug Buckley", + "Male", + "1979-06-18", + "2020-09-08", + "771 Battery Caulfield Motorway", + "Yonkers", + "VT", + "38125", + ), + ( + "Margarito Wallace", + "Male", + "1991-08-17", + "2020-11-01", + "639 Brook Park", + "Terre Haute", + "OR", + "41704", + ), + ( + "Mckenzie Ashley", + "Female", + "1997-09-13", + "2021-02-22", + "1119 Hunter Glen", + "Ormond Beach", + "MD", + "30864", + ), + ( + "Merrie Oliver", + "Other", + "1979-11-08", + "2021-03-11", + "267 Vega Freeway", + "West Palm Beach", + "FL", + "24411", + ), + ( + "Naoma Blake", + "Female", + "1987-07-10", + "2021-06-21", + "649 Conrad Road", + "Thousand Oaks", + "CT", + "97929", + ), + ( + "Donnell Fry", + "Male", + "1994-07-27", + "2021-06-24", + "504 Starr King Canyon", + "Norwalk", + "OR", + "46845", + ), + ( + "Shalanda Peterson", + "Female", + "1999-10-04", + "2021-08-01", + "109 Seventh Parkway", + "Urbana", + "DE", + "55975", + ), ] def create_test_data(): setup_accounts() - settings = frappe._dict({ - 'day': datetime.date(int(frappe.defaults.get_defaults().get('fiscal_year')), 1 ,1), - 'company': frappe.defaults.get_defaults().get('company'), - 'company_account': frappe.get_value("Account", - {"account_type": "Bank", "company": frappe.defaults.get_defaults().get('company'), "is_group": 0}), - }) + settings = frappe._dict( + { + "day": datetime.date(int(frappe.defaults.get_defaults().get("fiscal_year")), 1, 1), + "company": frappe.defaults.get_defaults().get("company"), + "company_account": frappe.get_value( + "Account", + { + "account_type": "Bank", + "company": frappe.defaults.get_defaults().get("company"), + "is_group": 0, + }, + ), + } + ) create_bank_and_bank_account(settings) create_payment_terms_templates(settings) create_suppliers(settings) @@ -212,58 +295,68 @@ def create_test_data(): create_invoices(settings) config_expense_claim(settings) create_employees(settings) - for month in range(1,13): + for month in range(1, 13): create_payroll_journal_entry(settings) settings.day = settings.day.replace(month=month) def create_bank_and_bank_account(settings): - if not frappe.db.exists('Mode of Payment', 'ACH/EFT'): - mop = frappe.new_doc('Mode of Payment') - mop.mode_of_payment = 'ACH/EFT' + if not frappe.db.exists("Mode of Payment", "ACH/EFT"): + mop = frappe.new_doc("Mode of Payment") + mop.mode_of_payment = "ACH/EFT" mop.enabled = 1 - mop.type = 'Electronic' - mop.append('accounts', {'company': settings.company, 'default_account': settings.company_account}) + mop.type = "Electronic" + mop.append( + "accounts", {"company": settings.company, "default_account": settings.company_account} + ) mop.save() - wire_transfer = frappe.get_doc('Mode of Payment', 'Wire Transfer') - wire_transfer.type = 'General' - wire_transfer.append('accounts', {'company': settings.company, 'default_account': settings.company_account}) + wire_transfer = frappe.get_doc("Mode of Payment", "Wire Transfer") + wire_transfer.type = "General" + wire_transfer.append( + "accounts", {"company": settings.company, "default_account": settings.company_account} + ) wire_transfer.save() - credit_card = frappe.get_doc('Mode of Payment', 'Credit Card') - credit_card.type = 'General' - credit_card.append('accounts', {'company': settings.company, 'default_account': settings.company_account}) + credit_card = frappe.get_doc("Mode of Payment", "Credit Card") + credit_card.type = "General" + credit_card.append( + "accounts", {"company": settings.company, "default_account": settings.company_account} + ) credit_card.save() - bank_draft = frappe.get_doc('Mode of Payment', 'Bank Draft') - bank_draft.type = 'General' - bank_draft.append('accounts', {'company': settings.company, 'default_account': settings.company_account}) + bank_draft = frappe.get_doc("Mode of Payment", "Bank Draft") + bank_draft.type = "General" + bank_draft.append( + "accounts", {"company": settings.company, "default_account": settings.company_account} + ) bank_draft.save() - check_mop = frappe.get_doc('Mode of Payment', 'Check') - check_mop.type = 'Bank' - check_mop.append('accounts', {'company': settings.company, 'default_account': settings.company_account}) + check_mop = frappe.get_doc("Mode of Payment", "Check") + check_mop.type = "Bank" + check_mop.append( + "accounts", {"company": settings.company, "default_account": settings.company_account} + ) check_mop.save() - if not frappe.db.exists('Bank', 'Local Bank'): - bank = frappe.new_doc('Bank') + if not frappe.db.exists("Bank", "Local Bank"): + bank = frappe.new_doc("Bank") bank.bank_name = "Local Bank" - bank.aba_number = '07200091' + bank.aba_number = "07200091" bank.save() - if not frappe.db.exists('Bank Account', 'Primary Checking - Local Bank'): - bank_account = frappe.new_doc('Bank Account') - bank_account.account_name = 'Primary Checking' + if not frappe.db.exists("Bank Account", "Primary Checking - Local Bank"): + bank_account = frappe.new_doc("Bank Account") + bank_account.account_name = "Primary Checking" bank_account.bank = bank.name bank_account.is_default = 1 bank_account.is_company_account = 1 bank_account.company = settings.company bank_account.account = settings.company_account bank_account.check_number = 2500 - bank_account.company_ach_id = '1381655417' - bank_account.bank_account_no = '072000915' - bank_account.branch_code = '07200091' + bank_account.company_ach_id = "1381655417" + bank_account.bank_account_no = "072000915" + bank_account.branch_code = "07200091" bank_account.save() doc = frappe.new_doc("Journal Entry") @@ -271,48 +364,76 @@ def create_bank_and_bank_account(settings): doc.voucher_type = "Opening Entry" doc.company = settings.company opening_balance = 50000.00 - doc.append("accounts", {"account": settings.company_account, "debit_in_account_currency": opening_balance}) - retained_earnings = frappe.get_value('Account', {'account_name': "Retained Earnings", 'company': settings.company}) - doc.append("accounts", {"account": retained_earnings, "credit_in_account_currency": opening_balance}) + doc.append( + "accounts", {"account": settings.company_account, "debit_in_account_currency": opening_balance} + ) + retained_earnings = frappe.get_value( + "Account", {"account_name": "Retained Earnings", "company": settings.company} + ) + doc.append( + "accounts", {"account": retained_earnings, "credit_in_account_currency": opening_balance} + ) doc.save() doc.submit() + def setup_accounts(): - frappe.rename_doc('Account', '1000 - Application of Funds (Assets) - CFC', '1000 - Assets - CFC', force=True) - frappe.rename_doc('Account', '2000 - Source of Funds (Liabilities) - CFC', '2000 - Liabilities - CFC', force=True) - frappe.rename_doc('Account', '1310 - Debtors - CFC', '1310 - Accounts Receivable - CFC', force=True) - frappe.rename_doc('Account', '2110 - Creditors - CFC', '2110 - Accounts Payable - CFC', force=True) - update_account_number('1110 - Cash - CFC', 'Petty Cash', account_number='1110') - update_account_number('Primary Checking - CFC', 'Primary Checking', account_number='1201') + frappe.rename_doc( + "Account", "1000 - Application of Funds (Assets) - CFC", "1000 - Assets - CFC", force=True + ) + frappe.rename_doc( + "Account", "2000 - Source of Funds (Liabilities) - CFC", "2000 - Liabilities - CFC", force=True + ) + frappe.rename_doc( + "Account", "1310 - Debtors - CFC", "1310 - Accounts Receivable - CFC", force=True + ) + frappe.rename_doc( + "Account", "2110 - Creditors - CFC", "2110 - Accounts Payable - CFC", force=True + ) + update_account_number("1110 - Cash - CFC", "Petty Cash", account_number="1110") + update_account_number("Primary Checking - CFC", "Primary Checking", account_number="1201") + def create_payment_terms_templates(settings): if not frappe.db.exists("Payment Terms Template", "Net 30"): doc = frappe.new_doc("Payment Terms Template") doc.template_name = "Net 30" - doc.append("terms", { - "payment_term": "Net 30", - "invoice_portion": 100, - "due_date_based_on": "Day(s) after invoice date", - "credit_days": 30}) + doc.append( + "terms", + { + "payment_term": "Net 30", + "invoice_portion": 100, + "due_date_based_on": "Day(s) after invoice date", + "credit_days": 30, + }, + ) doc.save() if not frappe.db.exists("Payment Terms Template", "Due on Receipt"): doc = frappe.new_doc("Payment Terms Template") doc.template_name = "Due on Receipt" - doc.append("terms", { - "payment_term": "Due on Receipt", - "invoice_portion": 100, - "due_date_based_on": "Day(s) after invoice date", - "credit_days": 0}) + doc.append( + "terms", + { + "payment_term": "Due on Receipt", + "invoice_portion": 100, + "due_date_based_on": "Day(s) after invoice date", + "credit_days": 0, + }, + ) doc.save() if not frappe.db.exists("Payment Terms Template", "Net 14"): doc = frappe.new_doc("Payment Terms Template") doc.template_name = "Net 14" - doc.append("terms", { - "payment_term": "Net 14", - "invoice_portion": 100, - "due_date_based_on": "Day(s) after invoice date", - "credit_days": 14}) + doc.append( + "terms", + { + "payment_term": "Net 14", + "invoice_portion": 100, + "due_date_based_on": "Day(s) after invoice date", + "credit_days": 14, + }, + ) doc.save() @@ -324,40 +445,34 @@ def create_suppliers(settings): biz.supplier_group = "Services" biz.country = "United States" biz.supplier_default_mode_of_payment = supplier[2] - if biz.supplier_default_mode_of_payment == 'ACH/EFT': - biz.bank = 'Local Bank' + if biz.supplier_default_mode_of_payment == "ACH/EFT": + biz.bank = "Local Bank" biz.bank_account = "123456789" biz.currency = "USD" biz.default_price_list = "Standard Buying" biz.payment_terms = supplier[4] biz.save() - addr = frappe.new_doc('Address') + addr = frappe.new_doc("Address") addr.address_title = f"{supplier[0]} - {supplier[5]['city']}" - addr.address_type = 'Billing' - addr.address_line1 = supplier[5]['address_line1'] - addr.city = supplier[5]['city'] - addr.state = supplier[5]['state'] - addr.country = supplier[5]['country'] - addr.pincode = supplier[5]['pincode'] - addr.append('links', { - 'link_doctype': 'Supplier', - 'link_name': supplier[0] - }) + addr.address_type = "Billing" + addr.address_line1 = supplier[5]["address_line1"] + addr.city = supplier[5]["city"] + addr.state = supplier[5]["state"] + addr.country = supplier[5]["country"] + addr.pincode = supplier[5]["pincode"] + addr.append("links", {"link_doctype": "Supplier", "link_name": supplier[0]}) addr.save() - addr = frappe.new_doc('Address') - addr.address_type = 'Billing' + addr = frappe.new_doc("Address") + addr.address_type = "Billing" addr.address_title = "HIJ Telecom - Burlingame" - addr.address_line1 = '167 Auto Terrace' - addr.city = 'Burlingame' + addr.address_line1 = "167 Auto Terrace" + addr.city = "Burlingame" addr.state = "ME" addr.country = "United States" - addr.pincode = '79749' - addr.append('links', { - 'link_doctype': 'Supplier', - 'link_name': 'HIJ Telecom, Inc' - }) + addr.pincode = "79749" + addr.append("links", {"link_doctype": "Supplier", "link_name": "HIJ Telecom, Inc"}) addr.save() @@ -372,49 +487,62 @@ def create_items(settings): item.grant_commission = 0 item.is_purchase_item = 1 item.append("supplier_items", {"supplier": supplier[0]}) - item.append("item_defaults", {"company": settings.company, "default_warehouse": "", "default_supplier": supplier[0]}) + item.append( + "item_defaults", + {"company": settings.company, "default_warehouse": "", "default_supplier": supplier[0]}, + ) item.save() + def create_invoices(settings): # first month - already paid for supplier in suppliers: - pi = frappe.new_doc('Purchase Invoice') + pi = frappe.new_doc("Purchase Invoice") pi.company = settings.company pi.set_posting_time = 1 pi.posting_date = settings.day pi.supplier = supplier[0] - pi.append('items', { - 'item_code': supplier[1], - 'rate': supplier[3], - 'qty': 1, - }) + pi.append( + "items", + { + "item_code": supplier[1], + "rate": supplier[3], + "qty": 1, + }, + ) pi.save() pi.submit() # two electric meters / test invoice aggregation - pi = frappe.new_doc('Purchase Invoice') + pi = frappe.new_doc("Purchase Invoice") pi.company = settings.company pi.set_posting_time = 1 pi.posting_date = settings.day pi.supplier = suppliers[0][0] - pi.append('items', { - 'item_code': suppliers[0][1], - 'rate': 75.00, - 'qty': 1, - }) + pi.append( + "items", + { + "item_code": suppliers[0][1], + "rate": 75.00, + "qty": 1, + }, + ) pi.save() pi.submit() # two phone bills / test address splitting - pi = frappe.new_doc('Purchase Invoice') + pi = frappe.new_doc("Purchase Invoice") pi.company = settings.company pi.set_posting_time = 1 pi.posting_date = settings.day pi.supplier = suppliers[4][0] - pi.append('items', { - 'item_code': suppliers[4][1], - 'rate': 122.50, - 'qty': 1, - }) + pi.append( + "items", + { + "item_code": suppliers[4][1], + "rate": 122.50, + "qty": 1, + }, + ) pi.supplier_address = "HIJ Telecom - Burlingame-Billing" pi.save() pi.submit() @@ -423,127 +551,160 @@ def create_invoices(settings): next_day = settings.day + datetime.timedelta(days=31) for supplier in suppliers: - pi = frappe.new_doc('Purchase Invoice') + pi = frappe.new_doc("Purchase Invoice") pi.company = settings.company pi.set_posting_time = 1 pi.posting_date = next_day pi.supplier = supplier[0] - pi.append('items', { - 'item_code': supplier[1], - 'rate': supplier[3], - 'qty': 1, - }) + pi.append( + "items", + { + "item_code": supplier[1], + "rate": supplier[3], + "qty": 1, + }, + ) pi.save() pi.submit() # two electric meters / test invoice aggregation - pi = frappe.new_doc('Purchase Invoice') + pi = frappe.new_doc("Purchase Invoice") pi.company = settings.company pi.set_posting_time = 1 pi.posting_date = next_day pi.supplier = suppliers[0][0] - pi.append('items', { - 'item_code': suppliers[0][1], - 'rate': 75.00, - 'qty': 1, - }) + pi.append( + "items", + { + "item_code": suppliers[0][1], + "rate": 75.00, + "qty": 1, + }, + ) pi.save() pi.submit() # two phone bills / test address splitting - pi = frappe.new_doc('Purchase Invoice') + pi = frappe.new_doc("Purchase Invoice") pi.company = settings.company pi.set_posting_time = 1 pi.posting_date = settings.day pi.supplier = suppliers[4][0] - pi.append('items', { - 'item_code': suppliers[4][1], - 'rate': 122.50, - 'qty': 1, - }) + pi.append( + "items", + { + "item_code": suppliers[4][1], + "rate": 122.50, + "qty": 1, + }, + ) pi.supplier_address = "HIJ Telecom - Burlingame-Billing" pi.save() pi.submit() # test on-hold invoice - pi = frappe.new_doc('Purchase Invoice') + pi = frappe.new_doc("Purchase Invoice") pi.company = settings.company pi.set_posting_time = 1 pi.posting_date = settings.day pi.supplier = suppliers[1][0] - pi.append('items', { - 'item_code': suppliers[1][1], - 'rate': 4000.00, - 'qty': 1, - }) - pi.on_hold =1 + pi.append( + "items", + { + "item_code": suppliers[1][1], + "rate": 4000.00, + "qty": 1, + }, + ) + pi.on_hold = 1 pi.release_date = settings.day + datetime.timedelta(days=60) - pi.hold_comment = 'Testing for on hold invoices' - pi.validate_release_date = types.MethodType(validate_release_date, pi) # allow date to be backdated for testing + pi.hold_comment = "Testing for on hold invoices" + pi.validate_release_date = types.MethodType( + validate_release_date, pi + ) # allow date to be backdated for testing pi.save() pi.submit() + def validate_release_date(self): pass + def config_expense_claim(settings): try: - travel_expense_account = frappe.get_value('Account', {'account_name': 'Travel Expenses', 'company': settings.company}) - travel = frappe.get_doc('Expense Claim Type', 'Travel') - travel.append('accounts', {'company': settings.company, 'default_account': travel_expense_account}) + travel_expense_account = frappe.get_value( + "Account", {"account_name": "Travel Expenses", "company": settings.company} + ) + travel = frappe.get_doc("Expense Claim Type", "Travel") + travel.append( + "accounts", {"company": settings.company, "default_account": travel_expense_account} + ) travel.save() except: pass - payroll_payable = frappe.db.get_value('Account', {'account_name': 'Payroll Payable', 'company': settings.company}) + payroll_payable = frappe.db.get_value( + "Account", {"account_name": "Payroll Payable", "company": settings.company} + ) if payroll_payable: - frappe.db.set_value('Account', payroll_payable, 'account_type', 'Payable') + frappe.db.set_value("Account", payroll_payable, "account_type", "Payable") - if frappe.db.exists('Account', {'account_name': 'Payroll Taxes', 'company': settings.company}): + if frappe.db.exists("Account", {"account_name": "Payroll Taxes", "company": settings.company}): return - pta = frappe.new_doc('Account') + pta = frappe.new_doc("Account") pta.account_name = "Payroll Taxes" - pta.account_number = max([int(a.account_number or 1) for a in frappe.get_all('Account', {'is_group': 0},['account_number'])]) + 1 + pta.account_number = ( + max( + int(a.account_number or 1) + for a in frappe.get_all("Account", {"is_group": 0}, ["account_number"]) + ) + + 1 + ) pta.account_type = "Expense Account" pta.company = settings.company - pta.parent_account = frappe.get_value('Account', {'account_name': 'Indirect Expenses', 'company': settings.company}) + pta.parent_account = frappe.get_value( + "Account", {"account_name": "Indirect Expenses", "company": settings.company} + ) pta.save() def create_employees(settings): for employee_number, employee in enumerate(employees, start=10): - emp = frappe.new_doc('Employee') - emp.first_name = employee[0].split(' ')[0] - emp.last_name = employee[0].split(' ')[1] + emp = frappe.new_doc("Employee") + emp.first_name = employee[0].split(" ")[0] + emp.last_name = employee[0].split(" ")[1] emp.employment_type = "Full-time" emp.company = settings.company emp.status = "Active" emp.gender = employee[1] emp.date_of_birth = employee[2] emp.date_of_joining = employee[3] - emp.mode_of_payment = 'Check' if employee_number % 3 == 0 else 'ACH/EFT' - emp.mode_of_payment = 'Cash' if employee_number == 10 else emp.mode_of_payment - emp.expense_approver = 'Administrator' - if emp.mode_of_payment == 'ACH/EFT': - emp.bank = 'Local Bank' - emp.bank_account = f'{employee_number}12345' + emp.mode_of_payment = "Check" if employee_number % 3 == 0 else "ACH/EFT" + emp.mode_of_payment = "Cash" if employee_number == 10 else emp.mode_of_payment + emp.expense_approver = "Administrator" + if emp.mode_of_payment == "ACH/EFT": + emp.bank = "Local Bank" + emp.bank_account = f"{employee_number}12345" emp.save() def create_expense_claim(settings): - cost_center = frappe.get_value('Company', settings.company, 'cost_center') - payable_acct = frappe.get_value('Company', settings.company, 'default_payable_account') + cost_center = frappe.get_value("Company", settings.company, "cost_center") + payable_acct = frappe.get_value("Company", settings.company, "default_payable_account") # first month - paid - ec = frappe.new_doc('Expense Claim') + ec = frappe.new_doc("Expense Claim") ec.employee = "HR-EMP-00002" ec.expense_approver = "Administrator" - ec.approval_status = 'Approved' - ec.append('expenses', { - 'expense_date': settings.day, - 'expense_type': 'Travel', - 'amount': 50.0, - 'sanctioned_amount': 50.0, - 'cost_center': cost_center - }) + ec.approval_status = "Approved" + ec.append( + "expenses", + { + "expense_date": settings.day, + "expense_type": "Travel", + "amount": 50.0, + "sanctioned_amount": 50.0, + "cost_center": cost_center, + }, + ) ec.posting_date = settings.day ec.company = settings.company ec.payable_account = payable_acct @@ -552,34 +713,40 @@ def create_expense_claim(settings): # second month - open next_day = settings.day + datetime.timedelta(days=31) - ec = frappe.new_doc('Expense Claim') + ec = frappe.new_doc("Expense Claim") ec.employee = "HR-EMP-00002" ec.expense_approver = "Administrator" - ec.approval_status = 'Approved' - ec.append('expenses', { - 'expense_date': next_day, - 'expense_type': 'Travel', - 'amount': 50.0, - 'sanctioned_amount': 50.0, - 'cost_center': cost_center - }) + ec.approval_status = "Approved" + ec.append( + "expenses", + { + "expense_date": next_day, + "expense_type": "Travel", + "amount": 50.0, + "sanctioned_amount": 50.0, + "cost_center": cost_center, + }, + ) ec.posting_date = next_day ec.company = settings.company ec.payable_account = payable_acct ec.save() ec.submit() # two expense claims to test aggregation - ec = frappe.new_doc('Expense Claim') + ec = frappe.new_doc("Expense Claim") ec.employee = "HR-EMP-00002" ec.expense_approver = "Administrator" - ec.approval_status = 'Approved' - ec.append('expenses', { - 'expense_date': next_day, - 'expense_type': 'Travel', - 'amount': 50.0, - 'sanctioned_amount': 50.0, - 'cost_center': cost_center - }) + ec.approval_status = "Approved" + ec.append( + "expenses", + { + "expense_date": next_day, + "expense_type": "Travel", + "amount": 50.0, + "sanctioned_amount": 50.0, + "cost_center": cost_center, + }, + ) ec.posting_date = next_day ec.company = settings.company ec.payable_account = payable_acct @@ -588,64 +755,98 @@ def create_expense_claim(settings): def create_payroll_journal_entry(settings): - emps = frappe.get_list('Employee', {'company': settings.company}) - cost_center = frappe.get_value('Company', settings.company, 'cost_center') - payroll_account = frappe.get_value('Account', {'company': settings.company, 'account_name': 'Payroll Payable', 'is_group': 0}) - salary_account = frappe.get_value('Account', {'company': settings.company, 'account_name': 'Salary', 'is_group': 0}) - payroll_expense = frappe.get_value('Account', {'company': settings.company, 'account_name': 'Payroll Taxes', 'is_group': 0}) - payable_account= frappe.get_value('Company', settings.company, 'default_payable_account') - je = frappe.new_doc('Journal Entry') - je.entry_type = 'Journal Entry' + emps = frappe.get_list("Employee", {"company": settings.company}) + cost_center = frappe.get_value("Company", settings.company, "cost_center") + payroll_account = frappe.get_value( + "Account", {"company": settings.company, "account_name": "Payroll Payable", "is_group": 0} + ) + salary_account = frappe.get_value( + "Account", {"company": settings.company, "account_name": "Salary", "is_group": 0} + ) + payroll_expense = frappe.get_value( + "Account", {"company": settings.company, "account_name": "Payroll Taxes", "is_group": 0} + ) + payable_account = frappe.get_value("Company", settings.company, "default_payable_account") + je = frappe.new_doc("Journal Entry") + je.entry_type = "Journal Entry" je.company = settings.company je.posting_date = settings.day je.due_date = settings.day total_payroll = 0.0 for idx, emp in enumerate(emps): - employee_name = frappe.get_value('Employee', {'company': settings.company, 'name': emp.name}, 'employee_name') - je.append('accounts', { - 'account': payroll_account, - 'bank_account': frappe.get_value("Bank Account", {'account': settings.company_account}), - 'party_type': 'Employee', - 'party': emp.name, - 'cost_center': cost_center, - 'account_currency': 'USD', - 'credit': 1000.00, - 'credit_in_account_currency': 1000.00, - 'debit': 0.00, - 'debit_in_account_currency': 0.00, - 'user_remark': employee_name + ' Paycheck', - 'idx': idx + 2 - }) + employee_name = frappe.get_value( + "Employee", {"company": settings.company, "name": emp.name}, "employee_name" + ) + je.append( + "accounts", + { + "account": payroll_account, + "bank_account": frappe.get_value("Bank Account", {"account": settings.company_account}), + "party_type": "Employee", + "party": emp.name, + "cost_center": cost_center, + "account_currency": "USD", + "credit": 1000.00, + "credit_in_account_currency": 1000.00, + "debit": 0.00, + "debit_in_account_currency": 0.00, + "user_remark": employee_name + " Paycheck", + "idx": idx + 2, + }, + ) total_payroll += 1000.00 - je.append('accounts', { - 'account': salary_account, - 'cost_center': cost_center, - 'account_currency': 'USD', - 'credit': 0.00, - 'credit_in_account_currency': 0.00, - 'debit': total_payroll, - 'debit_in_account_currency': total_payroll, - 'idx': 1, - }) - je.append('accounts', { - 'account': payroll_expense, - 'cost_center': cost_center, - 'account_currency': 'USD', - 'credit': 0.00, - 'credit_in_account_currency': 0.00, - 'debit': total_payroll * 0.15, - 'debit_in_account_currency': total_payroll * 0.15, - }) - je.append('accounts', { - 'account': payable_account, - 'cost_center': cost_center, - 'party_type': 'Supplier', - 'party': tax_authority[0][0], - 'account_currency': 'USD', - 'credit': total_payroll * 0.15, - 'credit_in_account_currency':total_payroll * 0.15, - 'debit': 0.00, - 'debit_in_account_currency': 0.0, - }) + je.append( + "accounts", + { + "account": salary_account, + "cost_center": cost_center, + "account_currency": "USD", + "credit": 0.00, + "credit_in_account_currency": 0.00, + "debit": total_payroll, + "debit_in_account_currency": total_payroll, + "idx": 1, + }, + ) + je.append( + "accounts", + { + "account": payroll_expense, + "cost_center": cost_center, + "account_currency": "USD", + "credit": 0.00, + "credit_in_account_currency": 0.00, + "debit": total_payroll * 0.15, + "debit_in_account_currency": total_payroll * 0.15, + }, + ) + je.append( + "accounts", + { + "account": payable_account, + "cost_center": cost_center, + "party_type": "Supplier", + "party": tax_authority[0][0], + "account_currency": "USD", + "credit": total_payroll * 0.15, + "credit_in_account_currency": total_payroll * 0.15, + "debit": 0.00, + "debit_in_account_currency": 0.0, + }, + ) je.save() je.submit() + + +""" +Set in check run settings +check_run.test_setup.example_post_processing_hook +""" + + +def example_post_processing_hook( + check_run: "CheckRun", settings: "CheckRun Settings", nacha: "NACHAFile" +) -> str: + b = "$$AAPAACH0094[TEST[NL$$\n" + a = str(nacha) + return b + a diff --git a/docs/achgeneration.md b/docs/achgeneration.md index 7f1e5cd6..dbbbf8d1 100644 --- a/docs/achgeneration.md +++ b/docs/achgeneration.md @@ -2,11 +2,19 @@ For electronic bank transfers, banking institutions require specifically-formatted plain-text files to encode all necessary information. This includes data about the type of payment, the parties, their bank accounts, and payment amounts. These files conform to Automated Clearing House (ACH) standards, which is an electronic-funds transfer system run by the National Automated Clearing House Association (NACHA). ACH files are intended to represent electronic inter-bank transactions. -After submission, a Check Run will automatically generate this file, but only if the run includes payments using an "Electronic" Mode of Payment. See the [configuration page](./configuration.md) for details on how to set the `Mode of Payment` `type` field to mark it as an electronic bank transfer. +A Check Run will automatically generate this on demand, but only if the run includes payments using an "Electronic" Mode of Payment. See the [configuration page](./configuration.md) for details on how to set the `Mode of Payment` `type` field to mark it as an electronic bank transfer. The system defaults to using the "ach" file extension, but you can change this as needed in [Check Run Settings](./settings.md). The settings page also includes options to set two other mandatory fields in an ACH file: 1. **ACH Service Class Code** indicates the types of transactions in the batch. Code 200 is for both debit and credit transactions, code 220 is for only credit transactions, and code 225 is for only debit transactions 2. **ACH Standard Class Code** indicates how the transaction was authorized. Currently, the Check Run application only supports Prearranged Payment and Deposit Entries (code PPD) +Other fields available to help configure your ACH generation include: +- ACH Description, which goes into the Batch header +- Company Discretionary Data, also in the Batch header +- Immediate Origin, which can override the ABA number that the bank is expecting +- Custom Post Processing Hook, which allows you to provide a custom function to further manipulate the ACH file. For example, Royal Bank of Canada requires a non-standard first line. + +The 'Custom Post Processing Hook' is a read-only field and not intended to be set by non-technical users. The RBC example noted above can be set by entering the following into the browser console: `cur_frm.set_value('custom_post_processing_hook','check_run.test_setup.example_post_processing_hook')`. Provide the dotted path to your function with a signature matching that of the example. + ![Example ACH file data with properly-formatted header and batch entries.](./assets/ACHFile.png) \ No newline at end of file