Skip to content

Commit

Permalink
feat/wip: check run settings
Browse files Browse the repository at this point in the history
- include invoices, journal entries and expense claims discreetly
- pre-check overdue items
- number of invoices per voucher
- ACH file extension and company description

Not implemented yet:
 - allow/ disallow cancellation w/ unlinking
 - cascade cancellation t ocancel payment entries
 - ACH service class code and standard class code
  • Loading branch information
agritheory committed Aug 23, 2022
1 parent 5d05e38 commit 2e03e13
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 93 deletions.
27 changes: 24 additions & 3 deletions check_run/check_run/doctype/check_run/check_run.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,26 @@ frappe.ui.form.on("Check Run", {
frm.set_df_property('initial_check_number', 'read_only', 1)
frm.set_df_property('final_check_number', 'read_only', 1)
}

if (frm.doc.docstatus < 1 && frm.doc.__onload && frm.doc.__onload.settings_missing){
frappe.xcall('check_run.check_run.doctype.check_run.check_run.get_check_run_settings', {doc: frm.doc})
.then(r => {
if(r == undefined){
frappe.confirm(
__(`No settings found for <b>${frm.doc.bank_account}</b> and <b>${frm.doc.pay_to_account}</b>`),
() => {
frappe.xcall("check_run.check_run.doctype.check_run_settings.check_run_settings.create",
{ company: frm.doc.company, bank_account: frm.doc.bank_account, pay_to_account: frm.doc.pay_to_account }
).then(r => {
frappe.set_route("Form", "Check Run Settings", r)
})
},
() => {}
)
} else {
frm.doc.__onload.settings_missing = false
}
})
}
},
onload_post_render: frm => {
frm.page.wrapper.find('.layout-side-section').hide()
Expand All @@ -62,6 +81,9 @@ frappe.ui.form.on("Check Run", {
end_date: frm => {
get_entries(frm)
},
posting_date: frm => {
get_entries(frm)
},
start_date: frm => {
frappe.xcall('check_run.check_run.doctype.check_run.check_run.get_balance',{ doc: frm.doc })
.then(r => {
Expand Down Expand Up @@ -95,7 +117,6 @@ function get_balance(frm){
})
}


function set_queries(frm){
frm.set_query("bank_account", function() {
return {
Expand Down Expand Up @@ -286,4 +307,4 @@ function download_nacha(frm) {
window.setTimeout(() => {
frm.reload_doc()
}, 1000)
}
}
200 changes: 122 additions & 78 deletions check_run/check_run/doctype/check_run/check_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
from atnacha import ACHEntry, ACHBatch, NACHAFile

class CheckRun(Document):
def onload(self):
settings = get_check_run_settings(self)
if not settings:
self.set_onload('settings_missing', True)

def validate(self):
self.set_status()
gl_account = frappe.get_value('Bank Account', self.bank_account, 'account')
Expand Down Expand Up @@ -121,14 +126,19 @@ def ach_only(self):
return ach_only

def create_payment_entries(self, transactions):
settings = get_check_run_settings(self)
split = 5
if settings and settings.number_of_invoices_per_voucher:
split = settings.number_of_invoices_per_voucher
check_count = 0
_transactions = []
gl_account = frappe.get_value('Bank Account', self.bank_account, 'account')
for party, _group in groupby(transactions, key=lambda x: x.party):
_group = list(_group)
# split checks in groups of 5 if first reference is a check
# TODO: refactor split number into a settings page
groups = list(zip_longest(*[iter(_group)] * 5)) if frappe.db.get_value('Mode of Payment', _group[0].mode_of_payment, 'type') == 'Bank' else [_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:
Expand Down Expand Up @@ -260,86 +270,112 @@ 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})
else:
settings = None
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}
# TODO: introduce setting for excluding journal entries from settings
transactions = frappe.db.sql("""
(
SELECT
'Purchase Invoice' as doctype,
'Supplier' AS party_type,
`tabPurchase Invoice`.name,
`tabPurchase Invoice`.bill_no AS ref_number,
`tabPurchase Invoice`.supplier_name AS party,
`tabSupplier`.supplier_name AS party_name,
`tabPurchase Invoice`.outstanding_amount AS amount,
`tabPurchase Invoice`.due_date,
`tabPurchase Invoice`.posting_date,
COALESCE(`tabPurchase Invoice`.supplier_default_mode_of_payment, `tabSupplier`.supplier_default_mode_of_payment, '\n') AS mode_of_payment
FROM `tabPurchase Invoice`, `tabSupplier`
WHERE `tabPurchase Invoice`.outstanding_amount > 0
AND `tabPurchase Invoice`.supplier = `tabSupplier`.name
AND `tabPurchase Invoice`.company = %(company)s
AND `tabPurchase Invoice`.docstatus = 1
AND `tabPurchase Invoice`.credit_to = %(pay_to_account)s
AND `tabPurchase Invoice`.status != 'On Hold'
AND `tabPurchase Invoice`.due_date <= %(end_date)s
)
UNION
(
SELECT
'Expense Claim' as doctype,
'Employee' AS party_type,
`tabExpense Claim`.name,
`tabExpense Claim`.name AS ref_number,
`tabExpense Claim`.employee_name AS party,
`tabEmployee`.employee_name AS party_name,
`tabExpense Claim`.grand_total AS amount,
`tabExpense Claim`.posting_date AS due_date,
`tabExpense Claim`.posting_date,
COALESCE(`tabExpense Claim`.mode_of_payment, `tabEmployee`.mode_of_payment, '\n') AS mode_of_payment
FROM `tabExpense Claim`, `tabEmployee`
WHERE `tabExpense Claim`.grand_total > `tabExpense Claim`.total_amount_reimbursed
AND `tabExpense Claim`.employee = `tabEmployee`.name
AND `tabExpense Claim`.company = %(company)s
AND `tabExpense Claim`.docstatus = 1
AND `tabExpense Claim`.payable_account = %(pay_to_account)s
AND `tabExpense Claim`.posting_date <= %(end_date)s
)
UNION (
SELECT
'Journal Entry' AS doctype,
`tabJournal Entry Account`.party_type,
`tabJournal Entry`.name,
`tabJournal Entry`.name AS ref_number,
`tabJournal Entry Account`.party,
`tabJournal Entry Account`.party AS party_name,
`tabJournal Entry Account`.credit_in_account_currency AS amount,
`tabJournal Entry`.due_date,
`tabJournal Entry`.posting_date,
COALESCE(`tabJournal Entry`.mode_of_payment, '\n') AS mode_of_payment
FROM `tabJournal Entry`, `tabJournal Entry Account`
WHERE `tabJournal Entry`.name = `tabJournal Entry Account`.parent
AND `tabJournal Entry`.company = %(company)s
AND `tabJournal Entry`.docstatus = 1
AND `tabJournal Entry Account`.account = %(pay_to_account)s
AND `tabJournal Entry`.due_date <= %(end_date)s
AND `tabJournal Entry`.name NOT in (
SELECT `tabPayment Entry Reference`.reference_name
FROM `tabPayment Entry`, `tabPayment Entry Reference`
WHERE `tabPayment Entry Reference`.parent = `tabPayment Entry`.name
AND `tabPayment Entry Reference`.reference_doctype = 'Journal Entry'
AND `tabPayment Entry`.docstatus = 1

pi_select = """
(
SELECT
'Purchase Invoice' as doctype,
'Supplier' AS party_type,
`tabPurchase Invoice`.name,
`tabPurchase Invoice`.bill_no AS ref_number,
`tabPurchase Invoice`.supplier_name AS party,
`tabSupplier`.supplier_name AS party_name,
`tabPurchase Invoice`.outstanding_amount AS amount,
`tabPurchase Invoice`.due_date,
`tabPurchase Invoice`.posting_date,
COALESCE(`tabPurchase Invoice`.supplier_default_mode_of_payment, `tabSupplier`.supplier_default_mode_of_payment, '\n') AS mode_of_payment
FROM `tabPurchase Invoice`, `tabSupplier`
WHERE `tabPurchase Invoice`.outstanding_amount > 0
AND `tabPurchase Invoice`.supplier = `tabSupplier`.name
AND `tabPurchase Invoice`.company = %(company)s
AND `tabPurchase Invoice`.docstatus = 1
AND `tabPurchase Invoice`.credit_to = %(pay_to_account)s
AND `tabPurchase Invoice`.status != 'On Hold'
AND `tabPurchase Invoice`.due_date <= %(end_date)s
)
"""
ec_select = """
(
SELECT
'Expense Claim' as doctype,
'Employee' AS party_type,
`tabExpense Claim`.name,
`tabExpense Claim`.name AS ref_number,
`tabExpense Claim`.employee_name AS party,
`tabEmployee`.employee_name AS party_name,
`tabExpense Claim`.grand_total AS amount,
`tabExpense Claim`.posting_date AS due_date,
`tabExpense Claim`.posting_date,
COALESCE(`tabExpense Claim`.mode_of_payment, `tabEmployee`.mode_of_payment, '\n') AS mode_of_payment
FROM `tabExpense Claim`, `tabEmployee`
WHERE `tabExpense Claim`.grand_total > `tabExpense Claim`.total_amount_reimbursed
AND `tabExpense Claim`.employee = `tabEmployee`.name
AND `tabExpense Claim`.company = %(company)s
AND `tabExpense Claim`.docstatus = 1
AND `tabExpense Claim`.payable_account = %(pay_to_account)s
AND `tabExpense Claim`.posting_date <= %(end_date)s
)
)
ORDER BY due_date, name
""", {
"""

je_select = """
(
SELECT
'Journal Entry' AS doctype,
`tabJournal Entry Account`.party_type,
`tabJournal Entry`.name,
`tabJournal Entry`.name AS ref_number,
`tabJournal Entry Account`.party,
`tabJournal Entry Account`.party AS party_name,
`tabJournal Entry Account`.credit_in_account_currency AS amount,
`tabJournal Entry`.due_date,
`tabJournal Entry`.posting_date,
COALESCE(`tabJournal Entry`.mode_of_payment, '\n') AS mode_of_payment
FROM `tabJournal Entry`, `tabJournal Entry Account`
WHERE `tabJournal Entry`.name = `tabJournal Entry Account`.parent
AND `tabJournal Entry`.company = %(company)s
AND `tabJournal Entry`.docstatus = 1
AND `tabJournal Entry Account`.account = %(pay_to_account)s
AND `tabJournal Entry`.due_date <= %(end_date)s
AND `tabJournal Entry`.name NOT in (
SELECT `tabPayment Entry Reference`.reference_name
FROM `tabPayment Entry`, `tabPayment Entry Reference`
WHERE `tabPayment Entry Reference`.parent = `tabPayment Entry`.name
AND `tabPayment Entry Reference`.reference_doctype = 'Journal Entry'
AND `tabPayment Entry`.docstatus = 1
)
)
"""
query = ""
if not settings or settings.include_purchase_invoices:
query += pi_select
if not settings or settings.include_expense_claims:
if len(query) > 1:
query += "\nUNION\n"
query += ec_select
if not settings or settings.include_journal_entries:
if len(query) > 1:
query += "\nUNION\n"
query += je_select
query += "\nORDER BY due_date, name"

transactions = frappe.db.sql(query, {
'company': doc.company, 'pay_to_account': doc.pay_to_account, 'end_date': doc.end_date
}, as_dict=True)
for transaction in transactions:
if settings and settings.pre_check_overdue_items:
print(transaction.due_date, doc.posting_date)
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')
Expand Down Expand Up @@ -373,7 +409,9 @@ 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)
ach_file = doc.build_nacha_file()
frappe.local.response.filename = f'{docname.replace(" ", "-").replace("/", "-")}.ach'
settings = get_check_run_settings(doc)
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')
Expand Down Expand Up @@ -406,7 +444,6 @@ def build_nacha_file_from_payment_entries(doc, payment_entries):
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)
print(party_bank_account)
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')
Expand All @@ -433,7 +470,7 @@ def build_nacha_file_from_payment_entries(doc, payment_entries):
frappe.throw('<br>'.join(e for e in exceptions))

batch = ACHBatch(
service_class_code=220,
service_class_code=220, # TODO: pass in from settings
company_name=doc.get('company'),
company_discretionary_data='',
company_id=company_ach_id,
Expand All @@ -460,4 +497,11 @@ def build_nacha_file_from_payment_entries(doc, payment_entries):
reference_code='',
batches=[batch]
)
return nacha_file
return nacha_file


@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})
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright (c) 2022, AgriTheory and contributors
// For license information, please see license.txt

frappe.ui.form.on('Check Run Settings', {
// refresh: function(frm) {

// }
});
Loading

0 comments on commit 2e03e13

Please sign in to comment.