From 6f718a31f0a04a75018a408e285a3092db10b064 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 27 Jan 2021 16:59:07 +0530 Subject: [PATCH 1/8] refactor: tax withholding category against customer --- .../tax_withholding_category.py | 296 ++++++++++-------- .../test_tax_withholding_category.py | 11 + 2 files changed, 175 insertions(+), 132 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 32ad4cb03a..3e0ba9ac6a 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -12,37 +12,54 @@ from erpnext.accounts.utils import get_fiscal_year class TaxWithholdingCategory(Document): pass -def get_party_tax_withholding_details(ref_doc, tax_withholding_category=None): +def get_party_details(ref_doc): + party_type, party = '', '' + if ref_doc.doctype == 'Sales Invoice': + party_type = 'Customer' + party = ref_doc.customer + else: + party_type = 'Supplier' + party = ref_doc.supplier + + return party_type, party + +def get_party_tax_withholding_details(ref_doc, tax_withholding_category=None): pan_no = '' - suppliers = [] + parties = [] + party_type, party = get_party_details(ref_doc) if not tax_withholding_category: - tax_withholding_category, pan_no = frappe.db.get_value('Supplier', ref_doc.supplier, ['tax_withholding_category', 'pan']) + tax_withholding_category, pan_no = frappe.db.get_value(party_type, party, ['tax_withholding_category', 'pan']) if not tax_withholding_category: return + # if tax_withholding_category passed as an argument but not pan_no if not pan_no: - pan_no = frappe.db.get_value('Supplier', ref_doc.supplier, 'pan') + pan_no = frappe.db.get_value(party_type, party, 'pan') # Get others suppliers with the same PAN No if pan_no: - suppliers = [d.name for d in frappe.get_all('Supplier', fields=['name'], filters={'pan': pan_no})] + parties = frappe.get_all(party_type, filters={ 'pan': pan_no }, pluck='name') - if not suppliers: - suppliers.append(ref_doc.supplier) + if not parties: + parties.append(party) + + fiscal_year = get_fiscal_year(ref_doc.posting_date, company=ref_doc.company) + tax_details = get_tax_withholding_details(tax_withholding_category, fiscal_year[0], ref_doc.company) - fy = get_fiscal_year(ref_doc.posting_date, company=ref_doc.company) - tax_details = get_tax_withholding_details(tax_withholding_category, fy[0], ref_doc.company) if not tax_details: frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}') .format(tax_withholding_category, ref_doc.company)) - tds_amount = get_tds_amount(suppliers, ref_doc.net_total, ref_doc.company, - tax_details, fy, ref_doc.posting_date, pan_no) + tax_amount = get_tax_amount( + party_type, parties, + ref_doc, tax_details, + fiscal_year, pan_no + ) - tax_row = get_tax_row(tax_details, tds_amount) + tax_row = get_tax_row(tax_details, tax_amount) return tax_row @@ -69,147 +86,162 @@ def get_tax_withholding_rates(tax_withholding, fiscal_year): frappe.throw(_("No Tax Withholding data found for the current Fiscal Year.")) -def get_tax_row(tax_details, tds_amount): - +def get_tax_row(tax_details, tax_amount): return { "category": "Total", "add_deduct_tax": "Deduct", "charge_type": "Actual", "account_head": tax_details.account_head, "description": tax_details.description, - "tax_amount": tds_amount + "tax_amount": tax_amount } -def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_details, posting_date, pan_no=None): - fiscal_year, year_start_date, year_end_date = fiscal_year_details - tds_amount = 0 - tds_deducted = 0 - - def _get_tds(amount, rate): - if amount <= 0: - return 0 - - return amount * rate / 100 - - ldc_name = frappe.db.get_value('Lower Deduction Certificate', - { - 'pan_no': pan_no, - 'fiscal_year': fiscal_year - }, 'name') - ldc = '' - +def get_lower_deduction_certificate(fiscal_year, pan_no): + ldc_name = frappe.db.get_value('Lower Deduction Certificate', { 'pan_no': pan_no, 'fiscal_year': fiscal_year }, 'name') if ldc_name: - ldc = frappe.get_doc('Lower Deduction Certificate', ldc_name) + return frappe.get_doc('Lower Deduction Certificate', ldc_name) - entries = frappe.db.sql(""" - select voucher_no, credit - from `tabGL Entry` - where company = %s and - party in %s and fiscal_year=%s and credit > 0 - and is_opening = 'No' - """, (company, tuple(suppliers), fiscal_year), as_dict=1) +def get_tax_amount(party_type, parties, ref_doc, tax_details, fiscal_year_details, pan_no=None): + fiscal_year = fiscal_year_details[0] - vouchers = [d.voucher_no for d in entries] - advance_vouchers = get_advance_vouchers(suppliers, fiscal_year=fiscal_year, company=company) + vouchers = get_invoice_vouchers(parties, fiscal_year, ref_doc.company, party_type=party_type) or [""] + advance_vouchers = get_advance_vouchers(parties, fiscal_year, ref_doc.company, party_type=party_type) + tax_vouchers = vouchers + advance_vouchers - tds_vouchers = vouchers + advance_vouchers + tax_deducted = 0 + dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit' + if tax_vouchers: + filters = { + dr_or_cr: ['>', 0], + 'account': tax_details.account_head, + 'fiscal_year': fiscal_year, + 'voucher_no': ['in', tax_vouchers], + 'is_cancelled': 0 + } + field = "sum({})".format(dr_or_cr) - if tds_vouchers: - tds_deducted = frappe.db.sql(""" - SELECT sum(credit) FROM `tabGL Entry` - WHERE - account=%s and fiscal_year=%s and credit > 0 - and voucher_no in ({0})""". format(','.join(['%s'] * len(tds_vouchers))), - ((tax_details.account_head, fiscal_year) + tuple(tds_vouchers))) + tax_deducted = frappe.db.get_value('GL Entry', filters, field) or 0.0 - tds_deducted = tds_deducted[0][0] if tds_deducted and tds_deducted[0][0] else 0 + tax_amount = 0 + if party_type == 'Supplier': + net_total = ref_doc.net_total + posting_date = ref_doc.posting_date + ldc = get_lower_deduction_certificate(fiscal_year, pan_no) - if tds_deducted: - if ldc: - limit_consumed = frappe.db.get_value('Purchase Invoice', - { - 'supplier': ('in', suppliers), - 'apply_tds': 1, - 'docstatus': 1 - }, 'sum(net_total)') - - if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, limit_consumed, net_total, - ldc.certificate_limit): - - tds_amount = get_ltds_amount(net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details) - else: - tds_amount = _get_tds(net_total, tax_details.rate) - else: - supplier_credit_amount = frappe.get_all('Purchase Invoice', - fields = ['sum(net_total)'], - filters = {'name': ('in', vouchers), 'docstatus': 1, "apply_tds": 1}, as_list=1) - - supplier_credit_amount = (supplier_credit_amount[0][0] - if supplier_credit_amount and supplier_credit_amount[0][0] else 0) - - jv_supplier_credit_amt = frappe.get_all('Journal Entry Account', - fields = ['sum(credit_in_account_currency)'], - filters = { - 'parent': ('in', vouchers), 'docstatus': 1, - 'party': ('in', suppliers), - 'reference_type': ('not in', ['Purchase Invoice']) - }, as_list=1) - - supplier_credit_amount += (jv_supplier_credit_amt[0][0] - if jv_supplier_credit_amt and jv_supplier_credit_amt[0][0] else 0) - - supplier_credit_amount += net_total - - debit_note_amount = get_debit_note_amount(suppliers, year_start_date, year_end_date) - supplier_credit_amount -= debit_note_amount - - if ((tax_details.get('threshold', 0) and supplier_credit_amount >= tax_details.threshold) - or (tax_details.get('cumulative_threshold', 0) and supplier_credit_amount >= tax_details.cumulative_threshold)): - - if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, tds_deducted, net_total, - ldc.certificate_limit): - tds_amount = get_ltds_amount(supplier_credit_amount, 0, ldc.certificate_limit, ldc.rate, - tax_details) + if tax_deducted: + if ldc: + tax_amount = get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total) else: - tds_amount = _get_tds(supplier_credit_amount, tax_details.rate) + tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0 + else: + tax_amount = get_tds_amount( + ldc, parties, ref_doc, tax_details, + fiscal_year_details, vouchers + ) + + return tax_amount + +def get_invoice_vouchers(parties, fiscal_year, company, party_type='Supplier'): + dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit' + + filters = { + dr_or_cr: ['>', 0], + 'company': company, + 'party_type': party_type, + 'party': ['in', parties], + 'fiscal_year': fiscal_year, + 'is_opening': 'No', + 'is_cancelled': 0 + } + + return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck="voucher_no") + +def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None, to_date=None, party_type='Supplier'): + # for advance vouchers, debit and credit is reversed + dr_or_cr = 'debit' if party_type == 'Supplier' else 'credit' + + filters = { + dr_or_cr: ['>', 0], + 'party_type': party_type, + 'party': ['in', parties], + 'is_opening': 'No', + 'is_cancelled': 0 + } + + if fiscal_year: + filters['fiscal_year'] = fiscal_year + if company: + filters['company'] = company + if from_date and to_date: + filters['posting_date'] = ['between', (from_date, to_date)] + + return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') + +def get_tds_amount(ldc, parties, ref_doc, tax_details, fiscal_year_details, vouchers): + tds_amount = 0 + + supp_credit_amt = frappe.db.get_value('Purchase Invoice', { + 'name': ('in', vouchers), 'docstatus': 1, 'apply_tds': 1 + }, 'sum(net_total)') or 0.0 + + supp_jv_credit_amt = frappe.db.get_value('Journal Entry Account', { + 'parent': ('in', vouchers), 'docstatus': 1, + 'party': ('in', parties), 'reference_type': ('!=', 'Purchase Invoice') + }, 'sum(credit_in_account_currency)') or 0.0 + + supp_credit_amt += supp_jv_credit_amt + supp_credit_amt += ref_doc.net_total + + debit_note_amount = get_debit_note_amount(parties, fiscal_year_details, ref_doc.company) + supp_credit_amt -= debit_note_amount + + threshold = tax_details.get('threshold', 0) + cumulative_threshold = tax_details.get('cumulative_threshold', 0) + + if ((threshold and supp_credit_amt >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)): + if ldc and is_valid_certificate( + ldc.valid_from, ldc.valid_upto, + ref_doc.posting_date, tax_deducted, + net_total, ldc.certificate_limit + ): + tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details) + else: + tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0 return tds_amount -def get_advance_vouchers(suppliers, fiscal_year=None, company=None, from_date=None, to_date=None): - condition = "fiscal_year=%s" % fiscal_year +def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total): + tds_amount = 0 + limit_consumed = frappe.db.get_value('Purchase Invoice', { + 'supplier': ('in', parties), + 'apply_tds': 1, + 'docstatus': 1 + }, 'sum(net_total)') + + if is_valid_certificate( + ldc.valid_from, ldc.valid_upto, + posting_date, limit_consumed, + net_total, ldc.certificate_limit + ): + tds_amount = get_ltds_amount(net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details) + + return tds_amount + +def get_debit_note_amount(suppliers, fiscal_year_details, company=None): + _, year_start_date, year_end_date = fiscal_year_details + + filters = { + 'supplier': ['in', suppliers], + 'is_return': 1, + 'docstatus': 1, + 'posting_date': ['between', (year_start_date, year_end_date)] + } + fields = ['abs(sum(net_total)) as net_total'] if company: - condition += "and company =%s" % (company) - if from_date and to_date: - condition += "and posting_date between %s and %s" % (from_date, to_date) + filters['company'] = company - ## Appending the same supplier again if length of suppliers list is 1 - ## since tuple of single element list contains None, For example ('Test Supplier 1', ) - ## and the below query fails - if len(suppliers) == 1: - suppliers.append(suppliers[0]) - - return frappe.db.sql_list(""" - select distinct voucher_no - from `tabGL Entry` - where party in %s and %s and debit > 0 - and is_opening = 'No' - """, (tuple(suppliers), condition)) or [] - -def get_debit_note_amount(suppliers, year_start_date, year_end_date, company=None): - condition = "and 1=1" - if company: - condition = " and company=%s " % company - - if len(suppliers) == 1: - suppliers.append(suppliers[0]) - - return flt(frappe.db.sql(""" - select abs(sum(net_total)) - from `tabPurchase Invoice` - where supplier in %s and is_return=1 and docstatus=1 - and posting_date between %s and %s %s - """, (tuple(suppliers), year_start_date, year_end_date, condition))) + return frappe.get_all('Purchase Invoice', filters, fields)[0].get('net_total') or 0.0 def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details): if current_amount < (certificate_limit - deducted_amount): @@ -227,4 +259,4 @@ def is_valid_certificate(valid_from, valid_upto, posting_date, deducted_amount, certificate_limit > deducted_amount): valid = True - return valid \ No newline at end of file + return valid diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index ef77674372..2b387f965a 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -18,6 +18,17 @@ class TestTaxWithholdingCategory(unittest.TestCase): create_records() create_tax_with_holding_category() + def tearDown(self): + frappe.db.sql('delete from `tabPurchase Invoice` where supplier = "Test TDS Supplier"') + frappe.db.sql('delete from `tabPurchase Invoice` where supplier = "Test TDS Supplier1"') + frappe.db.sql('delete from `tabPurchase Invoice` where supplier = "Test TDS Supplier2"') + frappe.db.sql('delete from `tabPurchase Invoice` where supplier = "Test TDS Supplier ABC"') + + frappe.db.sql('delete from `tabGL Entry` where party = "Test TDS Supplier"') + frappe.db.sql('delete from `tabGL Entry` where party = "Test TDS Supplier1"') + frappe.db.sql('delete from `tabGL Entry` where party = "Test TDS Supplier2"') + frappe.db.sql('delete from `tabGL Entry` where party = "Test TDS Supplier ABC"') + def test_cumulative_threshold_tds(self): frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS") invoices = [] From 58250aac10b15ec65785f6adb0a6ab68d40721ce Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 27 Jan 2021 16:59:36 +0530 Subject: [PATCH 2/8] feat: pan and tax withholding category fields for customer --- .../selling/doctype/customer/customer.json | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 557c7151d9..8fb3580747 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -16,6 +16,8 @@ "customer_name", "gender", "customer_type", + "pan", + "tax_withholding_category", "default_bank_account", "lead_name", "image", @@ -210,7 +212,8 @@ "fieldtype": "Link", "ignore_user_permissions": 1, "label": "Represents Company", - "options": "Company" + "options": "Company", + "unique": 1 }, { "depends_on": "represents_company", @@ -479,13 +482,25 @@ "fieldname": "dn_required", "fieldtype": "Check", "label": "Allow Sales Invoice Creation Without Delivery Note" + }, + { + "fieldname": "pan", + "fieldtype": "Data", + "label": "PAN" + }, + { + "fieldname": "tax_withholding_category", + "fieldtype": "Link", + "label": "Tax Withholding Category", + "options": "Tax Withholding Category" } ], "icon": "fa fa-user", "idx": 363, "image_field": "image", + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-03-17 11:03:42.706907", + "modified": "2021-01-27 12:54:57.258959", "modified_by": "Administrator", "module": "Selling", "name": "Customer", From 6db14d02d21010e6af3c659f8c58d52530993feb Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 27 Jan 2021 17:11:34 +0530 Subject: [PATCH 3/8] fix: test --- .../test_tax_withholding_category.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index 2b387f965a..ef77674372 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -18,17 +18,6 @@ class TestTaxWithholdingCategory(unittest.TestCase): create_records() create_tax_with_holding_category() - def tearDown(self): - frappe.db.sql('delete from `tabPurchase Invoice` where supplier = "Test TDS Supplier"') - frappe.db.sql('delete from `tabPurchase Invoice` where supplier = "Test TDS Supplier1"') - frappe.db.sql('delete from `tabPurchase Invoice` where supplier = "Test TDS Supplier2"') - frappe.db.sql('delete from `tabPurchase Invoice` where supplier = "Test TDS Supplier ABC"') - - frappe.db.sql('delete from `tabGL Entry` where party = "Test TDS Supplier"') - frappe.db.sql('delete from `tabGL Entry` where party = "Test TDS Supplier1"') - frappe.db.sql('delete from `tabGL Entry` where party = "Test TDS Supplier2"') - frappe.db.sql('delete from `tabGL Entry` where party = "Test TDS Supplier ABC"') - def test_cumulative_threshold_tds(self): frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS") invoices = [] From ff12f914867e22734f5cf2d9a280b43401988d06 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 27 Jan 2021 19:17:38 +0530 Subject: [PATCH 4/8] feat: charging tcs on sales invoice --- .../doctype/sales_invoice/sales_invoice.py | 27 ++++ .../tax_withholding_category.py | 92 +++++++++++--- .../test_tax_withholding_category.py | 117 +++++++++++++++++- 3 files changed, 215 insertions(+), 21 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 566734e7d1..0be63a8c31 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -21,6 +21,7 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente from erpnext.accounts.doctype.loyalty_program.loyalty_program import \ get_loyalty_program_details_with_points, get_loyalty_details, validate_loyalty_points from erpnext.accounts.deferred_revenue import validate_service_stop_date +from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details from erpnext.healthcare.utils import manage_invoice_submit_cancel @@ -73,6 +74,8 @@ class SalesInvoice(SellingController): if not self.is_pos: self.so_dn_required() + + self.set_tax_withholding() self.validate_proj_cust() self.validate_pos_return() @@ -151,6 +154,30 @@ class SalesInvoice(SellingController): if cost_center_company != self.company: frappe.throw(_("Row #{0}: Cost Center {1} does not belong to company {2}").format(frappe.bold(item.idx), frappe.bold(item.cost_center), frappe.bold(self.company))) + def set_tax_withholding(self): + tax_withholding_details = get_party_tax_withholding_details(self) + + if not tax_withholding_details: + return + + accounts = [] + for d in self.taxes: + if d.account_head == tax_withholding_details.get("account_head"): + d.update(tax_withholding_details) + accounts.append(d.account_head) + + if not accounts or tax_withholding_details.get("account_head") not in accounts: + self.append("taxes", tax_withholding_details) + + to_remove = [d for d in self.taxes + if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")] + + for d in to_remove: + self.remove(d) + + # calculate totals again after applying TDS + self.calculate_taxes_and_totals() + def before_save(self): set_account_for_mode_of_payment(self) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 3e0ba9ac6a..36ed6decad 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -104,31 +104,30 @@ def get_lower_deduction_certificate(fiscal_year, pan_no): def get_tax_amount(party_type, parties, ref_doc, tax_details, fiscal_year_details, pan_no=None): fiscal_year = fiscal_year_details[0] - vouchers = get_invoice_vouchers(parties, fiscal_year, ref_doc.company, party_type=party_type) or [""] + vouchers = get_invoice_vouchers(parties, fiscal_year, ref_doc.company, party_type=party_type) advance_vouchers = get_advance_vouchers(parties, fiscal_year, ref_doc.company, party_type=party_type) - tax_vouchers = vouchers + advance_vouchers + taxable_vouchers = vouchers + advance_vouchers tax_deducted = 0 - dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit' - if tax_vouchers: + if taxable_vouchers: + # check if tds / tcs is already charged on taxable vouchers filters = { - dr_or_cr: ['>', 0], - 'account': tax_details.account_head, + 'is_cancelled': 0, + 'credit': ['>', 0], 'fiscal_year': fiscal_year, - 'voucher_no': ['in', tax_vouchers], - 'is_cancelled': 0 + 'account': tax_details.account_head, + 'voucher_no': ['in', taxable_vouchers], } - field = "sum({})".format(dr_or_cr) + field = "sum(credit)" tax_deducted = frappe.db.get_value('GL Entry', filters, field) or 0.0 tax_amount = 0 + posting_date = ref_doc.posting_date if party_type == 'Supplier': - net_total = ref_doc.net_total - posting_date = ref_doc.posting_date ldc = get_lower_deduction_certificate(fiscal_year, pan_no) - if tax_deducted: + net_total = ref_doc.net_total if ldc: tax_amount = get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total) else: @@ -139,6 +138,19 @@ def get_tax_amount(party_type, parties, ref_doc, tax_details, fiscal_year_detail fiscal_year_details, vouchers ) + elif party_type == 'Customer': + if tax_deducted: + grand_total = get_invoice_total_without_tcs(ref_doc, tax_details) + # if already tcs is charged, then (net total + gst amount) of invoice is chargeable + tax_amount = grand_total * tax_details.rate / 100 if grand_total > 0 else 0 + else: + # if no tcs has been charged in FY, + # then (prev invoices + advances) value crossing the threshold are chargeable + tax_amount = get_tcs_amount( + parties, ref_doc, tax_details, + fiscal_year_details, vouchers, advance_vouchers + ) + return tax_amount def get_invoice_vouchers(parties, fiscal_year, company, party_type='Supplier'): @@ -154,7 +166,7 @@ def get_invoice_vouchers(parties, fiscal_year, company, party_type='Supplier'): 'is_cancelled': 0 } - return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck="voucher_no") + return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck="voucher_no") or [""] def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None, to_date=None, party_type='Supplier'): # for advance vouchers, debit and credit is reversed @@ -162,10 +174,11 @@ def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None filters = { dr_or_cr: ['>', 0], + 'is_opening': 'No', + 'is_cancelled': 0, 'party_type': party_type, 'party': ['in', parties], - 'is_opening': 'No', - 'is_cancelled': 0 + 'against_voucher': ['is', 'not set'] } if fiscal_year: @@ -175,7 +188,7 @@ def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None if from_date and to_date: filters['posting_date'] = ['between', (from_date, to_date)] - return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') + return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""] def get_tds_amount(ldc, parties, ref_doc, tax_details, fiscal_year_details, vouchers): tds_amount = 0 @@ -210,6 +223,53 @@ def get_tds_amount(ldc, parties, ref_doc, tax_details, fiscal_year_details, vouc return tds_amount +def get_tcs_amount(parties, ref_doc, tax_details, fiscal_year_details, vouchers, adv_vouchers): + tcs_amount = 0 + fiscal_year, _, _ = fiscal_year_details + + # sum of debit entries made from sales invoices + invoiced_amt = frappe.db.get_value('GL Entry', { + 'is_cancelled': 0, + 'party': ['in', parties], + 'company': ref_doc.company, + 'voucher_no': ['in', vouchers], + }, 'sum(debit)') or 0.0 + + # sum of credit entries made from PE / JV with unset 'against voucher' + advance_amt = frappe.db.get_value('GL Entry', { + 'is_cancelled': 0, + 'party': ['in', parties], + 'company': ref_doc.company, + 'voucher_no': ['in', adv_vouchers], + }, 'sum(credit)') or 0.0 + + # sum of credit entries made from sales invoice + credit_note_amt = frappe.db.get_value('GL Entry', { + 'is_cancelled': 0, + 'credit': ['>', 0], + 'party': ['in', parties], + 'fiscal_year': fiscal_year, + 'company': ref_doc.company, + 'voucher_type': 'Sales Invoice', + }, 'sum(credit)') or 0.0 + + current_invoice_total = get_invoice_total_without_tcs(ref_doc, tax_details) + chargeable_amt = current_invoice_total + invoiced_amt + advance_amt - credit_note_amt + + threshold = tax_details.get('threshold', 0) + cumulative_threshold = tax_details.get('cumulative_threshold', 0) + + if ((threshold and chargeable_amt >= threshold) or (cumulative_threshold and chargeable_amt >= cumulative_threshold)): + tcs_amount = chargeable_amt * tax_details.rate / 100 if chargeable_amt > 0 else 0 + + return tcs_amount + +def get_invoice_total_without_tcs(ref_doc, tax_details): + tcs_tax_row = [d for d in ref_doc.taxes if d.account_head == tax_details.account_head] + tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0 + + return ref_doc.grand_total - tcs_tax_row_amount + def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total): tds_amount = 0 limit_consumed = frappe.db.get_value('Purchase Invoice', { diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index ef77674372..c8bd0834d8 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -9,7 +9,7 @@ from frappe.utils import today from erpnext.accounts.utils import get_fiscal_year from erpnext.buying.doctype.supplier.test_supplier import create_supplier -test_dependencies = ["Supplier Group"] +test_dependencies = ["Supplier Group", "Customer Group"] class TestTaxWithholdingCategory(unittest.TestCase): @classmethod @@ -128,9 +128,42 @@ class TestTaxWithholdingCategory(unittest.TestCase): for d in invoices: d.cancel() + def test_cumulative_threshold_tcs(self): + frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS") + invoices = [] + + # create invoices for lower than single threshold tax rate + for _ in range(2): + si = create_sales_invoice(customer = "Test TCS Customer") + si.submit() + invoices.append(si) + + # create another invoice whose total when added to previously created invoice, + # surpasses cumulative threshhold + si = create_sales_invoice(customer = "Test TCS Customer") + si.submit() + + # assert tax collection on total invoice amount created until now + tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC']) + self.assertEqual(tcs_charged, 3000) + self.assertEqual(si.grand_total, 13000) + invoices.append(si) + + # TCS is already collected once, so going forward system will collect TCS on every invoice + si = create_sales_invoice(customer = "Test TCS Customer", rate=5000) + si.submit() + + tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC']) + self.assertEqual(tcs_charged, 500) + invoices.append(si) + + #delete invoices to avoid clashing + for d in invoices: + d.cancel() + def create_purchase_invoice(**args): # return sales invoice doc object - item = frappe.get_doc('Item', {'item_name': 'TDS Item'}) + item = frappe.db.get_value('Item', {'item_name': 'TDS Item'}, "name") args = frappe._dict(args) pi = frappe.get_doc({ @@ -145,7 +178,7 @@ def create_purchase_invoice(**args): "taxes": [], "items": [{ 'doctype': 'Purchase Invoice Item', - 'item_code': item.name, + 'item_code': item, 'qty': args.qty or 1, 'rate': args.rate or 10000, 'cost_center': 'Main - _TC', @@ -156,6 +189,33 @@ def create_purchase_invoice(**args): pi.save() return pi +def create_sales_invoice(**args): + # return sales invoice doc object + item = frappe.db.get_value('Item', {'item_name': 'TCS Item'}, "name") + + args = frappe._dict(args) + si = frappe.get_doc({ + "doctype": "Sales Invoice", + "posting_date": today(), + "customer": args.customer, + "company": '_Test Company', + "taxes_and_charges": "", + "currency": "INR", + "debit_to": "Debtors - _TC", + "taxes": [], + "items": [{ + 'doctype': 'Sales Invoice Item', + 'item_code': item, + 'qty': args.qty or 1, + 'rate': args.rate or 10000, + 'cost_center': 'Main - _TC', + 'expense_account': 'Cost of Goods Sold - _TC' + }] + }) + + si.save() + return si + def create_records(): # create a new suppliers for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']: @@ -168,7 +228,17 @@ def create_records(): "doctype": "Supplier", }).insert() - # create an item + for name in ['Test TCS Customer']: + if frappe.db.exists('Customer', name): + continue + + frappe.get_doc({ + "customer_group": "_Test Customer Group", + "customer_name": name, + "doctype": "Customer" + }).insert() + + # create item if not frappe.db.exists('Item', "TDS Item"): frappe.get_doc({ "doctype": "Item", @@ -178,7 +248,16 @@ def create_records(): "is_stock_item": 0, }).insert() - # create an account + if not frappe.db.exists('Item', "TCS Item"): + frappe.get_doc({ + "doctype": "Item", + "item_code": "TCS Item", + "item_name": "TCS Item", + "item_group": "All Item Groups", + "is_stock_item": 1 + }).insert() + + # create tds account if not frappe.db.exists("Account", "TDS - _TC"): frappe.get_doc({ 'doctype': 'Account', @@ -189,6 +268,17 @@ def create_records(): 'root_type': 'Asset' }).insert() + # create tcs account + if not frappe.db.exists("Account", "TCS - _TC"): + frappe.get_doc({ + 'doctype': 'Account', + 'company': '_Test Company', + 'account_name': 'TCS', + 'parent_account': 'Duties and Taxes - _TC', + 'report_type': 'Balance Sheet', + 'root_type': 'Liability' + }).insert() + def create_tax_with_holding_category(): fiscal_year = get_fiscal_year(today(), company="_Test Company")[0] @@ -210,6 +300,23 @@ def create_tax_with_holding_category(): }] }).insert() + if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TCS"): + frappe.get_doc({ + "doctype": "Tax Withholding Category", + "name": "Cumulative Threshold TCS", + "category_name": "10% TCS", + "rates": [{ + 'fiscal_year': fiscal_year, + 'tax_withholding_rate': 10, + 'single_threshold': 0, + 'cumulative_threshold': 30000.00 + }], + "accounts": [{ + 'company': '_Test Company', + 'account': 'TCS - _TC' + }] + }).insert() + # Single thresold if not frappe.db.exists("Tax Withholding Category", "Single Threshold TDS"): frappe.get_doc({ From ae9a9a216785f29d6c3886f327385a4ab9428953 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 27 Jan 2021 19:24:42 +0530 Subject: [PATCH 5/8] fix: tcs chargable amount --- .../tax_withholding_category.py | 16 ++++++++++------ .../test_tax_withholding_category.py | 6 +++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 36ed6decad..f2c4973caf 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -53,6 +53,10 @@ def get_party_tax_withholding_details(ref_doc, tax_withholding_category=None): frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}') .format(tax_withholding_category, ref_doc.company)) + if party_type == 'Customer' and not tax_details.cumulative_threshold: + frappe.throw(_('Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value.') + .format(tax_withholding_category, ref_doc.company, party)) + tax_amount = get_tax_amount( party_type, parties, ref_doc, tax_details, @@ -145,7 +149,7 @@ def get_tax_amount(party_type, parties, ref_doc, tax_details, fiscal_year_detail tax_amount = grand_total * tax_details.rate / 100 if grand_total > 0 else 0 else: # if no tcs has been charged in FY, - # then (prev invoices + advances) value crossing the threshold are chargeable + # then chargeable value is "prev invoices + advances" value which cross the threshold tax_amount = get_tcs_amount( parties, ref_doc, tax_details, fiscal_year_details, vouchers, advance_vouchers @@ -253,13 +257,13 @@ def get_tcs_amount(parties, ref_doc, tax_details, fiscal_year_details, vouchers, 'voucher_type': 'Sales Invoice', }, 'sum(credit)') or 0.0 - current_invoice_total = get_invoice_total_without_tcs(ref_doc, tax_details) - chargeable_amt = current_invoice_total + invoiced_amt + advance_amt - credit_note_amt - - threshold = tax_details.get('threshold', 0) cumulative_threshold = tax_details.get('cumulative_threshold', 0) - if ((threshold and chargeable_amt >= threshold) or (cumulative_threshold and chargeable_amt >= cumulative_threshold)): + current_invoice_total = get_invoice_total_without_tcs(ref_doc, tax_details) + total_invoiced_amt = current_invoice_total + invoiced_amt + advance_amt - credit_note_amt + + if ((cumulative_threshold and total_invoiced_amt >= cumulative_threshold)): + chargeable_amt = total_invoiced_amt - cumulative_threshold tcs_amount = chargeable_amt * tax_details.rate / 100 if chargeable_amt > 0 else 0 return tcs_amount diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index c8bd0834d8..1d8fa4532f 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -140,13 +140,13 @@ class TestTaxWithholdingCategory(unittest.TestCase): # create another invoice whose total when added to previously created invoice, # surpasses cumulative threshhold - si = create_sales_invoice(customer = "Test TCS Customer") + si = create_sales_invoice(customer = "Test TCS Customer", rate=12000) si.submit() # assert tax collection on total invoice amount created until now tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC']) - self.assertEqual(tcs_charged, 3000) - self.assertEqual(si.grand_total, 13000) + self.assertEqual(tcs_charged, 200) + self.assertEqual(si.grand_total, 12200) invoices.append(si) # TCS is already collected once, so going forward system will collect TCS on every invoice From 4d9b6066a2de64c231464ed8c33044bbf479af0f Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 28 Jan 2021 18:07:08 +0530 Subject: [PATCH 6/8] fix: tcs amount calculation --- .../doctype/sales_invoice/sales_invoice.py | 8 +- .../tax_withholding_category.py | 140 +++++++++++------- .../test_tax_withholding_category.py | 20 +++ 3 files changed, 113 insertions(+), 55 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 0be63a8c31..a106af7fa4 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -161,16 +161,18 @@ class SalesInvoice(SellingController): return accounts = [] + tax_withholding_account = tax_withholding_details.get("account_head") + for d in self.taxes: - if d.account_head == tax_withholding_details.get("account_head"): + if d.account_head == tax_withholding_account: d.update(tax_withholding_details) accounts.append(d.account_head) - if not accounts or tax_withholding_details.get("account_head") not in accounts: + if not accounts or tax_withholding_account not in accounts: self.append("taxes", tax_withholding_details) to_remove = [d for d in self.taxes - if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")] + if not d.tax_amount and d.charge_type == "Actual" and d.account_head == tax_withholding_account] for d in to_remove: self.remove(d) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index f2c4973caf..4cbca6c08b 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -12,22 +12,22 @@ from erpnext.accounts.utils import get_fiscal_year class TaxWithholdingCategory(Document): pass -def get_party_details(ref_doc): +def get_party_details(inv): party_type, party = '', '' - if ref_doc.doctype == 'Sales Invoice': + if inv.doctype == 'Sales Invoice': party_type = 'Customer' - party = ref_doc.customer + party = inv.customer else: party_type = 'Supplier' - party = ref_doc.supplier + party = inv.supplier return party_type, party -def get_party_tax_withholding_details(ref_doc, tax_withholding_category=None): +def get_party_tax_withholding_details(inv, tax_withholding_category=None): pan_no = '' parties = [] - party_type, party = get_party_details(ref_doc) + party_type, party = get_party_details(inv) if not tax_withholding_category: tax_withholding_category, pan_no = frappe.db.get_value(party_type, party, ['tax_withholding_category', 'pan']) @@ -46,24 +46,28 @@ def get_party_tax_withholding_details(ref_doc, tax_withholding_category=None): if not parties: parties.append(party) - fiscal_year = get_fiscal_year(ref_doc.posting_date, company=ref_doc.company) - tax_details = get_tax_withholding_details(tax_withholding_category, fiscal_year[0], ref_doc.company) + fiscal_year = get_fiscal_year(inv.posting_date, company=inv.company) + tax_details = get_tax_withholding_details(tax_withholding_category, fiscal_year[0], inv.company) if not tax_details: frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}') - .format(tax_withholding_category, ref_doc.company)) + .format(tax_withholding_category, inv.company)) if party_type == 'Customer' and not tax_details.cumulative_threshold: + # TCS is only chargeable on sum of invoiced value frappe.throw(_('Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value.') - .format(tax_withholding_category, ref_doc.company, party)) + .format(tax_withholding_category, inv.company, party)) - tax_amount = get_tax_amount( + tax_amount, tax_deducted = get_tax_amount( party_type, parties, - ref_doc, tax_details, + inv, tax_details, fiscal_year, pan_no ) - tax_row = get_tax_row(tax_details, tax_amount) + if party_type == 'Supplier': + tax_row = get_tax_row_for_tds(tax_details, tax_amount) + else: + tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted) return tax_row @@ -90,14 +94,44 @@ def get_tax_withholding_rates(tax_withholding, fiscal_year): frappe.throw(_("No Tax Withholding data found for the current Fiscal Year.")) -def get_tax_row(tax_details, tax_amount): +def get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted): + row = { + "category": "Total", + "charge_type": "Actual", + "tax_amount": tax_amount, + "description": tax_details.description, + "account_head": tax_details.account_head + } + + if tax_deducted: + # TCS already deducted on previous invoices + # So, TCS will be calculated by 'Previous Row Total' + + taxes_excluding_tcs = [d for d in inv.taxes if d.account_head != tax_details.account_head] + if taxes_excluding_tcs: + # chargeable amount is the total amount after other charges are applied + row.update({ + "charge_type": "On Previous Row Total", + "row_id": len(taxes_excluding_tcs), + "rate": tax_details.rate + }) + else: + # if only TCS is to be charged, then net total is chargeable amount + row.update({ + "charge_type": "On Net Total", + "rate": tax_details.rate + }) + + return row + +def get_tax_row_for_tds(tax_details, tax_amount): return { "category": "Total", - "add_deduct_tax": "Deduct", "charge_type": "Actual", - "account_head": tax_details.account_head, + "tax_amount": tax_amount, + "add_deduct_tax": "Deduct", "description": tax_details.description, - "tax_amount": tax_amount + "account_head": tax_details.account_head } def get_lower_deduction_certificate(fiscal_year, pan_no): @@ -105,57 +139,46 @@ def get_lower_deduction_certificate(fiscal_year, pan_no): if ldc_name: return frappe.get_doc('Lower Deduction Certificate', ldc_name) -def get_tax_amount(party_type, parties, ref_doc, tax_details, fiscal_year_details, pan_no=None): +def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None): fiscal_year = fiscal_year_details[0] - vouchers = get_invoice_vouchers(parties, fiscal_year, ref_doc.company, party_type=party_type) - advance_vouchers = get_advance_vouchers(parties, fiscal_year, ref_doc.company, party_type=party_type) + vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type) + advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type) taxable_vouchers = vouchers + advance_vouchers tax_deducted = 0 if taxable_vouchers: - # check if tds / tcs is already charged on taxable vouchers - filters = { - 'is_cancelled': 0, - 'credit': ['>', 0], - 'fiscal_year': fiscal_year, - 'account': tax_details.account_head, - 'voucher_no': ['in', taxable_vouchers], - } - field = "sum(credit)" - - tax_deducted = frappe.db.get_value('GL Entry', filters, field) or 0.0 + tax_deducted = get_deducted_tax(taxable_vouchers, fiscal_year, tax_details) tax_amount = 0 - posting_date = ref_doc.posting_date + posting_date = inv.posting_date if party_type == 'Supplier': ldc = get_lower_deduction_certificate(fiscal_year, pan_no) if tax_deducted: - net_total = ref_doc.net_total + net_total = inv.net_total if ldc: tax_amount = get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total) else: tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0 else: tax_amount = get_tds_amount( - ldc, parties, ref_doc, tax_details, + ldc, parties, inv, tax_details, fiscal_year_details, vouchers ) elif party_type == 'Customer': if tax_deducted: - grand_total = get_invoice_total_without_tcs(ref_doc, tax_details) - # if already tcs is charged, then (net total + gst amount) of invoice is chargeable - tax_amount = grand_total * tax_details.rate / 100 if grand_total > 0 else 0 + # if already TCS is charged, then amount will be calculated based on 'Previous Row Total' + tax_amount = 0 else: - # if no tcs has been charged in FY, + # if no TCS has been charged in FY, # then chargeable value is "prev invoices + advances" value which cross the threshold tax_amount = get_tcs_amount( - parties, ref_doc, tax_details, + parties, inv, tax_details, fiscal_year_details, vouchers, advance_vouchers ) - return tax_amount + return tax_amount, tax_deducted def get_invoice_vouchers(parties, fiscal_year, company, party_type='Supplier'): dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit' @@ -194,7 +217,20 @@ def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""] -def get_tds_amount(ldc, parties, ref_doc, tax_details, fiscal_year_details, vouchers): +def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details): + # check if TDS / TCS account is already charged on taxable vouchers + filters = { + 'is_cancelled': 0, + 'credit': ['>', 0], + 'fiscal_year': fiscal_year, + 'account': tax_details.account_head, + 'voucher_no': ['in', taxable_vouchers], + } + field = "sum(credit)" + + return frappe.db.get_value('GL Entry', filters, field) or 0.0 + +def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, vouchers): tds_amount = 0 supp_credit_amt = frappe.db.get_value('Purchase Invoice', { @@ -207,9 +243,9 @@ def get_tds_amount(ldc, parties, ref_doc, tax_details, fiscal_year_details, vouc }, 'sum(credit_in_account_currency)') or 0.0 supp_credit_amt += supp_jv_credit_amt - supp_credit_amt += ref_doc.net_total + supp_credit_amt += inv.net_total - debit_note_amount = get_debit_note_amount(parties, fiscal_year_details, ref_doc.company) + debit_note_amount = get_debit_note_amount(parties, fiscal_year_details, inv.company) supp_credit_amt -= debit_note_amount threshold = tax_details.get('threshold', 0) @@ -218,7 +254,7 @@ def get_tds_amount(ldc, parties, ref_doc, tax_details, fiscal_year_details, vouc if ((threshold and supp_credit_amt >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)): if ldc and is_valid_certificate( ldc.valid_from, ldc.valid_upto, - ref_doc.posting_date, tax_deducted, + inv.posting_date, tax_deducted, net_total, ldc.certificate_limit ): tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details) @@ -227,7 +263,7 @@ def get_tds_amount(ldc, parties, ref_doc, tax_details, fiscal_year_details, vouc return tds_amount -def get_tcs_amount(parties, ref_doc, tax_details, fiscal_year_details, vouchers, adv_vouchers): +def get_tcs_amount(parties, inv, tax_details, fiscal_year_details, vouchers, adv_vouchers): tcs_amount = 0 fiscal_year, _, _ = fiscal_year_details @@ -235,7 +271,7 @@ def get_tcs_amount(parties, ref_doc, tax_details, fiscal_year_details, vouchers, invoiced_amt = frappe.db.get_value('GL Entry', { 'is_cancelled': 0, 'party': ['in', parties], - 'company': ref_doc.company, + 'company': inv.company, 'voucher_no': ['in', vouchers], }, 'sum(debit)') or 0.0 @@ -243,7 +279,7 @@ def get_tcs_amount(parties, ref_doc, tax_details, fiscal_year_details, vouchers, advance_amt = frappe.db.get_value('GL Entry', { 'is_cancelled': 0, 'party': ['in', parties], - 'company': ref_doc.company, + 'company': inv.company, 'voucher_no': ['in', adv_vouchers], }, 'sum(credit)') or 0.0 @@ -253,13 +289,13 @@ def get_tcs_amount(parties, ref_doc, tax_details, fiscal_year_details, vouchers, 'credit': ['>', 0], 'party': ['in', parties], 'fiscal_year': fiscal_year, - 'company': ref_doc.company, + 'company': inv.company, 'voucher_type': 'Sales Invoice', }, 'sum(credit)') or 0.0 cumulative_threshold = tax_details.get('cumulative_threshold', 0) - current_invoice_total = get_invoice_total_without_tcs(ref_doc, tax_details) + current_invoice_total = get_invoice_total_without_tcs(inv, tax_details) total_invoiced_amt = current_invoice_total + invoiced_amt + advance_amt - credit_note_amt if ((cumulative_threshold and total_invoiced_amt >= cumulative_threshold)): @@ -268,11 +304,11 @@ def get_tcs_amount(parties, ref_doc, tax_details, fiscal_year_details, vouchers, return tcs_amount -def get_invoice_total_without_tcs(ref_doc, tax_details): - tcs_tax_row = [d for d in ref_doc.taxes if d.account_head == tax_details.account_head] +def get_invoice_total_without_tcs(inv, tax_details): + tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head] tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0 - return ref_doc.grand_total - tcs_tax_row_amount + return inv.grand_total - tcs_tax_row_amount def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total): tds_amount = 0 diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index 1d8fa4532f..9ce8e3fe83 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -18,6 +18,9 @@ class TestTaxWithholdingCategory(unittest.TestCase): create_records() create_tax_with_holding_category() + def tearDown(self): + cancel_invoices() + def test_cumulative_threshold_tds(self): frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS") invoices = [] @@ -161,6 +164,23 @@ class TestTaxWithholdingCategory(unittest.TestCase): for d in invoices: d.cancel() +def cancel_invoices(): + purchase_invoices = frappe.get_all("Purchase Invoice", { + 'supplier': ['in', ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']], + 'docstatus': 1 + }, pluck="name") + + sales_invoices = frappe.get_all("Sales Invoice", { + 'customer': 'Test TCS Customer', + 'docstatus': 1 + }, pluck="name") + + for d in purchase_invoices: + frappe.get_doc('Purchase Invoice', d).cancel() + + for d in sales_invoices: + frappe.get_doc('Sales Invoice', d).cancel() + def create_purchase_invoice(**args): # return sales invoice doc object item = frappe.db.get_value('Item', {'item_name': 'TDS Item'}, "name") From 3b2327ba8d28937410b87b03ff72ce4acbb22177 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 29 Jan 2021 12:08:35 +0530 Subject: [PATCH 7/8] fix: sider --- .../tax_withholding_category/tax_withholding_category.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 4cbca6c08b..961bdb147f 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -163,7 +163,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, p else: tax_amount = get_tds_amount( ldc, parties, inv, tax_details, - fiscal_year_details, vouchers + fiscal_year_details, tax_deducted, vouchers ) elif party_type == 'Customer': @@ -230,7 +230,7 @@ def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details): return frappe.db.get_value('GL Entry', filters, field) or 0.0 -def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, vouchers): +def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers): tds_amount = 0 supp_credit_amt = frappe.db.get_value('Purchase Invoice', { @@ -255,7 +255,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, vouchers if ldc and is_valid_certificate( ldc.valid_from, ldc.valid_upto, inv.posting_date, tax_deducted, - net_total, ldc.certificate_limit + inv.net_total, ldc.certificate_limit ): tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details) else: From 85c91ca2dbda23ca239ef9e0446db845ed7b7174 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 23 Feb 2021 15:56:29 +0530 Subject: [PATCH 8/8] Merge branch 'develop' into tcs_calculation --- erpnext/__init__.py | 2 +- .../accounting_dimension.py | 15 +- .../doctype/bank_account/bank_account.json | 3 +- .../__init__.py | 0 .../bank_reconciliation_tool.js | 162 ++++ .../bank_reconciliation_tool.json | 113 +++ .../bank_reconciliation_tool.py | 452 ++++++++++ .../test_bank_reconciliation_tool.py | 10 + .../__init__.py | 0 .../bank_statement_import.css | 3 + .../bank_statement_import.js | 574 +++++++++++++ .../bank_statement_import.json | 227 +++++ .../bank_statement_import.py | 205 +++++ .../bank_statement_import_list.js | 36 + .../test_bank_statement_import.py | 10 + .../bank_statement_settings.js | 8 - .../bank_statement_settings.json | 272 ------ .../bank_statement_settings.py | 11 - .../test_bank_statement_settings.js | 23 - .../test_bank_statement_settings.py | 10 - .../bank_statement_settings_item.json | 101 --- .../bank_statement_settings_item.py | 10 - .../bank_statement_transaction_entry.js | 100 --- .../bank_statement_transaction_entry.json | 792 ------------------ .../bank_statement_transaction_entry.py | 443 ---------- .../test_bank_statement_transaction_entry.js | 23 - .../test_bank_statement_transaction_entry.py | 10 - ...nk_statement_transaction_invoice_item.json | 365 -------- ...nk_statement_transaction_payment_item.json | 494 ----------- .../bank_statement_transaction_settings.js | 8 - .../bank_statement_transaction_settings.json | 266 ------ .../bank_statement_transaction_settings.py | 11 - ...est_bank_statement_transaction_settings.js | 23 - ...est_bank_statement_transaction_settings.py | 10 - .../__init__.py | 0 ...k_statement_transaction_settings_item.json | 166 ---- ...ank_statement_transaction_settings_item.py | 10 - .../bank_transaction/bank_transaction.js | 66 +- .../bank_transaction/bank_transaction.json | 756 ++--------------- .../bank_transaction/bank_transaction.py | 24 +- .../bank_transaction/test_bank_transaction.py | 78 +- erpnext/accounts/doctype/gl_entry/gl_entry.py | 50 +- .../doctype/journal_entry/journal_entry.py | 12 +- .../payment_request/payment_request.py | 50 +- .../payment_request/test_payment_request.py | 12 +- .../pos_closing_entry/pos_closing_entry.js | 2 +- .../doctype/pos_invoice/pos_invoice.js | 41 +- .../doctype/pos_invoice/pos_invoice.py | 81 +- .../doctype/pos_invoice/test_pos_invoice.py | 59 ++ .../pos_invoice_item/pos_invoice_item.json | 12 +- .../pos_invoice_merge_log.py | 21 +- .../purchase_invoice_item.json | 11 +- .../sales_invoice_item.json | 11 +- erpnext/accounts/general_ledger.py | 14 +- .../page/bank_reconciliation/__init__.py | 0 .../bank_reconciliation.js | 583 ------------- .../bank_reconciliation.json | 29 - .../bank_reconciliation.py | 369 -------- .../bank_transaction_header.html | 21 - .../bank_transaction_row.html | 36 - .../linked_payment_header.html | 21 - .../linked_payment_row.html | 36 - .../bank_clearance_summary.py | 54 +- .../consolidated_financial_statement.py | 28 +- erpnext/accounts/utils.py | 24 +- .../purchase_order_item.json | 11 +- .../request_for_quotation.py | 4 + erpnext/buying/doctype/supplier/supplier.json | 4 +- erpnext/controllers/accounts_controller.py | 51 +- .../controllers/sales_and_purchase_return.py | 9 +- erpnext/controllers/selling_controller.py | 6 +- erpnext/controllers/stock_controller.py | 19 +- erpnext/controllers/taxes_and_totals.py | 19 + erpnext/crm/doctype/lead/lead.json | 11 +- .../crm/doctype/opportunity/opportunity.js | 6 + .../crm/doctype/opportunity/opportunity.json | 9 +- erpnext/crm/doctype/utils.py | 4 +- .../prospects_engaged_but_not_converted.py | 53 +- .../doctype/mpesa_settings/mpesa_connector.py | 8 +- .../mpesa_settings/mpesa_settings.json | 18 +- .../doctype/mpesa_settings/mpesa_settings.py | 115 ++- .../mpesa_settings/test_mpesa_settings.py | 180 +++- .../doctype/plaid_settings/plaid_connector.py | 4 +- .../doctype/plaid_settings/plaid_settings.py | 4 +- .../appointment_type/appointment_type.js | 78 ++ .../appointment_type/appointment_type.json | 24 +- .../appointment_type/appointment_type.py | 49 +- .../__init__.py | 0 .../appointment_type_service_item.json | 67 ++ .../appointment_type_service_item.py} | 6 +- .../clinical_procedure/clinical_procedure.py | 1 + .../test_clinical_procedure.py | 5 +- .../healthcare_practitioner.json | 6 +- .../patient_appointment.js | 112 +-- .../patient_appointment.json | 26 +- .../patient_appointment.py | 67 +- .../test_patient_appointment.py | 84 +- erpnext/healthcare/utils.py | 85 +- erpnext/hooks.py | 4 + .../leave_application_dashboard.html | 14 +- .../employee_leave_balance.py | 6 +- .../loan_disbursements.json | 29 + .../loan_interest_accrual.json | 31 + .../dashboard_chart/new_loans/new_loans.json | 31 + .../top_10_pledged_loan_securities.json | 31 + .../dashboard_chart_source}/__init__.py | 0 .../__init__.py | 0 .../top_10_pledged_loan_securities.js | 14 + .../top_10_pledged_loan_securities.json | 13 + .../top_10_pledged_loan_securities.py | 76 ++ erpnext/loan_management/doctype/loan/loan.py | 14 +- .../loan_management/doctype/loan/test_loan.py | 2 +- .../loan_application/loan_application.py | 2 +- .../loan_interest_accrual.py | 4 +- .../doctype/loan_repayment/loan_repayment.py | 19 +- .../loan_dashboard/loan_dashboard.json | 70 ++ .../active_loans/active_loans.json | 23 + .../active_securities/active_securities.json | 23 + .../applicants_with_unpaid_shortfall.json | 21 + .../closed_loans/closed_loans.json | 23 + .../last_interest_accrual.json | 21 + .../new_loan_applications.json | 23 + .../number_card/new_loans/new_loans.json | 23 + .../open_loan_applications.json | 23 + .../total_disbursed/total_disbursed.json | 23 + .../total_repayment/total_repayment.json | 24 + .../total_sanctioned_amount.json | 23 + .../total_shortfall_amount.json | 23 + .../total_write_off/total_write_off.json | 24 + .../applicant_wise_loan_security_exposure.py | 4 +- .../loan_interest_report.py | 2 +- .../loan_security_exposure.py | 4 +- .../loan_management/loan_management.json | 9 +- .../doctype/job_card/job_card.py | 18 +- .../doctype/job_card_item/job_card_item.json | 455 +++------- .../doctype/work_order/test_work_order.py | 10 +- .../bom_stock_calculated.py | 4 +- erpnext/patches.txt | 4 + .../patches/v11_0/refactor_autoname_naming.py | 2 +- .../v11_1/update_bank_transaction_status.py | 23 +- .../v12_0/add_state_code_for_ladakh.py | 16 + ...delete_old_bank_reconciliation_doctypes.py | 26 + .../item_reposting_for_incorrect_sl_and_gl.py | 46 + .../v13_0/update_vehicle_no_reqd_condition.py | 9 + ...lds_in_custom_scripts_and_print_formats.py | 2 +- .../v7_0/remove_doctypes_and_reports.py | 2 +- .../doctype/salary_slip/salary_slip.py | 4 +- erpnext/public/build.json | 5 + erpnext/public/images/erpnext-logo.png | Bin 0 -> 2360 bytes .../data_table_manager.js | 220 +++++ .../dialog_manager.js | 594 +++++++++++++ .../bank_reconciliation_tool/number_card.js | 75 ++ erpnext/public/js/controllers/buying.js | 1 - .../public/js/controllers/taxes_and_totals.js | 33 +- erpnext/public/js/controllers/transaction.js | 12 +- erpnext/public/js/telephony.js | 7 +- .../js/utils/serial_no_batch_selector.js | 9 +- erpnext/public/less/hub.less | 7 +- .../doctype/gst_settings/gst_settings.json | 290 ++----- .../gstr_3b_report/test_gstr_3b_report.py | 67 +- .../lower_deduction_certificate.py | 23 +- erpnext/regional/india/__init__.py | 4 +- erpnext/regional/india/e_invoice/einvoice.js | 1 - erpnext/regional/india/e_invoice/utils.py | 68 +- .../regional/india/gst_state_code_data.json | 5 + erpnext/regional/india/utils.py | 29 + erpnext/regional/report/gstr_1/gstr_1.py | 4 +- .../selling/doctype/customer/customer.json | 4 +- erpnext/selling/doctype/customer/customer.py | 4 +- .../quotation_item/quotation_item.json | 11 +- .../doctype/sales_order/sales_order.js | 2 +- .../doctype/sales_order/sales_order.py | 1 + .../doctype/sales_order/test_sales_order.py | 46 +- .../sales_order_item/sales_order_item.json | 12 +- .../page/point_of_sale/pos_item_cart.js | 2 +- .../selling/page/point_of_sale/pos_payment.js | 74 +- .../company/delete_company_transactions.py | 2 +- erpnext/shopping_cart/cart.py | 3 + .../shopping_cart_settings.json | 12 +- erpnext/stock/__init__.py | 4 +- erpnext/stock/doctype/batch/test_batch.py | 6 +- erpnext/stock/doctype/bin/bin.py | 13 +- .../delivery_note/test_delivery_note.py | 5 +- .../delivery_note_item.json | 11 +- erpnext/stock/doctype/item/item.py | 2 +- .../item_quality_inspection_parameter.json | 11 +- .../test_landed_cost_voucher.py | 5 +- .../purchase_receipt/test_purchase_receipt.py | 9 +- .../purchase_receipt_item.json | 11 +- .../quality_inspection_parameter.json | 12 +- .../__init__.py | 0 .../quality_inspection_parameter_group.js | 8 + .../quality_inspection_parameter_group.json | 82 ++ .../quality_inspection_parameter_group.py} | 6 +- ...test_quality_inspection_parameter_group.py | 10 + .../quality_inspection_reading.json | 11 +- .../repost_item_valuation.py | 5 +- .../stock/doctype/shipment/test_shipment.py | 1 + .../stock/doctype/stock_entry/stock_entry.py | 3 +- .../stock_entry_detail.json | 14 +- .../stock_ledger_entry/stock_ledger_entry.py | 1 + erpnext/stock/stock_ledger.py | 90 +- requirements.txt | 4 +- 203 files changed, 5722 insertions(+), 6140 deletions(-) rename erpnext/accounts/doctype/{bank_statement_settings => bank_reconciliation_tool}/__init__.py (100%) create mode 100644 erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js create mode 100644 erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json create mode 100644 erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py create mode 100644 erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py rename erpnext/accounts/doctype/{bank_statement_settings_item => bank_statement_import}/__init__.py (100%) create mode 100644 erpnext/accounts/doctype/bank_statement_import/bank_statement_import.css create mode 100644 erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js create mode 100644 erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json create mode 100644 erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py create mode 100644 erpnext/accounts/doctype/bank_statement_import/bank_statement_import_list.js create mode 100644 erpnext/accounts/doctype/bank_statement_import/test_bank_statement_import.py delete mode 100644 erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.js delete mode 100644 erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.json delete mode 100644 erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.py delete mode 100644 erpnext/accounts/doctype/bank_statement_settings/test_bank_statement_settings.js delete mode 100644 erpnext/accounts/doctype/bank_statement_settings/test_bank_statement_settings.py delete mode 100644 erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.json delete mode 100644 erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.py delete mode 100644 erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.js delete mode 100644 erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.json delete mode 100644 erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py delete mode 100644 erpnext/accounts/doctype/bank_statement_transaction_entry/test_bank_statement_transaction_entry.js delete mode 100644 erpnext/accounts/doctype/bank_statement_transaction_entry/test_bank_statement_transaction_entry.py delete mode 100644 erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.json delete mode 100644 erpnext/accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_item.json delete mode 100644 erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.js delete mode 100644 erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.json delete mode 100644 erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.py delete mode 100644 erpnext/accounts/doctype/bank_statement_transaction_settings/test_bank_statement_transaction_settings.js delete mode 100644 erpnext/accounts/doctype/bank_statement_transaction_settings/test_bank_statement_transaction_settings.py delete mode 100644 erpnext/accounts/doctype/bank_statement_transaction_settings_item/__init__.py delete mode 100644 erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.json delete mode 100644 erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.py delete mode 100644 erpnext/accounts/page/bank_reconciliation/__init__.py delete mode 100644 erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js delete mode 100644 erpnext/accounts/page/bank_reconciliation/bank_reconciliation.json delete mode 100644 erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py delete mode 100644 erpnext/accounts/page/bank_reconciliation/bank_transaction_header.html delete mode 100644 erpnext/accounts/page/bank_reconciliation/bank_transaction_row.html delete mode 100644 erpnext/accounts/page/bank_reconciliation/linked_payment_header.html delete mode 100644 erpnext/accounts/page/bank_reconciliation/linked_payment_row.html rename erpnext/{accounts/doctype/bank_statement_transaction_entry => healthcare/doctype/appointment_type_service_item}/__init__.py (100%) create mode 100644 erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json rename erpnext/{accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.py => healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py} (56%) create mode 100644 erpnext/loan_management/dashboard_chart/loan_disbursements/loan_disbursements.json create mode 100644 erpnext/loan_management/dashboard_chart/loan_interest_accrual/loan_interest_accrual.json create mode 100644 erpnext/loan_management/dashboard_chart/new_loans/new_loans.json create mode 100644 erpnext/loan_management/dashboard_chart/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json rename erpnext/{accounts/doctype/bank_statement_transaction_invoice_item => loan_management/dashboard_chart_source}/__init__.py (100%) rename erpnext/{accounts/doctype/bank_statement_transaction_payment_item => loan_management/dashboard_chart_source/top_10_pledged_loan_securities}/__init__.py (100%) create mode 100644 erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.js create mode 100644 erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json create mode 100644 erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py create mode 100644 erpnext/loan_management/loan_management_dashboard/loan_dashboard/loan_dashboard.json create mode 100644 erpnext/loan_management/number_card/active_loans/active_loans.json create mode 100644 erpnext/loan_management/number_card/active_securities/active_securities.json create mode 100644 erpnext/loan_management/number_card/applicants_with_unpaid_shortfall/applicants_with_unpaid_shortfall.json create mode 100644 erpnext/loan_management/number_card/closed_loans/closed_loans.json create mode 100644 erpnext/loan_management/number_card/last_interest_accrual/last_interest_accrual.json create mode 100644 erpnext/loan_management/number_card/new_loan_applications/new_loan_applications.json create mode 100644 erpnext/loan_management/number_card/new_loans/new_loans.json create mode 100644 erpnext/loan_management/number_card/open_loan_applications/open_loan_applications.json create mode 100644 erpnext/loan_management/number_card/total_disbursed/total_disbursed.json create mode 100644 erpnext/loan_management/number_card/total_repayment/total_repayment.json create mode 100644 erpnext/loan_management/number_card/total_sanctioned_amount/total_sanctioned_amount.json create mode 100644 erpnext/loan_management/number_card/total_shortfall_amount/total_shortfall_amount.json create mode 100644 erpnext/loan_management/number_card/total_write_off/total_write_off.json create mode 100644 erpnext/patches/v12_0/add_state_code_for_ladakh.py create mode 100644 erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py create mode 100644 erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py create mode 100644 erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py create mode 100644 erpnext/public/images/erpnext-logo.png create mode 100644 erpnext/public/js/bank_reconciliation_tool/data_table_manager.js create mode 100644 erpnext/public/js/bank_reconciliation_tool/dialog_manager.js create mode 100644 erpnext/public/js/bank_reconciliation_tool/number_card.js rename erpnext/{accounts/doctype/bank_statement_transaction_settings => stock/doctype/quality_inspection_parameter_group}/__init__.py (100%) create mode 100644 erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.js create mode 100644 erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.json rename erpnext/{accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_item.py => stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.py} (55%) create mode 100644 erpnext/stock/doctype/quality_inspection_parameter_group/test_quality_inspection_parameter_group.py diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 5a5c448026..199a183e47 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -109,7 +109,7 @@ def get_region(company=None): ''' if company or frappe.flags.company: return frappe.get_cached_value('Company', - company or frappe.flags.company, 'country') + company or frappe.flags.company, 'country') elif frappe.flags.country: return frappe.flags.country else: diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index dd26c4cec2..1bd42f5aba 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -43,11 +43,11 @@ class AccountingDimension(Document): if frappe.flags.in_test: make_dimension_in_accounting_doctypes(doc=self) else: - frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self) + frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue='long') def on_trash(self): if frappe.flags.in_test: - delete_accounting_dimension(doc=self) + delete_accounting_dimension(doc=self, queue='long') else: frappe.enqueue(delete_accounting_dimension, doc=self) @@ -58,6 +58,9 @@ class AccountingDimension(Document): if not self.fieldname: self.fieldname = scrub(self.label) + def on_update(self): + frappe.flags.accounting_dimensions = None + def make_dimension_in_accounting_doctypes(doc): doclist = get_doctypes_with_dimensions() doc_count = len(get_accounting_dimensions()) @@ -186,12 +189,14 @@ def get_doctypes_with_dimensions(): return doclist def get_accounting_dimensions(as_list=True): - accounting_dimensions = frappe.get_all("Accounting Dimension", fields=["label", "fieldname", "disabled", "document_type"]) + if frappe.flags.accounting_dimensions is None: + frappe.flags.accounting_dimensions = frappe.get_all("Accounting Dimension", + fields=["label", "fieldname", "disabled", "document_type"]) if as_list: - return [d.fieldname for d in accounting_dimensions] + return [d.fieldname for d in frappe.flags.accounting_dimensions] else: - return accounting_dimensions + return frappe.flags.accounting_dimensions def get_checks_for_pl_and_bs_accounts(): dimensions = frappe.db.sql("""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs diff --git a/erpnext/accounts/doctype/bank_account/bank_account.json b/erpnext/accounts/doctype/bank_account/bank_account.json index b42f1f9d58..de67ab1ce5 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.json +++ b/erpnext/accounts/doctype/bank_account/bank_account.json @@ -86,6 +86,7 @@ }, { "default": "0", + "description": "Setting the account as a Company Account is necessary for Bank Reconciliation", "fieldname": "is_company_account", "fieldtype": "Check", "label": "Is Company Account" @@ -207,7 +208,7 @@ } ], "links": [], - "modified": "2020-07-17 13:59:50.795412", + "modified": "2020-10-23 16:48:06.303658", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Account", diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/accounts/doctype/bank_reconciliation_tool/__init__.py similarity index 100% rename from erpnext/accounts/doctype/bank_statement_settings/__init__.py rename to erpnext/accounts/doctype/bank_reconciliation_tool/__init__.py diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js new file mode 100644 index 0000000000..297dd4333f --- /dev/null +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js @@ -0,0 +1,162 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +frappe.provide("erpnext.accounts.bank_reconciliation"); + +frappe.ui.form.on("Bank Reconciliation Tool", { + setup: function (frm) { + frm.set_query("bank_account", function () { + return { + filters: { + company: ["in", frm.doc.company], + }, + }; + }); + }, + + refresh: function (frm) { + frappe.require("assets/js/bank-reconciliation-tool.min.js", () => + frm.trigger("make_reconciliation_tool") + ); + frm.upload_statement_button = frm.page.set_secondary_action( + __("Upload Bank Statement"), + () => + frappe.call({ + method: + "erpnext.accounts.doctype.bank_statement_import.bank_statement_import.upload_bank_statement", + args: { + dt: frm.doc.doctype, + dn: frm.doc.name, + company: frm.doc.company, + bank_account: frm.doc.bank_account, + }, + callback: function (r) { + if (!r.exc) { + var doc = frappe.model.sync(r.message); + frappe.set_route( + "Form", + doc[0].doctype, + doc[0].name + ); + } + }, + }) + ); + }, + + after_save: function (frm) { + frm.trigger("make_reconciliation_tool"); + }, + + bank_account: function (frm) { + frappe.db.get_value( + "Bank Account", + frm.bank_account, + "account", + (r) => { + frappe.db.get_value( + "Account", + r.account, + "account_currency", + (r) => { + frm.currency = r.account_currency; + } + ); + } + ); + frm.trigger("get_account_opening_balance"); + }, + + bank_statement_from_date: function (frm) { + frm.trigger("get_account_opening_balance"); + }, + + make_reconciliation_tool(frm) { + frm.get_field("reconciliation_tool_cards").$wrapper.empty(); + if (frm.doc.bank_account && frm.doc.bank_statement_to_date) { + frm.trigger("get_cleared_balance").then(() => { + if ( + frm.doc.bank_account && + frm.doc.bank_statement_from_date && + frm.doc.bank_statement_to_date && + frm.doc.bank_statement_closing_balance + ) { + frm.trigger("render_chart"); + frm.trigger("render"); + frappe.utils.scroll_to( + frm.get_field("reconciliation_tool_cards").$wrapper, + true, + 30 + ); + } + }); + } + }, + + get_account_opening_balance(frm) { + if (frm.doc.bank_account && frm.doc.bank_statement_from_date) { + frappe.call({ + method: + "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance", + args: { + bank_account: frm.doc.bank_account, + till_date: frm.doc.bank_statement_from_date, + }, + callback: (response) => { + frm.set_value("account_opening_balance", response.message); + }, + }); + } + }, + + get_cleared_balance(frm) { + if (frm.doc.bank_account && frm.doc.bank_statement_to_date) { + return frappe.call({ + method: + "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance", + args: { + bank_account: frm.doc.bank_account, + till_date: frm.doc.bank_statement_to_date, + }, + callback: (response) => { + frm.cleared_balance = response.message; + }, + }); + } + }, + + render_chart(frm) { + frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager( + { + $reconciliation_tool_cards: frm.get_field( + "reconciliation_tool_cards" + ).$wrapper, + bank_statement_closing_balance: + frm.doc.bank_statement_closing_balance, + cleared_balance: frm.cleared_balance, + currency: frm.currency, + } + ); + }, + + render(frm) { + if (frm.doc.bank_account) { + frm.bank_reconciliation_data_table_manager = new erpnext.accounts.bank_reconciliation.DataTableManager( + { + company: frm.doc.company, + bank_account: frm.doc.bank_account, + $reconciliation_tool_dt: frm.get_field( + "reconciliation_tool_dt" + ).$wrapper, + $no_bank_transactions: frm.get_field( + "no_bank_transactions" + ).$wrapper, + bank_statement_from_date: frm.doc.bank_statement_from_date, + bank_statement_to_date: frm.doc.bank_statement_to_date, + bank_statement_closing_balance: + frm.doc.bank_statement_closing_balance, + cards_manager: frm.cards_manager, + } + ); + } + }, +}); diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json new file mode 100644 index 0000000000..4837db3b86 --- /dev/null +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json @@ -0,0 +1,113 @@ +{ + "actions": [], + "creation": "2020-12-02 10:13:02.148040", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "bank_account", + "column_break_1", + "bank_statement_from_date", + "bank_statement_to_date", + "column_break_2", + "account_opening_balance", + "bank_statement_closing_balance", + "section_break_1", + "reconciliation_tool_cards", + "reconciliation_tool_dt", + "no_bank_transactions" + ], + "fields": [ + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "fieldname": "bank_account", + "fieldtype": "Link", + "label": "Bank Account", + "options": "Bank Account" + }, + { + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: doc.bank_account", + "fieldname": "bank_statement_from_date", + "fieldtype": "Date", + "label": "Bank Statement From Date" + }, + { + "depends_on": "eval: doc.bank_statement_from_date", + "fieldname": "bank_statement_to_date", + "fieldtype": "Date", + "label": "Bank Statement To Date" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: doc.bank_statement_from_date", + "fieldname": "account_opening_balance", + "fieldtype": "Currency", + "label": "Account Opening Balance", + "options": "Currency", + "read_only": 1 + }, + { + "depends_on": "eval: doc.bank_statement_to_date", + "fieldname": "bank_statement_closing_balance", + "fieldtype": "Currency", + "label": "Bank Statement Closing Balance", + "options": "Currency" + }, + { + "depends_on": "eval: doc.bank_statement_closing_balance", + "fieldname": "section_break_1", + "fieldtype": "Section Break", + "label": "Reconcile" + }, + { + "fieldname": "reconciliation_tool_cards", + "fieldtype": "HTML" + }, + { + "fieldname": "reconciliation_tool_dt", + "fieldtype": "HTML" + }, + { + "fieldname": "no_bank_transactions", + "fieldtype": "HTML", + "options": "
No Matching Bank Transactions Found
" + } + ], + "hide_toolbar": 1, + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2021-02-02 01:35:53.043578", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Bank Reconciliation Tool", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py new file mode 100644 index 0000000000..8a17233cf7 --- /dev/null +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -0,0 +1,452 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import json + +import frappe +from frappe.model.document import Document +from frappe import _ +from frappe.utils import flt + +from erpnext import get_company_currency +from erpnext.accounts.utils import get_balance_on +from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import get_entries, get_amounts_not_reflected_in_system +from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_paid_amount + + +class BankReconciliationTool(Document): + pass + +@frappe.whitelist() +def get_bank_transactions(bank_account, from_date = None, to_date = None): + # returns bank transactions for a bank account + filters = [] + filters.append(['bank_account', '=', bank_account]) + filters.append(['docstatus', '=', 1]) + filters.append(['unallocated_amount', '>', 0]) + if to_date: + filters.append(['date', '<=', to_date]) + if from_date: + filters.append(['date', '>=', from_date]) + transactions = frappe.get_all( + 'Bank Transaction', + fields = ['date', 'deposit', 'withdrawal', 'currency', + 'description', 'name', 'bank_account', 'company', + 'unallocated_amount', 'reference_number', 'party_type', 'party'], + filters = filters + ) + return transactions + +@frappe.whitelist() +def get_account_balance(bank_account, till_date): + # returns account balance till the specified date + account = frappe.db.get_value('Bank Account', bank_account, 'account') + filters = frappe._dict({ + "account": account, + "report_date": till_date, + "include_pos_transactions": 1 + }) + data = get_entries(filters) + + balance_as_per_system = get_balance_on(filters["account"], filters["report_date"]) + + total_debit, total_credit = 0,0 + for d in data: + total_debit += flt(d.debit) + total_credit += flt(d.credit) + + amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters) + + bank_bal = flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) \ + + amounts_not_reflected_in_system + + return bank_bal + + +@frappe.whitelist() +def update_bank_transaction(bank_transaction_name, reference_number, party_type=None, party=None): + # updates bank transaction based on the new parameters provided by the user from Vouchers + bank_transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) + bank_transaction.reference_number = reference_number + bank_transaction.party_type = party_type + bank_transaction.party = party + bank_transaction.save() + return frappe.db.get_all('Bank Transaction', + filters={ + 'name': bank_transaction_name + }, + fields=['date', 'deposit', 'withdrawal', 'currency', + 'description', 'name', 'bank_account', 'company', + 'unallocated_amount', 'reference_number', + 'party_type', 'party'], + )[0] + + +@frappe.whitelist() +def create_journal_entry_bts( bank_transaction_name, reference_number=None, reference_date=None, posting_date=None, entry_type=None, + second_account=None, mode_of_payment=None, party_type=None, party=None, allow_edit=None): + # Create a new journal entry based on the bank transaction + bank_transaction = frappe.db.get_values( + "Bank Transaction", bank_transaction_name, + fieldname=["name", "deposit", "withdrawal", "bank_account"] , + as_dict=True + )[0] + company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account") + account_type = frappe.db.get_value("Account", second_account, "account_type") + if account_type in ["Receivable", "Payable"]: + if not (party_type and party): + frappe.throw(_("Party Type and Party is required for Receivable / Payable account {0}").format( second_account)) + accounts = [] + # Multi Currency? + accounts.append({ + "account": second_account, + "credit_in_account_currency": bank_transaction.deposit + if bank_transaction.deposit > 0 + else 0, + "debit_in_account_currency":bank_transaction.withdrawal + if bank_transaction.withdrawal > 0 + else 0, + "party_type":party_type, + "party":party, + }) + + accounts.append({ + "account": company_account, + "bank_account": bank_transaction.bank_account, + "credit_in_account_currency": bank_transaction.withdrawal + if bank_transaction.withdrawal > 0 + else 0, + "debit_in_account_currency":bank_transaction.deposit + if bank_transaction.deposit > 0 + else 0, + }) + + company = frappe.get_value("Account", company_account, "company") + + journal_entry_dict = { + "voucher_type" : entry_type, + "company" : company, + "posting_date" : posting_date, + "cheque_date" : reference_date, + "cheque_no" : reference_number, + "mode_of_payment" : mode_of_payment + } + journal_entry = frappe.new_doc('Journal Entry') + journal_entry.update(journal_entry_dict) + journal_entry.set("accounts", accounts) + + + if allow_edit: + return journal_entry + + journal_entry.insert() + journal_entry.submit() + + if bank_transaction.deposit > 0: + paid_amount = bank_transaction.deposit + else: + paid_amount = bank_transaction.withdrawal + + vouchers = json.dumps([{ + "payment_doctype":"Journal Entry", + "payment_name":journal_entry.name, + "amount":paid_amount}]) + + return reconcile_vouchers(bank_transaction.name, vouchers) + +@frappe.whitelist() +def create_payment_entry_bts( bank_transaction_name, reference_number=None, reference_date=None, party_type=None, party=None, posting_date=None, + mode_of_payment=None, project=None, cost_center=None, allow_edit=None): + # Create a new payment entry based on the bank transaction + bank_transaction = frappe.db.get_values( + "Bank Transaction", bank_transaction_name, + fieldname=["name", "unallocated_amount", "deposit", "bank_account"] , + as_dict=True + )[0] + paid_amount = bank_transaction.unallocated_amount + payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay" + + company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account") + company = frappe.get_value("Account", company_account, "company") + payment_entry_dict = { + "company" : company, + "payment_type" : payment_type, + "reference_no" : reference_number, + "reference_date" : reference_date, + "party_type" : party_type, + "party" : party, + "posting_date" : posting_date, + "paid_amount": paid_amount, + "received_amount": paid_amount + } + payment_entry = frappe.new_doc("Payment Entry") + + + payment_entry.update(payment_entry_dict) + + if mode_of_payment: + payment_entry.mode_of_payment = mode_of_payment + if project: + payment_entry.project = project + if cost_center: + payment_entry.cost_center = cost_center + if payment_type == "Receive": + payment_entry.paid_to = company_account + else: + payment_entry.paid_from = company_account + + payment_entry.validate() + + if allow_edit: + return payment_entry + + payment_entry.insert() + + payment_entry.submit() + vouchers = json.dumps([{ + "payment_doctype":"Payment Entry", + "payment_name":payment_entry.name, + "amount":paid_amount}]) + return reconcile_vouchers(bank_transaction.name, vouchers) + +@frappe.whitelist() +def reconcile_vouchers(bank_transaction_name, vouchers): + # updated clear date of all the vouchers based on the bank transaction + vouchers = json.loads(vouchers) + transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) + if transaction.unallocated_amount == 0: + frappe.throw(_("This bank transaction is already fully reconciled")) + total_amount = 0 + for voucher in vouchers: + voucher['payment_entry'] = frappe.get_doc(voucher['payment_doctype'], voucher['payment_name']) + total_amount += get_paid_amount(frappe._dict({ + 'payment_document': voucher['payment_doctype'], + 'payment_entry': voucher['payment_name'], + }), transaction.currency) + + if total_amount > transaction.unallocated_amount: + frappe.throw(_("The Sum Total of Amounts of All Selected Vouchers Should be Less than the Unallocated Amount of the Bank Transaction")) + account = frappe.db.get_value("Bank Account", transaction.bank_account, "account") + + for voucher in vouchers: + gl_entry = frappe.db.get_value("GL Entry", dict(account=account, voucher_type=voucher['payment_doctype'], voucher_no=voucher['payment_name']), ['credit', 'debit'], as_dict=1) + gl_amount, transaction_amount = (gl_entry.credit, transaction.deposit) if gl_entry.credit > 0 else (gl_entry.debit, transaction.withdrawal) + allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount + + transaction.append("payment_entries", { + "payment_document": voucher['payment_entry'].doctype, + "payment_entry": voucher['payment_entry'].name, + "allocated_amount": allocated_amount + }) + + transaction.save() + transaction.update_allocations() + return frappe.get_doc("Bank Transaction", bank_transaction_name) + +@frappe.whitelist() +def get_linked_payments(bank_transaction_name, document_types = None): + # get all matching payments for a bank transaction + transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) + bank_account = frappe.db.get_values( + "Bank Account", + transaction.bank_account, + ["account", "company"], + as_dict=True)[0] + (account, company) = (bank_account.account, bank_account.company) + matching = check_matching(account, company, transaction, document_types) + return matching + +def check_matching(bank_account, company, transaction, document_types): + # combine all types of vocuhers + subquery = get_queries(bank_account, company, transaction, document_types) + filters = { + "amount": transaction.unallocated_amount, + "payment_type" : "Receive" if transaction.deposit > 0 else "Pay", + "reference_no": transaction.reference_number, + "party_type": transaction.party_type, + "party": transaction.party, + "bank_account": bank_account + } + + matching_vouchers = [] + for query in subquery: + matching_vouchers.extend( + frappe.db.sql(query, filters,) + ) + + return sorted(matching_vouchers, key = lambda x: x[0], reverse=True) if matching_vouchers else [] + +def get_queries(bank_account, company, transaction, document_types): + # get queries to get matching vouchers + amount_condition = "=" if "exact_match" in document_types else "<=" + account_from_to = "paid_to" if transaction.deposit > 0 else "paid_from" + queries = [] + + if "payment_entry" in document_types: + pe_amount_matching = get_pe_matching_query(amount_condition, account_from_to, transaction) + queries.extend([pe_amount_matching]) + + if "journal_entry" in document_types: + je_amount_matching = get_je_matching_query(amount_condition, transaction) + queries.extend([je_amount_matching]) + + if transaction.deposit > 0 and "sales_invoice" in document_types: + si_amount_matching = get_si_matching_query(amount_condition) + queries.extend([si_amount_matching]) + + if transaction.withdrawal > 0: + if "purchase_invoice" in document_types: + pi_amount_matching = get_pi_matching_query(amount_condition) + queries.extend([pi_amount_matching]) + + if "expense_claim" in document_types: + ec_amount_matching = get_ec_matching_query(bank_account, company, amount_condition) + queries.extend([ec_amount_matching]) + + return queries + +def get_pe_matching_query(amount_condition, account_from_to, transaction): + # get matching payment entries query + if transaction.deposit > 0: + currency_field = "paid_to_account_currency as currency" + else: + currency_field = "paid_from_account_currency as currency" + return f""" + SELECT + (CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END + + CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END + + 1 ) AS rank, + 'Payment Entry' as doctype, + name, + paid_amount, + reference_no, + reference_date, + party, + party_type, + posting_date, + {currency_field} + FROM + `tabPayment Entry` + WHERE + paid_amount {amount_condition} %(amount)s + AND docstatus = 1 + AND payment_type IN (%(payment_type)s, 'Internal Transfer') + AND ifnull(clearance_date, '') = "" + AND {account_from_to} = %(bank_account)s + """ + + +def get_je_matching_query(amount_condition, transaction): + # get matching journal entry query + cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit" + return f""" + + SELECT + (CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END + + 1) AS rank , + 'Journal Entry' as doctype, + je.name, + jea.{cr_or_dr}_in_account_currency as paid_amount, + je.cheque_no as reference_no, + je.cheque_date as reference_date, + je.pay_to_recd_from as party, + jea.party_type, + je.posting_date, + jea.account_currency as currency + FROM + `tabJournal Entry Account` as jea + JOIN + `tabJournal Entry` as je + ON + jea.parent = je.name + WHERE + (je.clearance_date is null or je.clearance_date='0000-00-00') + AND jea.account = %(bank_account)s + AND jea.{cr_or_dr}_in_account_currency {amount_condition} %(amount)s + AND je.docstatus = 1 + """ + + +def get_si_matching_query(amount_condition): + # get matchin sales invoice query + return f""" + SELECT + ( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END + + 1 ) AS rank, + 'Sales Invoice' as doctype, + si.name, + sip.amount as paid_amount, + '' as reference_no, + '' as reference_date, + si.customer as party, + 'Customer' as party_type, + si.posting_date, + si.currency + + FROM + `tabSales Invoice Payment` as sip + JOIN + `tabSales Invoice` as si + ON + sip.parent = si.name + WHERE (sip.clearance_date is null or sip.clearance_date='0000-00-00') + AND sip.account = %(bank_account)s + AND sip.amount {amount_condition} %(amount)s + AND si.docstatus = 1 + """ + +def get_pi_matching_query(amount_condition): + # get matching purchase invoice query + return f""" + SELECT + ( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END + + 1 ) AS rank, + 'Purchase Invoice' as doctype, + name, + paid_amount, + '' as reference_no, + '' as reference_date, + supplier as party, + 'Supplier' as party_type, + posting_date, + currency + FROM + `tabPurchase Invoice` + WHERE + paid_amount {amount_condition} %(amount)s + AND docstatus = 1 + AND is_paid = 1 + AND ifnull(clearance_date, '') = "" + AND cash_bank_account = %(bank_account)s + """ + +def get_ec_matching_query(bank_account, company, amount_condition): + # get matching Expense Claim query + mode_of_payments = [x["parent"] for x in frappe.db.get_list("Mode of Payment Account", + filters={"default_account": bank_account}, fields=["parent"])] + mode_of_payments = '(\'' + '\', \''.join(mode_of_payments) + '\' )' + company_currency = get_company_currency(company) + return f""" + SELECT + ( CASE WHEN employee = %(party)s THEN 1 ELSE 0 END + + 1 ) AS rank, + 'Expense Claim' as doctype, + name, + total_sanctioned_amount as paid_amount, + '' as reference_no, + '' as reference_date, + employee as party, + 'Employee' as party_type, + posting_date, + '{company_currency}' as currency + FROM + `tabExpense Claim` + WHERE + total_sanctioned_amount {amount_condition} %(amount)s + AND docstatus = 1 + AND is_paid = 1 + AND ifnull(clearance_date, '') = "" + AND mode_of_payment in {mode_of_payments} + """ diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py new file mode 100644 index 0000000000..d96950abbc --- /dev/null +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestBankReconciliationTool(unittest.TestCase): + pass diff --git a/erpnext/accounts/doctype/bank_statement_settings_item/__init__.py b/erpnext/accounts/doctype/bank_statement_import/__init__.py similarity index 100% rename from erpnext/accounts/doctype/bank_statement_settings_item/__init__.py rename to erpnext/accounts/doctype/bank_statement_import/__init__.py diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.css b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.css new file mode 100644 index 0000000000..5206540a33 --- /dev/null +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.css @@ -0,0 +1,3 @@ +.warnings .warning { + margin-bottom: 40px; +} diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js new file mode 100644 index 0000000000..ad4ff9ee60 --- /dev/null +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js @@ -0,0 +1,574 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Bank Statement Import", { + setup(frm) { + frappe.realtime.on("data_import_refresh", ({ data_import }) => { + frm.import_in_progress = false; + if (data_import !== frm.doc.name) return; + frappe.model.clear_doc("Bank Statement Import", frm.doc.name); + frappe.model + .with_doc("Bank Statement Import", frm.doc.name) + .then(() => { + frm.refresh(); + }); + }); + frappe.realtime.on("data_import_progress", (data) => { + frm.import_in_progress = true; + if (data.data_import !== frm.doc.name) { + return; + } + let percent = Math.floor((data.current * 100) / data.total); + let seconds = Math.floor(data.eta); + let minutes = Math.floor(data.eta / 60); + let eta_message = + // prettier-ignore + seconds < 60 + ? __('About {0} seconds remaining', [seconds]) + : minutes === 1 + ? __('About {0} minute remaining', [minutes]) + : __('About {0} minutes remaining', [minutes]); + + let message; + if (data.success) { + let message_args = [data.current, data.total, eta_message]; + message = + frm.doc.import_type === "Insert New Records" + ? __("Importing {0} of {1}, {2}", message_args) + : __("Updating {0} of {1}, {2}", message_args); + } + if (data.skipping) { + message = __( + "Skipping {0} of {1}, {2}", + [ + data.current, + data.total, + eta_message, + ] + ); + } + frm.dashboard.show_progress( + __("Import Progress"), + percent, + message + ); + frm.page.set_indicator(__("In Progress"), "orange"); + + // hide progress when complete + if (data.current === data.total) { + setTimeout(() => { + frm.dashboard.hide(); + frm.refresh(); + }, 2000); + } + }); + + frm.set_query("reference_doctype", () => { + return { + filters: { + name: ["in", frappe.boot.user.can_import], + }, + }; + }); + + frm.get_field("import_file").df.options = { + restrictions: { + allowed_file_types: [".csv", ".xls", ".xlsx"], + }, + }; + + frm.has_import_file = () => { + return frm.doc.import_file || frm.doc.google_sheets_url; + }; + }, + + refresh(frm) { + frm.page.hide_icon_group(); + frm.trigger("update_indicators"); + frm.trigger("import_file"); + frm.trigger("show_import_log"); + frm.trigger("show_import_warnings"); + frm.trigger("toggle_submit_after_import"); + frm.trigger("show_import_status"); + frm.trigger("show_report_error_button"); + + if (frm.doc.status === "Partial Success") { + frm.add_custom_button(__("Export Errored Rows"), () => + frm.trigger("export_errored_rows") + ); + } + + if (frm.doc.status.includes("Success")) { + frm.add_custom_button( + __("Go to {0} List", [frm.doc.reference_doctype]), + () => frappe.set_route("List", frm.doc.reference_doctype) + ); + } + }, + + onload_post_render(frm) { + frm.trigger("update_primary_action"); + }, + + update_primary_action(frm) { + if (frm.is_dirty()) { + frm.enable_save(); + return; + } + frm.disable_save(); + if (frm.doc.status !== "Success") { + if (!frm.is_new() && frm.has_import_file()) { + let label = + frm.doc.status === "Pending" + ? __("Start Import") + : __("Retry"); + frm.page.set_primary_action(label, () => + frm.events.start_import(frm) + ); + } else { + frm.page.set_primary_action(__("Save"), () => frm.save()); + } + } + }, + + update_indicators(frm) { + const indicator = frappe.get_indicator(frm.doc); + if (indicator) { + frm.page.set_indicator(indicator[0], indicator[1]); + } else { + frm.page.clear_indicator(); + } + }, + + show_import_status(frm) { + let import_log = JSON.parse(frm.doc.import_log || "[]"); + let successful_records = import_log.filter((log) => log.success); + let failed_records = import_log.filter((log) => !log.success); + if (successful_records.length === 0) return; + + let message; + if (failed_records.length === 0) { + let message_args = [successful_records.length]; + if (frm.doc.import_type === "Insert New Records") { + message = + successful_records.length > 1 + ? __("Successfully imported {0} records.", message_args) + : __("Successfully imported {0} record.", message_args); + } else { + message = + successful_records.length > 1 + ? __("Successfully updated {0} records.", message_args) + : __("Successfully updated {0} record.", message_args); + } + } else { + let message_args = [successful_records.length, import_log.length]; + if (frm.doc.import_type === "Insert New Records") { + message = + successful_records.length > 1 + ? __( + "Successfully imported {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.", + message_args + ) + : __( + "Successfully imported {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.", + message_args + ); + } else { + message = + successful_records.length > 1 + ? __( + "Successfully updated {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.", + message_args + ) + : __( + "Successfully updated {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.", + message_args + ); + } + } + frm.dashboard.set_headline(message); + }, + + show_report_error_button(frm) { + if (frm.doc.status === "Error") { + frappe.db + .get_list("Error Log", { + filters: { method: frm.doc.name }, + fields: ["method", "error"], + order_by: "creation desc", + limit: 1, + }) + .then((result) => { + if (result.length > 0) { + frm.add_custom_button("Report Error", () => { + let fake_xhr = { + responseText: JSON.stringify({ + exc: result[0].error, + }), + }; + frappe.request.report_error(fake_xhr, {}); + }); + } + }); + } + }, + + start_import(frm) { + frm.call({ + method: "form_start_import", + args: { data_import: frm.doc.name }, + btn: frm.page.btn_primary, + }).then((r) => { + if (r.message === true) { + frm.disable_save(); + } + }); + }, + + download_template() { + let method = + "/api/method/frappe.core.doctype.data_import.data_import.download_template"; + + open_url_post(method, { + doctype: "Bank Transaction", + export_records: "5_records", + export_fields: { + "Bank Transaction": [ + "date", + "deposit", + "withdrawal", + "description", + "reference_number", + ], + }, + }); + }, + + reference_doctype(frm) { + frm.trigger("toggle_submit_after_import"); + }, + + toggle_submit_after_import(frm) { + frm.toggle_display("submit_after_import", false); + let doctype = frm.doc.reference_doctype; + if (doctype) { + frappe.model.with_doctype(doctype, () => { + let meta = frappe.get_meta(doctype); + frm.toggle_display("submit_after_import", meta.is_submittable); + }); + } + }, + + google_sheets_url(frm) { + if (!frm.is_dirty()) { + frm.trigger("import_file"); + } else { + frm.trigger("update_primary_action"); + } + }, + + refresh_google_sheet(frm) { + frm.trigger("import_file"); + }, + + import_file(frm) { + frm.toggle_display("section_import_preview", frm.has_import_file()); + if (!frm.has_import_file()) { + frm.get_field("import_preview").$wrapper.empty(); + return; + } else { + frm.trigger("update_primary_action"); + } + + // load import preview + frm.get_field("import_preview").$wrapper.empty(); + $('') + .html(__("Loading import file...")) + .appendTo(frm.get_field("import_preview").$wrapper); + + frm.call({ + method: "get_preview_from_template", + args: { + data_import: frm.doc.name, + import_file: frm.doc.import_file, + google_sheets_url: frm.doc.google_sheets_url, + }, + error_handlers: { + TimestampMismatchError() { + // ignore this error + }, + }, + }).then((r) => { + let preview_data = r.message; + frm.events.show_import_preview(frm, preview_data); + frm.events.show_import_warnings(frm, preview_data); + }); + }, + // method: 'frappe.core.doctype.data_import.data_import.get_preview_from_template', + + show_import_preview(frm, preview_data) { + let import_log = JSON.parse(frm.doc.import_log || "[]"); + + if ( + frm.import_preview && + frm.import_preview.doctype === frm.doc.reference_doctype + ) { + frm.import_preview.preview_data = preview_data; + frm.import_preview.import_log = import_log; + frm.import_preview.refresh(); + return; + } + + frappe.require("/assets/js/data_import_tools.min.js", () => { + frm.import_preview = new frappe.data_import.ImportPreview({ + wrapper: frm.get_field("import_preview").$wrapper, + doctype: frm.doc.reference_doctype, + preview_data, + import_log, + frm, + events: { + remap_column(changed_map) { + let template_options = JSON.parse( + frm.doc.template_options || "{}" + ); + template_options.column_to_field_map = + template_options.column_to_field_map || {}; + Object.assign( + template_options.column_to_field_map, + changed_map + ); + frm.set_value( + "template_options", + JSON.stringify(template_options) + ); + frm.save().then(() => frm.trigger("import_file")); + }, + }, + }); + }); + }, + + export_errored_rows(frm) { + open_url_post( + "/api/method/frappe.core.doctype.data_import.data_import.download_errored_template", + { + data_import_name: frm.doc.name, + } + ); + }, + + show_import_warnings(frm, preview_data) { + let columns = preview_data.columns; + let warnings = JSON.parse(frm.doc.template_warnings || "[]"); + warnings = warnings.concat(preview_data.warnings || []); + + frm.toggle_display("import_warnings_section", warnings.length > 0); + if (warnings.length === 0) { + frm.get_field("import_warnings").$wrapper.html(""); + return; + } + + // group warnings by row + let warnings_by_row = {}; + let other_warnings = []; + for (let warning of warnings) { + if (warning.row) { + warnings_by_row[warning.row] = + warnings_by_row[warning.row] || []; + warnings_by_row[warning.row].push(warning); + } else { + other_warnings.push(warning); + } + } + + let html = ""; + html += Object.keys(warnings_by_row) + .map((row_number) => { + let message = warnings_by_row[row_number] + .map((w) => { + if (w.field) { + let label = + w.field.label + + (w.field.parent !== frm.doc.reference_doctype + ? ` (${w.field.parent})` + : ""); + return `
  • ${label}: ${w.message}
  • `; + } + return `
  • ${w.message}
  • `; + }) + .join(""); + return ` +
    +
    ${__("Row {0}", [row_number])}
    +
      ${message}
    +
    + `; + }) + .join(""); + + html += other_warnings + .map((warning) => { + let header = ""; + if (warning.col) { + let column_number = `${__( + "Column {0}", + [warning.col] + )}`; + let column_header = columns[warning.col].header_title; + header = `${column_number} (${column_header})`; + } + return ` +
    +
    ${header}
    +
    ${warning.message}
    +
    + `; + }) + .join(""); + frm.get_field("import_warnings").$wrapper.html(` +
    +
    ${html}
    +
    + `); + }, + + show_failed_logs(frm) { + frm.trigger("show_import_log"); + }, + + show_import_log(frm) { + let import_log = JSON.parse(frm.doc.import_log || "[]"); + let logs = import_log; + frm.toggle_display("import_log", false); + frm.toggle_display("import_log_section", logs.length > 0); + + if (logs.length === 0) { + frm.get_field("import_log_preview").$wrapper.empty(); + return; + } + + let rows = logs + .map((log) => { + let html = ""; + if (log.success) { + if (frm.doc.import_type === "Insert New Records") { + html = __( + "Successfully imported {0}", [ + `${frappe.utils.get_form_link( + frm.doc.reference_doctype, + log.docname, + true + )}`, + ] + ); + } else { + html = __( + "Successfully updated {0}", [ + `${frappe.utils.get_form_link( + frm.doc.reference_doctype, + log.docname, + true + )}`, + ] + ); + } + } else { + let messages = log.messages + .map(JSON.parse) + .map((m) => { + let title = m.title + ? `${m.title}` + : ""; + let message = m.message + ? `
    ${m.message}
    ` + : ""; + return title + message; + }) + .join(""); + let id = frappe.dom.get_unique_id(); + html = `${messages} + +
    +
    +
    ${log.exception}
    +
    +
    `; + } + let indicator_color = log.success ? "green" : "red"; + let title = log.success ? __("Success") : __("Failure"); + + if (frm.doc.show_failed_logs && log.success) { + return ""; + } + + return ` + ${log.row_indexes.join(", ")} + +
    ${title}
    + + + ${html} + + `; + }) + .join(""); + + if (!rows && frm.doc.show_failed_logs) { + rows = ` + ${__("No failed logs")} + `; + } + + frm.get_field("import_log_preview").$wrapper.html(` + + + + + + + ${rows} +
    ${__("Row Number")}${__("Status")}${__("Message")}
    + `); + }, + + show_missing_link_values(frm, missing_link_values) { + let can_be_created_automatically = missing_link_values.every( + (d) => d.has_one_mandatory_field + ); + + let html = missing_link_values + .map((d) => { + let doctype = d.doctype; + let values = d.missing_values; + return ` +
    ${doctype}
    +
      ${values.map((v) => `
    • ${v}
    • `).join("")}
    + `; + }) + .join(""); + + if (can_be_created_automatically) { + // prettier-ignore + let message = __('There are some linked records which needs to be created before we can import your file. Do you want to create the following missing records automatically?'); + frappe.confirm(message + html, () => { + frm.call("create_missing_link_values", { + missing_link_values, + }).then((r) => { + let records = r.message; + frappe.msgprint(__( + "Created {0} records successfully.", [ + records.length, + ] + )); + }); + }); + } else { + frappe.msgprint( + // prettier-ignore + __('The following records needs to be created before we can import your file.') + html + ); + } + }, +}); diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json new file mode 100644 index 0000000000..5e913cc2aa --- /dev/null +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json @@ -0,0 +1,227 @@ +{ + "actions": [], + "autoname": "format:Bank Statement Import on {creation}", + "beta": 1, + "creation": "2019-08-04 14:16:08.318714", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "bank_account", + "bank", + "column_break_4", + "google_sheets_url", + "refresh_google_sheet", + "html_5", + "import_file", + "download_template", + "status", + "template_options", + "import_warnings_section", + "template_warnings", + "import_warnings", + "section_import_preview", + "import_preview", + "import_log_section", + "import_log", + "show_failed_logs", + "import_log_preview", + "reference_doctype", + "import_type", + "submit_after_import", + "mute_emails" + ], + "fields": [ + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "bank_account", + "fieldtype": "Link", + "label": "Bank Account", + "options": "Bank Account", + "reqd": 1, + "set_only_once": 1 + }, + { + "depends_on": "eval:doc.bank_account", + "fetch_from": "bank_account.bank", + "fieldname": "bank", + "fieldtype": "Link", + "label": "Bank", + "options": "Bank", + "read_only": 1, + "set_only_once": 1 + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "download_template", + "fieldtype": "Button", + "label": "Download Template" + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "import_file", + "fieldtype": "Attach", + "in_list_view": 1, + "label": "Import File" + }, + { + "fieldname": "import_preview", + "fieldtype": "HTML", + "label": "Import Preview" + }, + { + "fieldname": "section_import_preview", + "fieldtype": "Section Break", + "label": "Preview" + }, + { + "fieldname": "template_options", + "fieldtype": "Code", + "hidden": 1, + "label": "Template Options", + "options": "JSON", + "read_only": 1 + }, + { + "fieldname": "import_log", + "fieldtype": "Code", + "label": "Import Log", + "options": "JSON" + }, + { + "fieldname": "import_log_section", + "fieldtype": "Section Break", + "label": "Import Log" + }, + { + "fieldname": "import_log_preview", + "fieldtype": "HTML", + "label": "Import Log Preview" + }, + { + "default": "Pending", + "fieldname": "status", + "fieldtype": "Select", + "hidden": 1, + "label": "Status", + "options": "Pending\nSuccess\nPartial Success\nError", + "read_only": 1 + }, + { + "fieldname": "template_warnings", + "fieldtype": "Code", + "hidden": 1, + "label": "Template Warnings", + "options": "JSON" + }, + { + "fieldname": "import_warnings_section", + "fieldtype": "Section Break", + "label": "Import File Errors and Warnings" + }, + { + "fieldname": "import_warnings", + "fieldtype": "HTML", + "label": "Import Warnings" + }, + { + "default": "0", + "fieldname": "show_failed_logs", + "fieldtype": "Check", + "label": "Show Failed Logs" + }, + { + "depends_on": "eval:!doc.__islocal && !doc.import_file", + "fieldname": "html_5", + "fieldtype": "HTML", + "options": "
    Or
    " + }, + { + "depends_on": "eval:!doc.__islocal && !doc.import_file\n", + "description": "Must be a publicly accessible Google Sheets URL", + "fieldname": "google_sheets_url", + "fieldtype": "Data", + "label": "Import from Google Sheets" + }, + { + "depends_on": "eval:doc.google_sheets_url && !doc.__unsaved", + "fieldname": "refresh_google_sheet", + "fieldtype": "Button", + "label": "Refresh Google Sheet" + }, + { + "default": "Bank Transaction", + "fieldname": "reference_doctype", + "fieldtype": "Link", + "hidden": 1, + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1, + "set_only_once": 1 + }, + { + "default": "Insert New Records", + "fieldname": "import_type", + "fieldtype": "Select", + "hidden": 1, + "in_list_view": 1, + "label": "Import Type", + "options": "\nInsert New Records\nUpdate Existing Records", + "reqd": 1, + "set_only_once": 1 + }, + { + "default": "1", + "fieldname": "submit_after_import", + "fieldtype": "Check", + "hidden": 1, + "label": "Submit After Import", + "set_only_once": 1 + }, + { + "default": "1", + "fieldname": "mute_emails", + "fieldtype": "Check", + "hidden": 1, + "label": "Don't Send Emails", + "set_only_once": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + } + ], + "hide_toolbar": 1, + "links": [], + "modified": "2021-02-10 19:29:59.027325", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Bank Statement Import", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py new file mode 100644 index 0000000000..9f41b13f4b --- /dev/null +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import csv +import json +import re + +import openpyxl +from openpyxl.styles import Font +from openpyxl.utils import get_column_letter +from six import string_types + +import frappe +from frappe.core.doctype.data_import.importer import Importer, ImportFile +from frappe.utils.background_jobs import enqueue +from frappe.utils.xlsxutils import handle_html, ILLEGAL_CHARACTERS_RE +from frappe import _ + +from frappe.core.doctype.data_import.data_import import DataImport + +class BankStatementImport(DataImport): + def __init__(self, *args, **kwargs): + super(BankStatementImport, self).__init__(*args, **kwargs) + + def validate(self): + doc_before_save = self.get_doc_before_save() + if ( + not (self.import_file or self.google_sheets_url) + or (doc_before_save and doc_before_save.import_file != self.import_file) + or (doc_before_save and doc_before_save.google_sheets_url != self.google_sheets_url) + ): + + template_options_dict = {} + column_to_field_map = {} + bank = frappe.get_doc("Bank", self.bank) + for i in bank.bank_transaction_mapping: + column_to_field_map[i.file_field] = i.bank_transaction_field + template_options_dict["column_to_field_map"] = column_to_field_map + self.template_options = json.dumps(template_options_dict) + + self.template_warnings = "" + + self.validate_import_file() + self.validate_google_sheets_url() + + def start_import(self): + + from frappe.core.page.background_jobs.background_jobs import get_info + from frappe.utils.scheduler import is_scheduler_inactive + + if is_scheduler_inactive() and not frappe.flags.in_test: + frappe.throw( + _("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive") + ) + + enqueued_jobs = [d.get("job_name") for d in get_info()] + + if self.name not in enqueued_jobs: + enqueue( + start_import, + queue="default", + timeout=6000, + event="data_import", + job_name=self.name, + data_import=self.name, + bank_account=self.bank_account, + import_file_path=self.import_file, + bank=self.bank, + template_options=self.template_options, + now=frappe.conf.developer_mode or frappe.flags.in_test, + ) + return True + + return False + +@frappe.whitelist() +def get_preview_from_template(data_import, import_file=None, google_sheets_url=None): + return frappe.get_doc("Bank Statement Import", data_import).get_preview_from_template( + import_file, google_sheets_url + ) + +@frappe.whitelist() +def form_start_import(data_import): + return frappe.get_doc("Bank Statement Import", data_import).start_import() + +@frappe.whitelist() +def download_errored_template(data_import_name): + data_import = frappe.get_doc("Bank Statement Import", data_import_name) + data_import.export_errored_rows() + +def start_import(data_import, bank_account, import_file_path, bank, template_options): + """This method runs in background job""" + + update_mapping_db(bank, template_options) + + data_import = frappe.get_doc("Bank Statement Import", data_import) + + import_file = ImportFile("Bank Transaction", file = import_file_path, import_type="Insert New Records") + data = import_file.raw_data + + add_bank_account(data, bank_account) + write_files(import_file, data) + + try: + i = Importer(data_import.reference_doctype, data_import=data_import) + i.import_data() + except Exception: + frappe.db.rollback() + data_import.db_set("status", "Error") + frappe.log_error(title=data_import.name) + finally: + frappe.flags.in_import = False + + frappe.publish_realtime("data_import_refresh", {"data_import": data_import.name}) + +def update_mapping_db(bank, template_options): + bank = frappe.get_doc("Bank", bank) + for d in bank.bank_transaction_mapping: + d.delete() + + for d in json.loads(template_options)["column_to_field_map"].items(): + bank.append("bank_transaction_mapping", {"bank_transaction_field": d[1] ,"file_field": d[0]} ) + + bank.save() + +def add_bank_account(data, bank_account): + bank_account_loc = None + if "Bank Account" not in data[0]: + data[0].append("Bank Account") + else: + for loc, header in enumerate(data[0]): + if header == "Bank Account": + bank_account_loc = loc + + for row in data[1:]: + if bank_account_loc: + row[bank_account_loc] = bank_account + else: + row.append(bank_account) + +def write_files(import_file, data): + full_file_path = import_file.file_doc.get_full_path() + parts = import_file.file_doc.get_extension() + extension = parts[1] + extension = extension.lstrip(".") + + if extension == "csv": + with open(full_file_path, 'w', newline='') as file: + writer = csv.writer(file) + writer.writerows(data) + elif extension == "xlsx" or "xls": + write_xlsx(data, "trans", file_path = full_file_path) + +def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None): + # from xlsx utils with changes + column_widths = column_widths or [] + if wb is None: + wb = openpyxl.Workbook(write_only=True) + + ws = wb.create_sheet(sheet_name, 0) + + for i, column_width in enumerate(column_widths): + if column_width: + ws.column_dimensions[get_column_letter(i + 1)].width = column_width + + row1 = ws.row_dimensions[1] + row1.font = Font(name='Calibri', bold=True) + + for row in data: + clean_row = [] + for item in row: + if isinstance(item, string_types) and (sheet_name not in ['Data Import Template', 'Data Export']): + value = handle_html(item) + else: + value = item + + if isinstance(item, string_types) and next(ILLEGAL_CHARACTERS_RE.finditer(value), None): + # Remove illegal characters from the string + value = re.sub(ILLEGAL_CHARACTERS_RE, '', value) + + clean_row.append(value) + + ws.append(clean_row) + + wb.save(file_path) + return True + +@frappe.whitelist() +def upload_bank_statement(**args): + args = frappe._dict(args) + bsi = frappe.new_doc("Bank Statement Import") + + if args.company: + bsi.update({ + "company": args.company, + }) + + if args.bank_account: + bsi.update({ + "bank_account": args.bank_account + }) + + return bsi diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import_list.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import_list.js new file mode 100644 index 0000000000..6c754022e6 --- /dev/null +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import_list.js @@ -0,0 +1,36 @@ +let imports_in_progress = []; + +frappe.listview_settings['Bank Statement Import'] = { + onload(listview) { + frappe.realtime.on('data_import_progress', data => { + if (!imports_in_progress.includes(data.data_import)) { + imports_in_progress.push(data.data_import); + } + }); + frappe.realtime.on('data_import_refresh', data => { + imports_in_progress = imports_in_progress.filter( + d => d !== data.data_import + ); + listview.refresh(); + }); + }, + get_indicator: function(doc) { + var colors = { + 'Pending': 'orange', + 'Not Started': 'orange', + 'Partial Success': 'orange', + 'Success': 'green', + 'In Progress': 'orange', + 'Error': 'red' + }; + let status = doc.status; + if (imports_in_progress.includes(doc.name)) { + status = 'In Progress'; + } + if (status == 'Pending') { + status = 'Not Started'; + } + return [__(status), colors[status], 'status,=,' + doc.status]; + }, + hide_name_column: true +}; diff --git a/erpnext/accounts/doctype/bank_statement_import/test_bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/test_bank_statement_import.py new file mode 100644 index 0000000000..cd5831412d --- /dev/null +++ b/erpnext/accounts/doctype/bank_statement_import/test_bank_statement_import.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestBankStatementImport(unittest.TestCase): + pass diff --git a/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.js b/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.js deleted file mode 100644 index 46aa4f2031..0000000000 --- a/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2017, sathishpy@gmail.com and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Bank Statement Settings', { - refresh: function(frm) { - - } -}); diff --git a/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.json b/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.json deleted file mode 100644 index 53fbf7d446..0000000000 --- a/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.json +++ /dev/null @@ -1,272 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 1, - "beta": 0, - "creation": "2017-11-13 13:38:10.863592", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "bank", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Bank Account", - "length": 0, - "no_copy": 0, - "options": "Bank", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "'%d/%m/%Y'", - "fieldname": "date_format", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Date Format", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "statement_header_mapping", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Statement Header Mapping", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "header_items", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Statement Headers", - "length": 0, - "no_copy": 0, - "options": "Bank Statement Settings Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "transaction_data_mapping", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Transaction Data Mapping", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "mapped_items", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Mapped Items", - "length": 0, - "no_copy": 0, - "options": "Bank Statement Transaction Settings Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-04-07 18:57:04.048423", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Bank Statement Settings", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.py b/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.py deleted file mode 100644 index 6c4dd1b85b..0000000000 --- a/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2017, sathishpy@gmail.com and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe.model.document import Document - -class BankStatementSettings(Document): - def autoname(self): - self.name = self.bank + "-Statement-Settings" diff --git a/erpnext/accounts/doctype/bank_statement_settings/test_bank_statement_settings.js b/erpnext/accounts/doctype/bank_statement_settings/test_bank_statement_settings.js deleted file mode 100644 index f2381c042e..0000000000 --- a/erpnext/accounts/doctype/bank_statement_settings/test_bank_statement_settings.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Bank Statement Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Bank Statement Settings - () => frappe.tests.make('Bank Statement Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/bank_statement_settings/test_bank_statement_settings.py b/erpnext/accounts/doctype/bank_statement_settings/test_bank_statement_settings.py deleted file mode 100644 index aa7fe83328..0000000000 --- a/erpnext/accounts/doctype/bank_statement_settings/test_bank_statement_settings.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2017, sathishpy@gmail.com and Contributors -# See license.txt -from __future__ import unicode_literals - -import frappe -import unittest - -class TestBankStatementSettings(unittest.TestCase): - pass diff --git a/erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.json b/erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.json deleted file mode 100644 index 7c93f268f5..0000000000 --- a/erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2018-01-08 00:16:42.762980", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "mapped_header", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Mapped Header", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "stmt_header", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Bank Header", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-01-08 00:19:14.841134", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Bank Statement Settings Item", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.py b/erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.py deleted file mode 100644 index 9438e9a63f..0000000000 --- a/erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2018, sathishpy@gmail.com and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe.model.document import Document - -class BankStatementSettingsItem(Document): - pass diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.js b/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.js deleted file mode 100644 index 736ed35ae1..0000000000 --- a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.js +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) 2017, sathishpy@gmail.com and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Bank Statement Transaction Entry', { - setup: function(frm) { - frm.events.account_filters(frm) - frm.events.invoice_filter(frm) - }, - refresh: function(frm) { - frm.set_df_property("bank_account", "read_only", frm.doc.__islocal ? 0 : 1); - frm.set_df_property("from_date", "read_only", frm.doc.__islocal ? 0 : 1); - frm.set_df_property("to_date", "read_only", frm.doc.__islocal ? 0 : 1); - }, - invoke_doc_function(frm, method) { - frappe.call({ - doc: frm.doc, - method: method, - callback: function(r) { - if(!r.exe) { - frm.refresh_fields(); - } - } - }); - }, - account_filters: function(frm) { - frm.fields_dict['bank_account'].get_query = function(doc, dt, dn) { - return { - filters:[ - ["Account", "account_type", "in", ["Bank"]] - ] - } - }; - frm.fields_dict['receivable_account'].get_query = function(doc, dt, dn) { - return { - filters: {"account_type": "Receivable"} - } - }; - frm.fields_dict['payable_account'].get_query = function(doc, dt, dn) { - return { - filters: {"account_type": "Payable"} - } - }; - }, - - invoice_filter: function(frm) { - frm.set_query("invoice", "payment_invoice_items", function(doc, cdt, cdn) { - let row = locals[cdt][cdn] - if (row.party_type == "Customer") { - return { - filters:[[row.invoice_type, "customer", "in", [row.party]], - [row.invoice_type, "status", "!=", "Cancelled" ], - [row.invoice_type, "posting_date", "<", row.transaction_date ], - [row.invoice_type, "outstanding_amount", ">", 0 ]] - } - } else if (row.party_type == "Supplier") { - return { - filters:[[row.invoice_type, "supplier", "in", [row.party]], - [row.invoice_type, "status", "!=", "Cancelled" ], - [row.invoice_type, "posting_date", "<", row.transaction_date ], - [row.invoice_type, "outstanding_amount", ">", 0 ]] - } - } - }); - }, - - match_invoices: function(frm) { - frm.events.invoke_doc_function(frm, "populate_matching_invoices"); - }, - create_payments: function(frm) { - frm.events.invoke_doc_function(frm, "create_payment_entries"); - }, - submit_payments: function(frm) { - frm.events.invoke_doc_function(frm, "submit_payment_entries"); - }, -}); - - -frappe.ui.form.on('Bank Statement Transaction Invoice Item', { - party_type: function(frm, cdt, cdn) { - let row = locals[cdt][cdn]; - if (row.party_type == "Customer") { - row.invoice_type = "Sales Invoice"; - } else if (row.party_type == "Supplier") { - row.invoice_type = "Purchase Invoice"; - } else if (row.party_type == "Account") { - row.invoice_type = "Journal Entry"; - } - refresh_field("invoice_type", row.name, "payment_invoice_items"); - - }, - invoice_type: function(frm, cdt, cdn) { - let row = locals[cdt][cdn]; - if (row.invoice_type == "Purchase Invoice") { - row.party_type = "Supplier"; - } else if (row.invoice_type == "Sales Invoice") { - row.party_type = "Customer"; - } - refresh_field("party_type", row.name, "payment_invoice_items"); - } -}); \ No newline at end of file diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.json b/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.json deleted file mode 100644 index fb80169c37..0000000000 --- a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.json +++ /dev/null @@ -1,792 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 1, - "beta": 0, - "creation": "2017-11-07 13:48:13.123185", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "bank_account", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Bank Account", - "length": 0, - "no_copy": 0, - "options": "Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "from_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "From Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "to_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "To Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "bank_settings", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Bank Statement Settings", - "length": 0, - "no_copy": 0, - "options": "Bank Statement Settings", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "bank", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Bank", - "length": 0, - "no_copy": 0, - "options": "Bank", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "receivable_account", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Receivable Account", - "length": 0, - "no_copy": 0, - "options": "Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "payable_account", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Payable Account", - "length": 0, - "no_copy": 0, - "options": "Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "bank_statement", - "fieldtype": "Attach", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Bank Statement", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "section_break_6", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Bank Transaction Entries", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "new_transaction_items", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "New Transactions", - "length": 0, - "no_copy": 0, - "options": "Bank Statement Transaction Payment Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.new_transaction_items && doc.new_transaction_items.length", - "fieldname": "section_break_9", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "match_invoices", - "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Match Transaction to Invoices", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_14", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "create_payments", - "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Create New Payment/Journal Entry", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_16", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "submit_payments", - "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Submit/Reconcile Payments", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.new_transaction_items && doc.new_transaction_items.length", - "fieldname": "section_break_18", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Matching Invoices", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "payment_invoice_items", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Payment Invoice Items", - "length": 0, - "no_copy": 0, - "options": "Bank Statement Transaction Invoice Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reconciled_transactions", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Reconciled Transactions", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reconciled_transaction_items", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Reconciled Transactions", - "length": 0, - "no_copy": 0, - "options": "Bank Statement Transaction Payment Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amended_from", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Bank Statement Transaction Entry", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-09-14 18:04:44.170455", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Bank Statement Transaction Entry", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, - "write": 1 - } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py b/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py deleted file mode 100644 index 27dd8e463f..0000000000 --- a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py +++ /dev/null @@ -1,443 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2017, sathishpy@gmail.com and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe import _ -from frappe.model.document import Document -from erpnext.accounts.utils import get_outstanding_invoices -from frappe.utils import nowdate -from datetime import datetime -import csv, os, re, io -import difflib -import copy - -class BankStatementTransactionEntry(Document): - def autoname(self): - self.name = self.bank_account + "-" + self.from_date + "-" + self.to_date - if self.bank: - mapper_name = self.bank + "-Statement-Settings" - if not frappe.db.exists("Bank Statement Settings", mapper_name): - self.create_settings(self.bank) - self.bank_settings = mapper_name - - def create_settings(self, bank): - mapper = frappe.new_doc("Bank Statement Settings") - mapper.bank = bank - mapper.date_format = "%Y-%m-%d" - mapper.bank_account = self.bank_account - for header in ["Date", "Particulars", "Withdrawals", "Deposits", "Balance"]: - header_item = mapper.append("header_items", {}) - header_item.mapped_header = header_item.stmt_header = header - mapper.save() - - def on_update(self): - if (not self.bank_statement): - self.reconciled_transaction_items = self.new_transaction_items = [] - return - - if len(self.new_transaction_items + self.reconciled_transaction_items) == 0: - self.populate_payment_entries() - else: - self.match_invoice_to_payment() - - def validate(self): - if not self.new_transaction_items: - self.populate_payment_entries() - - def get_statement_headers(self): - if not self.bank_settings: - frappe.throw(_("Bank Data mapper doesn't exist")) - mapper_doc = frappe.get_doc("Bank Statement Settings", self.bank_settings) - headers = {entry.mapped_header:entry.stmt_header for entry in mapper_doc.header_items} - return headers - - def populate_payment_entries(self): - if self.bank_statement is None: return - file_url = self.bank_statement - if (len(self.new_transaction_items + self.reconciled_transaction_items) > 0): - frappe.throw(_("Transactions already retreived from the statement")) - - date_format = frappe.get_value("Bank Statement Settings", self.bank_settings, "date_format") - if (date_format is None): - date_format = '%Y-%m-%d' - if self.bank_settings: - mapped_items = frappe.get_doc("Bank Statement Settings", self.bank_settings).mapped_items - statement_headers = self.get_statement_headers() - transactions = get_transaction_entries(file_url, statement_headers) - for entry in transactions: - date = entry[statement_headers["Date"]].strip() - #print("Processing entry DESC:{0}-W:{1}-D:{2}-DT:{3}".format(entry["Particulars"], entry["Withdrawals"], entry["Deposits"], entry["Date"])) - if (not date): continue - transaction_date = datetime.strptime(date, date_format).date() - if (self.from_date and transaction_date < datetime.strptime(self.from_date, '%Y-%m-%d').date()): continue - if (self.to_date and transaction_date > datetime.strptime(self.to_date, '%Y-%m-%d').date()): continue - bank_entry = self.append('new_transaction_items', {}) - bank_entry.transaction_date = transaction_date - bank_entry.description = entry[statement_headers["Particulars"]] - - mapped_item = next((entry for entry in mapped_items if entry.mapping_type == "Transaction" and frappe.safe_decode(entry.bank_data.lower()) in frappe.safe_decode(bank_entry.description.lower())), None) - if (mapped_item is not None): - bank_entry.party_type = mapped_item.mapped_data_type - bank_entry.party = mapped_item.mapped_data - else: - bank_entry.party_type = "Supplier" if not entry[statement_headers["Deposits"]].strip() else "Customer" - party_list = frappe.get_all(bank_entry.party_type, fields=["name"]) - parties = [party.name for party in party_list] - matches = difflib.get_close_matches(frappe.safe_decode(bank_entry.description.lower()), parties, 1, 0.4) - if len(matches) > 0: bank_entry.party = matches[0] - bank_entry.amount = -float(entry[statement_headers["Withdrawals"]]) if not entry[statement_headers["Deposits"]].strip() else float(entry[statement_headers["Deposits"]]) - self.map_unknown_transactions() - self.map_transactions_on_journal_entry() - - def map_transactions_on_journal_entry(self): - for entry in self.new_transaction_items: - vouchers = frappe.db.sql("""select name, posting_date from `tabJournal Entry` - where posting_date='{0}' and total_credit={1} and cheque_no='{2}' and docstatus != 2 - """.format(entry.transaction_date, abs(entry.amount), frappe.safe_decode(entry.description)), as_dict=True) - if (len(vouchers) == 1): - entry.reference_name = vouchers[0].name - - def populate_matching_invoices(self): - self.payment_invoice_items = [] - self.map_unknown_transactions() - added_invoices = [] - for entry in self.new_transaction_items: - if (not entry.party or entry.party_type == "Account"): continue - account = self.receivable_account if entry.party_type == "Customer" else self.payable_account - invoices = get_outstanding_invoices(entry.party_type, entry.party, account) - transaction_date = datetime.strptime(entry.transaction_date, "%Y-%m-%d").date() - outstanding_invoices = [invoice for invoice in invoices if invoice.posting_date <= transaction_date] - amount = abs(entry.amount) - matching_invoices = [invoice for invoice in outstanding_invoices if invoice.outstanding_amount == amount] - sorted(outstanding_invoices, key=lambda k: k['posting_date']) - for e in (matching_invoices + outstanding_invoices): - added = next((inv for inv in added_invoices if inv == e.get('voucher_no')), None) - if (added is not None): continue - ent = self.append('payment_invoice_items', {}) - ent.transaction_date = entry.transaction_date - ent.payment_description = frappe.safe_decode(entry.description) - ent.party_type = entry.party_type - ent.party = entry.party - ent.invoice = e.get('voucher_no') - added_invoices += [ent.invoice] - ent.invoice_type = "Sales Invoice" if entry.party_type == "Customer" else "Purchase Invoice" - ent.invoice_date = e.get('posting_date') - ent.outstanding_amount = e.get('outstanding_amount') - ent.allocated_amount = min(float(e.get('outstanding_amount')), amount) - amount -= float(e.get('outstanding_amount')) - if (amount <= 5): break - self.match_invoice_to_payment() - self.populate_matching_vouchers() - self.map_transactions_on_journal_entry() - - def match_invoice_to_payment(self): - added_payments = [] - for entry in self.new_transaction_items: - if (not entry.party or entry.party_type == "Account"): continue - entry.account = self.receivable_account if entry.party_type == "Customer" else self.payable_account - amount = abs(entry.amount) - payment, matching_invoices = None, [] - for inv_entry in self.payment_invoice_items: - if (inv_entry.payment_description != frappe.safe_decode(entry.description) or inv_entry.transaction_date != entry.transaction_date): continue - if (inv_entry.party != entry.party): continue - matching_invoices += [inv_entry.invoice_type + "|" + inv_entry.invoice] - payment = get_payments_matching_invoice(inv_entry.invoice, entry.amount, entry.transaction_date) - doc = frappe.get_doc(inv_entry.invoice_type, inv_entry.invoice) - inv_entry.invoice_date = doc.posting_date - inv_entry.outstanding_amount = doc.outstanding_amount - inv_entry.allocated_amount = min(float(doc.outstanding_amount), amount) - amount -= inv_entry.allocated_amount - if (amount < 0): break - - amount = abs(entry.amount) - if (payment is None): - order_doctype = "Sales Order" if entry.party_type=="Customer" else "Purchase Order" - from erpnext.controllers.accounts_controller import get_advance_payment_entries - payment_entries = get_advance_payment_entries(entry.party_type, entry.party, entry.account, order_doctype, against_all_orders=True) - payment_entries += self.get_matching_payments(entry.party, amount, entry.transaction_date) - payment = next((payment for payment in payment_entries if payment.amount == amount and payment not in added_payments), None) - if (payment is None): - print("Failed to find payments for {0}:{1}".format(entry.party, amount)) - continue - added_payments += [payment] - entry.reference_type = payment.reference_type - entry.reference_name = payment.reference_name - entry.mode_of_payment = "Wire Transfer" - entry.outstanding_amount = min(amount, 0) - if (entry.payment_reference is None): - entry.payment_reference = frappe.safe_decode(entry.description) - entry.invoices = ",".join(matching_invoices) - #print("Matching payment is {0}:{1}".format(entry.reference_type, entry.reference_name)) - - def get_matching_payments(self, party, amount, pay_date): - query = """select 'Payment Entry' as reference_type, name as reference_name, paid_amount as amount - from `tabPayment Entry` where party='{0}' and paid_amount={1} and posting_date='{2}' and docstatus != 2 - """.format(party, amount, pay_date) - matching_payments = frappe.db.sql(query, as_dict=True) - return matching_payments - - def map_unknown_transactions(self): - for entry in self.new_transaction_items: - if (entry.party): continue - inv_type = "Sales Invoice" if (entry.amount > 0) else "Purchase Invoice" - party_type = "customer" if (entry.amount > 0) else "supplier" - - query = """select posting_date, name, {0}, outstanding_amount - from `tab{1}` where ROUND(outstanding_amount)={2} and posting_date < '{3}' - """.format(party_type, inv_type, round(abs(entry.amount)), entry.transaction_date) - invoices = frappe.db.sql(query, as_dict = True) - if(len(invoices) > 0): - entry.party = invoices[0].get(party_type) - - def populate_matching_vouchers(self): - for entry in self.new_transaction_items: - if (not entry.party or entry.reference_name): continue - print("Finding matching voucher for {0}".format(frappe.safe_decode(entry.description))) - amount = abs(entry.amount) - invoices = [] - vouchers = get_matching_journal_entries(self.from_date, self.to_date, entry.party, self.bank_account, amount) - if len(vouchers) == 0: continue - for voucher in vouchers: - added = next((entry.invoice for entry in self.payment_invoice_items if entry.invoice == voucher.voucher_no), None) - if (added): - print("Found voucher {0}".format(added)) - continue - print("Adding voucher {0} {1} {2}".format(voucher.voucher_no, voucher.posting_date, voucher.debit)) - ent = self.append('payment_invoice_items', {}) - ent.invoice_date = voucher.posting_date - ent.invoice_type = "Journal Entry" - ent.invoice = voucher.voucher_no - ent.payment_description = frappe.safe_decode(entry.description) - ent.allocated_amount = max(voucher.debit, voucher.credit) - - invoices += [ent.invoice_type + "|" + ent.invoice] - entry.reference_type = "Journal Entry" - entry.mode_of_payment = "Wire Transfer" - entry.reference_name = ent.invoice - #entry.account = entry.party - entry.invoices = ",".join(invoices) - break - - - def create_payment_entries(self): - for payment_entry in self.new_transaction_items: - if (not payment_entry.party): continue - if (payment_entry.reference_name): continue - print("Creating payment entry for {0}".format(frappe.safe_decode(payment_entry.description))) - if (payment_entry.party_type == "Account"): - payment = self.create_journal_entry(payment_entry) - invoices = [payment.doctype + "|" + payment.name] - payment_entry.invoices = ",".join(invoices) - else: - payment = self.create_payment_entry(payment_entry) - invoices = [entry.reference_doctype + "|" + entry.reference_name for entry in payment.references if entry is not None] - payment_entry.invoices = ",".join(invoices) - payment_entry.mode_of_payment = payment.mode_of_payment - payment_entry.account = self.receivable_account if payment_entry.party_type == "Customer" else self.payable_account - payment_entry.reference_name = payment.name - payment_entry.reference_type = payment.doctype - frappe.msgprint(_("Successfully created payment entries")) - - def create_payment_entry(self, pe): - payment = frappe.new_doc("Payment Entry") - payment.posting_date = pe.transaction_date - payment.payment_type = "Receive" if pe.party_type == "Customer" else "Pay" - payment.mode_of_payment = "Wire Transfer" - payment.party_type = pe.party_type - payment.party = pe.party - payment.paid_to = self.bank_account if pe.party_type == "Customer" else self.payable_account - payment.paid_from = self.receivable_account if pe.party_type == "Customer" else self.bank_account - payment.paid_amount = payment.received_amount = abs(pe.amount) - payment.reference_no = pe.description - payment.reference_date = pe.transaction_date - payment.save() - for inv_entry in self.payment_invoice_items: - if (pe.description != inv_entry.payment_description or pe.transaction_date != inv_entry.transaction_date): continue - if (pe.party != inv_entry.party): continue - reference = payment.append("references", {}) - reference.reference_doctype = inv_entry.invoice_type - reference.reference_name = inv_entry.invoice - reference.allocated_amount = inv_entry.allocated_amount - print ("Adding invoice {0} {1}".format(reference.reference_name, reference.allocated_amount)) - payment.setup_party_account_field() - payment.set_missing_values() - #payment.set_exchange_rate() - #payment.set_amounts() - #print("Created payment entry {0}".format(payment.as_dict())) - payment.save() - return payment - - def create_journal_entry(self, pe): - je = frappe.new_doc("Journal Entry") - je.is_opening = "No" - je.voucher_type = "Bank Entry" - je.cheque_no = pe.description - je.cheque_date = pe.transaction_date - je.remark = pe.description - je.posting_date = pe.transaction_date - if (pe.amount < 0): - je.append("accounts", {"account": pe.party, "debit_in_account_currency": abs(pe.amount)}) - je.append("accounts", {"account": self.bank_account, "credit_in_account_currency": abs(pe.amount)}) - else: - je.append("accounts", {"account": pe.party, "credit_in_account_currency": pe.amount}) - je.append("accounts", {"account": self.bank_account, "debit_in_account_currency": pe.amount}) - je.save() - return je - - def update_payment_entry(self, payment): - lst = [] - invoices = payment.invoices.strip().split(',') - if (len(invoices) == 0): return - amount = float(abs(payment.amount)) - for invoice_entry in invoices: - if (not invoice_entry.strip()): continue - invs = invoice_entry.split('|') - invoice_type, invoice = invs[0], invs[1] - outstanding_amount = frappe.get_value(invoice_type, invoice, 'outstanding_amount') - - lst.append(frappe._dict({ - 'voucher_type': payment.reference_type, - 'voucher_no' : payment.reference_name, - 'against_voucher_type' : invoice_type, - 'against_voucher' : invoice, - 'account' : payment.account, - 'party_type': payment.party_type, - 'party': frappe.get_value("Payment Entry", payment.reference_name, "party"), - 'unadjusted_amount' : float(amount), - 'allocated_amount' : min(outstanding_amount, amount) - })) - amount -= outstanding_amount - if lst: - from erpnext.accounts.utils import reconcile_against_document - try: - reconcile_against_document(lst) - except: - frappe.throw(_("Exception occurred while reconciling {0}").format(payment.reference_name)) - - def submit_payment_entries(self): - for payment in self.new_transaction_items: - if payment.reference_name is None: continue - doc = frappe.get_doc(payment.reference_type, payment.reference_name) - if doc.docstatus == 1: - if (payment.reference_type == "Journal Entry"): continue - if doc.unallocated_amount == 0: continue - print("Reconciling payment {0}".format(payment.reference_name)) - self.update_payment_entry(payment) - else: - print("Submitting payment {0}".format(payment.reference_name)) - if (payment.reference_type == "Payment Entry"): - if (payment.payment_reference): - doc.reference_no = payment.payment_reference - doc.mode_of_payment = payment.mode_of_payment - doc.save() - doc.submit() - self.move_reconciled_entries() - self.populate_matching_invoices() - - def move_reconciled_entries(self): - idx = 0 - while idx < len(self.new_transaction_items): - entry = self.new_transaction_items[idx] - try: - print("Checking transaction {0}: {2} in {1} entries".format(idx, len(self.new_transaction_items), frappe.safe_decode(entry.description))) - except UnicodeEncodeError: - pass - idx += 1 - if entry.reference_name is None: continue - doc = frappe.get_doc(entry.reference_type, entry.reference_name) - if doc.docstatus == 1 and (entry.reference_type == "Journal Entry" or doc.unallocated_amount == 0): - self.remove(entry) - rc_entry = self.append('reconciled_transaction_items', {}) - dentry = entry.as_dict() - dentry.pop('idx', None) - rc_entry.update(dentry) - idx -= 1 - - -def get_matching_journal_entries(from_date, to_date, account, against, amount): - query = """select voucher_no, posting_date, account, against, debit_in_account_currency as debit, credit_in_account_currency as credit - from `tabGL Entry` - where posting_date between '{0}' and '{1}' and account = '{2}' and against = '{3}' and debit = '{4}' - """.format(from_date, to_date, account, against, amount) - jv_entries = frappe.db.sql(query, as_dict=True) - #print("voucher query:{0}\n Returned {1} entries".format(query, len(jv_entries))) - return jv_entries - -def get_payments_matching_invoice(invoice, amount, pay_date): - query = """select pe.name as reference_name, per.reference_doctype as reference_type, per.outstanding_amount, per.allocated_amount - from `tabPayment Entry Reference` as per JOIN `tabPayment Entry` as pe on pe.name = per.parent - where per.reference_name='{0}' and (posting_date='{1}' or reference_date='{1}') and pe.docstatus != 2 - """.format(invoice, pay_date) - payments = frappe.db.sql(query, as_dict=True) - if (len(payments) == 0): return - payment = next((payment for payment in payments if payment.allocated_amount == amount), payments[0]) - #Hack: Update the reference type which is set to invoice type - payment.reference_type = "Payment Entry" - return payment - -def is_headers_present(headers, row): - for header in headers: - if header not in row: - return False - return True - -def get_header_index(headers, row): - header_index = {} - for header in headers: - if header in row: - header_index[header] = row.index(header) - return header_index - -def get_transaction_info(headers, header_index, row): - transaction = {} - for header in headers: - transaction[header] = row[header_index[header]] - if (transaction[header] == None): - transaction[header] = "" - return transaction - -def get_transaction_entries(file_url, headers): - header_index = {} - rows, transactions = [], [] - - if (file_url.lower().endswith("xlsx")): - from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file - rows = read_xlsx_file_from_attached_file(file_url=file_url) - elif (file_url.lower().endswith("csv")): - from frappe.utils.csvutils import read_csv_content - _file = frappe.get_doc("File", {"file_url": file_url}) - filepath = _file.get_full_path() - with open(filepath,'rb') as csvfile: - rows = read_csv_content(csvfile.read()) - elif (file_url.lower().endswith("xls")): - filename = file_url.split("/")[-1] - rows = get_rows_from_xls_file(filename) - else: - frappe.throw(_("Only .csv and .xlsx files are supported currently")) - - stmt_headers = headers.values() - for row in rows: - if len(row) == 0 or row[0] == None or not row[0]: continue - #print("Processing row {0}".format(row)) - if header_index: - transaction = get_transaction_info(stmt_headers, header_index, row) - transactions.append(transaction) - elif is_headers_present(stmt_headers, row): - header_index = get_header_index(stmt_headers, row) - return transactions - -def get_rows_from_xls_file(filename): - _file = frappe.get_doc("File", {"file_name": filename}) - filepath = _file.get_full_path() - import xlrd - book = xlrd.open_workbook(filepath) - sheets = book.sheets() - rows = [] - for row in range(1, sheets[0].nrows): - row_values = [] - for col in range(1, sheets[0].ncols): - row_values.append(sheets[0].cell_value(row, col)) - rows.append(row_values) - return rows diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/test_bank_statement_transaction_entry.js b/erpnext/accounts/doctype/bank_statement_transaction_entry/test_bank_statement_transaction_entry.js deleted file mode 100644 index 46d570f515..0000000000 --- a/erpnext/accounts/doctype/bank_statement_transaction_entry/test_bank_statement_transaction_entry.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Bank Statement Transaction Entry", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Bank Statement Transaction Entry - () => frappe.tests.make('Bank Statement Transaction Entry', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/test_bank_statement_transaction_entry.py b/erpnext/accounts/doctype/bank_statement_transaction_entry/test_bank_statement_transaction_entry.py deleted file mode 100644 index 458948372f..0000000000 --- a/erpnext/accounts/doctype/bank_statement_transaction_entry/test_bank_statement_transaction_entry.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2017, sathishpy@gmail.com and Contributors -# See license.txt -from __future__ import unicode_literals - -import frappe -import unittest - -class TestBankStatementTransactionEntry(unittest.TestCase): - pass diff --git a/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.json b/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.json deleted file mode 100644 index d96c94d8ca..0000000000 --- a/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.json +++ /dev/null @@ -1,365 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-11-07 13:58:53.827058", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "transaction_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Transaction Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 4, - "fieldname": "payment_description", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Payment Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "party_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Party Type", - "length": 0, - "no_copy": 0, - "options": "Customer\nSupplier\nAccount", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "party", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Party", - "length": 0, - "no_copy": 0, - "options": "party_type", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_4", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "invoice_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Invoice Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "invoice_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Invoice Type", - "length": 0, - "no_copy": 0, - "options": "Sales Invoice\nPurchase Invoice\nJournal Entry", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "invoice", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "invoice", - "length": 0, - "no_copy": 0, - "options": "invoice_type", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "outstanding_amount", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Outstanding Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "allocated_amount", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Allocated Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-09-14 19:03:30.949831", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Bank Statement Transaction Invoice Item", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} \ No newline at end of file diff --git a/erpnext/accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_item.json b/erpnext/accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_item.json deleted file mode 100644 index 177dccd82c..0000000000 --- a/erpnext/accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_item.json +++ /dev/null @@ -1,494 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-11-07 14:03:05.651413", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "transaction_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Transaction Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 4, - "fieldname": "description", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "party_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Party Type", - "length": 0, - "no_copy": 0, - "options": "Customer\nSupplier\nAccount", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "party", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Party", - "length": 0, - "no_copy": 0, - "options": "party_type", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_6", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reference_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Reference Type", - "length": 0, - "no_copy": 0, - "options": "Payment Entry\nJournal Entry", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "account", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Account", - "length": 0, - "no_copy": 0, - "options": "Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "mode_of_payment", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Mode of Payment", - "length": 0, - "no_copy": 0, - "options": "Mode of Payment", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "outstanding_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "outstanding_amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_10", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "reference_name", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Reference Name", - "length": 0, - "no_copy": 0, - "options": "reference_type", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "payment_reference", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Payment Reference", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "invoices", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Invoices", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-11-15 19:18:52.876221", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Bank Statement Transaction Payment Item", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.js b/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.js deleted file mode 100644 index 46aa4f2031..0000000000 --- a/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2017, sathishpy@gmail.com and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Bank Statement Settings', { - refresh: function(frm) { - - } -}); diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.json b/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.json deleted file mode 100644 index 474bb90db7..0000000000 --- a/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.json +++ /dev/null @@ -1,266 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 1, - "beta": 0, - "creation": "2017-11-13 13:38:10.863592", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "bank_account", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Bank Account", - "length": 0, - "no_copy": 0, - "options": "Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "'%d/%m/%Y'", - "fieldname": "date_format", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Date Format", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "statement_header_mapping", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Statement Header Mapping", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "header_items", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Statement Headers", - "length": 0, - "no_copy": 0, - "options": "Bank Statement Settings Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "transaction_data_mapping", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Transaction Data Mapping", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "mapped_items", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Mapped Items", - "length": 0, - "no_copy": 0, - "options": "Bank Statement Transaction Settings Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-01-12 10:34:32.840487", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Bank Statement Settings", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.py b/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.py deleted file mode 100644 index de9a85fe5c..0000000000 --- a/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2017, sathishpy@gmail.com and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe.model.document import Document - -class BankStatementSettings(Document): - def autoname(self): - self.name = self.bank_account + "-Mappings" diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings/test_bank_statement_transaction_settings.js b/erpnext/accounts/doctype/bank_statement_transaction_settings/test_bank_statement_transaction_settings.js deleted file mode 100644 index f2381c042e..0000000000 --- a/erpnext/accounts/doctype/bank_statement_transaction_settings/test_bank_statement_transaction_settings.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Bank Statement Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Bank Statement Settings - () => frappe.tests.make('Bank Statement Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings/test_bank_statement_transaction_settings.py b/erpnext/accounts/doctype/bank_statement_transaction_settings/test_bank_statement_transaction_settings.py deleted file mode 100644 index aa7fe83328..0000000000 --- a/erpnext/accounts/doctype/bank_statement_transaction_settings/test_bank_statement_transaction_settings.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2017, sathishpy@gmail.com and Contributors -# See license.txt -from __future__ import unicode_literals - -import frappe -import unittest - -class TestBankStatementSettings(unittest.TestCase): - pass diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings_item/__init__.py b/erpnext/accounts/doctype/bank_statement_transaction_settings_item/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.json b/erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.json deleted file mode 100644 index 47c32097a9..0000000000 --- a/erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.json +++ /dev/null @@ -1,166 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-11-13 13:42:00.335432", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Transaction", - "fieldname": "mapping_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Mapping Type", - "length": 0, - "no_copy": 0, - "options": "Transaction", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "bank_data", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Bank Data", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Account", - "fieldname": "mapped_data_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Mapped Data Type", - "length": 0, - "no_copy": 0, - "options": "Account\nCustomer\nSupplier\nAccount", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "mapped_data", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Mapped Data", - "length": 0, - "no_copy": 0, - "options": "mapped_data_type", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-01-08 00:13:49.973501", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Bank Statement Transaction Settings Item", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.py b/erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.py deleted file mode 100644 index bf0a590d48..0000000000 --- a/erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2017, sathishpy@gmail.com and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe.model.document import Document - -class BankStatementTransactionSettingsItem(Document): - pass diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.js b/erpnext/accounts/doctype/bank_transaction/bank_transaction.js index 8b1bab1618..3758b524da 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.js +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.js @@ -1,32 +1,70 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Bank Transaction', { +frappe.ui.form.on("Bank Transaction", { onload(frm) { - frm.set_query('payment_document', 'payment_entries', function() { + frm.set_query("payment_document", "payment_entries", function () { return { - "filters": { - "name": ["in", ["Payment Entry", "Journal Entry", "Sales Invoice", "Purchase Invoice", "Expense Claim"]] - } + filters: { + name: [ + "in", + [ + "Payment Entry", + "Journal Entry", + "Sales Invoice", + "Purchase Invoice", + "Expense Claim", + ], + ], + }, }; }); - } + }, + bank_account: function (frm) { + set_bank_statement_filter(frm); + }, + + setup: function (frm) { + frm.set_query("party_type", function () { + return { + filters: { + name: ["in", Object.keys(frappe.boot.party_account_types)], + }, + }; + }); + }, }); -frappe.ui.form.on('Bank Transaction Payments', { - payment_entries_remove: function(frm, cdt, cdn) { +frappe.ui.form.on("Bank Transaction Payments", { + payment_entries_remove: function (frm, cdt, cdn) { update_clearance_date(frm, cdt, cdn); - } + }, }); const update_clearance_date = (frm, cdt, cdn) => { if (frm.doc.docstatus === 1) { - frappe.xcall('erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment', - {doctype: cdt, docname: cdn}) - .then(e => { + frappe + .xcall( + "erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment", + { doctype: cdt, docname: cdn } + ) + .then((e) => { if (e == "success") { - frappe.show_alert({message:__("Document {0} successfully uncleared", [e]), indicator:'green'}); + frappe.show_alert({ + message: __("Document {0} successfully uncleared", [e]), + indicator: "green", + }); } }); } -}; \ No newline at end of file +}; + +function set_bank_statement_filter(frm) { + frm.set_query("bank_statement", function () { + return { + filters: { + bank_account: frm.doc.bank_account, + }, + }; + }); +} diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json index 39937bb364..69ee4971cd 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json @@ -1,833 +1,245 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, + "actions": [], "allow_import": 1, - "allow_rename": 0, "autoname": "naming_series:", - "beta": 0, "creation": "2018-10-22 18:19:02.784533", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "naming_series", + "date", + "column_break_2", + "status", + "bank_account", + "company", + "section_break_4", + "deposit", + "withdrawal", + "column_break_7", + "currency", + "section_break_10", + "description", + "section_break_14", + "reference_number", + "transaction_id", + "payment_entries", + "section_break_18", + "allocated_amount", + "amended_from", + "column_break_17", + "unallocated_amount", + "party_section", + "party_type", + "party" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "ACC-BTN-.YYYY.-", - "fetch_if_empty": 0, "fieldname": "naming_series", "fieldtype": "Select", "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Series", - "length": 0, "no_copy": 1, "options": "ACC-BTN-.YYYY.-", - "permlevel": 0, - "precision": "", "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 0, - "set_only_once": 1, - "translatable": 0, - "unique": 0 + "set_only_once": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "date", "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Date" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Pending", - "fetch_if_empty": 0, "fieldname": "status", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, "in_standard_filter": 1, "label": "Status", - "length": 0, - "no_copy": 0, - "options": "\nPending\nSettled\nUnreconciled\nReconciled", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "\nPending\nSettled\nUnreconciled\nReconciled" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "bank_account", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, "in_standard_filter": 1, "label": "Bank Account", - "length": 0, - "no_copy": 0, - "options": "Bank Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Bank Account" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", "fetch_from": "bank_account.company", - "fetch_if_empty": 0, "fieldname": "company", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, "in_standard_filter": 1, "label": "Company", - "length": 0, - "no_copy": 0, "options": "Company", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "section_break_4", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "debit", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Debit", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "credit", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Credit", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "column_break_7", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "currency", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Currency", - "length": 0, - "no_copy": 0, - "options": "Currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Currency" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "section_break_10", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "description", "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Description" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "section_break_14", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "allow_on_submit": 1, "fieldname": "reference_number", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Reference Number", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Reference Number" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "transaction_id", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Transaction ID", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, "unique": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "payment_entries", "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Payment Entries", - "length": 0, - "no_copy": 0, - "options": "Bank Transaction Payments", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Bank Transaction Payments" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "section_break_18", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "allocated_amount", "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Allocated Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Allocated Amount" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "amended_from", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Amended From", - "length": 0, "no_copy": 1, "options": "Bank Transaction", - "permlevel": 0, "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "column_break_17", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, "fieldname": "unallocated_amount", "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Unallocated Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Unallocated Amount" + }, + { + "fieldname": "party_section", + "fieldtype": "Section Break", + "label": "Payment From / To" + }, + { + "allow_on_submit": 1, + "fieldname": "party_type", + "fieldtype": "Link", + "label": "Party Type", + "options": "DocType" + }, + { + "allow_on_submit": 1, + "fieldname": "party", + "fieldtype": "Dynamic Link", + "label": "Party", + "options": "party_type" + }, + { + "fieldname": "deposit", + "oldfieldname": "debit", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Deposit" + }, + { + "fieldname": "withdrawal", + "oldfieldname": "credit", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Withdrawal" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-05-11 05:27:55.244721", + "links": [], + "modified": "2020-12-30 19:40:54.221070", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Transaction", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, "cancel": 1, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, "submit": 1, "write": 1 }, { - "amend": 0, "cancel": 1, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Accounts Manager", - "set_user_permissions": 0, "share": 1, "submit": 1, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Accounts User", - "set_user_permissions": 0, "share": 1, "submit": 1, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "date", "sort_order": "DESC", "title_field": "bank_account", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 0e45db3dbc..5246baa02b 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -11,7 +11,7 @@ from frappe import _ class BankTransaction(StatusUpdater): def after_insert(self): - self.unallocated_amount = abs(flt(self.credit) - flt(self.debit)) + self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit)) def on_submit(self): self.clear_linked_payment_entries() @@ -30,13 +30,13 @@ class BankTransaction(StatusUpdater): if allocated_amount: frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount)) - frappe.db.set_value(self.doctype, self.name, "unallocated_amount", abs(flt(self.credit) - flt(self.debit)) - flt(allocated_amount)) + frappe.db.set_value(self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount)) else: frappe.db.set_value(self.doctype, self.name, "allocated_amount", 0) - frappe.db.set_value(self.doctype, self.name, "unallocated_amount", abs(flt(self.credit) - flt(self.debit))) + frappe.db.set_value(self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit))) - amount = self.debit or self.credit + amount = self.deposit or self.withdrawal if amount == self.allocated_amount: frappe.db.set_value(self.doctype, self.name, "status", "Reconciled") @@ -44,18 +44,11 @@ class BankTransaction(StatusUpdater): def clear_linked_payment_entries(self): for payment_entry in self.payment_entries: - allocated_amount = get_total_allocated_amount(payment_entry) - paid_amount = get_paid_amount(payment_entry, self.currency) + if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]: + self.clear_simple_entry(payment_entry) - if paid_amount and allocated_amount: - if flt(allocated_amount[0]["allocated_amount"]) > flt(paid_amount): - frappe.throw(_("The total allocated amount ({0}) is greated than the paid amount ({1}).").format(flt(allocated_amount[0]["allocated_amount"]), flt(paid_amount))) - else: - if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]: - self.clear_simple_entry(payment_entry) - - elif payment_entry.payment_document == "Sales Invoice": - self.clear_sales_invoice(payment_entry) + elif payment_entry.payment_document == "Sales Invoice": + self.clear_sales_invoice(payment_entry) def clear_simple_entry(self, payment_entry): frappe.db.set_value(payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", self.date) @@ -112,3 +105,4 @@ def unclear_reference_payment(doctype, docname): frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None) return doc.payment_entry + diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index e9fc5f0a1d..3b14e4efa0 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -5,10 +5,11 @@ from __future__ import unicode_literals import frappe import unittest +import json from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry -from erpnext.accounts.page.bank_reconciliation.bank_reconciliation import reconcile, get_linked_payments +from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import reconcile_vouchers, get_linked_payments from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile test_dependencies = ["Item", "Cost Center"] @@ -17,7 +18,7 @@ class TestBankTransaction(unittest.TestCase): def setUp(self): make_pos_profile() add_transactions() - add_payments() + add_vouchers() def tearDown(self): for bt in frappe.get_all("Bank Transaction"): @@ -38,14 +39,18 @@ class TestBankTransaction(unittest.TestCase): # This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction. def test_linked_payments(self): bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic")) - linked_payments = get_linked_payments(bank_transaction.name) - self.assertTrue(linked_payments[0].party == "Conrad Electronic") + linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match']) + self.assertTrue(linked_payments[0][6] == "Conrad Electronic") # This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment def test_reconcile(self): bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G")) payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200)) - reconcile(bank_transaction.name, "Payment Entry", payment.name) + vouchers = json.dumps([{ + "payment_doctype":"Payment Entry", + "payment_name":payment.name, + "amount":bank_transaction.unallocated_amount}]) + reconcile_vouchers(bank_transaction.name, vouchers) unallocated_amount = frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount") self.assertTrue(unallocated_amount == 0) @@ -53,45 +58,40 @@ class TestBankTransaction(unittest.TestCase): clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date") self.assertTrue(clearance_date is not None) - # Check if ERPNext can correctly fetch a linked payment based on the party - def test_linked_payments_based_on_party(self): - bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G")) - linked_payments = get_linked_payments(bank_transaction.name) - self.assertTrue(len(linked_payments)==1) - # Check if ERPNext can correctly filter a linked payments based on the debit/credit amount def test_debit_credit_output(self): bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07")) - linked_payments = get_linked_payments(bank_transaction.name) - self.assertTrue(linked_payments[0].payment_type == "Pay") + linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match']) + print(linked_payments) + self.assertTrue(linked_payments[0][3]) # Check error if already reconciled def test_already_reconciled(self): bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G")) payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200)) - reconcile(bank_transaction.name, "Payment Entry", payment.name) + vouchers = json.dumps([{ + "payment_doctype":"Payment Entry", + "payment_name":payment.name, + "amount":bank_transaction.unallocated_amount}]) + reconcile_vouchers(bank_transaction.name, vouchers) bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G")) payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200)) - self.assertRaises(frappe.ValidationError, reconcile, bank_transaction=bank_transaction.name, payment_doctype="Payment Entry", payment_name=payment.name) - - # Raise an error if creditor transaction vs creditor payment - def test_invalid_creditor_reconcilation(self): - bank_transaction = frappe.get_doc("Bank Transaction", dict(description="I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio")) - payment = frappe.get_doc("Payment Entry", dict(party="Conrad Electronic", paid_amount=690)) - self.assertRaises(frappe.ValidationError, reconcile, bank_transaction=bank_transaction.name, payment_doctype="Payment Entry", payment_name=payment.name) - - # Raise an error if debitor transaction vs debitor payment - def test_invalid_debitor_reconcilation(self): - bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07")) - payment = frappe.get_doc("Payment Entry", dict(party="Fayva", paid_amount=109080)) - self.assertRaises(frappe.ValidationError, reconcile, bank_transaction=bank_transaction.name, payment_doctype="Payment Entry", payment_name=payment.name) + vouchers = json.dumps([{ + "payment_doctype":"Payment Entry", + "payment_name":payment.name, + "amount":bank_transaction.unallocated_amount}]) + self.assertRaises(frappe.ValidationError, reconcile_vouchers, bank_transaction_name=bank_transaction.name, vouchers=vouchers) # Raise an error if debitor transaction vs debitor payment def test_clear_sales_invoice(self): bank_transaction = frappe.get_doc("Bank Transaction", dict(description="I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio")) payment = frappe.get_doc("Sales Invoice", dict(customer="Fayva", status=["=", "Paid"])) - reconcile(bank_transaction.name, "Sales Invoice", payment.name) + vouchers = json.dumps([{ + "payment_doctype":"Sales Invoice", + "payment_name":payment.name, + "amount":bank_transaction.unallocated_amount}]) + reconcile_vouchers(bank_transaction.name, vouchers=vouchers) self.assertEqual(frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0) self.assertTrue(frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date") is not None) @@ -126,7 +126,7 @@ def add_transactions(): "doctype": "Bank Transaction", "description":"1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G", "date": "2018-10-23", - "debit": 1200, + "deposit": 1200, "currency": "INR", "bank_account": "Checking Account - Citi Bank" }).insert() @@ -136,7 +136,7 @@ def add_transactions(): "doctype": "Bank Transaction", "description":"1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G", "date": "2018-10-23", - "debit": 1700, + "deposit": 1700, "currency": "INR", "bank_account": "Checking Account - Citi Bank" }).insert() @@ -146,7 +146,7 @@ def add_transactions(): "doctype": "Bank Transaction", "description":"Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic", "date": "2018-10-26", - "debit": 690, + "withdrawal": 690, "currency": "INR", "bank_account": "Checking Account - Citi Bank" }).insert() @@ -156,7 +156,7 @@ def add_transactions(): "doctype": "Bank Transaction", "description":"Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07", "date": "2018-10-27", - "debit": 3900, + "deposit": 3900, "currency": "INR", "bank_account": "Checking Account - Citi Bank" }).insert() @@ -166,7 +166,7 @@ def add_transactions(): "doctype": "Bank Transaction", "description":"I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio", "date": "2018-10-27", - "credit": 109080, + "withdrawal": 109080, "currency": "INR", "bank_account": "Checking Account - Citi Bank" }).insert() @@ -174,7 +174,7 @@ def add_transactions(): frappe.flags.test_bank_transactions_created = True -def add_payments(): +def add_vouchers(): if frappe.flags.test_payments_created: return @@ -192,6 +192,7 @@ def add_payments(): pass pi = make_purchase_invoice(supplier="Conrad Electronic", qty=1, rate=690) + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") pe.reference_no = "Conrad Oct 18" pe.reference_date = "2018-10-24" @@ -242,10 +243,15 @@ def add_payments(): except frappe.DuplicateEntryError: pass - pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900) + pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save =1) + pi.cash_bank_account = "_Test Bank - _TC" + pi.insert() + pi.submit() pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") pe.reference_no = "Poore Simon's Oct 18" pe.reference_date = "2018-10-28" + pe.paid_amount = 690 + pe.received_amount = 690 pe.insert() pe.submit() @@ -295,4 +301,4 @@ def add_payments(): si.save() si.submit() - frappe.flags.test_payments_created = True \ No newline at end of file + frappe.flags.test_payments_created = True diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index b0a864f76c..ce76d0a39c 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -27,30 +27,30 @@ class GLEntry(Document): def validate(self): self.flags.ignore_submit_comment = True - self.check_mandatory() self.validate_and_set_fiscal_year() self.pl_must_have_cost_center() - self.validate_cost_center() if not self.flags.from_repost: + self.check_mandatory() + self.validate_cost_center() self.check_pl_account() self.validate_party() self.validate_currency() - def on_update_with_args(self, adv_adj, update_outstanding = 'Yes', from_repost=False): - if not from_repost: + def on_update(self): + adv_adj = self.flags.adv_adj + if not self.flags.from_repost: self.validate_account_details(adv_adj) self.validate_dimensions_for_pl_and_bs() self.validate_allowed_dimensions() + validate_balance_type(self.account, adv_adj) + validate_frozen_account(self.account, adv_adj) - validate_frozen_account(self.account, adv_adj) - validate_balance_type(self.account, adv_adj) - - # Update outstanding amt on against voucher - if self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] \ - and self.against_voucher and update_outstanding == 'Yes' and not from_repost: - update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type, - self.against_voucher) + # Update outstanding amt on against voucher + if (self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] + and self.against_voucher and self.flags.update_outstanding == 'Yes'): + update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type, + self.against_voucher) def check_mandatory(self): mandatory = ['account','voucher_type','voucher_no','company'] @@ -58,7 +58,7 @@ class GLEntry(Document): if not self.get(k): frappe.throw(_("{0} is required").format(_(self.meta.get_label(k)))) - account_type = frappe.db.get_value("Account", self.account, "account_type") + account_type = frappe.get_cached_value("Account", self.account, "account_type") if not (self.party_type and self.party): if account_type == "Receivable": frappe.throw(_("{0} {1}: Customer is required against Receivable account {2}") @@ -73,7 +73,7 @@ class GLEntry(Document): .format(self.voucher_type, self.voucher_no, self.account)) def pl_must_have_cost_center(self): - if frappe.db.get_value("Account", self.account, "report_type") == "Profit and Loss": + if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss": if not self.cost_center and self.voucher_type != 'Period Closing Voucher': frappe.throw(_("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}. Please set up a default Cost Center for the Company.") .format(self.voucher_type, self.voucher_no, self.account)) @@ -140,25 +140,16 @@ class GLEntry(Document): .format(self.voucher_type, self.voucher_no, self.account, self.company)) def validate_cost_center(self): - if not hasattr(self, "cost_center_company"): - self.cost_center_company = {} + if not self.cost_center: return - def _get_cost_center_company(): - if not self.cost_center_company.get(self.cost_center): - self.cost_center_company[self.cost_center] = frappe.db.get_value( - "Cost Center", self.cost_center, "company") + is_group, company = frappe.get_cached_value('Cost Center', + self.cost_center, ['is_group', 'company']) - return self.cost_center_company[self.cost_center] - - def _check_is_group(): - return cint(frappe.get_cached_value('Cost Center', self.cost_center, 'is_group')) - - if self.cost_center and _get_cost_center_company() != self.company: + if company != self.company: frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}") .format(self.voucher_type, self.voucher_no, self.cost_center, self.company)) - if not self.flags.from_repost and not self.voucher_type == 'Period Closing Voucher' \ - and self.cost_center and _check_is_group(): + if (self.voucher_type != 'Period Closing Voucher' and is_group): frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""").format( self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))) @@ -184,7 +175,6 @@ class GLEntry(Document): if not self.fiscal_year: self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0] - def validate_balance_type(account, adv_adj=False): if not adv_adj and account: balance_must_be = frappe.db.get_value("Account", account, "balance_must_be") @@ -250,7 +240,7 @@ def update_outstanding_amt(account, party_type, party, against_voucher_type, aga def validate_frozen_account(account, adv_adj=None): - frozen_account = frappe.db.get_value("Account", account, "freeze_account") + frozen_account = frappe.get_cached_value("Account", account, "freeze_account") if frozen_account == 'Yes' and not adv_adj: frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None, 'frozen_accounts_modifier') diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index cb90f8036e..3419bb6c3e 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -102,7 +102,7 @@ class JournalEntry(AccountsController): if account_currency == previous_account_currency: if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit: frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry")) - + def validate_stock_accounts(self): stock_accounts = get_stock_accounts(self.company, self.doctype, self.name) for account in stock_accounts: @@ -229,11 +229,11 @@ class JournalEntry(AccountsController): if d.reference_type=="Journal Entry": account_root_type = frappe.db.get_value("Account", d.account, "root_type") if account_root_type == "Asset" and flt(d.debit) > 0: - frappe.throw(_("For {0}, only credit accounts can be linked against another debit entry") - .format(d.account)) + frappe.throw(_("Row #{0}: For {1}, you can select reference document only if account gets credited") + .format(d.idx, d.account)) elif account_root_type == "Liability" and flt(d.credit) > 0: - frappe.throw(_("For {0}, only debit accounts can be linked against another credit entry") - .format(d.account)) + frappe.throw(_("Row #{0}: For {1}, you can select reference document only if account gets debited") + .format(d.idx, d.account)) if d.reference_name == self.name: frappe.throw(_("You can not enter current voucher in 'Against Journal Entry' column")) @@ -1077,4 +1077,4 @@ def make_reverse_journal_entry(source_name, target_doc=None): }, }, target_doc) - return doclist \ No newline at end of file + return doclist diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 1b97050eb1..53ac996290 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -3,6 +3,7 @@ # For license information, please see license.txt from __future__ import unicode_literals +import json import frappe from frappe import _ from frappe.model.document import Document @@ -82,18 +83,37 @@ class PaymentRequest(Document): self.make_communication_entry() elif self.payment_channel == "Phone": - controller = get_payment_gateway_controller(self.payment_gateway) - payment_record = dict( - reference_doctype="Payment Request", - reference_docname=self.name, - payment_reference=self.reference_name, - grand_total=self.grand_total, - sender=self.email_to, - currency=self.currency, - payment_gateway=self.payment_gateway - ) - controller.validate_transaction_currency(self.currency) - controller.request_for_payment(**payment_record) + self.request_phone_payment() + + def request_phone_payment(self): + controller = get_payment_gateway_controller(self.payment_gateway) + request_amount = self.get_request_amount() + + payment_record = dict( + reference_doctype="Payment Request", + reference_docname=self.name, + payment_reference=self.reference_name, + request_amount=request_amount, + sender=self.email_to, + currency=self.currency, + payment_gateway=self.payment_gateway + ) + + controller.validate_transaction_currency(self.currency) + controller.request_for_payment(**payment_record) + + def get_request_amount(self): + data_of_completed_requests = frappe.get_all("Integration Request", filters={ + 'reference_doctype': self.doctype, + 'reference_docname': self.name, + 'status': 'Completed' + }, pluck="data") + + if not data_of_completed_requests: + return self.grand_total + + request_amounts = sum([json.loads(d).get('request_amount') for d in data_of_completed_requests]) + return request_amounts def on_cancel(self): self.check_if_payment_entry_exists() @@ -351,8 +371,8 @@ def make_payment_request(**args): if args.order_type == "Shopping Cart" or args.mute_email: pr.flags.mute_email = True + pr.insert(ignore_permissions=True) if args.submit_doc: - pr.insert(ignore_permissions=True) pr.submit() if args.order_type == "Shopping Cart": @@ -412,8 +432,8 @@ def get_existing_payment_request_amount(ref_dt, ref_dn): def get_gateway_details(args): """return gateway and payment account of default payment gateway""" - if args.get("payment_gateway"): - return get_payment_gateway_account(args.get("payment_gateway")) + if args.get("payment_gateway_account"): + return get_payment_gateway_account(args.get("payment_gateway_account")) if args.order_type == "Shopping Cart": payment_gateway_account = frappe.get_doc("Shopping Cart Settings").payment_gateway_account diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 8a10e2cbd9..5eba62c0b3 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -45,7 +45,8 @@ class TestPaymentRequest(unittest.TestCase): def test_payment_request_linkings(self): so_inr = make_sales_order(currency="INR") - pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com") + pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com", + payment_gateway_account="_Test Gateway - INR") self.assertEqual(pr.reference_doctype, "Sales Order") self.assertEqual(pr.reference_name, so_inr.name) @@ -54,7 +55,8 @@ class TestPaymentRequest(unittest.TestCase): conversion_rate = get_exchange_rate("USD", "INR") si_usd = create_sales_invoice(currency="USD", conversion_rate=conversion_rate) - pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com") + pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com", + payment_gateway_account="_Test Gateway - USD") self.assertEqual(pr.reference_doctype, "Sales Invoice") self.assertEqual(pr.reference_name, si_usd.name) @@ -68,7 +70,7 @@ class TestPaymentRequest(unittest.TestCase): so_inr = make_sales_order(currency="INR") pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com", - mute_email=1, submit_doc=1, return_doc=1) + mute_email=1, payment_gateway_account="_Test Gateway - INR", submit_doc=1, return_doc=1) pe = pr.set_as_paid() so_inr = frappe.get_doc("Sales Order", so_inr.name) @@ -79,7 +81,7 @@ class TestPaymentRequest(unittest.TestCase): currency="USD", conversion_rate=50) pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com", - mute_email=1, payment_gateway="_Test Gateway - USD", submit_doc=1, return_doc=1) + mute_email=1, payment_gateway_account="_Test Gateway - USD", submit_doc=1, return_doc=1) pe = pr.set_as_paid() @@ -106,7 +108,7 @@ class TestPaymentRequest(unittest.TestCase): currency="USD", conversion_rate=50) pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com", - mute_email=1, payment_gateway="_Test Gateway - USD", submit_doc=1, return_doc=1) + mute_email=1, payment_gateway_account="_Test Gateway - USD", submit_doc=1, return_doc=1) pe = pr.create_payment_entry() pr.load_from_db() diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js index 73367fd736..9ea616f8e7 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js @@ -21,7 +21,7 @@ frappe.ui.form.on('POS Closing Entry', { return { filters: { 'status': 'Open', 'docstatus': 1 } }; }); - if (frm.doc.docstatus === 0) frm.set_value("period_end_date", frappe.datetime.now_datetime()); + if (frm.doc.docstatus === 0 && !frm.doc.amended_from) frm.set_value("period_end_date", frappe.datetime.now_datetime()); if (frm.doc.docstatus === 1) set_html_data(frm); }, diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js index 07c8e44e77..493bd44802 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js @@ -195,18 +195,43 @@ frappe.ui.form.on('POS Invoice', { }, request_for_payment: function (frm) { + if (!frm.doc.contact_mobile) { + frappe.throw(__('Please enter mobile number first.')); + } + frm.dirty(); frm.save().then(() => { - frappe.dom.freeze(); - frappe.call({ - method: 'create_payment_request', - doc: frm.doc, - }) + frappe.dom.freeze(__('Waiting for payment...')); + frappe + .call({ + method: 'create_payment_request', + doc: frm.doc + }) .fail(() => { frappe.dom.unfreeze(); - frappe.msgprint('Payment request failed'); + frappe.msgprint(__('Payment request failed')); }) - .then(() => { - frappe.msgprint('Payment request sent successfully'); + .then(({ message }) => { + const payment_request_name = message.name; + setTimeout(() => { + frappe.db.get_value('Payment Request', payment_request_name, ['status', 'grand_total']).then(({ message }) => { + if (message.status != 'Paid') { + frappe.dom.unfreeze(); + frappe.msgprint({ + message: __('Payment Request took too long to respond. Please try requesting for payment again.'), + title: __('Request Timeout') + }); + } else if (frappe.dom.freeze_count != 0) { + frappe.dom.unfreeze(); + cur_frm.reload_doc(); + cur_pos.payment.events.submit_invoice(); + + frappe.show_alert({ + message: __("Payment of {0} received successfully.", [format_currency(message.grand_total, frm.doc.currency, 0)]), + indicator: 'green' + }); + } + }); + }, 60000); }); }); } diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 8d8babbe75..76e00923c4 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -179,10 +179,18 @@ class POSInvoice(SalesInvoice): if d.get("serial_no"): serial_nos = get_serial_nos(d.serial_no) for sr in serial_nos: - serial_no_exists = frappe.db.exists("POS Invoice Item", { - "parent": self.return_against, - "serial_no": ["like", d.get("serial_no")] - }) + serial_no_exists = frappe.db.sql(""" + SELECT name + FROM `tabPOS Invoice Item` + WHERE + parent = %s + and (serial_no = %s + or serial_no like %s + or serial_no like %s + or serial_no like %s + ) + """, (self.return_against, sr, sr+'\n%', '%\n'+sr, '%\n'+sr+'\n%')) + if not serial_no_exists: bold_return_against = frappe.bold(self.return_against) bold_serial_no = frappe.bold(sr) @@ -190,7 +198,7 @@ class POSInvoice(SalesInvoice): _("Row #{}: Serial No {} cannot be returned since it was not transacted in original invoice {}") .format(d.idx, bold_serial_no, bold_return_against) ) - + def validate_non_stock_items(self): for d in self.get("items"): is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item") @@ -292,7 +300,7 @@ class POSInvoice(SalesInvoice): if not self.get('payments') and not for_validate: update_multi_mode_option(self, profile) - + if self.is_return and not for_validate: add_return_modes(self, profile) @@ -317,13 +325,14 @@ class POSInvoice(SalesInvoice): ) customer_group_price_list = frappe.db.get_value("Customer Group", customer_group, 'default_price_list') selling_price_list = customer_price_list or customer_group_price_list or profile.get('selling_price_list') + if customer_currency != profile.get('currency'): + self.set('currency', customer_currency) + else: selling_price_list = profile.get('selling_price_list') if selling_price_list: self.set('selling_price_list', selling_price_list) - if customer_currency != profile.get('currency'): - self.set('currency', customer_currency) # set pos values in items for item in self.get("items"): @@ -383,22 +392,48 @@ class POSInvoice(SalesInvoice): if not self.contact_mobile: frappe.throw(_("Please enter the phone number first")) - payment_gateway = frappe.db.get_value("Payment Gateway Account", { - "payment_account": pay.account, - }) - record = { - "payment_gateway": payment_gateway, - "dt": "POS Invoice", - "dn": self.name, - "payment_request_type": "Inward", - "party_type": "Customer", - "party": self.customer, - "mode_of_payment": pay.mode_of_payment, - "recipient_id": self.contact_mobile, - "submit_doc": True - } + pay_req = self.get_existing_payment_request(pay) + if not pay_req: + pay_req = self.get_new_payment_request(pay) + pay_req.submit() + else: + pay_req.request_phone_payment() - return make_payment_request(**record) + return pay_req + + def get_new_payment_request(self, mop): + payment_gateway_account = frappe.db.get_value("Payment Gateway Account", { + "payment_account": mop.account, + }, ["name"]) + + args = { + "dt": "POS Invoice", + "dn": self.name, + "recipient_id": self.contact_mobile, + "mode_of_payment": mop.mode_of_payment, + "payment_gateway_account": payment_gateway_account, + "payment_request_type": "Inward", + "party_type": "Customer", + "party": self.customer, + "return_doc": True + } + return make_payment_request(**args) + + def get_existing_payment_request(self, pay): + payment_gateway_account = frappe.db.get_value("Payment Gateway Account", { + "payment_account": pay.account, + }, ["name"]) + + args = { + 'doctype': 'Payment Request', + 'reference_doctype': 'POS Invoice', + 'reference_name': self.name, + 'payment_gateway_account': payment_gateway_account, + 'email_to': self.contact_mobile + } + pr = frappe.db.exists(args) + if pr: + return frappe.get_doc('Payment Request', pr[0][0]) @frappe.whitelist() def get_stock_availability(item_code, warehouse): diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 57a23af8af..15875afe87 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -198,6 +198,65 @@ class TestPOSInvoice(unittest.TestCase): self.assertEqual(pos_return.get('payments')[0].amount, -500) self.assertEqual(pos_return.get('payments')[1].amount, -500) + def test_pos_return_for_serialized_item(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + se = make_serialized_item(company='_Test Company', + target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC') + + serial_nos = get_serial_nos(se.get("items")[0].serial_no) + + pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC', + account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC', + expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC', + item=se.get("items")[0].item_code, rate=1000, do_not_save=1) + + pos.get("items")[0].serial_no = serial_nos[0] + pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000, 'default': 1}) + + pos.insert() + pos.submit() + + pos_return = make_sales_return(pos.name) + + pos_return.insert() + pos_return.submit() + self.assertEqual(pos_return.get('items')[0].serial_no, serial_nos[0]) + + def test_partial_pos_returns(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + se = make_serialized_item(company='_Test Company', + target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC') + + serial_nos = get_serial_nos(se.get("items")[0].serial_no) + + pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC', + account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC', + expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC', + item=se.get("items")[0].item_code, qty=2, rate=1000, do_not_save=1) + + pos.get("items")[0].serial_no = serial_nos[0] + "\n" + serial_nos[1] + pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000, 'default': 1}) + + pos.insert() + pos.submit() + + pos_return1 = make_sales_return(pos.name) + + # partial return 1 + pos_return1.get('items')[0].qty = -1 + pos_return1.get('items')[0].serial_no = serial_nos[0] + pos_return1.insert() + pos_return1.submit() + + # partial return 2 + pos_return2 = make_sales_return(pos.name) + self.assertEqual(pos_return2.get('items')[0].qty, -1) + self.assertEqual(pos_return2.get('items')[0].serial_no, serial_nos[1]) + def test_pos_change_amount(self): pos = create_pos_invoice(company= "_Test Company", debit_to="Debtors - _TC", income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105, diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json index 2b6e7de118..8b71eb02fd 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json @@ -87,6 +87,7 @@ "edit_references", "sales_order", "so_detail", + "pos_invoice_item", "column_break_74", "delivery_note", "dn_detail", @@ -790,11 +791,20 @@ "fieldtype": "Link", "label": "Project", "options": "Project" + }, + { + "fieldname": "pos_invoice_item", + "fieldtype": "Data", + "ignore_user_permissions": 1, + "label": "POS Invoice Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2020-07-22 13:40:34.418346", + "modified": "2021-01-04 17:34:49.924531", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Item", diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index c88d67989b..40f77b4088 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -29,7 +29,7 @@ class POSInvoiceMergeLog(Document): for d in self.pos_invoices: status, docstatus, is_return, return_against = frappe.db.get_value( 'POS Invoice', d.pos_invoice, ['status', 'docstatus', 'is_return', 'return_against']) - + bold_pos_invoice = frappe.bold(d.pos_invoice) bold_status = frappe.bold(status) if docstatus != 1: @@ -58,7 +58,7 @@ class POSInvoiceMergeLog(Document): sales_invoice, credit_note = "", "" if sales: sales_invoice = self.process_merging_into_sales_invoice(sales) - + if returns: credit_note = self.process_merging_into_credit_note(returns) @@ -74,7 +74,7 @@ class POSInvoiceMergeLog(Document): def process_merging_into_sales_invoice(self, data): sales_invoice = self.get_new_sales_invoice() - + sales_invoice = self.merge_pos_invoice_into(sales_invoice, data) sales_invoice.is_consolidated = 1 @@ -98,19 +98,19 @@ class POSInvoiceMergeLog(Document): self.consolidated_credit_note = credit_note.name return credit_note.name - + def merge_pos_invoice_into(self, invoice, data): items, payments, taxes = [], [], [] loyalty_amount_sum, loyalty_points_sum = 0, 0 for doc in data: map_doc(doc, invoice, table_map={ "doctype": invoice.doctype }) - + if doc.redeem_loyalty_points: invoice.loyalty_redemption_account = doc.loyalty_redemption_account invoice.loyalty_redemption_cost_center = doc.loyalty_redemption_cost_center loyalty_points_sum += doc.loyalty_points loyalty_amount_sum += doc.loyalty_amount - + for item in doc.get('items'): found = False for i in items: @@ -118,12 +118,13 @@ class POSInvoiceMergeLog(Document): i.uom == item.uom and i.net_rate == item.net_rate): found = True i.qty = i.qty + item.qty + if not found: item.rate = item.net_rate item.price_list_rate = 0 si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"}) items.append(si_item) - + for tax in doc.get('taxes'): found = False for t in taxes: @@ -162,7 +163,7 @@ class POSInvoiceMergeLog(Document): invoice.ignore_pricing_rule = 1 return invoice - + def get_new_sales_invoice(self): sales_invoice = frappe.new_doc('Sales Invoice') sales_invoice.customer = self.customer @@ -194,7 +195,7 @@ def get_all_unconsolidated_invoices(): } pos_invoices = frappe.db.get_all('POS Invoice', filters=filters, fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer']) - + return pos_invoices def get_invoice_customer_map(pos_invoices): @@ -204,7 +205,7 @@ def get_invoice_customer_map(pos_invoices): customer = invoice.get('customer') pos_invoice_customer_map.setdefault(customer, []) pos_invoice_customer_map[customer].append(invoice) - + return pos_invoice_customer_map def consolidate_pos_invoices(pos_invoices=[], closing_entry={}): diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 1f7853dbf7..07e75acb41 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -40,6 +40,7 @@ "base_rate", "base_amount", "pricing_rules", + "stock_uom_rate", "is_free_item", "section_break_22", "net_rate", @@ -783,6 +784,14 @@ "print_hide": 1, "read_only": 1 }, + { + "depends_on": "eval: doc.uom != doc.stock_uom", + "fieldname": "stock_uom_rate", + "fieldtype": "Currency", + "label": "Rate of Stock UOM", + "options": "currency", + "read_only": 1 + }, { "fieldname": "sales_invoice_item", "fieldtype": "Data", @@ -795,7 +804,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-12-26 17:20:36.415791", + "modified": "2021-01-30 21:43:21.488258", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 7a98afff36..b403c7b237 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -45,6 +45,7 @@ "base_rate", "base_amount", "pricing_rules", + "stock_uom_rate", "is_free_item", "section_break_21", "net_rate", @@ -811,12 +812,20 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "depends_on": "eval: doc.uom != doc.stock_uom", + "fieldname": "stock_uom_rate", + "fieldtype": "Currency", + "label": "Rate of Stock UOM", + "options": "currency", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-12-26 17:25:04.090630", + "modified": "2021-01-30 21:42:37.796771", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 287c79f13f..b42c0c61d9 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -44,9 +44,9 @@ def validate_accounting_period(gl_map): frappe.throw(_("You cannot create or cancel any accounting entries with in the closed Accounting Period {0}") .format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod) -def process_gl_map(gl_map, merge_entries=True): +def process_gl_map(gl_map, merge_entries=True, precision=None): if merge_entries: - gl_map = merge_similar_entries(gl_map) + gl_map = merge_similar_entries(gl_map, precision) for entry in gl_map: # toggle debit, credit if negative entry if flt(entry.debit) < 0: @@ -69,7 +69,7 @@ def process_gl_map(gl_map, merge_entries=True): return gl_map -def merge_similar_entries(gl_map): +def merge_similar_entries(gl_map, precision=None): merged_gl_map = [] accounting_dimensions = get_accounting_dimensions() for entry in gl_map: @@ -88,7 +88,9 @@ def merge_similar_entries(gl_map): company = gl_map[0].company if gl_map else erpnext.get_default_company() company_currency = erpnext.get_company_currency(company) - precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency) + + if not precision: + precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency) # filter zero debit and credit entries merged_gl_map = filter(lambda x: flt(x.debit, precision)!=0 or flt(x.credit, precision)!=0, merged_gl_map) @@ -132,8 +134,8 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False): gle.update(args) gle.flags.ignore_permissions = 1 gle.flags.from_repost = from_repost - gle.insert() - gle.run_method("on_update_with_args", adv_adj, update_outstanding, from_repost) + gle.flags.adv_adj = adv_adj + gle.flags.update_outstanding = update_outstanding or 'Yes' gle.submit() if not from_repost: diff --git a/erpnext/accounts/page/bank_reconciliation/__init__.py b/erpnext/accounts/page/bank_reconciliation/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js deleted file mode 100644 index 6ae81d7402..0000000000 --- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js +++ /dev/null @@ -1,583 +0,0 @@ -frappe.provide("erpnext.accounts"); - -frappe.pages['bank-reconciliation'].on_page_load = function(wrapper) { - new erpnext.accounts.bankReconciliation(wrapper); -} - -erpnext.accounts.bankReconciliation = class BankReconciliation { - constructor(wrapper) { - this.page = frappe.ui.make_app_page({ - parent: wrapper, - title: __("Bank Reconciliation"), - single_column: true - }); - this.parent = wrapper; - this.page = this.parent.page; - - this.check_plaid_status(); - this.make(); - } - - make() { - const me = this; - - me.$main_section = $(`
    `).appendTo(me.page.main); - const empty_state = __("Upload a bank statement, link or reconcile a bank account") - me.$main_section.append(`
    ${empty_state}
    `) - - me.page.add_field({ - fieldtype: 'Link', - label: __('Company'), - fieldname: 'company', - options: "Company", - onchange: function() { - if (this.value) { - me.company = this.value; - } else { - me.company = null; - me.bank_account = null; - } - } - }) - me.page.add_field({ - fieldtype: 'Link', - label: __('Bank Account'), - fieldname: 'bank_account', - options: "Bank Account", - get_query: function() { - if(!me.company) { - frappe.throw(__("Please select company first")); - return - } - - return { - filters: { - "company": me.company - } - } - }, - onchange: function() { - if (this.value) { - me.bank_account = this.value; - me.add_actions(); - } else { - me.bank_account = null; - me.page.hide_actions_menu(); - } - } - }) - } - - check_plaid_status() { - const me = this; - frappe.db.get_value("Plaid Settings", "Plaid Settings", "enabled", (r) => { - if (r && r.enabled === "1") { - me.plaid_status = "active" - } else { - me.plaid_status = "inactive" - } - }) - } - - add_actions() { - const me = this; - - me.page.show_menu() - - me.page.add_menu_item(__("Upload a statement"), function() { - me.clear_page_content(); - new erpnext.accounts.bankTransactionUpload(me); - }, true) - - if (me.plaid_status==="active") { - me.page.add_menu_item(__("Synchronize this account"), function() { - me.clear_page_content(); - new erpnext.accounts.bankTransactionSync(me); - }, true) - } - - me.page.add_menu_item(__("Reconcile this account"), function() { - me.clear_page_content(); - me.make_reconciliation_tool(); - }, true) - } - - clear_page_content() { - const me = this; - $(me.page.body).find('.frappe-list').remove(); - me.$main_section.empty(); - } - - make_reconciliation_tool() { - const me = this; - frappe.model.with_doctype("Bank Transaction", () => { - erpnext.accounts.ReconciliationList = new erpnext.accounts.ReconciliationTool({ - parent: me.parent, - doctype: "Bank Transaction" - }); - }) - } -} - - -erpnext.accounts.bankTransactionUpload = class bankTransactionUpload { - constructor(parent) { - this.parent = parent; - this.data = []; - - const assets = [ - "/assets/frappe/css/frappe-datatable.css", - "/assets/frappe/js/lib/clusterize.min.js", - "/assets/frappe/js/lib/Sortable.min.js", - "/assets/frappe/js/lib/frappe-datatable.js" - ]; - - frappe.require(assets, () => { - this.make(); - }); - } - - make() { - const me = this; - new frappe.ui.FileUploader({ - method: 'erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.upload_bank_statement', - allow_multiple: 0, - on_success: function(attachment, r) { - if (!r.exc && r.message) { - me.data = r.message; - me.setup_transactions_dom(); - me.create_datatable(); - me.add_primary_action(); - } - } - }) - } - - setup_transactions_dom() { - const me = this; - me.parent.$main_section.append('
    '); - } - - create_datatable() { - try { - this.datatable = new DataTable('.transactions-table', { - columns: this.data.columns, - data: this.data.data - }) - } - catch(err) { - let msg = __("Your file could not be processed. It should be a standard CSV or XLSX file with headers in the first row."); - frappe.throw(msg) - } - - } - - add_primary_action() { - const me = this; - me.parent.page.set_primary_action(__("Submit"), function() { - me.add_bank_entries() - }, null, __("Creating bank entries...")) - } - - add_bank_entries() { - const me = this; - frappe.xcall('erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.create_bank_entries', - {columns: this.datatable.datamanager.columns, data: this.datatable.datamanager.data, bank_account: me.parent.bank_account} - ).then((result) => { - let result_title = result.errors == 0 ? __("{0} bank transaction(s) created", [result.success]) : __("{0} bank transaction(s) created and {1} errors", [result.success, result.errors]) - let result_msg = ` -
    -
    ${result_title}
    -
    ` - me.parent.page.clear_primary_action(); - me.parent.$main_section.empty(); - me.parent.$main_section.append(result_msg); - if (result.errors == 0) { - frappe.show_alert({message:__("All bank transactions have been created"), indicator:'green'}); - } else { - frappe.show_alert({message:__("Please check the error log for details about the import errors"), indicator:'red'}); - } - }) - } -} - -erpnext.accounts.bankTransactionSync = class bankTransactionSync { - constructor(parent) { - this.parent = parent; - this.data = []; - - this.init_config() - } - - init_config() { - const me = this; - frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_plaid_configuration') - .then(result => { - me.plaid_env = result.plaid_env; - me.client_name = result.client_name; - me.link_token = result.link_token; - me.sync_transactions(); - }) - } - - sync_transactions() { - const me = this; - frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (r) => { - frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions', { - bank: r.bank, - bank_account: me.parent.bank_account, - freeze: true - }) - .then((result) => { - let result_title = (result && result.length > 0) - ? __("{0} bank transaction(s) created", [result.length]) - : __("This bank account is already synchronized"); - - let result_msg = ` -
    -
    ${result_title}
    -
    ` - - this.parent.$main_section.append(result_msg) - frappe.show_alert({ message: __("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator: 'green' }); - }) - }) - } -} - - -erpnext.accounts.ReconciliationTool = class ReconciliationTool extends frappe.views.BaseList { - constructor(opts) { - super(opts); - this.show(); - } - - setup_defaults() { - super.setup_defaults(); - - this.page_title = __("Bank Reconciliation"); - this.doctype = 'Bank Transaction'; - this.fields = ['date', 'description', 'debit', 'credit', 'currency'] - - } - - setup_view() { - this.render_header(); - } - - setup_side_bar() { - // - } - - make_standard_filters() { - // - } - - freeze() { - this.$result.find('.list-count').html(`${__('Refreshing')}...`); - } - - get_args() { - const args = super.get_args(); - - return Object.assign({}, args, { - ...args.filters.push(["Bank Transaction", "docstatus", "=", 1], - ["Bank Transaction", "unallocated_amount", ">", 0]) - }); - - } - - update_data(r) { - let data = r.message || []; - - if (this.start === 0) { - this.data = data; - } else { - this.data = this.data.concat(data); - } - } - - render() { - const me = this; - this.$result.find('.list-row-container').remove(); - $('[data-fieldname="name"]').remove(); - me.data.map((value) => { - const row = $('
    ').data("data", value).appendTo(me.$result).get(0); - new erpnext.accounts.ReconciliationRow(row, value); - }) - } - - render_header() { - const me = this; - if ($(this.wrapper).find('.transaction-header').length === 0) { - me.$result.append(frappe.render_template("bank_transaction_header")); - } - } -} - -erpnext.accounts.ReconciliationRow = class ReconciliationRow { - constructor(row, data) { - this.data = data; - this.row = row; - this.make(); - this.bind_events(); - } - - make() { - $(this.row).append(frappe.render_template("bank_transaction_row", this.data)) - } - - bind_events() { - const me = this; - $(me.row).on('click', '.clickable-section', function() { - me.bank_entry = $(this).attr("data-name"); - me.show_dialog($(this).attr("data-name")); - }) - - $(me.row).on('click', '.new-reconciliation', function() { - me.bank_entry = $(this).attr("data-name"); - me.show_dialog($(this).attr("data-name")); - }) - - $(me.row).on('click', '.new-payment', function() { - me.bank_entry = $(this).attr("data-name"); - me.new_payment(); - }) - - $(me.row).on('click', '.new-invoice', function() { - me.bank_entry = $(this).attr("data-name"); - me.new_invoice(); - }) - - $(me.row).on('click', '.new-expense', function() { - me.bank_entry = $(this).attr("data-name"); - me.new_expense(); - }) - } - - new_payment() { - const me = this; - const paid_amount = me.data.credit > 0 ? me.data.credit : me.data.debit; - const payment_type = me.data.credit > 0 ? "Receive": "Pay"; - const party_type = me.data.credit > 0 ? "Customer": "Supplier"; - - frappe.new_doc("Payment Entry", {"payment_type": payment_type, "paid_amount": paid_amount, - "party_type": party_type, "paid_from": me.data.bank_account}) - } - - new_invoice() { - const me = this; - const invoice_type = me.data.credit > 0 ? "Sales Invoice" : "Purchase Invoice"; - - frappe.new_doc(invoice_type) - } - - new_expense() { - frappe.new_doc("Expense Claim") - } - - - show_dialog(data) { - const me = this; - - frappe.db.get_value("Bank Account", me.data.bank_account, "account", (r) => { - me.gl_account = r.account; - }) - - frappe.xcall('erpnext.accounts.page.bank_reconciliation.bank_reconciliation.get_linked_payments', - { bank_transaction: data, freeze: true, freeze_message: __("Finding linked payments") } - ).then((result) => { - me.make_dialog(result) - }) - } - - make_dialog(data) { - const me = this; - me.selected_payment = null; - - const fields = [ - { - fieldtype: 'Section Break', - fieldname: 'section_break_1', - label: __('Automatic Reconciliation') - }, - { - fieldtype: 'HTML', - fieldname: 'payment_proposals' - }, - { - fieldtype: 'Section Break', - fieldname: 'section_break_2', - label: __('Search for a payment') - }, - { - fieldtype: 'Link', - fieldname: 'payment_doctype', - options: 'DocType', - label: 'Payment DocType', - get_query: () => { - return { - filters : { - "name": ["in", ["Payment Entry", "Journal Entry", "Sales Invoice", "Purchase Invoice", "Expense Claim"]] - } - } - }, - }, - { - fieldtype: 'Column Break', - fieldname: 'column_break_1', - }, - { - fieldtype: 'Dynamic Link', - fieldname: 'payment_entry', - options: 'payment_doctype', - label: 'Payment Document', - get_query: () => { - let dt = this.dialog.fields_dict.payment_doctype.value; - if (dt === "Payment Entry") { - return { - query: "erpnext.accounts.page.bank_reconciliation.bank_reconciliation.payment_entry_query", - filters : { - "bank_account": this.data.bank_account, - "company": this.data.company - } - } - } else if (dt === "Journal Entry") { - return { - query: "erpnext.accounts.page.bank_reconciliation.bank_reconciliation.journal_entry_query", - filters : { - "bank_account": this.data.bank_account, - "company": this.data.company - } - } - } else if (dt === "Sales Invoice") { - return { - query: "erpnext.accounts.page.bank_reconciliation.bank_reconciliation.sales_invoices_query" - } - } else if (dt === "Purchase Invoice") { - return { - filters : [ - ["Purchase Invoice", "ifnull(clearance_date, '')", "=", ""], - ["Purchase Invoice", "docstatus", "=", 1], - ["Purchase Invoice", "company", "=", this.data.company] - ] - } - } else if (dt === "Expense Claim") { - return { - filters : [ - ["Expense Claim", "ifnull(clearance_date, '')", "=", ""], - ["Expense Claim", "docstatus", "=", 1], - ["Expense Claim", "company", "=", this.data.company] - ] - } - } - }, - onchange: function() { - if (me.selected_payment !== this.value) { - me.selected_payment = this.value; - me.display_payment_details(this); - } - } - }, - { - fieldtype: 'Section Break', - fieldname: 'section_break_3' - }, - { - fieldtype: 'HTML', - fieldname: 'payment_details' - }, - ]; - - me.dialog = new frappe.ui.Dialog({ - title: __("Choose a corresponding payment"), - fields: fields, - size: "large" - }); - - const proposals_wrapper = me.dialog.fields_dict.payment_proposals.$wrapper; - if (data && data.length > 0) { - proposals_wrapper.append(frappe.render_template("linked_payment_header")); - data.map(value => { - proposals_wrapper.append(frappe.render_template("linked_payment_row", value)) - }) - } else { - const empty_data_msg = __("ERPNext could not find any matching payment entry") - proposals_wrapper.append(`
    ${empty_data_msg}
    `) - } - - $(me.dialog.body).on('click', '.reconciliation-btn', (e) => { - const payment_entry = $(e.target).attr('data-name'); - const payment_doctype = $(e.target).attr('data-doctype'); - frappe.xcall('erpnext.accounts.page.bank_reconciliation.bank_reconciliation.reconcile', - {bank_transaction: me.bank_entry, payment_doctype: payment_doctype, payment_name: payment_entry}) - .then((result) => { - setTimeout(function(){ - erpnext.accounts.ReconciliationList.refresh(); - }, 2000); - me.dialog.hide(); - }) - }) - - me.dialog.show(); - } - - display_payment_details(event) { - const me = this; - if (event.value) { - let dt = me.dialog.fields_dict.payment_doctype.value; - me.dialog.fields_dict['payment_details'].$wrapper.empty(); - frappe.db.get_doc(dt, event.value) - .then(doc => { - let displayed_docs = [] - let payment = [] - if (dt === "Payment Entry") { - payment.currency = doc.payment_type == "Receive" ? doc.paid_to_account_currency : doc.paid_from_account_currency; - payment.doctype = dt - payment.posting_date = doc.posting_date; - payment.party = doc.party; - payment.reference_no = doc.reference_no; - payment.reference_date = doc.reference_date; - payment.paid_amount = doc.paid_amount; - payment.name = doc.name; - displayed_docs.push(payment); - } else if (dt === "Journal Entry") { - doc.accounts.forEach(payment => { - if (payment.account === me.gl_account) { - payment.doctype = dt; - payment.posting_date = doc.posting_date; - payment.party = doc.pay_to_recd_from; - payment.reference_no = doc.cheque_no; - payment.reference_date = doc.cheque_date; - payment.currency = payment.account_currency; - payment.paid_amount = payment.credit > 0 ? payment.credit : payment.debit; - payment.name = doc.name; - displayed_docs.push(payment); - } - }) - } else if (dt === "Sales Invoice") { - doc.payments.forEach(payment => { - if (payment.clearance_date === null || payment.clearance_date === "") { - payment.doctype = dt; - payment.posting_date = doc.posting_date; - payment.party = doc.customer; - payment.reference_no = doc.remarks; - payment.currency = doc.currency; - payment.paid_amount = payment.amount; - payment.name = doc.name; - displayed_docs.push(payment); - } - }) - } - - const details_wrapper = me.dialog.fields_dict.payment_details.$wrapper; - details_wrapper.append(frappe.render_template("linked_payment_header")); - displayed_docs.forEach(payment => { - details_wrapper.append(frappe.render_template("linked_payment_row", payment)); - }) - }) - } - - } -} diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.json b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.json deleted file mode 100644 index feea36860b..0000000000 --- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "content": null, - "creation": "2018-11-24 12:03:14.646669", - "docstatus": 0, - "doctype": "Page", - "idx": 0, - "modified": "2018-11-24 12:03:14.646669", - "modified_by": "Administrator", - "module": "Accounts", - "name": "bank-reconciliation", - "owner": "Administrator", - "page_name": "bank-reconciliation", - "roles": [ - { - "role": "System Manager" - }, - { - "role": "Accounts Manager" - }, - { - "role": "Accounts User" - } - ], - "script": null, - "standard": "Yes", - "style": null, - "system_page": 0, - "title": "Bank Reconciliation" -} \ No newline at end of file diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py deleted file mode 100644 index 8abe20c00a..0000000000 --- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py +++ /dev/null @@ -1,369 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe import _ -import difflib -from frappe.utils import flt -from six import iteritems -from erpnext import get_company_currency - -@frappe.whitelist() -def reconcile(bank_transaction, payment_doctype, payment_name): - transaction = frappe.get_doc("Bank Transaction", bank_transaction) - payment_entry = frappe.get_doc(payment_doctype, payment_name) - - account = frappe.db.get_value("Bank Account", transaction.bank_account, "account") - gl_entry = frappe.get_doc("GL Entry", dict(account=account, voucher_type=payment_doctype, voucher_no=payment_name)) - - if payment_doctype == "Payment Entry" and payment_entry.unallocated_amount > transaction.unallocated_amount: - frappe.throw(_("The unallocated amount of Payment Entry {0} is greater than the Bank Transaction's unallocated amount").format(payment_name)) - - if transaction.unallocated_amount == 0: - frappe.throw(_("This bank transaction is already fully reconciled")) - - if transaction.credit > 0 and gl_entry.credit > 0: - frappe.throw(_("The selected payment entry should be linked with a debtor bank transaction")) - - if transaction.debit > 0 and gl_entry.debit > 0: - frappe.throw(_("The selected payment entry should be linked with a creditor bank transaction")) - - add_payment_to_transaction(transaction, payment_entry, gl_entry) - - return 'reconciled' - -def add_payment_to_transaction(transaction, payment_entry, gl_entry): - gl_amount, transaction_amount = (gl_entry.credit, transaction.debit) if gl_entry.credit > 0 else (gl_entry.debit, transaction.credit) - allocated_amount = gl_amount if gl_amount <= transaction_amount else transaction_amount - transaction.append("payment_entries", { - "payment_document": payment_entry.doctype, - "payment_entry": payment_entry.name, - "allocated_amount": allocated_amount - }) - - transaction.save() - transaction.update_allocations() - -@frappe.whitelist() -def get_linked_payments(bank_transaction): - transaction = frappe.get_doc("Bank Transaction", bank_transaction) - bank_account = frappe.db.get_values("Bank Account", transaction.bank_account, ["account", "company"], as_dict=True) - - # Get all payment entries with a matching amount - amount_matching = check_matching_amount(bank_account[0].account, bank_account[0].company, transaction) - - # Get some data from payment entries linked to a corresponding bank transaction - description_matching = get_matching_descriptions_data(bank_account[0].company, transaction) - - if amount_matching: - return check_amount_vs_description(amount_matching, description_matching) - - elif description_matching: - description_matching = filter(lambda x: not x.get('clearance_date'), description_matching) - if not description_matching: - return [] - - return sorted(list(description_matching), key = lambda x: x["posting_date"], reverse=True) - - else: - return [] - -def check_matching_amount(bank_account, company, transaction): - payments = [] - amount = transaction.credit if transaction.credit > 0 else transaction.debit - - payment_type = "Receive" if transaction.credit > 0 else "Pay" - account_from_to = "paid_to" if transaction.credit > 0 else "paid_from" - currency_field = "paid_to_account_currency as currency" if transaction.credit > 0 else "paid_from_account_currency as currency" - - payment_entries = frappe.get_all("Payment Entry", fields=["'Payment Entry' as doctype", "name", "paid_amount", "payment_type", "reference_no", "reference_date", - "party", "party_type", "posting_date", "{0}".format(currency_field)], filters=[["paid_amount", "like", "{0}%".format(amount)], - ["docstatus", "=", "1"], ["payment_type", "=", [payment_type, "Internal Transfer"]], ["ifnull(clearance_date, '')", "=", ""], ["{0}".format(account_from_to), "=", "{0}".format(bank_account)]]) - - jea_side = "debit" if transaction.credit > 0 else "credit" - journal_entries = frappe.db.sql(f""" - SELECT - 'Journal Entry' as doctype, je.name, je.posting_date, je.cheque_no as reference_no, - jea.account_currency as currency, je.pay_to_recd_from as party, je.cheque_date as reference_date, - jea.{jea_side}_in_account_currency as paid_amount - FROM - `tabJournal Entry Account` as jea - JOIN - `tabJournal Entry` as je - ON - jea.parent = je.name - WHERE - (je.clearance_date is null or je.clearance_date='0000-00-00') - AND - jea.account = %(bank_account)s - AND - jea.{jea_side}_in_account_currency like %(txt)s - AND - je.docstatus = 1 - """, { - 'bank_account': bank_account, - 'txt': '%%%s%%' % amount - }, as_dict=True) - - if transaction.credit > 0: - sales_invoices = frappe.db.sql(""" - SELECT - 'Sales Invoice' as doctype, si.name, si.customer as party, - si.posting_date, sip.amount as paid_amount - FROM - `tabSales Invoice Payment` as sip - JOIN - `tabSales Invoice` as si - ON - sip.parent = si.name - WHERE - (sip.clearance_date is null or sip.clearance_date='0000-00-00') - AND - sip.account = %s - AND - sip.amount like %s - AND - si.docstatus = 1 - """, (bank_account, amount), as_dict=True) - else: - sales_invoices = [] - - if transaction.debit > 0: - purchase_invoices = frappe.get_all("Purchase Invoice", - fields = ["'Purchase Invoice' as doctype", "name", "paid_amount", "supplier as party", "posting_date", "currency"], - filters=[ - ["paid_amount", "like", "{0}%".format(amount)], - ["docstatus", "=", "1"], - ["is_paid", "=", "1"], - ["ifnull(clearance_date, '')", "=", ""], - ["cash_bank_account", "=", "{0}".format(bank_account)] - ] - ) - - mode_of_payments = [x["parent"] for x in frappe.db.get_list("Mode of Payment Account", - filters={"default_account": bank_account}, fields=["parent"])] - - company_currency = get_company_currency(company) - - expense_claims = frappe.get_all("Expense Claim", - fields=["'Expense Claim' as doctype", "name", "total_sanctioned_amount as paid_amount", - "employee as party", "posting_date", "'{0}' as currency".format(company_currency)], - filters=[ - ["total_sanctioned_amount", "like", "{0}%".format(amount)], - ["docstatus", "=", "1"], - ["is_paid", "=", "1"], - ["ifnull(clearance_date, '')", "=", ""], - ["mode_of_payment", "in", "{0}".format(tuple(mode_of_payments))] - ] - ) - else: - purchase_invoices = expense_claims = [] - - for data in [payment_entries, journal_entries, sales_invoices, purchase_invoices, expense_claims]: - if data: - payments.extend(data) - - return payments - -def get_matching_descriptions_data(company, transaction): - if not transaction.description : - return [] - - bank_transactions = frappe.db.sql(""" - SELECT - bt.name, bt.description, bt.date, btp.payment_document, btp.payment_entry - FROM - `tabBank Transaction` as bt - LEFT JOIN - `tabBank Transaction Payments` as btp - ON - bt.name = btp.parent - WHERE - bt.allocated_amount > 0 - AND - bt.docstatus = 1 - """, as_dict=True) - - selection = [] - for bank_transaction in bank_transactions: - if bank_transaction.description: - seq=difflib.SequenceMatcher(lambda x: x == " ", transaction.description, bank_transaction.description) - - if seq.ratio() > 0.6: - bank_transaction["ratio"] = seq.ratio() - selection.append(bank_transaction) - - document_types = set([x["payment_document"] for x in selection]) - - links = {} - for document_type in document_types: - links[document_type] = [x["payment_entry"] for x in selection if x["payment_document"]==document_type] - - - data = [] - company_currency = get_company_currency(company) - for key, value in iteritems(links): - if key == "Payment Entry": - data.extend(frappe.get_all("Payment Entry", filters=[["name", "in", value]], - fields=["'Payment Entry' as doctype", "posting_date", "party", "reference_no", - "reference_date", "paid_amount", "paid_to_account_currency as currency", "clearance_date"])) - if key == "Journal Entry": - journal_entries = frappe.get_all("Journal Entry", filters=[["name", "in", value]], - fields=["name", "'Journal Entry' as doctype", "posting_date", - "pay_to_recd_from as party", "cheque_no as reference_no", "cheque_date as reference_date", - "total_credit as paid_amount", "clearance_date"]) - for journal_entry in journal_entries: - journal_entry_accounts = frappe.get_all("Journal Entry Account", filters={"parenttype": journal_entry["doctype"], "parent": journal_entry["name"]}, fields=["account_currency"]) - journal_entry["currency"] = journal_entry_accounts[0]["account_currency"] if journal_entry_accounts else company_currency - data.extend(journal_entries) - if key == "Sales Invoice": - data.extend(frappe.get_all("Sales Invoice", filters=[["name", "in", value]], fields=["'Sales Invoice' as doctype", "posting_date", "customer_name as party", "paid_amount", "currency"])) - if key == "Purchase Invoice": - data.extend(frappe.get_all("Purchase Invoice", filters=[["name", "in", value]], fields=["'Purchase Invoice' as doctype", "posting_date", "supplier_name as party", "paid_amount", "currency"])) - if key == "Expense Claim": - expense_claims = frappe.get_all("Expense Claim", filters=[["name", "in", value]], fields=["'Expense Claim' as doctype", "posting_date", "employee_name as party", "total_amount_reimbursed as paid_amount"]) - data.extend([dict(x,**{"currency": company_currency}) for x in expense_claims]) - - return data - -def check_amount_vs_description(amount_matching, description_matching): - result = [] - - if description_matching: - for am_match in amount_matching: - for des_match in description_matching: - if des_match.get("clearance_date"): - continue - - if am_match["party"] == des_match["party"]: - if am_match not in result: - result.append(am_match) - continue - - if "reference_no" in am_match and "reference_no" in des_match: - # Sequence Matcher does not handle None as input - am_reference = am_match["reference_no"] or "" - des_reference = des_match["reference_no"] or "" - - if difflib.SequenceMatcher(lambda x: x == " ", am_reference, des_reference).ratio() > 70: - if am_match not in result: - result.append(am_match) - if result: - return sorted(result, key = lambda x: x["posting_date"], reverse=True) - else: - return sorted(amount_matching, key = lambda x: x["posting_date"], reverse=True) - - else: - return sorted(amount_matching, key = lambda x: x["posting_date"], reverse=True) - -def get_matching_transactions_payments(description_matching): - payments = [x["payment_entry"] for x in description_matching] - - payment_by_ratio = {x["payment_entry"]: x["ratio"] for x in description_matching} - - if payments: - reference_payment_list = frappe.get_all("Payment Entry", fields=["name", "paid_amount", "payment_type", "reference_no", "reference_date", - "party", "party_type", "posting_date", "paid_to_account_currency"], filters=[["name", "in", payments]]) - - return sorted(reference_payment_list, key=lambda x: payment_by_ratio[x["name"]]) - - else: - return [] - -@frappe.whitelist() -@frappe.validate_and_sanitize_search_inputs -def payment_entry_query(doctype, txt, searchfield, start, page_len, filters): - account = frappe.db.get_value("Bank Account", filters.get("bank_account"), "account") - if not account: - return - - return frappe.db.sql(""" - SELECT - name, party, paid_amount, received_amount, reference_no - FROM - `tabPayment Entry` - WHERE - (clearance_date is null or clearance_date='0000-00-00') - AND (paid_from = %(account)s or paid_to = %(account)s) - AND (name like %(txt)s or party like %(txt)s) - AND docstatus = 1 - ORDER BY - if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), name - LIMIT - %(start)s, %(page_len)s""", - { - 'txt': "%%%s%%" % txt, - '_txt': txt.replace("%", ""), - 'start': start, - 'page_len': page_len, - 'account': account - } - ) - -@frappe.whitelist() -@frappe.validate_and_sanitize_search_inputs -def journal_entry_query(doctype, txt, searchfield, start, page_len, filters): - account = frappe.db.get_value("Bank Account", filters.get("bank_account"), "account") - - return frappe.db.sql(""" - SELECT - jea.parent, je.pay_to_recd_from, - if(jea.debit_in_account_currency > 0, jea.debit_in_account_currency, jea.credit_in_account_currency) - FROM - `tabJournal Entry Account` as jea - LEFT JOIN - `tabJournal Entry` as je - ON - jea.parent = je.name - WHERE - (je.clearance_date is null or je.clearance_date='0000-00-00') - AND - jea.account = %(account)s - AND - (jea.parent like %(txt)s or je.pay_to_recd_from like %(txt)s) - AND - je.docstatus = 1 - ORDER BY - if(locate(%(_txt)s, jea.parent), locate(%(_txt)s, jea.parent), 99999), - jea.parent - LIMIT - %(start)s, %(page_len)s""", - { - 'txt': "%%%s%%" % txt, - '_txt': txt.replace("%", ""), - 'start': start, - 'page_len': page_len, - 'account': account - } - ) - -@frappe.whitelist() -@frappe.validate_and_sanitize_search_inputs -def sales_invoices_query(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql(""" - SELECT - sip.parent, si.customer, sip.amount, sip.mode_of_payment - FROM - `tabSales Invoice Payment` as sip - LEFT JOIN - `tabSales Invoice` as si - ON - sip.parent = si.name - WHERE - (sip.clearance_date is null or sip.clearance_date='0000-00-00') - AND - (sip.parent like %(txt)s or si.customer like %(txt)s) - ORDER BY - if(locate(%(_txt)s, sip.parent), locate(%(_txt)s, sip.parent), 99999), - sip.parent - LIMIT - %(start)s, %(page_len)s""", - { - 'txt': "%%%s%%" % txt, - '_txt': txt.replace("%", ""), - 'start': start, - 'page_len': page_len - } - ) diff --git a/erpnext/accounts/page/bank_reconciliation/bank_transaction_header.html b/erpnext/accounts/page/bank_reconciliation/bank_transaction_header.html deleted file mode 100644 index 94f183b793..0000000000 --- a/erpnext/accounts/page/bank_reconciliation/bank_transaction_header.html +++ /dev/null @@ -1,21 +0,0 @@ -
    -
    - -
    - {{ __("Description") }} -
    - - - -
    -
    -
    -
    diff --git a/erpnext/accounts/page/bank_reconciliation/bank_transaction_row.html b/erpnext/accounts/page/bank_reconciliation/bank_transaction_row.html deleted file mode 100644 index 742b84c63f..0000000000 --- a/erpnext/accounts/page/bank_reconciliation/bank_transaction_row.html +++ /dev/null @@ -1,36 +0,0 @@ -
    -
    -
    - -
    - {{ description }} -
    - - - -
    - -
    -
    diff --git a/erpnext/accounts/page/bank_reconciliation/linked_payment_header.html b/erpnext/accounts/page/bank_reconciliation/linked_payment_header.html deleted file mode 100644 index 4542c36e0d..0000000000 --- a/erpnext/accounts/page/bank_reconciliation/linked_payment_header.html +++ /dev/null @@ -1,21 +0,0 @@ -
    -
    -
    - {{ __("Payment Name") }} -
    -
    - {{ __("Reference Date") }} -
    - - -
    - {{ __("Reference Number") }} -
    -
    -
    -
    -
    diff --git a/erpnext/accounts/page/bank_reconciliation/linked_payment_row.html b/erpnext/accounts/page/bank_reconciliation/linked_payment_row.html deleted file mode 100644 index bdbc9fce03..0000000000 --- a/erpnext/accounts/page/bank_reconciliation/linked_payment_row.html +++ /dev/null @@ -1,36 +0,0 @@ -
    -
    -
    - {{ name }} -
    -
    - {% if (typeof reference_date !== "undefined") %} - {%= frappe.datetime.str_to_user(reference_date) %} - {% else %} - {% if (typeof posting_date !== "undefined") %} - {%= frappe.datetime.str_to_user(posting_date) %} - {% endif %} - {% endif %} -
    - - -
    - {% if (typeof reference_no !== "undefined") %} - {{ reference_no }} - {% else %} - {{ "" }} - {% endif %} -
    -
    -
    - -
    -
    -
    -
    \ No newline at end of file diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py index 0861b20f14..79b0a6f30e 100644 --- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py +++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py @@ -15,15 +15,51 @@ def execute(filters=None): return columns, data def get_columns(): - return [ - _("Payment Document") + "::130", - _("Payment Entry") + ":Dynamic Link/"+_("Payment Document")+":110", - _("Posting Date") + ":Date:100", - _("Cheque/Reference No") + "::120", - _("Clearance Date") + ":Date:100", - _("Against Account") + ":Link/Account:170", - _("Amount") + ":Currency:120" - ] + columns = [{ + "label": _("Payment Document Type"), + "fieldname": "payment_document_type", + "fieldtype": "Link", + "options": "Doctype", + "width": 130 + }, + { + "label": _("Payment Entry"), + "fieldname": "payment_entry", + "fieldtype": "Dynamic Link", + "options": "payment_document_type", + "width": 140 + }, + { + "label": _("Posting Date"), + "fieldname": "posting_date", + "fieldtype": "Date", + "width": 100 + }, + { + "label": _("Cheque/Reference No"), + "fieldname": "cheque_no", + "width": 120 + }, + { + "label": _("Clearance Date"), + "fieldname": "clearance_date", + "fieldtype": "Date", + "width": 100 + }, + { + "label": _("Against Account"), + "fieldname": "against", + "fieldtype": "Link", + "options": "Account", + "width": 170 + }, + { + "label": _("Amount"), + "fieldname": "amount", + "width": 120 + }] + + return columns def get_conditions(filters): conditions = "" diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index d0116890b6..76f3c50578 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -222,7 +222,7 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i set_gl_entries_by_account(start_date, end_date, root.lft, root.rgt, filters, - gl_entries_by_account, accounts_by_name, ignore_closing_entries=False) + gl_entries_by_account, accounts_by_name, accounts, ignore_closing_entries=False) calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters) accumulate_values_into_parents(accounts, accounts_by_name, companies) @@ -339,7 +339,7 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com return data def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, gl_entries_by_account, - accounts_by_name, ignore_closing_entries=False): + accounts_by_name, accounts, ignore_closing_entries=False): """Returns a dict like { "account": [gl entries], ... }""" company_lft, company_rgt = frappe.get_cached_value('Company', @@ -382,15 +382,31 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g for entry in gl_entries: key = entry.account_number or entry.account_name - validate_entries(key, entry, accounts_by_name) + validate_entries(key, entry, accounts_by_name, accounts) gl_entries_by_account.setdefault(key, []).append(entry) return gl_entries_by_account -def validate_entries(key, entry, accounts_by_name): +def get_account_details(account): + return frappe.get_cached_value('Account', account, ['name', 'report_type', 'root_type', 'company', + 'is_group', 'account_name', 'account_number', 'parent_account', 'lft', 'rgt'], as_dict=1) + +def validate_entries(key, entry, accounts_by_name, accounts): if key not in accounts_by_name: - field = "Account number" if entry.account_number else "Account name" - frappe.throw(_("{0} {1} is not present in the parent company").format(field, key)) + args = get_account_details(entry.account) + + if args.parent_account: + parent_args = get_account_details(args.parent_account) + + args.update({ + 'lft': parent_args.lft + 1, + 'rgt': parent_args.rgt - 1, + 'root_type': parent_args.root_type, + 'report_type': parent_args.report_type + }) + + accounts_by_name.setdefault(key, args) + accounts.append(args) def get_additional_conditions(from_date, ignore_closing_entries, filters): additional_conditions = [] diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 67c7fd2d22..5eb2aab393 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -82,7 +82,7 @@ def get_fiscal_years(transaction_date=None, fiscal_year=None, label="Date", verb error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(label, formatdate(transaction_date)) if company: error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company)) - + if verbose==1: frappe.msgprint(error_msg) raise FiscalYearError(error_msg) @@ -888,19 +888,23 @@ def get_coa(doctype, parent, is_root, chart=None): def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None, warehouse_account=None, company=None): + stock_vouchers = get_future_stock_vouchers(posting_date, posting_time, for_warehouses, for_items, company) + repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company, warehouse_account) + + +def repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company=None, warehouse_account=None): def _delete_gl_entries(voucher_type, voucher_no): frappe.db.sql("""delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no)) + if not warehouse_account: warehouse_account = get_warehouse_account_map(company) - future_stock_vouchers = get_future_stock_vouchers(posting_date, posting_time, for_warehouses, for_items) - gle = get_voucherwise_gl_entries(future_stock_vouchers, posting_date) - - for voucher_type, voucher_no in future_stock_vouchers: + gle = get_voucherwise_gl_entries(stock_vouchers, posting_date) + for voucher_type, voucher_no in stock_vouchers: existing_gle = gle.get((voucher_type, voucher_no), []) - voucher_obj = frappe.get_doc(voucher_type, voucher_no) + voucher_obj = frappe.get_cached_doc(voucher_type, voucher_no) expected_gle = voucher_obj.get_gl_entries(warehouse_account) if expected_gle: if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle): @@ -909,7 +913,7 @@ def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for else: _delete_gl_entries(voucher_type, voucher_no) -def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None): +def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None, company=None): future_stock_vouchers = [] values = [] @@ -922,6 +926,10 @@ def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, f condition += " and warehouse in ({})".format(", ".join(["%s"] * len(for_warehouses))) values += for_warehouses + if company: + condition += " and company = %s" + values.append(company) + for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no from `tabStock Ledger Entry` sle where @@ -982,7 +990,7 @@ def check_if_stock_and_account_balance_synced(posting_date, company, voucher_typ error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses as on {3}.").format( stock_bal, account_bal, frappe.bold(account), posting_date) error_resolution = _("Please create an adjustment Journal Entry for amount {0} on {1}")\ - .format(frappe.bold(diff), frappe.bold(posting_date)) + .format(frappe.bold(diff), frappe.bold(posting_date)) frappe.msgprint( msg="""{0}

    {1}

    """.format(error_reason, error_resolution), diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index c691e9f9f8..75b2954ddd 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -40,6 +40,7 @@ "base_rate", "base_amount", "pricing_rules", + "stock_uom_rate", "is_free_item", "section_break_29", "net_rate", @@ -726,13 +727,21 @@ "fieldname": "more_info_section_break", "fieldtype": "Section Break", "label": "More Information" + }, + { + "depends_on": "eval: doc.uom != doc.stock_uom", + "fieldname": "stock_uom_rate", + "fieldtype": "Currency", + "label": "Rate of Stock UOM", + "options": "currency", + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-12-07 11:59:47.670951", + "modified": "2021-01-30 21:44:41.816974", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index a51498e935..7cf22f87e4 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -127,6 +127,10 @@ class RequestforQuotation(BuyingController): 'link_doctype': 'Supplier', 'link_name': rfq_supplier.supplier }) + contact.append('email_ids', { + 'email_id': user.name, + 'is_primary': 1 + }) if not contact.email_id and not contact.user: contact.email_id = user.name diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index 40362b1d40..4cc5753cbd 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -26,7 +26,6 @@ "supplier_group", "supplier_type", "pan", - "language", "allow_purchase_invoice_creation_without_purchase_order", "allow_purchase_invoice_creation_without_purchase_receipt", "disabled", @@ -57,6 +56,7 @@ "website", "supplier_details", "column_break_30", + "language", "is_frozen" ], "fields": [ @@ -384,7 +384,7 @@ "idx": 370, "image_field": "image", "links": [], - "modified": "2020-06-17 23:18:20", + "modified": "2021-01-06 19:51:40.939087", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 991eef1d21..12a81c7887 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1309,45 +1309,28 @@ def add_taxes_from_tax_template(child_item, parent_doc): }) tax_row.db_insert() -def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item): +def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, trans_item): """ - Returns a Sales Order Item child item containing the default values + Returns a Sales/Purchase Order Item child item containing the default values """ p_doc = frappe.get_doc(parent_doctype, parent_doctype_name) - child_item = frappe.new_doc('Sales Order Item', p_doc, child_docname) + child_item = frappe.new_doc(child_doctype, p_doc, child_docname) item = frappe.get_doc("Item", trans_item.get('item_code')) - child_item.item_code = item.item_code - child_item.item_name = item.item_name - child_item.description = item.description - child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date + for field in ("item_code", "item_name", "description", "item_group"): + child_item.update({field: item.get(field)}) + date_fieldname = "delivery_date" if child_doctype == "Sales Order Item" else "schedule_date" + child_item.update({date_fieldname: trans_item.get(date_fieldname) or p_doc.get(date_fieldname)}) child_item.uom = trans_item.get("uom") or item.stock_uom conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor")) child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor - set_child_tax_template_and_map(item, child_item, p_doc) - add_taxes_from_tax_template(child_item, p_doc) - child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) - if not child_item.warehouse: - frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.") - .format(frappe.bold("default warehouse"), frappe.bold(item.item_code))) - return child_item - - -def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item): - """ - Returns a Purchase Order Item child item containing the default values - """ - p_doc = frappe.get_doc(parent_doctype, parent_doctype_name) - child_item = frappe.new_doc('Purchase Order Item', p_doc, child_docname) - item = frappe.get_doc("Item", trans_item.get('item_code')) - child_item.item_code = item.item_code - child_item.item_name = item.item_name - child_item.description = item.description - child_item.schedule_date = trans_item.get('schedule_date') or p_doc.schedule_date - child_item.uom = trans_item.get("uom") or item.stock_uom - conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor")) - child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor - child_item.base_rate = 1 # Initiallize value will update in parent validation - child_item.base_amount = 1 # Initiallize value will update in parent validation + if child_doctype == "Purchase Order Item": + child_item.base_rate = 1 # Initiallize value will update in parent validation + child_item.base_amount = 1 # Initiallize value will update in parent validation + if child_doctype == "Sales Order Item": + child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) + if not child_item.warehouse: + frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.") + .format(frappe.bold("default warehouse"), frappe.bold(item.item_code))) set_child_tax_template_and_map(item, child_item, p_doc) add_taxes_from_tax_template(child_item, p_doc) return child_item @@ -1411,8 +1394,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil ) def get_new_child_item(item_row): - new_child_function = set_sales_order_defaults if parent_doctype == "Sales Order" else set_purchase_order_defaults - return new_child_function(parent_doctype, parent_doctype_name, child_docname, item_row) + child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item" + return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row) def validate_quantity(child_item, d): if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty): diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 0e1829a767..de61b35316 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -204,8 +204,6 @@ def get_already_returned_items(doc): return items def get_returned_qty_map_for_row(row_name, doctype): - if doctype == "POS Invoice": return {} - child_doctype = doctype + " Item" reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype) @@ -354,7 +352,12 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.so_detail = source_doc.so_detail target_doc.dn_detail = source_doc.dn_detail target_doc.expense_account = source_doc.expense_account - target_doc.sales_invoice_item = source_doc.name + + if doctype == "Sales Invoice": + target_doc.sales_invoice_item = source_doc.name + else: + target_doc.pos_invoice_item = source_doc.name + target_doc.price_list_rate = 0 if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 6abfe04db9..c61b67b0a4 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -446,9 +446,13 @@ class SellingController(StockController): check_list, chk_dupl_itm = [], [] if cint(frappe.db.get_single_value("Selling Settings", "allow_multiple_items")): return + if self.doctype == "Sales Invoice" and self.is_consolidated: + return + if self.doctype == "POS Invoice": + return for d in self.get('items'): - if self.doctype in ["POS Invoice","Sales Invoice"]: + if self.doctype == "Sales Invoice": stock_items = [d.item_code, d.description, d.warehouse, d.sales_order or d.delivery_note, d.batch_no or ''] non_stock_items = [d.item_code, d.description, d.sales_order or d.delivery_note] elif self.doctype == "Delivery Note": diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 2ae9dc7102..e0031c9c69 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -24,6 +24,7 @@ class StockController(AccountsController): self.validate_inspection() self.validate_serialized_batch() self.validate_customer_provided_item() + self.set_rate_of_stock_uom() self.validate_internal_transfer() self.validate_putaway_capacity() @@ -73,7 +74,7 @@ class StockController(AccountsController): gl_list = [] warehouse_with_no_account = [] - precision = frappe.get_precision("GL Entry", "debit_in_account_currency") + precision = self.get_debit_field_precision() for item_row in voucher_details: sle_list = sle_map.get(item_row.name) @@ -130,7 +131,13 @@ class StockController(AccountsController): if frappe.db.get_value("Warehouse", wh, "company"): frappe.throw(_("Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.").format(wh, self.company)) - return process_gl_map(gl_list) + return process_gl_map(gl_list, precision=precision) + + def get_debit_field_precision(self): + if not frappe.flags.debit_field_precision: + frappe.flags.debit_field_precision = frappe.get_precision("GL Entry", "debit_in_account_currency") + + return frappe.flags.debit_field_precision def update_stock_ledger_entries(self, sle): sle.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, @@ -243,7 +250,7 @@ class StockController(AccountsController): .format(item.idx, frappe.bold(item.item_code), msg), title=_("Expense Account Missing")) else: - is_expense_account = frappe.db.get_value("Account", + is_expense_account = frappe.get_cached_value("Account", item.get("expense_account"), "report_type")=="Profit and Loss" if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Stock Reconciliation", "Stock Entry") and not is_expense_account: frappe.throw(_("Expense / Difference account ({0}) must be a 'Profit or Loss' account") @@ -396,6 +403,11 @@ class StockController(AccountsController): if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'): d.allow_zero_valuation_rate = 1 + def set_rate_of_stock_uom(self): + if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]: + for d in self.get("items"): + d.stock_uom_rate = d.rate / d.conversion_factor + def validate_internal_transfer(self): if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \ and self.is_internal_transfer(): @@ -482,7 +494,6 @@ class StockController(AccountsController): "voucher_no": self.name, "company": self.company }) - if check_if_future_sle_exists(args): create_repost_item_valuation_entry(args) elif not is_reposting_pending(): diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index cfa499191c..10271cbcc9 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -15,6 +15,8 @@ from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_ra class calculate_taxes_and_totals(object): def __init__(self, doc): self.doc = doc + frappe.flags.round_off_applicable_accounts = [] + get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts) self.calculate() def calculate(self): @@ -332,10 +334,18 @@ class calculate_taxes_and_totals(object): elif tax.charge_type == "On Item Quantity": current_tax_amount = tax_rate * item.qty + current_tax_amount = self.get_final_current_tax_amount(tax, current_tax_amount) self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount) return current_tax_amount + def get_final_current_tax_amount(self, tax, current_tax_amount): + # Some countries need individual tax components to be rounded + # Handeled via regional doctypess + if tax.account_head in frappe.flags.round_off_applicable_accounts: + current_tax_amount = round(current_tax_amount, 0) + return current_tax_amount + def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount): # store tax breakup for each item key = item.item_code or item.item_name @@ -693,6 +703,15 @@ def get_itemised_tax_breakup_html(doc): ) ) +@frappe.whitelist() +def get_round_off_applicable_accounts(company, account_list): + account_list = get_regional_round_off_accounts(company, account_list) + + return account_list + +@erpnext.allow_regional +def get_regional_round_off_accounts(company, account_list): + pass @erpnext.allow_regional def update_itemised_tax_data(doc): diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json index 2df1793fdb..1b33fd73ac 100644 --- a/erpnext/crm/doctype/lead/lead.json +++ b/erpnext/crm/doctype/lead/lead.json @@ -49,6 +49,7 @@ "phone", "mobile_no", "fax", + "website", "more_info", "type", "market_segment", @@ -56,8 +57,8 @@ "request_type", "column_break3", "company", - "website", "territory", + "language", "unsubscribed", "blog_subscriber", "title" @@ -447,13 +448,19 @@ "fieldtype": "Select", "label": "Address Type", "options": "Billing\nShipping\nOffice\nPersonal\nPlant\nPostal\nShop\nSubsidiary\nWarehouse\nCurrent\nPermanent\nOther" + }, + { + "fieldname": "language", + "fieldtype": "Link", + "label": "Print Language", + "options": "Language" } ], "icon": "fa fa-user", "idx": 5, "image_field": "image", "links": [], - "modified": "2020-10-13 15:24:00.094811", + "modified": "2021-01-06 19:39:58.748978", "modified_by": "Administrator", "module": "CRM", "name": "Lead", diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index 08958b7dd6..ac374a95f4 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -24,6 +24,12 @@ frappe.ui.form.on("Opportunity", { frm.trigger('set_contact_link'); } }, + contact_date: function(frm) { + if(frm.doc.contact_date < frappe.datetime.now_datetime()){ + frm.set_value("contact_date", ""); + frappe.throw(__("Next follow up date should be greater than now.")) + } + }, onload_post_render: function(frm) { frm.get_field("items").grid.set_multiple_add("item_code", "qty"); diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json index eee13f7e79..2e09a76c0f 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -54,6 +54,7 @@ "campaign", "column_break1", "transaction_date", + "language", "amended_from", "lost_reasons" ], @@ -419,12 +420,18 @@ "fieldtype": "Duration", "label": "First Response Time", "read_only": 1 + }, + { + "fieldname": "language", + "fieldtype": "Link", + "label": "Print Language", + "options": "Language" } ], "icon": "fa fa-info-sign", "idx": 195, "links": [], - "modified": "2020-08-12 17:34:35.066961", + "modified": "2021-01-06 19:42:46.190051", "modified_by": "Administrator", "module": "CRM", "name": "Opportunity", diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py index 4ccd9bd73b..f244daffea 100644 --- a/erpnext/crm/doctype/utils.py +++ b/erpnext/crm/doctype/utils.py @@ -78,7 +78,9 @@ def get_scheduled_employees_for_popup(communication_medium): def strip_number(number): if not number: return - # strip 0 from the start of the number for proper number comparisions + # strip + and 0 from the start of the number for proper number comparisions + # eg. +7888383332 should match with 7888383332 # eg. 07888383332 should match with 7888383332 + number = number.lstrip('+') number = number.lstrip('0') return number diff --git a/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.py b/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.py index b538a58189..3a9d57d607 100644 --- a/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.py +++ b/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.py @@ -19,15 +19,50 @@ def set_defaut_value_for_filters(filters): if not filters.get('lead_age'): filters["lead_age"] = 60 def get_columns(): - return [ - _("Lead") + ":Link/Lead:100", - _("Name") + "::100", - _("Organization") + "::100", - _("Reference Document") + "::150", - _("Reference Name") + ":Dynamic Link/"+_("Reference Document")+":120", - _("Last Communication") + ":Data:200", - _("Last Communication Date") + ":Date:180" - ] + columns = [{ + "label": _("Lead"), + "fieldname": "lead", + "fieldtype": "Link", + "options": "Lead", + "width": 130 + }, + { + "label": _("Name"), + "fieldname": "name", + "width": 120 + }, + { + "label": _("Organization"), + "fieldname": "organization", + "width": 120 + }, + { + "label": _("Reference Document Type"), + "fieldname": "reference_document_type", + "fieldtype": "Link", + "options": "Doctype", + "width": 100 + }, + { + "label": _("Reference Name"), + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "options": "reference_document_type", + "width": 140 + }, + { + "label": _("Last Communication"), + "fieldname": "last_communication", + "fieldtype": "Data", + "width": 200 + }, + { + "label": _("Last Communication Date"), + "fieldname": "last_communication_date", + "fieldtype": "Date", + "width": 100 + }] + return columns def get_data(filters): lead_details = [] diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py index d33b0a7089..554c6b0eb0 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py @@ -5,7 +5,7 @@ import datetime class MpesaConnector(): def __init__(self, env="sandbox", app_key=None, app_secret=None, sandbox_url="https://sandbox.safaricom.co.ke", - live_url="https://safaricom.co.ke"): + live_url="https://api.safaricom.co.ke"): """Setup configuration for Mpesa connector and generate new access token.""" self.env = env self.app_key = app_key @@ -102,14 +102,14 @@ class MpesaConnector(): "BusinessShortCode": business_shortcode, "Password": encoded.decode("utf-8"), "Timestamp": time, - "TransactionType": "CustomerPayBillOnline", "Amount": amount, "PartyA": int(phone_number), - "PartyB": business_shortcode, + "PartyB": reference_code, "PhoneNumber": int(phone_number), "CallBackURL": callback_url, "AccountReference": reference_code, - "TransactionDesc": description + "TransactionDesc": description, + "TransactionType": "CustomerPayBillOnline" if self.env == "sandbox" else "CustomerBuyGoodsOnline" } headers = {'Authorization': 'Bearer {0}'.format(self.authentication_token), 'Content-Type': "application/json"} diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json index fc7b310c08..407f82616f 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json @@ -11,8 +11,10 @@ "consumer_secret", "initiator_name", "till_number", + "transaction_limit", "sandbox", "column_break_4", + "business_shortcode", "online_passkey", "security_credential", "get_account_balance", @@ -84,10 +86,24 @@ "fieldname": "get_account_balance", "fieldtype": "Button", "label": "Get Account Balance" + }, + { + "depends_on": "eval:(doc.sandbox==0)", + "fieldname": "business_shortcode", + "fieldtype": "Data", + "label": "Business Shortcode", + "mandatory_depends_on": "eval:(doc.sandbox==0)" + }, + { + "default": "150000", + "fieldname": "transaction_limit", + "fieldtype": "Float", + "label": "Transaction Limit", + "non_negative": 1 } ], "links": [], - "modified": "2020-09-25 20:21:38.215494", + "modified": "2021-01-29 12:02:16.106942", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "Mpesa Settings", diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py index 1cad84dcde..b5718026c1 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py @@ -33,13 +33,34 @@ class MpesaSettings(Document): create_mode_of_payment('Mpesa-' + self.payment_gateway_name, payment_type="Phone") def request_for_payment(self, **kwargs): - if frappe.flags.in_test: - from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import get_payment_request_response_payload - response = frappe._dict(get_payment_request_response_payload()) - else: - response = frappe._dict(generate_stk_push(**kwargs)) + args = frappe._dict(kwargs) + request_amounts = self.split_request_amount_according_to_transaction_limit(args) - self.handle_api_response("CheckoutRequestID", kwargs, response) + for i, amount in enumerate(request_amounts): + args.request_amount = amount + if frappe.flags.in_test: + from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import get_payment_request_response_payload + response = frappe._dict(get_payment_request_response_payload(amount)) + else: + response = frappe._dict(generate_stk_push(**args)) + + self.handle_api_response("CheckoutRequestID", args, response) + + def split_request_amount_according_to_transaction_limit(self, args): + request_amount = args.request_amount + if request_amount > self.transaction_limit: + # make multiple requests + request_amounts = [] + requests_to_be_made = frappe.utils.ceil(request_amount / self.transaction_limit) # 480/150 = ceil(3.2) = 4 + for i in range(requests_to_be_made): + amount = self.transaction_limit + if i == requests_to_be_made - 1: + amount = request_amount - (self.transaction_limit * i) # for 4th request, 480 - (150 * 3) = 30 + request_amounts.append(amount) + else: + request_amounts = [request_amount] + + return request_amounts def get_account_balance_info(self): payload = dict( @@ -67,7 +88,8 @@ class MpesaSettings(Document): req_name = getattr(response, global_id) error = None - create_request_log(request_dict, "Host", "Mpesa", req_name, error) + if not frappe.db.exists('Integration Request', req_name): + create_request_log(request_dict, "Host", "Mpesa", req_name, error) if error: frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error")) @@ -80,6 +102,8 @@ def generate_stk_push(**kwargs): mpesa_settings = frappe.get_doc("Mpesa Settings", args.payment_gateway[6:]) env = "production" if not mpesa_settings.sandbox else "sandbox" + # for sandbox, business shortcode is same as till number + business_shortcode = mpesa_settings.business_shortcode if env == "production" else mpesa_settings.till_number connector = MpesaConnector(env=env, app_key=mpesa_settings.consumer_key, @@ -87,10 +111,12 @@ def generate_stk_push(**kwargs): mobile_number = sanitize_mobile_number(args.sender) - response = connector.stk_push(business_shortcode=mpesa_settings.till_number, - passcode=mpesa_settings.get_password("online_passkey"), amount=args.grand_total, + response = connector.stk_push( + business_shortcode=business_shortcode, amount=args.request_amount, + passcode=mpesa_settings.get_password("online_passkey"), callback_url=callback_url, reference_code=mpesa_settings.till_number, - phone_number=mobile_number, description="POS Payment") + phone_number=mobile_number, description="POS Payment" + ) return response @@ -108,29 +134,72 @@ def verify_transaction(**kwargs): transaction_response = frappe._dict(kwargs["Body"]["stkCallback"]) checkout_id = getattr(transaction_response, "CheckoutRequestID", "") - request = frappe.get_doc("Integration Request", checkout_id) - transaction_data = frappe._dict(loads(request.data)) + integration_request = frappe.get_doc("Integration Request", checkout_id) + transaction_data = frappe._dict(loads(integration_request.data)) + total_paid = 0 # for multiple integration request made against a pos invoice + success = False # for reporting successfull callback to point of sale ui if transaction_response['ResultCode'] == 0: - if request.reference_doctype and request.reference_docname: + if integration_request.reference_doctype and integration_request.reference_docname: try: - doc = frappe.get_doc(request.reference_doctype, - request.reference_docname) - doc.run_method("on_payment_authorized", 'Completed') - item_response = transaction_response["CallbackMetadata"]["Item"] + amount = fetch_param_value(item_response, "Amount", "Name") mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name") - frappe.db.set_value("POS Invoice", doc.reference_name, "mpesa_receipt_number", mpesa_receipt) - request.handle_success(transaction_response) + pr = frappe.get_doc(integration_request.reference_doctype, integration_request.reference_docname) + + mpesa_receipts, completed_payments = get_completed_integration_requests_info( + integration_request.reference_doctype, + integration_request.reference_docname, + checkout_id + ) + + total_paid = amount + sum(completed_payments) + mpesa_receipts = ', '.join(mpesa_receipts + [mpesa_receipt]) + + if total_paid >= pr.grand_total: + pr.run_method("on_payment_authorized", 'Completed') + success = True + + frappe.db.set_value("POS Invoice", pr.reference_name, "mpesa_receipt_number", mpesa_receipts) + integration_request.handle_success(transaction_response) except Exception: - request.handle_failure(transaction_response) + integration_request.handle_failure(transaction_response) frappe.log_error(frappe.get_traceback()) else: - request.handle_failure(transaction_response) + integration_request.handle_failure(transaction_response) - frappe.publish_realtime('process_phone_payment', doctype="POS Invoice", - docname=transaction_data.payment_reference, user=request.owner, message=transaction_response) + frappe.publish_realtime( + event='process_phone_payment', + doctype="POS Invoice", + docname=transaction_data.payment_reference, + user=integration_request.owner, + message={ + 'amount': total_paid, + 'success': success, + 'failure_message': transaction_response["ResultDesc"] if transaction_response['ResultCode'] != 0 else '' + }, + ) + +def get_completed_integration_requests_info(reference_doctype, reference_docname, checkout_id): + output_of_other_completed_requests = frappe.get_all("Integration Request", filters={ + 'name': ['!=', checkout_id], + 'reference_doctype': reference_doctype, + 'reference_docname': reference_docname, + 'status': 'Completed' + }, pluck="output") + + mpesa_receipts, completed_payments = [], [] + + for out in output_of_other_completed_requests: + out = frappe._dict(loads(out)) + item_response = out["CallbackMetadata"]["Item"] + completed_amount = fetch_param_value(item_response, "Amount", "Name") + completed_mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name") + completed_payments.append(completed_amount) + mpesa_receipts.append(completed_mpesa_receipt) + + return mpesa_receipts, completed_payments def get_account_balance(request_payload): """Call account balance API to send the request to the Mpesa Servers.""" diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py index 49f6d95a6e..29487962f6 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py @@ -9,6 +9,10 @@ from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import p from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice class TestMpesaSettings(unittest.TestCase): + def tearDown(self): + frappe.db.sql('delete from `tabMpesa Settings`') + frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"') + def test_creation_of_payment_gateway(self): create_mpesa_settings(payment_gateway_name="_Test") @@ -40,6 +44,8 @@ class TestMpesaSettings(unittest.TestCase): } })) + integration_request.delete() + def test_processing_of_callback_payload(self): create_mpesa_settings(payment_gateway_name="Payment") mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account") @@ -56,10 +62,16 @@ class TestMpesaSettings(unittest.TestCase): # test payment request creation self.assertEquals(pr.payment_gateway, "Mpesa-Payment") - callback_response = get_payment_callback_payload() + # submitting payment request creates integration requests with random id + integration_req_ids = frappe.get_all("Integration Request", filters={ + 'reference_doctype': pr.doctype, + 'reference_docname': pr.name, + }, pluck="name") + + callback_response = get_payment_callback_payload(Amount=500, CheckoutRequestID=integration_req_ids[0]) verify_transaction(**callback_response) # test creation of integration request - integration_request = frappe.get_doc("Integration Request", "ws_CO_061020201133231972") + integration_request = frappe.get_doc("Integration Request", integration_req_ids[0]) # test integration request creation and successful update of the status on receiving callback response self.assertTrue(integration_request) @@ -69,8 +81,120 @@ class TestMpesaSettings(unittest.TestCase): integration_request.reload() self.assertEquals(pos_invoice.mpesa_receipt_number, "LGR7OWQX0R") self.assertEquals(integration_request.status, "Completed") + + frappe.db.set_value("Customer", "_Test Customer", "default_currency", "") + integration_request.delete() + pr.reload() + pr.cancel() + pr.delete() + pos_invoice.delete() + + def test_processing_of_multiple_callback_payload(self): + create_mpesa_settings(payment_gateway_name="Payment") + mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account") + frappe.db.set_value("Account", mpesa_account, "account_currency", "KES") + frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500") + frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES") + + pos_invoice = create_pos_invoice(do_not_submit=1) + pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 1000}) + pos_invoice.contact_mobile = "093456543894" + pos_invoice.currency = "KES" + pos_invoice.save() + + pr = pos_invoice.create_payment_request() + # test payment request creation + self.assertEquals(pr.payment_gateway, "Mpesa-Payment") + + # submitting payment request creates integration requests with random id + integration_req_ids = frappe.get_all("Integration Request", filters={ + 'reference_doctype': pr.doctype, + 'reference_docname': pr.name, + }, pluck="name") + + # create random receipt nos and send it as response to callback handler + mpesa_receipt_numbers = [frappe.utils.random_string(5) for d in integration_req_ids] + + integration_requests = [] + for i in range(len(integration_req_ids)): + callback_response = get_payment_callback_payload( + Amount=500, + CheckoutRequestID=integration_req_ids[i], + MpesaReceiptNumber=mpesa_receipt_numbers[i] + ) + # handle response manually + verify_transaction(**callback_response) + # test completion of integration request + integration_request = frappe.get_doc("Integration Request", integration_req_ids[i]) + self.assertEquals(integration_request.status, "Completed") + integration_requests.append(integration_request) + + # check receipt number once all the integration requests are completed + pos_invoice.reload() + self.assertEquals(pos_invoice.mpesa_receipt_number, ', '.join(mpesa_receipt_numbers)) frappe.db.set_value("Customer", "_Test Customer", "default_currency", "") + [d.delete() for d in integration_requests] + pr.reload() + pr.cancel() + pr.delete() + pos_invoice.delete() + + def test_processing_of_only_one_succes_callback_payload(self): + create_mpesa_settings(payment_gateway_name="Payment") + mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account") + frappe.db.set_value("Account", mpesa_account, "account_currency", "KES") + frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500") + frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES") + + pos_invoice = create_pos_invoice(do_not_submit=1) + pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 1000}) + pos_invoice.contact_mobile = "093456543894" + pos_invoice.currency = "KES" + pos_invoice.save() + + pr = pos_invoice.create_payment_request() + # test payment request creation + self.assertEquals(pr.payment_gateway, "Mpesa-Payment") + + # submitting payment request creates integration requests with random id + integration_req_ids = frappe.get_all("Integration Request", filters={ + 'reference_doctype': pr.doctype, + 'reference_docname': pr.name, + }, pluck="name") + + # create random receipt nos and send it as response to callback handler + mpesa_receipt_numbers = [frappe.utils.random_string(5) for d in integration_req_ids] + + callback_response = get_payment_callback_payload( + Amount=500, + CheckoutRequestID=integration_req_ids[0], + MpesaReceiptNumber=mpesa_receipt_numbers[0] + ) + # handle response manually + verify_transaction(**callback_response) + # test completion of integration request + integration_request = frappe.get_doc("Integration Request", integration_req_ids[0]) + self.assertEquals(integration_request.status, "Completed") + + # now one request is completed + # second integration request fails + # now retrying payment request should make only one integration request again + pr = pos_invoice.create_payment_request() + new_integration_req_ids = frappe.get_all("Integration Request", filters={ + 'reference_doctype': pr.doctype, + 'reference_docname': pr.name, + 'name': ['not in', integration_req_ids] + }, pluck="name") + + self.assertEquals(len(new_integration_req_ids), 1) + + frappe.db.set_value("Customer", "_Test Customer", "default_currency", "") + frappe.db.sql("delete from `tabIntegration Request` where integration_request_service = 'Mpesa'") + pr.reload() + pr.cancel() + pr.delete() + pos_invoice.delete() def create_mpesa_settings(payment_gateway_name="Express"): if frappe.db.exists("Mpesa Settings", payment_gateway_name): @@ -160,16 +284,19 @@ def get_test_account_balance_response(): } } -def get_payment_request_response_payload(): +def get_payment_request_response_payload(Amount=500): """Response received after successfully calling the stk push process request API.""" + + CheckoutRequestID = frappe.utils.random_string(10) + return { "MerchantRequestID": "8071-27184008-1", - "CheckoutRequestID": "ws_CO_061020201133231972", + "CheckoutRequestID": CheckoutRequestID, "ResultCode": 0, "ResultDesc": "The service request is processed successfully.", "CallbackMetadata": { "Item": [ - { "Name": "Amount", "Value": 500.0 }, + { "Name": "Amount", "Value": Amount }, { "Name": "MpesaReceiptNumber", "Value": "LGR7OWQX0R" }, { "Name": "TransactionDate", "Value": 20201006113336 }, { "Name": "PhoneNumber", "Value": 254723575670 } @@ -177,41 +304,26 @@ def get_payment_request_response_payload(): } } - -def get_payment_callback_payload(): +def get_payment_callback_payload(Amount=500, CheckoutRequestID="ws_CO_061020201133231972", MpesaReceiptNumber="LGR7OWQX0R"): """Response received from the server as callback after calling the stkpush process request API.""" return { "Body":{ - "stkCallback":{ - "MerchantRequestID":"19465-780693-1", - "CheckoutRequestID":"ws_CO_061020201133231972", - "ResultCode":0, - "ResultDesc":"The service request is processed successfully.", - "CallbackMetadata":{ - "Item":[ - { - "Name":"Amount", - "Value":500 - }, - { - "Name":"MpesaReceiptNumber", - "Value":"LGR7OWQX0R" - }, - { - "Name":"Balance" - }, - { - "Name":"TransactionDate", - "Value":20170727154800 - }, - { - "Name":"PhoneNumber", - "Value":254721566839 + "stkCallback":{ + "MerchantRequestID":"19465-780693-1", + "CheckoutRequestID":CheckoutRequestID, + "ResultCode":0, + "ResultDesc":"The service request is processed successfully.", + "CallbackMetadata":{ + "Item":[ + { "Name":"Amount", "Value":Amount }, + { "Name":"MpesaReceiptNumber", "Value":MpesaReceiptNumber }, + { "Name":"Balance" }, + { "Name":"TransactionDate", "Value":20170727154800 }, + { "Name":"PhoneNumber", "Value":254721566839 } + ] } - ] } } - } } def get_account_balance_callback_payload(): diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py index 66d0e5f77d..5f990cdd03 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py @@ -20,7 +20,7 @@ class PlaidConnector(): client_id=self.settings.plaid_client_id, secret=self.settings.get_password("plaid_secret"), environment=self.settings.plaid_env, - api_version="2019-05-29" + api_version="2020-09-14" ) def get_access_token(self, public_token): @@ -29,7 +29,7 @@ class PlaidConnector(): response = self.client.Item.public_token.exchange(public_token) access_token = response["access_token"] return access_token - + def get_token_request(self, update_mode=False): country_codes = ["US", "CA", "FR", "IE", "NL", "ES", "GB"] if self.settings.enable_european_access else ["US", "CA"] args = { diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index 70c7f3fe5d..21f6fee79c 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -204,8 +204,8 @@ def new_bank_transaction(transaction): "date": getdate(transaction["date"]), "status": status, "bank_account": bank_account, - "debit": debit, - "credit": credit, + "deposit": debit, + "withdrawal": credit, "currency": transaction["iso_currency_code"], "transaction_id": transaction["transaction_id"], "reference_number": transaction["payment_meta"]["reference_number"], diff --git a/erpnext/healthcare/doctype/appointment_type/appointment_type.js b/erpnext/healthcare/doctype/appointment_type/appointment_type.js index 15916a5134..861675acea 100644 --- a/erpnext/healthcare/doctype/appointment_type/appointment_type.js +++ b/erpnext/healthcare/doctype/appointment_type/appointment_type.js @@ -2,4 +2,82 @@ // For license information, please see license.txt frappe.ui.form.on('Appointment Type', { + refresh: function(frm) { + frm.set_query('price_list', function() { + return { + filters: {'selling': 1} + }; + }); + + frm.set_query('medical_department', 'items', function(doc) { + let item_list = doc.items.map(({medical_department}) => medical_department); + return { + filters: [ + ['Medical Department', 'name', 'not in', item_list] + ] + }; + }); + + frm.set_query('op_consulting_charge_item', 'items', function() { + return { + filters: { + is_stock_item: 0 + } + }; + }); + + frm.set_query('inpatient_visit_charge_item', 'items', function() { + return { + filters: { + is_stock_item: 0 + } + }; + }); + } }); + +frappe.ui.form.on('Appointment Type Service Item', { + op_consulting_charge_item: function(frm, cdt, cdn) { + let d = locals[cdt][cdn]; + if (frm.doc.price_list && d.op_consulting_charge_item) { + frappe.call({ + 'method': 'frappe.client.get_value', + args: { + 'doctype': 'Item Price', + 'filters': { + 'item_code': d.op_consulting_charge_item, + 'price_list': frm.doc.price_list + }, + 'fieldname': ['price_list_rate'] + }, + callback: function(data) { + if (data.message.price_list_rate) { + frappe.model.set_value(cdt, cdn, 'op_consulting_charge', data.message.price_list_rate); + } + } + }); + } + }, + + inpatient_visit_charge_item: function(frm, cdt, cdn) { + let d = locals[cdt][cdn]; + if (frm.doc.price_list && d.inpatient_visit_charge_item) { + frappe.call({ + 'method': 'frappe.client.get_value', + args: { + 'doctype': 'Item Price', + 'filters': { + 'item_code': d.inpatient_visit_charge_item, + 'price_list': frm.doc.price_list + }, + 'fieldname': ['price_list_rate'] + }, + callback: function (data) { + if (data.message.price_list_rate) { + frappe.model.set_value(cdt, cdn, 'inpatient_visit_charge', data.message.price_list_rate); + } + } + }); + } + } +}); \ No newline at end of file diff --git a/erpnext/healthcare/doctype/appointment_type/appointment_type.json b/erpnext/healthcare/doctype/appointment_type/appointment_type.json index 58753bb4f0..3872318287 100644 --- a/erpnext/healthcare/doctype/appointment_type/appointment_type.json +++ b/erpnext/healthcare/doctype/appointment_type/appointment_type.json @@ -12,7 +12,10 @@ "appointment_type", "ip", "default_duration", - "color" + "color", + "billing_section", + "price_list", + "items" ], "fields": [ { @@ -52,10 +55,27 @@ "label": "Color", "no_copy": 1, "report_hide": 1 + }, + { + "fieldname": "billing_section", + "fieldtype": "Section Break", + "label": "Billing" + }, + { + "fieldname": "price_list", + "fieldtype": "Link", + "label": "Price List", + "options": "Price List" + }, + { + "fieldname": "items", + "fieldtype": "Table", + "label": "Appointment Type Service Items", + "options": "Appointment Type Service Item" } ], "links": [], - "modified": "2020-02-03 21:06:05.833050", + "modified": "2021-01-22 09:41:05.010524", "modified_by": "Administrator", "module": "Healthcare", "name": "Appointment Type", diff --git a/erpnext/healthcare/doctype/appointment_type/appointment_type.py b/erpnext/healthcare/doctype/appointment_type/appointment_type.py index 1dacffab35..67a24f31e0 100644 --- a/erpnext/healthcare/doctype/appointment_type/appointment_type.py +++ b/erpnext/healthcare/doctype/appointment_type/appointment_type.py @@ -4,6 +4,53 @@ from __future__ import unicode_literals from frappe.model.document import Document +import frappe class AppointmentType(Document): - pass + def validate(self): + if self.items and self.price_list: + for item in self.items: + existing_op_item_price = frappe.db.exists('Item Price', { + 'item_code': item.op_consulting_charge_item, + 'price_list': self.price_list + }) + + if not existing_op_item_price and item.op_consulting_charge_item and item.op_consulting_charge: + make_item_price(self.price_list, item.op_consulting_charge_item, item.op_consulting_charge) + + existing_ip_item_price = frappe.db.exists('Item Price', { + 'item_code': item.inpatient_visit_charge_item, + 'price_list': self.price_list + }) + + if not existing_ip_item_price and item.inpatient_visit_charge_item and item.inpatient_visit_charge: + make_item_price(self.price_list, item.inpatient_visit_charge_item, item.inpatient_visit_charge) + +@frappe.whitelist() +def get_service_item_based_on_department(appointment_type, department): + item_list = frappe.db.get_value('Appointment Type Service Item', + filters = {'medical_department': department, 'parent': appointment_type}, + fieldname = ['op_consulting_charge_item', + 'inpatient_visit_charge_item', 'op_consulting_charge', 'inpatient_visit_charge'], + as_dict = 1 + ) + + # if department wise items are not set up + # use the generic items + if not item_list: + item_list = frappe.db.get_value('Appointment Type Service Item', + filters = {'parent': appointment_type}, + fieldname = ['op_consulting_charge_item', + 'inpatient_visit_charge_item', 'op_consulting_charge', 'inpatient_visit_charge'], + as_dict = 1 + ) + + return item_list + +def make_item_price(price_list, item, item_price): + frappe.get_doc({ + 'doctype': 'Item Price', + 'price_list': price_list, + 'item_code': item, + 'price_list_rate': item_price + }).insert(ignore_permissions=True, ignore_mandatory=True) diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/__init__.py b/erpnext/healthcare/doctype/appointment_type_service_item/__init__.py similarity index 100% rename from erpnext/accounts/doctype/bank_statement_transaction_entry/__init__.py rename to erpnext/healthcare/doctype/appointment_type_service_item/__init__.py diff --git a/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json new file mode 100644 index 0000000000..5ff68cd682 --- /dev/null +++ b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json @@ -0,0 +1,67 @@ +{ + "actions": [], + "creation": "2021-01-22 09:34:53.373105", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "medical_department", + "op_consulting_charge_item", + "op_consulting_charge", + "column_break_4", + "inpatient_visit_charge_item", + "inpatient_visit_charge" + ], + "fields": [ + { + "fieldname": "medical_department", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Medical Department", + "options": "Medical Department" + }, + { + "fieldname": "op_consulting_charge_item", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Out Patient Consulting Charge Item", + "options": "Item" + }, + { + "fieldname": "op_consulting_charge", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Out Patient Consulting Charge" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "inpatient_visit_charge_item", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Inpatient Visit Charge Item", + "options": "Item" + }, + { + "fieldname": "inpatient_visit_charge", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Inpatient Visit Charge Item" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-01-22 09:35:26.503443", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Appointment Type Service Item", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.py b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py similarity index 56% rename from erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.py rename to erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py index cb1b15815f..b2e0e82bad 100644 --- a/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.py +++ b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2017, sathishpy@gmail.com and contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt from __future__ import unicode_literals -import frappe +# import frappe from frappe.model.document import Document -class BankStatementTransactionInvoiceItem(Document): +class AppointmentTypeServiceItem(Document): pass diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py index c324228467..325c2094fb 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py +++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py @@ -121,6 +121,7 @@ class ClinicalProcedure(Document): stock_entry.stock_entry_type = 'Material Receipt' stock_entry.to_warehouse = self.warehouse + stock_entry.company = self.company expense_account = get_account(None, 'expense_account', 'Healthcare Settings', self.company) for item in self.items: if item.qty > item.actual_qty: diff --git a/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py index 4ee5f6bad3..fb72073a07 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py +++ b/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- + # -*- coding: utf-8 -*- # Copyright (c) 2017, ESS LLP and Contributors # See license.txt from __future__ import unicode_literals @@ -60,6 +60,7 @@ def create_procedure(procedure_template, patient, practitioner): procedure.practitioner = practitioner procedure.consume_stock = procedure_template.allow_stock_consumption procedure.items = procedure_template.items - procedure.warehouse = frappe.db.get_single_value('Stock Settings', 'default_warehouse') + procedure.company = "_Test Company" + procedure.warehouse = "_Test Warehouse - _TC" procedure.submit() return procedure \ No newline at end of file diff --git a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json index cb747f95ef..8162f03f6d 100644 --- a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json +++ b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json @@ -159,6 +159,7 @@ "fieldname": "op_consulting_charge", "fieldtype": "Currency", "label": "Out Patient Consulting Charge", + "mandatory_depends_on": "op_consulting_charge_item", "options": "Currency" }, { @@ -174,7 +175,8 @@ { "fieldname": "inpatient_visit_charge", "fieldtype": "Currency", - "label": "Inpatient Visit Charge" + "label": "Inpatient Visit Charge", + "mandatory_depends_on": "inpatient_visit_charge_item" }, { "depends_on": "eval: !doc.__islocal", @@ -280,7 +282,7 @@ ], "image_field": "image", "links": [], - "modified": "2020-04-06 13:44:24.759623", + "modified": "2021-01-22 10:14:43.187675", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare Practitioner", diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js index 3d5073b13e..0354733dfb 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js @@ -24,11 +24,13 @@ frappe.ui.form.on('Patient Appointment', { }); frm.set_query('practitioner', function() { - return { - filters: { - 'department': frm.doc.department - } - }; + if (frm.doc.department) { + return { + filters: { + 'department': frm.doc.department + } + }; + } }); frm.set_query('service_unit', function() { @@ -140,6 +142,20 @@ frappe.ui.form.on('Patient Appointment', { patient: function(frm) { if (frm.doc.patient) { frm.trigger('toggle_payment_fields'); + frappe.call({ + method: 'frappe.client.get', + args: { + doctype: 'Patient', + name: frm.doc.patient + }, + callback: function (data) { + let age = null; + if (data.message.dob) { + age = calculate_age(data.message.dob); + } + frappe.model.set_value(frm.doctype, frm.docname, 'patient_age', age); + } + }); } else { frm.set_value('patient_name', ''); frm.set_value('patient_sex', ''); @@ -148,6 +164,37 @@ frappe.ui.form.on('Patient Appointment', { } }, + practitioner: function(frm) { + if (frm.doc.practitioner ) { + frm.events.set_payment_details(frm); + } + }, + + appointment_type: function(frm) { + if (frm.doc.appointment_type) { + frm.events.set_payment_details(frm); + } + }, + + set_payment_details: function(frm) { + frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing').then(val => { + if (val) { + frappe.call({ + method: 'erpnext.healthcare.utils.get_service_item_and_practitioner_charge', + args: { + doc: frm.doc + }, + callback: function(data) { + if (data.message) { + frappe.model.set_value(frm.doctype, frm.docname, 'paid_amount', data.message.practitioner_charge); + frappe.model.set_value(frm.doctype, frm.docname, 'billing_item', data.message.service_item); + } + } + }); + } + }); + }, + therapy_plan: function(frm) { frm.trigger('set_therapy_type_filter'); }, @@ -190,14 +237,18 @@ frappe.ui.form.on('Patient Appointment', { // show payment fields as non-mandatory frm.toggle_display('mode_of_payment', 0); frm.toggle_display('paid_amount', 0); + frm.toggle_display('billing_item', 0); frm.toggle_reqd('mode_of_payment', 0); frm.toggle_reqd('paid_amount', 0); + frm.toggle_reqd('billing_item', 0); } else { // if automated appointment invoicing is disabled, hide fields frm.toggle_display('mode_of_payment', data.message ? 1 : 0); frm.toggle_display('paid_amount', data.message ? 1 : 0); + frm.toggle_display('billing_item', data.message ? 1 : 0); frm.toggle_reqd('mode_of_payment', data.message ? 1 : 0); frm.toggle_reqd('paid_amount', data.message ? 1 :0); + frm.toggle_reqd('billing_item', data.message ? 1 : 0); } } }); @@ -540,57 +591,6 @@ let update_status = function(frm, status){ ); }; -frappe.ui.form.on('Patient Appointment', 'practitioner', function(frm) { - if (frm.doc.practitioner) { - frappe.call({ - method: 'frappe.client.get', - args: { - doctype: 'Healthcare Practitioner', - name: frm.doc.practitioner - }, - callback: function (data) { - frappe.model.set_value(frm.doctype, frm.docname, 'department', data.message.department); - frappe.model.set_value(frm.doctype, frm.docname, 'paid_amount', data.message.op_consulting_charge); - frappe.model.set_value(frm.doctype, frm.docname, 'billing_item', data.message.op_consulting_charge_item); - } - }); - } -}); - -frappe.ui.form.on('Patient Appointment', 'patient', function(frm) { - if (frm.doc.patient) { - frappe.call({ - method: 'frappe.client.get', - args: { - doctype: 'Patient', - name: frm.doc.patient - }, - callback: function (data) { - let age = null; - if (data.message.dob) { - age = calculate_age(data.message.dob); - } - frappe.model.set_value(frm.doctype,frm.docname, 'patient_age', age); - } - }); - } -}); - -frappe.ui.form.on('Patient Appointment', 'appointment_type', function(frm) { - if (frm.doc.appointment_type) { - frappe.call({ - method: 'frappe.client.get', - args: { - doctype: 'Appointment Type', - name: frm.doc.appointment_type - }, - callback: function(data) { - frappe.model.set_value(frm.doctype,frm.docname, 'duration',data.message.default_duration); - } - }); - } -}); - let calculate_age = function(birth) { let ageMS = Date.parse(Date()) - Date.parse(birth); let age = new Date(); diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json index 35600e4809..83c92af36a 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json @@ -19,19 +19,19 @@ "inpatient_record", "column_break_1", "company", + "practitioner", + "practitioner_name", + "department", "service_unit", + "section_break_12", + "appointment_type", + "duration", "procedure_template", "get_procedure_from_encounter", "procedure_prescription", "therapy_plan", "therapy_type", "get_prescribed_therapies", - "practitioner", - "practitioner_name", - "department", - "section_break_12", - "appointment_type", - "duration", "column_break_17", "appointment_date", "appointment_time", @@ -79,6 +79,7 @@ "set_only_once": 1 }, { + "fetch_from": "appointment_type.default_duration", "fieldname": "duration", "fieldtype": "Int", "in_filter": 1, @@ -144,7 +145,6 @@ "in_standard_filter": 1, "label": "Healthcare Practitioner", "options": "Healthcare Practitioner", - "read_only": 1, "reqd": 1, "search_index": 1, "set_only_once": 1 @@ -158,7 +158,6 @@ "in_standard_filter": 1, "label": "Department", "options": "Medical Department", - "read_only": 1, "search_index": 1, "set_only_once": 1 }, @@ -227,12 +226,14 @@ "fieldname": "mode_of_payment", "fieldtype": "Link", "label": "Mode of Payment", - "options": "Mode of Payment" + "options": "Mode of Payment", + "read_only_depends_on": "invoiced" }, { "fieldname": "paid_amount", "fieldtype": "Currency", - "label": "Paid Amount" + "label": "Paid Amount", + "read_only_depends_on": "invoiced" }, { "fieldname": "column_break_2", @@ -302,7 +303,8 @@ "fieldname": "therapy_plan", "fieldtype": "Link", "label": "Therapy Plan", - "options": "Therapy Plan" + "options": "Therapy Plan", + "set_only_once": 1 }, { "fieldname": "ref_sales_invoice", @@ -347,7 +349,7 @@ } ], "links": [], - "modified": "2020-12-16 13:16:58.578503", + "modified": "2021-02-08 13:13:15.116833", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient Appointment", diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index f2b94b8e9c..1f76cd624c 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -26,6 +26,7 @@ class PatientAppointment(Document): def after_insert(self): self.update_prescription_details() + self.set_payment_details() invoice_appointment(self) self.update_fee_validity() send_confirmation_msg(self) @@ -85,6 +86,13 @@ class PatientAppointment(Document): def set_appointment_datetime(self): self.appointment_datetime = "%s %s" % (self.appointment_date, self.appointment_time or "00:00:00") + def set_payment_details(self): + if frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing'): + details = get_service_item_and_practitioner_charge(self) + self.db_set('billing_item', details.get('service_item')) + if not self.paid_amount: + self.db_set('paid_amount', details.get('practitioner_charge')) + def validate_customer_created(self): if frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing'): if not frappe.db.get_value('Patient', self.patient, 'customer'): @@ -148,31 +156,37 @@ def invoice_appointment(appointment_doc): fee_validity = None if automate_invoicing and not appointment_invoiced and not fee_validity: - sales_invoice = frappe.new_doc('Sales Invoice') - sales_invoice.patient = appointment_doc.patient - sales_invoice.customer = frappe.get_value('Patient', appointment_doc.patient, 'customer') - sales_invoice.appointment = appointment_doc.name - sales_invoice.due_date = getdate() - sales_invoice.company = appointment_doc.company - sales_invoice.debit_to = get_receivable_account(appointment_doc.company) + create_sales_invoice(appointment_doc) - item = sales_invoice.append('items', {}) - item = get_appointment_item(appointment_doc, item) - # Add payments if payment details are supplied else proceed to create invoice as Unpaid - if appointment_doc.mode_of_payment and appointment_doc.paid_amount: - sales_invoice.is_pos = 1 - payment = sales_invoice.append('payments', {}) - payment.mode_of_payment = appointment_doc.mode_of_payment - payment.amount = appointment_doc.paid_amount +def create_sales_invoice(appointment_doc): + sales_invoice = frappe.new_doc('Sales Invoice') + sales_invoice.patient = appointment_doc.patient + sales_invoice.customer = frappe.get_value('Patient', appointment_doc.patient, 'customer') + sales_invoice.appointment = appointment_doc.name + sales_invoice.due_date = getdate() + sales_invoice.company = appointment_doc.company + sales_invoice.debit_to = get_receivable_account(appointment_doc.company) - sales_invoice.set_missing_values(for_validate=True) - sales_invoice.flags.ignore_mandatory = True - sales_invoice.save(ignore_permissions=True) - sales_invoice.submit() - frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True) - frappe.db.set_value('Patient Appointment', appointment_doc.name, 'invoiced', 1) - frappe.db.set_value('Patient Appointment', appointment_doc.name, 'ref_sales_invoice', sales_invoice.name) + item = sales_invoice.append('items', {}) + item = get_appointment_item(appointment_doc, item) + + # Add payments if payment details are supplied else proceed to create invoice as Unpaid + if appointment_doc.mode_of_payment and appointment_doc.paid_amount: + sales_invoice.is_pos = 1 + payment = sales_invoice.append('payments', {}) + payment.mode_of_payment = appointment_doc.mode_of_payment + payment.amount = appointment_doc.paid_amount + + sales_invoice.set_missing_values(for_validate=True) + sales_invoice.flags.ignore_mandatory = True + sales_invoice.save(ignore_permissions=True) + sales_invoice.submit() + frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True) + frappe.db.set_value('Patient Appointment', appointment_doc.name, { + 'invoiced': 1, + 'ref_sales_invoice': sales_invoice.name + }) def check_is_new_patient(patient, name=None): @@ -187,13 +201,14 @@ def check_is_new_patient(patient, name=None): def get_appointment_item(appointment_doc, item): - service_item, practitioner_charge = get_service_item_and_practitioner_charge(appointment_doc) - item.item_code = service_item + details = get_service_item_and_practitioner_charge(appointment_doc) + charge = appointment_doc.paid_amount or details.get('practitioner_charge') + item.item_code = details.get('service_item') item.description = _('Consulting Charges: {0}').format(appointment_doc.practitioner) item.income_account = get_income_account(appointment_doc.practitioner, appointment_doc.company) item.cost_center = frappe.get_cached_value('Company', appointment_doc.company, 'cost_center') - item.rate = practitioner_charge - item.amount = practitioner_charge + item.rate = charge + item.amount = charge item.qty = 1 item.reference_dt = 'Patient Appointment' item.reference_dn = appointment_doc.name diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index f7ec6f58fc..2bb8a53c45 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -32,7 +32,8 @@ class TestPatientAppointment(unittest.TestCase): patient, medical_department, practitioner = create_healthcare_docs() frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4), invoice = 1) - self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced'), 1) + appointment.reload() + self.assertEqual(appointment.invoiced, 1) encounter = make_encounter(appointment.name) self.assertTrue(encounter) self.assertEqual(encounter.company, appointment.company) @@ -41,7 +42,7 @@ class TestPatientAppointment(unittest.TestCase): # invoiced flag mapped from appointment self.assertEqual(encounter.invoiced, frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced')) - def test_invoicing(self): + def test_auto_invoicing(self): patient, medical_department, practitioner = create_healthcare_docs() frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0) @@ -57,6 +58,50 @@ class TestPatientAppointment(unittest.TestCase): self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'patient'), appointment.patient) self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount) + def test_auto_invoicing_based_on_department(self): + patient, medical_department, practitioner = create_healthcare_docs() + frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) + frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) + appointment_type = create_appointment_type() + + appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2), + invoice=1, appointment_type=appointment_type.name, department='_Test Medical Department') + appointment.reload() + + self.assertEqual(appointment.invoiced, 1) + self.assertEqual(appointment.billing_item, 'HLC-SI-001') + self.assertEqual(appointment.paid_amount, 200) + + sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent') + self.assertTrue(sales_invoice_name) + self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount) + + def test_auto_invoicing_according_to_appointment_type_charge(self): + patient, medical_department, practitioner = create_healthcare_docs() + frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) + frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) + + item = create_healthcare_service_items() + items = [{ + 'op_consulting_charge_item': item, + 'op_consulting_charge': 300 + }] + appointment_type = create_appointment_type(args={ + 'name': 'Generic Appointment Type charge', + 'items': items + }) + + appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2), + invoice=1, appointment_type=appointment_type.name) + appointment.reload() + + self.assertEqual(appointment.invoiced, 1) + self.assertEqual(appointment.billing_item, item) + self.assertEqual(appointment.paid_amount, 300) + + sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent') + self.assertTrue(sales_invoice_name) + def test_appointment_cancel(self): patient, medical_department, practitioner = create_healthcare_docs() frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1) @@ -178,14 +223,15 @@ def create_encounter(appointment): encounter.submit() return encounter -def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0, service_unit=None, save=1): +def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0, + service_unit=None, appointment_type=None, save=1, department=None): item = create_healthcare_service_items() frappe.db.set_value('Healthcare Settings', None, 'inpatient_visit_charge_item', item) frappe.db.set_value('Healthcare Settings', None, 'op_consulting_charge_item', item) appointment = frappe.new_doc('Patient Appointment') appointment.patient = patient appointment.practitioner = practitioner - appointment.department = '_Test Medical Department' + appointment.department = department or '_Test Medical Department' appointment.appointment_date = appointment_date appointment.company = '_Test Company' appointment.duration = 15 @@ -193,7 +239,8 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce appointment.service_unit = service_unit if invoice: appointment.mode_of_payment = 'Cash' - appointment.paid_amount = 500 + if appointment_type: + appointment.appointment_type = appointment_type if procedure_template: appointment.procedure_template = create_clinical_procedure_template().get('name') if save: @@ -223,4 +270,29 @@ def create_clinical_procedure_template(): template.description = 'Knee Surgery and Rehab' template.rate = 50000 template.save() - return template \ No newline at end of file + return template + +def create_appointment_type(args=None): + if not args: + args = frappe.local.form_dict + + name = args.get('name') or 'Test Appointment Type wise Charge' + + if frappe.db.exists('Appointment Type', name): + return frappe.get_doc('Appointment Type', name) + + else: + item = create_healthcare_service_items() + items = [{ + 'medical_department': '_Test Medical Department', + 'op_consulting_charge_item': item, + 'op_consulting_charge': 200 + }] + return frappe.get_doc({ + 'doctype': 'Appointment Type', + 'appointment_type': args.get('name') or 'Test Appointment Type wise Charge', + 'default_duration': args.get('default_duration') or 20, + 'color': args.get('color') or '#7575ff', + 'price_list': args.get('price_list') or frappe.db.get_value("Price List", {"selling": 1}), + 'items': args.get('items') or items + }).insert() \ No newline at end of file diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py index d4027dff4e..d3d22c80b6 100644 --- a/erpnext/healthcare/utils.py +++ b/erpnext/healthcare/utils.py @@ -5,9 +5,11 @@ from __future__ import unicode_literals import math import frappe +import json from frappe import _ from frappe.utils.formatters import format_value from frappe.utils import time_diff_in_hours, rounded +from six import string_types from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_income_account from erpnext.healthcare.doctype.fee_validity.fee_validity import create_fee_validity from erpnext.healthcare.doctype.lab_test.lab_test import create_multiple @@ -64,7 +66,9 @@ def get_appointments_to_invoice(patient, company): income_account = None service_item = None if appointment.practitioner: - service_item, practitioner_charge = get_service_item_and_practitioner_charge(appointment) + details = get_service_item_and_practitioner_charge(appointment) + service_item = details.get('service_item') + practitioner_charge = details.get('practitioner_charge') income_account = get_income_account(appointment.practitioner, appointment.company) appointments_to_invoice.append({ 'reference_type': 'Patient Appointment', @@ -97,7 +101,9 @@ def get_encounters_to_invoice(patient, company): frappe.db.get_single_value('Healthcare Settings', 'do_not_bill_inpatient_encounters'): continue - service_item, practitioner_charge = get_service_item_and_practitioner_charge(encounter) + details = get_service_item_and_practitioner_charge(encounter) + service_item = details.get('service_item') + practitioner_charge = details.get('practitioner_charge') income_account = get_income_account(encounter.practitioner, encounter.company) encounters_to_invoice.append({ @@ -173,7 +179,7 @@ def get_clinical_procedures_to_invoice(patient, company): if procedure.invoice_separately_as_consumables and procedure.consume_stock \ and procedure.status == 'Completed' and not procedure.consumption_invoiced: - service_item = get_healthcare_service_item('clinical_procedure_consumable_item') + service_item = frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item') if not service_item: msg = _('Please Configure Clinical Procedure Consumable Item in ') msg += '''Healthcare Settings''' @@ -304,24 +310,50 @@ def get_therapy_sessions_to_invoice(patient, company): return therapy_sessions_to_invoice - +@frappe.whitelist() def get_service_item_and_practitioner_charge(doc): + if isinstance(doc, string_types): + doc = json.loads(doc) + doc = frappe.get_doc(doc) + + service_item = None + practitioner_charge = None + department = doc.medical_department if doc.doctype == 'Patient Encounter' else doc.department + is_inpatient = doc.inpatient_record - if is_inpatient: - service_item = get_practitioner_service_item(doc.practitioner, 'inpatient_visit_charge_item') + + if doc.get('appointment_type'): + service_item, practitioner_charge = get_appointment_type_service_item(doc.appointment_type, department, is_inpatient) + + if not service_item and not practitioner_charge: + service_item, practitioner_charge = get_practitioner_service_item(doc.practitioner, is_inpatient) if not service_item: - service_item = get_healthcare_service_item('inpatient_visit_charge_item') - else: - service_item = get_practitioner_service_item(doc.practitioner, 'op_consulting_charge_item') - if not service_item: - service_item = get_healthcare_service_item('op_consulting_charge_item') + service_item = get_healthcare_service_item(is_inpatient) + if not service_item: throw_config_service_item(is_inpatient) - practitioner_charge = get_practitioner_charge(doc.practitioner, is_inpatient) if not practitioner_charge: throw_config_practitioner_charge(is_inpatient, doc.practitioner) + return {'service_item': service_item, 'practitioner_charge': practitioner_charge} + + +def get_appointment_type_service_item(appointment_type, department, is_inpatient): + from erpnext.healthcare.doctype.appointment_type.appointment_type import get_service_item_based_on_department + + item_list = get_service_item_based_on_department(appointment_type, department) + service_item = None + practitioner_charge = None + + if item_list: + if is_inpatient: + service_item = item_list.get('inpatient_visit_charge_item') + practitioner_charge = item_list.get('inpatient_visit_charge') + else: + service_item = item_list.get('op_consulting_charge_item') + practitioner_charge = item_list.get('op_consulting_charge') + return service_item, practitioner_charge @@ -345,12 +377,27 @@ def throw_config_practitioner_charge(is_inpatient, practitioner): frappe.throw(msg, title=_('Missing Configuration')) -def get_practitioner_service_item(practitioner, service_item_field): - return frappe.db.get_value('Healthcare Practitioner', practitioner, service_item_field) +def get_practitioner_service_item(practitioner, is_inpatient): + service_item = None + practitioner_charge = None + + if is_inpatient: + service_item, practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, ['inpatient_visit_charge_item', 'inpatient_visit_charge']) + else: + service_item, practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, ['op_consulting_charge_item', 'op_consulting_charge']) + + return service_item, practitioner_charge -def get_healthcare_service_item(service_item_field): - return frappe.db.get_single_value('Healthcare Settings', service_item_field) +def get_healthcare_service_item(is_inpatient): + service_item = None + + if is_inpatient: + service_item = frappe.db.get_single_value('Healthcare Settings', 'inpatient_visit_charge_item') + else: + service_item = frappe.db.get_single_value('Healthcare Settings', 'op_consulting_charge_item') + + return service_item def get_practitioner_charge(practitioner, is_inpatient): @@ -381,7 +428,8 @@ def set_invoiced(item, method, ref_invoice=None): invoiced = True if item.reference_dt == 'Clinical Procedure': - if get_healthcare_service_item('clinical_procedure_consumable_item') == item.item_code: + service_item = frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item') + if service_item == item.item_code: frappe.db.set_value(item.reference_dt, item.reference_dn, 'consumption_invoiced', invoiced) else: frappe.db.set_value(item.reference_dt, item.reference_dn, 'invoiced', invoiced) @@ -403,7 +451,8 @@ def set_invoiced(item, method, ref_invoice=None): def validate_invoiced_on_submit(item): - if item.reference_dt == 'Clinical Procedure' and get_healthcare_service_item('clinical_procedure_consumable_item') == item.item_code: + if item.reference_dt == 'Clinical Procedure' and \ + frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item') == item.item_code: is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'consumption_invoiced') else: is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'invoiced') diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 109d9216e7..39d3659b2b 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -272,6 +272,9 @@ doc_events = { 'Address': { 'validate': ['erpnext.regional.india.utils.validate_gstin_for_india', 'erpnext.regional.italy.utils.set_state_code', 'erpnext.regional.india.utils.update_gst_category'] }, + 'Supplier': { + 'validate': 'erpnext.regional.india.utils.validate_pan_for_india' + }, ('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): { 'validate': ['erpnext.regional.india.utils.set_place_of_supply'] }, @@ -399,6 +402,7 @@ regional_overrides = { 'erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_header': 'erpnext.regional.india.utils.get_itemised_tax_breakup_header', 'erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_data': 'erpnext.regional.india.utils.get_itemised_tax_breakup_data', 'erpnext.accounts.party.get_regional_address_details': 'erpnext.regional.india.utils.get_regional_address_details', + 'erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts': 'erpnext.regional.india.utils.get_regional_round_off_accounts', 'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption', 'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period', 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries', diff --git a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html index 6324b04927..9f667a6835 100644 --- a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html +++ b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html @@ -4,11 +4,11 @@ {{ __("Leave Type") }} - {{ __("Total Allocated Leaves") }} - {{ __("Expired Leaves") }} - {{ __("Used Leaves") }} - {{ __("Pending Leaves") }} - {{ __("Available Leaves") }} + {{ __("Total Allocated Leave") }} + {{ __("Expired Leave") }} + {{ __("Used Leave") }} + {{ __("Pending Leave") }} + {{ __("Available Leave") }} @@ -25,5 +25,5 @@ {% else %} -

    No Leaves have been allocated.

    -{% endif %} \ No newline at end of file +

    No Leave has been allocated.

    +{% endif %} diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index 1b92358184..06f9160363 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -40,17 +40,17 @@ def get_columns(): 'fieldname': 'opening_balance', 'width': 130, }, { - 'label': _('Leaves Allocated'), + 'label': _('Leave Allocated'), 'fieldtype': 'float', 'fieldname': 'leaves_allocated', 'width': 130, }, { - 'label': _('Leaves Taken'), + 'label': _('Leave Taken'), 'fieldtype': 'float', 'fieldname': 'leaves_taken', 'width': 130, }, { - 'label': _('Leaves Expired'), + 'label': _('Leave Expired'), 'fieldtype': 'float', 'fieldname': 'leaves_expired', 'width': 130, diff --git a/erpnext/loan_management/dashboard_chart/loan_disbursements/loan_disbursements.json b/erpnext/loan_management/dashboard_chart/loan_disbursements/loan_disbursements.json new file mode 100644 index 0000000000..b8abf210f8 --- /dev/null +++ b/erpnext/loan_management/dashboard_chart/loan_disbursements/loan_disbursements.json @@ -0,0 +1,29 @@ +{ + "based_on": "disbursement_date", + "chart_name": "Loan Disbursements", + "chart_type": "Sum", + "creation": "2021-02-06 18:40:36.148470", + "docstatus": 0, + "doctype": "Dashboard Chart", + "document_type": "Loan Disbursement", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Loan Disbursement\",\"docstatus\",\"=\",\"1\",false]]", + "group_by_type": "Count", + "idx": 0, + "is_public": 0, + "is_standard": 1, + "modified": "2021-02-06 18:40:49.308663", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Disbursements", + "number_of_groups": 0, + "owner": "Administrator", + "source": "", + "time_interval": "Daily", + "timeseries": 1, + "timespan": "Last Month", + "type": "Line", + "use_report_chart": 0, + "value_based_on": "disbursed_amount", + "y_axis": [] +} \ No newline at end of file diff --git a/erpnext/loan_management/dashboard_chart/loan_interest_accrual/loan_interest_accrual.json b/erpnext/loan_management/dashboard_chart/loan_interest_accrual/loan_interest_accrual.json new file mode 100644 index 0000000000..aa0f78a2f6 --- /dev/null +++ b/erpnext/loan_management/dashboard_chart/loan_interest_accrual/loan_interest_accrual.json @@ -0,0 +1,31 @@ +{ + "based_on": "posting_date", + "chart_name": "Loan Interest Accrual", + "chart_type": "Sum", + "color": "#39E4A5", + "creation": "2021-02-18 20:07:04.843876", + "docstatus": 0, + "doctype": "Dashboard Chart", + "document_type": "Loan Interest Accrual", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Loan Interest Accrual\",\"docstatus\",\"=\",\"1\",false]]", + "group_by_type": "Count", + "idx": 0, + "is_public": 0, + "is_standard": 1, + "last_synced_on": "2021-02-21 21:01:26.022634", + "modified": "2021-02-21 21:01:44.930712", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Interest Accrual", + "number_of_groups": 0, + "owner": "Administrator", + "source": "", + "time_interval": "Monthly", + "timeseries": 1, + "timespan": "Last Year", + "type": "Line", + "use_report_chart": 0, + "value_based_on": "interest_amount", + "y_axis": [] +} \ No newline at end of file diff --git a/erpnext/loan_management/dashboard_chart/new_loans/new_loans.json b/erpnext/loan_management/dashboard_chart/new_loans/new_loans.json new file mode 100644 index 0000000000..35bd43b994 --- /dev/null +++ b/erpnext/loan_management/dashboard_chart/new_loans/new_loans.json @@ -0,0 +1,31 @@ +{ + "based_on": "creation", + "chart_name": "New Loans", + "chart_type": "Count", + "color": "#449CF0", + "creation": "2021-02-06 16:59:27.509170", + "docstatus": 0, + "doctype": "Dashboard Chart", + "document_type": "Loan", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Loan\",\"docstatus\",\"=\",\"1\",false]]", + "group_by_type": "Count", + "idx": 0, + "is_public": 0, + "is_standard": 1, + "last_synced_on": "2021-02-21 20:55:33.515025", + "modified": "2021-02-21 21:00:33.900821", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "New Loans", + "number_of_groups": 0, + "owner": "Administrator", + "source": "", + "time_interval": "Daily", + "timeseries": 1, + "timespan": "Last Month", + "type": "Bar", + "use_report_chart": 0, + "value_based_on": "", + "y_axis": [] +} \ No newline at end of file diff --git a/erpnext/loan_management/dashboard_chart/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json b/erpnext/loan_management/dashboard_chart/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json new file mode 100644 index 0000000000..76c27b062d --- /dev/null +++ b/erpnext/loan_management/dashboard_chart/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json @@ -0,0 +1,31 @@ +{ + "based_on": "", + "chart_name": "Top 10 Pledged Loan Securities", + "chart_type": "Custom", + "color": "#EC864B", + "creation": "2021-02-06 22:02:46.284479", + "docstatus": 0, + "doctype": "Dashboard Chart", + "document_type": "", + "dynamic_filters_json": "[]", + "filters_json": "[]", + "group_by_type": "Count", + "idx": 0, + "is_public": 0, + "is_standard": 1, + "last_synced_on": "2021-02-21 21:00:57.043034", + "modified": "2021-02-21 21:01:10.048623", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Top 10 Pledged Loan Securities", + "number_of_groups": 0, + "owner": "Administrator", + "source": "Top 10 Pledged Loan Securities", + "time_interval": "Yearly", + "timeseries": 0, + "timespan": "Last Year", + "type": "Bar", + "use_report_chart": 0, + "value_based_on": "", + "y_axis": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/__init__.py b/erpnext/loan_management/dashboard_chart_source/__init__.py similarity index 100% rename from erpnext/accounts/doctype/bank_statement_transaction_invoice_item/__init__.py rename to erpnext/loan_management/dashboard_chart_source/__init__.py diff --git a/erpnext/accounts/doctype/bank_statement_transaction_payment_item/__init__.py b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/__init__.py similarity index 100% rename from erpnext/accounts/doctype/bank_statement_transaction_payment_item/__init__.py rename to erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/__init__.py diff --git a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.js b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.js new file mode 100644 index 0000000000..cf75cc8e41 --- /dev/null +++ b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.js @@ -0,0 +1,14 @@ +frappe.provide('frappe.dashboards.chart_sources'); + +frappe.dashboards.chart_sources["Top 10 Pledged Loan Securities"] = { + method: "erpnext.loan_management.dashboard_chart_source.top_10_pledged_loan_securities.top_10_pledged_loan_securities.get_data", + filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company") + } + ] +}; \ No newline at end of file diff --git a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json new file mode 100644 index 0000000000..42c9b1c335 --- /dev/null +++ b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json @@ -0,0 +1,13 @@ +{ + "creation": "2021-02-06 22:01:01.332628", + "docstatus": 0, + "doctype": "Dashboard Chart Source", + "idx": 0, + "modified": "2021-02-06 22:01:01.332628", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Top 10 Pledged Loan Securities", + "owner": "Administrator", + "source_name": "Top 10 Pledged Loan Securities ", + "timeseries": 0 +} \ No newline at end of file diff --git a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py new file mode 100644 index 0000000000..6bb04401be --- /dev/null +++ b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py @@ -0,0 +1,76 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils.dashboard import cache_source +from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applicant_wise_loan_security_exposure \ + import get_loan_security_details +from six import iteritems + +@frappe.whitelist() +@cache_source +def get_data(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None, + to_date = None, timespan = None, time_interval = None, heatmap_year = None): + if chart_name: + chart = frappe.get_doc('Dashboard Chart', chart_name) + else: + chart = frappe._dict(frappe.parse_json(chart)) + + filters = {} + current_pledges = {} + + if filters: + filters = frappe.parse_json(filters)[0] + + conditions = "" + labels = [] + values = [] + + if filters.get('company'): + conditions = "AND company = %(company)s" + + loan_security_details = get_loan_security_details() + + unpledges = frappe._dict(frappe.db.sql(""" + SELECT u.loan_security, sum(u.qty) as qty + FROM `tabLoan Security Unpledge` up, `tabUnpledge` u + WHERE u.parent = up.name + AND up.status = 'Approved' + {conditions} + GROUP BY u.loan_security + """.format(conditions=conditions), filters, as_list=1)) + + pledges = frappe._dict(frappe.db.sql(""" + SELECT p.loan_security, sum(p.qty) as qty + FROM `tabLoan Security Pledge` lp, `tabPledge`p + WHERE p.parent = lp.name + AND lp.status = 'Pledged' + {conditions} + GROUP BY p.loan_security + """.format(conditions=conditions), filters, as_list=1)) + + for security, qty in iteritems(pledges): + current_pledges.setdefault(security, qty) + current_pledges[security] -= unpledges.get(security, 0.0) + + sorted_pledges = dict(sorted(current_pledges.items(), key=lambda item: item[1], reverse=True)) + + count = 0 + for security, qty in iteritems(sorted_pledges): + values.append(qty * loan_security_details.get(security, {}).get('latest_price', 0)) + labels.append(security) + count +=1 + + ## Just need top 10 securities + if count == 10: + break + + return { + 'labels': labels, + 'datasets': [{ + 'name': 'Top 10 Securities', + 'chartType': 'bar', + 'values': values + }] + } \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index e607d4f3cb..83a813f947 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -201,7 +201,9 @@ def request_loan_closure(loan, posting_date=None): write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount') # checking greater than 0 as there may be some minor precision error - if pending_amount < write_off_limit: + if not pending_amount: + frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested') + elif pending_amount < write_off_limit: # Auto create loan write off and update status as loan closure requested write_off = make_loan_write_off(loan) write_off.submit() @@ -348,3 +350,13 @@ def validate_employee_currency_with_company_currency(applicant, company): if employee_currency != company_currency: frappe.throw(_("Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}") .format(applicant, employee_currency)) + +@frappe.whitelist() +def get_shortfall_applicants(): + loans = frappe.get_all('Loan Security Shortfall', {'status': 'Pending'}, pluck='loan') + applicants = set(frappe.get_all('Loan', {'name': ('in', loans)}, pluck='name')) + + return { + "value": len(applicants), + "fieldtype": "Int" + } \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index f3c9db6233..13a209418d 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -547,7 +547,7 @@ class TestLoan(unittest.TestCase): # 30 days - grace period penalty_days = 30 - 4 - penalty_applicable_amount = flt(amounts['interest_amount']/2, 2) + penalty_applicable_amount = flt(amounts['interest_amount']/2) penalty_amount = flt((((penalty_applicable_amount * 25) / 100) * penalty_days), 2) process = process_loan_interest_accrual_for_demand_loans(posting_date = '2019-11-30') diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py index e59db4c12d..9c0147e55b 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/loan_application.py @@ -197,7 +197,7 @@ def get_proposed_pledge(securities): security.qty = cint(security.amount/security.loan_security_price) security.amount = security.qty * security.loan_security_price - security.post_haircut_amount = security.amount - (security.amount * security.haircut/100) + security.post_haircut_amount = cint(security.amount - (security.amount * security.haircut/100)) maximum_loan_amount += security.post_haircut_amount diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index 7d7992d40a..7978350adf 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -246,7 +246,5 @@ def get_per_day_interest(principal_amount, rate_of_interest, posting_date=None): if not posting_date: posting_date = getdate() - precision = cint(frappe.db.get_default("currency_precision")) or 2 - - return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100), precision) + return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100)) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index ac30c91b67..bac06c4e9e 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -81,8 +81,8 @@ class LoanRepayment(AccountsController): last_accrual_date = get_last_accrual_date(self.against_loan) # get posting date upto which interest has to be accrued - per_day_interest = flt(get_per_day_interest(self.pending_principal_amount, - self.rate_of_interest, self.posting_date), 2) + per_day_interest = get_per_day_interest(self.pending_principal_amount, + self.rate_of_interest, self.posting_date) no_of_days = flt(flt(self.total_interest_paid - self.interest_payable, precision)/per_day_interest, 0) - 1 @@ -105,8 +105,6 @@ class LoanRepayment(AccountsController): }) def update_paid_amount(self): - precision = cint(frappe.db.get_default("currency_precision")) or 2 - loan = frappe.get_doc("Loan", self.against_loan) for payment in self.repayment_details: @@ -114,7 +112,7 @@ class LoanRepayment(AccountsController): SET paid_principal_amount = `paid_principal_amount` + %s, paid_interest_amount = `paid_interest_amount` + %s WHERE name = %s""", - (flt(payment.paid_principal_amount, precision), flt(payment.paid_interest_amount, precision), payment.loan_interest_accrual)) + (flt(payment.paid_principal_amount), flt(payment.paid_interest_amount), payment.loan_interest_accrual)) frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s WHERE name = %s """, (loan.total_amount_paid + self.amount_paid, @@ -148,8 +146,6 @@ class LoanRepayment(AccountsController): frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed") def allocate_amounts(self, repayment_details): - precision = cint(frappe.db.get_default("currency_precision")) or 2 - self.set('repayment_details', []) self.principal_amount_paid = 0 total_interest_paid = 0 @@ -185,21 +181,18 @@ class LoanRepayment(AccountsController): # no of days for which to accrue interest # Interest can only be accrued for an entire day and not partial if interest_paid > repayment_details['unaccrued_interest']: - per_day_interest = flt(get_per_day_interest(self.pending_principal_amount, - self.rate_of_interest, self.posting_date), precision) interest_paid -= repayment_details['unaccrued_interest'] total_interest_paid += repayment_details['unaccrued_interest'] else: # get no of days for which interest can be paid - per_day_interest = flt(get_per_day_interest(self.pending_principal_amount, - self.rate_of_interest, self.posting_date), precision) + per_day_interest = get_per_day_interest(self.pending_principal_amount, + self.rate_of_interest, self.posting_date) no_of_days = cint(interest_paid/per_day_interest) total_interest_paid += no_of_days * per_day_interest interest_paid -= no_of_days * per_day_interest self.total_interest_paid = total_interest_paid - if interest_paid: self.principal_amount_paid += interest_paid @@ -369,7 +362,7 @@ def get_amounts(amounts, against_loan, posting_date): if pending_days > 0: principal_amount = flt(pending_principal_amount, precision) per_day_interest = get_per_day_interest(principal_amount, loan_type_details.rate_of_interest, posting_date) - unaccrued_interest += (pending_days * flt(per_day_interest, precision)) + unaccrued_interest += (pending_days * per_day_interest) amounts["pending_principal_amount"] = flt(pending_principal_amount, precision) amounts["payable_principal_amount"] = flt(payable_principal_amount, precision) diff --git a/erpnext/loan_management/loan_management_dashboard/loan_dashboard/loan_dashboard.json b/erpnext/loan_management/loan_management_dashboard/loan_dashboard/loan_dashboard.json new file mode 100644 index 0000000000..e060253d34 --- /dev/null +++ b/erpnext/loan_management/loan_management_dashboard/loan_dashboard/loan_dashboard.json @@ -0,0 +1,70 @@ +{ + "cards": [ + { + "card": "New Loans" + }, + { + "card": "Active Loans" + }, + { + "card": "Closed Loans" + }, + { + "card": "Total Disbursed" + }, + { + "card": "Open Loan Applications" + }, + { + "card": "New Loan Applications" + }, + { + "card": "Total Sanctioned Amount" + }, + { + "card": "Active Securities" + }, + { + "card": "Applicants With Unpaid Shortfall" + }, + { + "card": "Total Shortfall Amount" + }, + { + "card": "Total Repayment" + }, + { + "card": "Total Write Off" + } + ], + "charts": [ + { + "chart": "New Loans", + "width": "Half" + }, + { + "chart": "Loan Disbursements", + "width": "Half" + }, + { + "chart": "Top 10 Pledged Loan Securities", + "width": "Half" + }, + { + "chart": "Loan Interest Accrual", + "width": "Half" + } + ], + "creation": "2021-02-06 16:52:43.484752", + "dashboard_name": "Loan Dashboard", + "docstatus": 0, + "doctype": "Dashboard", + "idx": 0, + "is_default": 0, + "is_standard": 1, + "modified": "2021-02-21 20:53:47.531699", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Dashboard", + "owner": "Administrator" +} \ No newline at end of file diff --git a/erpnext/loan_management/number_card/active_loans/active_loans.json b/erpnext/loan_management/number_card/active_loans/active_loans.json new file mode 100644 index 0000000000..7e0db47288 --- /dev/null +++ b/erpnext/loan_management/number_card/active_loans/active_loans.json @@ -0,0 +1,23 @@ +{ + "aggregate_function_based_on": "", + "creation": "2021-02-06 17:10:26.132493", + "docstatus": 0, + "doctype": "Number Card", + "document_type": "Loan", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Loan\",\"docstatus\",\"=\",\"1\",false],[\"Loan\",\"status\",\"in\",[\"Disbursed\",\"Partially Disbursed\",null],false]]", + "function": "Count", + "idx": 0, + "is_public": 0, + "is_standard": 1, + "label": "Active Loans", + "modified": "2021-02-06 17:29:20.304087", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Active Loans", + "owner": "Administrator", + "report_function": "Sum", + "show_percentage_stats": 1, + "stats_time_interval": "Monthly", + "type": "Document Type" +} \ No newline at end of file diff --git a/erpnext/loan_management/number_card/active_securities/active_securities.json b/erpnext/loan_management/number_card/active_securities/active_securities.json new file mode 100644 index 0000000000..298e41061a --- /dev/null +++ b/erpnext/loan_management/number_card/active_securities/active_securities.json @@ -0,0 +1,23 @@ +{ + "aggregate_function_based_on": "", + "creation": "2021-02-06 19:07:21.344199", + "docstatus": 0, + "doctype": "Number Card", + "document_type": "Loan Security", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Loan Security\",\"disabled\",\"=\",0,false]]", + "function": "Count", + "idx": 0, + "is_public": 0, + "is_standard": 1, + "label": "Active Securities", + "modified": "2021-02-06 19:07:26.671516", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Active Securities", + "owner": "Administrator", + "report_function": "Sum", + "show_percentage_stats": 1, + "stats_time_interval": "Daily", + "type": "Document Type" +} \ No newline at end of file diff --git a/erpnext/loan_management/number_card/applicants_with_unpaid_shortfall/applicants_with_unpaid_shortfall.json b/erpnext/loan_management/number_card/applicants_with_unpaid_shortfall/applicants_with_unpaid_shortfall.json new file mode 100644 index 0000000000..3b9eba1553 --- /dev/null +++ b/erpnext/loan_management/number_card/applicants_with_unpaid_shortfall/applicants_with_unpaid_shortfall.json @@ -0,0 +1,21 @@ +{ + "creation": "2021-02-07 18:55:12.632616", + "docstatus": 0, + "doctype": "Number Card", + "filters_json": "null", + "function": "Count", + "idx": 0, + "is_public": 0, + "is_standard": 1, + "label": "Applicants With Unpaid Shortfall", + "method": "erpnext.loan_management.doctype.loan.loan.get_shortfall_applicants", + "modified": "2021-02-07 21:46:27.369795", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Applicants With Unpaid Shortfall", + "owner": "Administrator", + "report_function": "Sum", + "show_percentage_stats": 1, + "stats_time_interval": "Daily", + "type": "Custom" +} \ No newline at end of file diff --git a/erpnext/loan_management/number_card/closed_loans/closed_loans.json b/erpnext/loan_management/number_card/closed_loans/closed_loans.json new file mode 100644 index 0000000000..c2f2244265 --- /dev/null +++ b/erpnext/loan_management/number_card/closed_loans/closed_loans.json @@ -0,0 +1,23 @@ +{ + "aggregate_function_based_on": "", + "creation": "2021-02-21 19:51:49.261813", + "docstatus": 0, + "doctype": "Number Card", + "document_type": "Loan", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Loan\",\"docstatus\",\"=\",\"1\",false],[\"Loan\",\"status\",\"=\",\"Closed\",false]]", + "function": "Count", + "idx": 0, + "is_public": 0, + "is_standard": 1, + "label": "Closed Loans", + "modified": "2021-02-21 19:51:54.087903", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Closed Loans", + "owner": "Administrator", + "report_function": "Sum", + "show_percentage_stats": 1, + "stats_time_interval": "Daily", + "type": "Document Type" +} \ No newline at end of file diff --git a/erpnext/loan_management/number_card/last_interest_accrual/last_interest_accrual.json b/erpnext/loan_management/number_card/last_interest_accrual/last_interest_accrual.json new file mode 100644 index 0000000000..65c8ce67d2 --- /dev/null +++ b/erpnext/loan_management/number_card/last_interest_accrual/last_interest_accrual.json @@ -0,0 +1,21 @@ +{ + "creation": "2021-02-07 21:57:14.758007", + "docstatus": 0, + "doctype": "Number Card", + "filters_json": "null", + "function": "Count", + "idx": 0, + "is_public": 0, + "is_standard": 1, + "label": "Last Interest Accrual", + "method": "erpnext.loan_management.doctype.loan.loan.get_last_accrual_date", + "modified": "2021-02-07 21:59:47.525197", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Last Interest Accrual", + "owner": "Administrator", + "report_function": "Sum", + "show_percentage_stats": 1, + "stats_time_interval": "Daily", + "type": "Custom" +} \ No newline at end of file diff --git a/erpnext/loan_management/number_card/new_loan_applications/new_loan_applications.json b/erpnext/loan_management/number_card/new_loan_applications/new_loan_applications.json new file mode 100644 index 0000000000..7e655ff35c --- /dev/null +++ b/erpnext/loan_management/number_card/new_loan_applications/new_loan_applications.json @@ -0,0 +1,23 @@ +{ + "aggregate_function_based_on": "", + "creation": "2021-02-06 17:59:10.051269", + "docstatus": 0, + "doctype": "Number Card", + "document_type": "Loan Application", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Loan Application\",\"docstatus\",\"=\",\"1\",false],[\"Loan Application\",\"creation\",\"Timespan\",\"today\",false]]", + "function": "Count", + "idx": 0, + "is_public": 0, + "is_standard": 1, + "label": "New Loan Applications", + "modified": "2021-02-06 17:59:21.880979", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "New Loan Applications", + "owner": "Administrator", + "report_function": "Sum", + "show_percentage_stats": 1, + "stats_time_interval": "Daily", + "type": "Document Type" +} \ No newline at end of file diff --git a/erpnext/loan_management/number_card/new_loans/new_loans.json b/erpnext/loan_management/number_card/new_loans/new_loans.json new file mode 100644 index 0000000000..424f0f1495 --- /dev/null +++ b/erpnext/loan_management/number_card/new_loans/new_loans.json @@ -0,0 +1,23 @@ +{ + "aggregate_function_based_on": "", + "creation": "2021-02-06 17:56:34.624031", + "docstatus": 0, + "doctype": "Number Card", + "document_type": "Loan", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Loan\",\"docstatus\",\"=\",\"1\",false],[\"Loan\",\"creation\",\"Timespan\",\"today\",false]]", + "function": "Count", + "idx": 0, + "is_public": 0, + "is_standard": 1, + "label": "New Loans", + "modified": "2021-02-06 17:58:20.209166", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "New Loans", + "owner": "Administrator", + "report_function": "Sum", + "show_percentage_stats": 1, + "stats_time_interval": "Daily", + "type": "Document Type" +} \ No newline at end of file diff --git a/erpnext/loan_management/number_card/open_loan_applications/open_loan_applications.json b/erpnext/loan_management/number_card/open_loan_applications/open_loan_applications.json new file mode 100644 index 0000000000..1d5e84ed7f --- /dev/null +++ b/erpnext/loan_management/number_card/open_loan_applications/open_loan_applications.json @@ -0,0 +1,23 @@ +{ + "aggregate_function_based_on": "", + "creation": "2021-02-06 17:23:32.509899", + "docstatus": 0, + "doctype": "Number Card", + "document_type": "Loan Application", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Loan Application\",\"docstatus\",\"=\",\"1\",false],[\"Loan Application\",\"status\",\"=\",\"Open\",false]]", + "function": "Count", + "idx": 0, + "is_public": 0, + "is_standard": 1, + "label": "Open Loan Applications", + "modified": "2021-02-06 17:29:09.761011", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Open Loan Applications", + "owner": "Administrator", + "report_function": "Sum", + "show_percentage_stats": 1, + "stats_time_interval": "Monthly", + "type": "Document Type" +} \ No newline at end of file diff --git a/erpnext/loan_management/number_card/total_disbursed/total_disbursed.json b/erpnext/loan_management/number_card/total_disbursed/total_disbursed.json new file mode 100644 index 0000000000..4a3f8699a0 --- /dev/null +++ b/erpnext/loan_management/number_card/total_disbursed/total_disbursed.json @@ -0,0 +1,23 @@ +{ + "aggregate_function_based_on": "disbursed_amount", + "creation": "2021-02-06 16:52:19.505462", + "docstatus": 0, + "doctype": "Number Card", + "document_type": "Loan Disbursement", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Loan Disbursement\",\"docstatus\",\"=\",\"1\",false]]", + "function": "Sum", + "idx": 0, + "is_public": 0, + "is_standard": 1, + "label": "Total Disbursed Amount", + "modified": "2021-02-06 17:29:38.453870", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Total Disbursed", + "owner": "Administrator", + "report_function": "Sum", + "show_percentage_stats": 1, + "stats_time_interval": "Monthly", + "type": "Document Type" +} \ No newline at end of file diff --git a/erpnext/loan_management/number_card/total_repayment/total_repayment.json b/erpnext/loan_management/number_card/total_repayment/total_repayment.json new file mode 100644 index 0000000000..38de42b89c --- /dev/null +++ b/erpnext/loan_management/number_card/total_repayment/total_repayment.json @@ -0,0 +1,24 @@ +{ + "aggregate_function_based_on": "amount_paid", + "color": "#29CD42", + "creation": "2021-02-21 19:27:45.989222", + "docstatus": 0, + "doctype": "Number Card", + "document_type": "Loan Repayment", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Loan Repayment\",\"docstatus\",\"=\",\"1\",false]]", + "function": "Sum", + "idx": 0, + "is_public": 0, + "is_standard": 1, + "label": "Total Repayment", + "modified": "2021-02-21 19:34:59.656546", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Total Repayment", + "owner": "Administrator", + "report_function": "Sum", + "show_percentage_stats": 1, + "stats_time_interval": "Daily", + "type": "Document Type" +} \ No newline at end of file diff --git a/erpnext/loan_management/number_card/total_sanctioned_amount/total_sanctioned_amount.json b/erpnext/loan_management/number_card/total_sanctioned_amount/total_sanctioned_amount.json new file mode 100644 index 0000000000..dfb9d24e92 --- /dev/null +++ b/erpnext/loan_management/number_card/total_sanctioned_amount/total_sanctioned_amount.json @@ -0,0 +1,23 @@ +{ + "aggregate_function_based_on": "loan_amount", + "creation": "2021-02-06 17:05:04.704162", + "docstatus": 0, + "doctype": "Number Card", + "document_type": "Loan", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Loan\",\"docstatus\",\"=\",\"1\",false],[\"Loan\",\"status\",\"=\",\"Sanctioned\",false]]", + "function": "Sum", + "idx": 0, + "is_public": 0, + "is_standard": 1, + "label": "Total Sanctioned Amount", + "modified": "2021-02-06 17:29:29.930557", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Total Sanctioned Amount", + "owner": "Administrator", + "report_function": "Sum", + "show_percentage_stats": 1, + "stats_time_interval": "Monthly", + "type": "Document Type" +} \ No newline at end of file diff --git a/erpnext/loan_management/number_card/total_shortfall_amount/total_shortfall_amount.json b/erpnext/loan_management/number_card/total_shortfall_amount/total_shortfall_amount.json new file mode 100644 index 0000000000..aa6b093732 --- /dev/null +++ b/erpnext/loan_management/number_card/total_shortfall_amount/total_shortfall_amount.json @@ -0,0 +1,23 @@ +{ + "aggregate_function_based_on": "shortfall_amount", + "creation": "2021-02-09 08:07:20.096995", + "docstatus": 0, + "doctype": "Number Card", + "document_type": "Loan Security Shortfall", + "dynamic_filters_json": "[]", + "filters_json": "[]", + "function": "Sum", + "idx": 0, + "is_public": 0, + "is_standard": 1, + "label": "Total Unpaid Shortfall Amount", + "modified": "2021-02-09 08:09:00.355547", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Total Shortfall Amount", + "owner": "Administrator", + "report_function": "Sum", + "show_percentage_stats": 1, + "stats_time_interval": "Daily", + "type": "Document Type" +} \ No newline at end of file diff --git a/erpnext/loan_management/number_card/total_write_off/total_write_off.json b/erpnext/loan_management/number_card/total_write_off/total_write_off.json new file mode 100644 index 0000000000..c85169acf8 --- /dev/null +++ b/erpnext/loan_management/number_card/total_write_off/total_write_off.json @@ -0,0 +1,24 @@ +{ + "aggregate_function_based_on": "write_off_amount", + "color": "#CB2929", + "creation": "2021-02-21 19:48:29.004429", + "docstatus": 0, + "doctype": "Number Card", + "document_type": "Loan Write Off", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Loan Write Off\",\"docstatus\",\"=\",\"1\",false]]", + "function": "Sum", + "idx": 0, + "is_public": 0, + "is_standard": 1, + "label": "Total Write Off", + "modified": "2021-02-21 19:48:58.604159", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Total Write Off", + "owner": "Administrator", + "report_function": "Sum", + "show_percentage_stats": 1, + "stats_time_interval": "Daily", + "type": "Document Type" +} \ No newline at end of file diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py index ab586bc09c..0ccd149e5f 100644 --- a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py +++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py @@ -36,7 +36,7 @@ def get_columns(filters): def get_data(filters): data = [] - loan_security_details = get_loan_security_details(filters) + loan_security_details = get_loan_security_details() pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(filters, loan_security_details) @@ -64,7 +64,7 @@ def get_data(filters): return data -def get_loan_security_details(filters): +def get_loan_security_details(): security_detail_map = {} loan_security_price_map = {} lsp_validity_map = {} diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py index a3e69bbfbf..0f72c3cce7 100644 --- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py @@ -171,7 +171,7 @@ def get_loan_wise_pledges(filters): return current_pledges def get_loan_wise_security_value(filters, current_pledges): - loan_security_details = get_loan_security_details(filters) + loan_security_details = get_loan_security_details() loan_wise_security_value = {} for key in current_pledges: diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py index adc8013c68..887a86a46c 100644 --- a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py +++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py @@ -35,7 +35,7 @@ def get_columns(filters): def get_data(filters): data = [] - loan_security_details = get_loan_security_details(filters) + loan_security_details = get_loan_security_details() current_pledges, total_portfolio_value = get_company_wise_loan_security_details(filters, loan_security_details) currency = erpnext.get_company_currency(filters.get('company')) @@ -76,7 +76,7 @@ def get_company_wise_loan_security_details(filters, loan_security_details): if qty: security_wise_map[key[1]]['applicant_count'] += 1 - total_portfolio_value += flt(qty * loan_security_details.get(key[1])['latest_price']) + total_portfolio_value += flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0)) return security_wise_map, total_portfolio_value diff --git a/erpnext/loan_management/workspace/loan_management/loan_management.json b/erpnext/loan_management/workspace/loan_management/loan_management.json index 2e8b5bf5b3..18559dceef 100644 --- a/erpnext/loan_management/workspace/loan_management/loan_management.json +++ b/erpnext/loan_management/workspace/loan_management/loan_management.json @@ -10,6 +10,7 @@ "hide_custom": 0, "icon": "loan", "idx": 0, + "is_default": 0, "is_standard": 1, "label": "Loan Management", "links": [ @@ -219,7 +220,7 @@ "type": "Link" } ], - "modified": "2021-01-12 11:27:56.079724", + "modified": "2021-02-18 17:31:53.586508", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Management", @@ -239,6 +240,12 @@ "label": "Loan", "link_to": "Loan", "type": "DocType" + }, + { + "doc_view": "", + "label": "Dashboard", + "link_to": "Loan Dashboard", + "type": "Dashboard" } ] } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index ec28eb7795..662a06b1ee 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -267,6 +267,17 @@ class JobCard(Document): fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id}) + def set_transferred_qty_in_job_card(self, ste_doc): + for row in ste_doc.items: + if not row.job_card_item: continue + + qty = frappe.db.sql(""" SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se + WHERE sed.job_card_item = %s and se.docstatus = 1 and sed.parent = se.name and + se.purpose = 'Material Transfer for Manufacture' + """, (row.job_card_item))[0][0] + + frappe.db.set_value('Job Card Item', row.job_card_item, 'transferred_qty', flt(qty)) + def set_transferred_qty(self, update_status=False): if not self.items: self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0 @@ -279,7 +290,8 @@ class JobCard(Document): self.transferred_qty = frappe.db.get_value('Stock Entry', { 'job_card': self.name, 'work_order': self.work_order, - 'docstatus': 1 + 'docstatus': 1, + 'purpose': 'Material Transfer for Manufacture' }, 'sum(fg_completed_qty)') or 0 self.db_set("transferred_qty", self.transferred_qty) @@ -420,6 +432,7 @@ def make_stock_entry(source_name, target_doc=None): target.purpose = "Material Transfer for Manufacture" target.from_bom = 1 target.fg_completed_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0) + target.set_transfer_qty() target.calculate_rate_and_amount() target.set_missing_values() target.set_stock_entry_type() @@ -437,9 +450,10 @@ def make_stock_entry(source_name, target_doc=None): "field_map": { "source_warehouse": "s_warehouse", "required_qty": "qty", - "uom": "stock_uom" + "name": "job_card_item" }, "postprocess": update_item, + "condition": lambda doc: doc.required_qty > 0 } }, target_doc, set_missing_values) diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json index bc9fe108ca..100ef4ca3a 100644 --- a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json +++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json @@ -1,363 +1,120 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2018-07-09 17:20:44.737289", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2018-07-09 17:20:44.737289", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "source_warehouse", + "uom", + "item_group", + "column_break_3", + "stock_uom", + "item_name", + "description", + "qty_section", + "required_qty", + "column_break_9", + "transferred_qty", + "allow_alternative_item" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_code", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Item Code", - "length": 0, - "no_copy": 0, - "options": "Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "source_warehouse", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Source Warehouse", - "length": 0, - "no_copy": 0, - "options": "Warehouse", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "source_warehouse", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "in_list_view": 1, + "label": "Source Warehouse", + "options": "Warehouse" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "uom", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "UOM", - "length": 0, - "no_copy": 0, - "options": "UOM", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Item Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "description", + "fieldtype": "Text", + "label": "Description", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "qty_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Qty", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "qty_section", + "fieldtype": "Section Break", + "label": "Qty" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "required_qty", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Required Qty", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "required_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Required Qty", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_9", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "allow_alternative_item", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Allow Alternative Item", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "default": "0", + "fieldname": "allow_alternative_item", + "fieldtype": "Check", + "label": "Allow Alternative Item" + }, + { + "fetch_from": "item_code.item_group", + "fieldname": "item_group", + "fieldtype": "Link", + "label": "Item Group", + "options": "Item Group", + "read_only": 1 + }, + { + "fetch_from": "item_code.stock_uom", + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM" + }, + { + "fieldname": "transferred_qty", + "fieldtype": "Float", + "label": "Transferred Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-08-28 15:23:48.099459", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Job Card Item", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-02-11 13:50:13.804108", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Job Card Item", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 06a8e1987d..00e8c5418a 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -94,11 +94,11 @@ class TestWorkOrder(unittest.TestCase): wo_order = make_wo_order_test_record(item="_Test FG Item", qty=2, source_warehouse=warehouse, skip_transfer=1) - bin1_on_submit = get_bin(item, warehouse) + reserved_qty_on_submission = cint(get_bin(item, warehouse).reserved_qty_for_production) # reserved qty for production is updated - self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2, - cint(bin1_on_submit.reserved_qty_for_production)) + self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2, reserved_qty_on_submission) + test_stock_entry.make_stock_entry(item_code="_Test Item", target=warehouse, qty=100, basic_rate=100) @@ -109,9 +109,9 @@ class TestWorkOrder(unittest.TestCase): s.submit() bin1_at_completion = get_bin(item, warehouse) - + self.assertEqual(cint(bin1_at_completion.reserved_qty_for_production), - cint(bin1_on_submit.reserved_qty_for_production) - 1) + reserved_qty_on_submission - 1) def test_production_item(self): wo_order = make_wo_order_test_record(item="_Test FG Item", qty=1, do_not_save=True) diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py index f7b407b792..ffd9242e1b 100644 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py +++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py @@ -88,11 +88,11 @@ def get_bom_stock(filters): GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1) def get_manufacturer_records(): - details = frappe.get_list('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no, parent"]) + details = frappe.get_list('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"]) manufacture_details = frappe._dict() for detail in details: dic = manufacture_details.setdefault(detail.get('parent'), {}) dic.setdefault('manufacturer', []).append(detail.get('manufacturer')) dic.setdefault('manufacturer_part', []).append(detail.get('manufacturer_part_no')) - return manufacture_details \ No newline at end of file + return manufacture_details diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 3b7c6ab48e..ba31feeefc 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -752,3 +752,7 @@ erpnext.patches.v13_0.set_company_in_leave_ledger_entry erpnext.patches.v13_0.convert_qi_parameter_to_link_field erpnext.patches.v13_0.setup_patient_history_settings_for_standard_doctypes erpnext.patches.v13_0.add_naming_series_to_old_projects # 1-02-2021 +erpnext.patches.v12_0.add_state_code_for_ladakh +erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl +erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes +erpnext.patches.v13_0.update_vehicle_no_reqd_condition diff --git a/erpnext/patches/v11_0/refactor_autoname_naming.py b/erpnext/patches/v11_0/refactor_autoname_naming.py index 5dc5d3bf0c..b997ba2db2 100644 --- a/erpnext/patches/v11_0/refactor_autoname_naming.py +++ b/erpnext/patches/v11_0/refactor_autoname_naming.py @@ -20,7 +20,7 @@ doctype_series_map = { 'Certified Consultant': 'NPO-CONS-.YYYY.-.#####', 'Chat Room': 'CHAT-ROOM-.#####', 'Compensatory Leave Request': 'HR-CMP-.YY.-.MM.-.#####', - 'Custom Script': 'SYS-SCR-.#####', + 'Client Script': 'SYS-SCR-.#####', 'Employee Benefit Application': 'HR-BEN-APP-.YY.-.MM.-.#####', 'Employee Benefit Application Detail': '', 'Employee Benefit Claim': 'HR-BEN-CLM-.YY.-.MM.-.#####', diff --git a/erpnext/patches/v11_1/update_bank_transaction_status.py b/erpnext/patches/v11_1/update_bank_transaction_status.py index 1acdfcccf9..544bc5e691 100644 --- a/erpnext/patches/v11_1/update_bank_transaction_status.py +++ b/erpnext/patches/v11_1/update_bank_transaction_status.py @@ -7,9 +7,20 @@ import frappe def execute(): frappe.reload_doc("accounts", "doctype", "bank_transaction") - frappe.db.sql(""" UPDATE `tabBank Transaction` - SET status = 'Reconciled' - WHERE - status = 'Settled' and (debit = allocated_amount or credit = allocated_amount) - and ifnull(allocated_amount, 0) > 0 - """) \ No newline at end of file + bank_transaction_fields = frappe.get_meta("Bank Transaction").get_valid_columns() + + if 'debit' in bank_transaction_fields: + frappe.db.sql(""" UPDATE `tabBank Transaction` + SET status = 'Reconciled' + WHERE + status = 'Settled' and (debit = allocated_amount or credit = allocated_amount) + and ifnull(allocated_amount, 0) > 0 + """) + + elif 'deposit' in bank_transaction_fields: + frappe.db.sql(""" UPDATE `tabBank Transaction` + SET status = 'Reconciled' + WHERE + status = 'Settled' and (deposit = allocated_amount or withdrawal = allocated_amount) + and ifnull(allocated_amount, 0) > 0 + """) \ No newline at end of file diff --git a/erpnext/patches/v12_0/add_state_code_for_ladakh.py b/erpnext/patches/v12_0/add_state_code_for_ladakh.py new file mode 100644 index 0000000000..d41101cc46 --- /dev/null +++ b/erpnext/patches/v12_0/add_state_code_for_ladakh.py @@ -0,0 +1,16 @@ +import frappe +from erpnext.regional.india import states + +def execute(): + + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + custom_fields = ['Address-gst_state', 'Tax Category-gst_state'] + + # Update options in gst_state custom fields + for field in custom_fields: + gst_state_field = frappe.get_doc('Custom Field', field) + gst_state_field.options = '\n'.join(states) + gst_state_field.save() diff --git a/erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py b/erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py new file mode 100644 index 0000000000..af1f6e7ec1 --- /dev/null +++ b/erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py @@ -0,0 +1,26 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe +from frappe.model.utils.rename_field import rename_field + +def execute(): + doctypes = [ + "Bank Statement Settings", + "Bank Statement Settings Item", + "Bank Statement Transaction Entry", + "Bank Statement Transaction Invoice Item", + "Bank Statement Transaction Payment Item", + "Bank Statement Transaction Settings Item", + "Bank Statement Transaction Settings", + ] + + for doctype in doctypes: + frappe.delete_doc("DocType", doctype, force=1) + + frappe.delete_doc("Page", "bank-reconciliation", force=1) + + rename_field("Bank Transaction", "debit", "deposit") + rename_field("Bank Transaction", "credit", "withdrawal") diff --git a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py new file mode 100644 index 0000000000..06f7f989bb --- /dev/null +++ b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py @@ -0,0 +1,46 @@ +import frappe +from frappe import _ +from erpnext.stock.stock_ledger import update_entries_after +from erpnext.accounts.utils import update_gl_entries_after + +def execute(): + frappe.reload_doc('stock', 'doctype', 'repost_item_valuation') + + reposting_project_deployed_on = frappe.db.get_value("DocType", "Repost Item Valuation", "creation") + + data = frappe.db.sql(''' + SELECT + name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time + FROM + `tabStock Ledger Entry` + WHERE + creation > %s + and is_cancelled = 0 + ORDER BY timestamp(posting_date, posting_time) asc, creation asc + ''', reposting_project_deployed_on, as_dict=1) + + frappe.db.auto_commit_on_many_writes = 1 + print("Reposting Stock Ledger Entries...") + total_sle = len(data) + i = 0 + for d in data: + update_entries_after({ + "item_code": d.item_code, + "warehouse": d.warehouse, + "posting_date": d.posting_date, + "posting_time": d.posting_time, + "voucher_type": d.voucher_type, + "voucher_no": d.voucher_no, + "sle_id": d.name + }, allow_negative_stock=True) + + i += 1 + if i%100 == 0: + print(i, "/", total_sle) + + + print("Reposting General Ledger Entries...") + for row in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}): + update_gl_entries_after('2020-12-25', '01:58:55', company=row.name) + + frappe.db.auto_commit_on_many_writes = 0 diff --git a/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py b/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py new file mode 100644 index 0000000000..c26cddbe4e --- /dev/null +++ b/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py @@ -0,0 +1,9 @@ +import frappe + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + if frappe.db.exists('Custom Field', { 'fieldname': 'vehicle_no' }): + frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'mandatory_depends_on', '') diff --git a/erpnext/patches/v5_0/replace_renamed_fields_in_custom_scripts_and_print_formats.py b/erpnext/patches/v5_0/replace_renamed_fields_in_custom_scripts_and_print_formats.py index ef3f1d6c0a..c564f8b02a 100644 --- a/erpnext/patches/v5_0/replace_renamed_fields_in_custom_scripts_and_print_formats.py +++ b/erpnext/patches/v5_0/replace_renamed_fields_in_custom_scripts_and_print_formats.py @@ -9,7 +9,7 @@ def execute(): # NOTE: sequence is important renamed_fields = get_all_renamed_fields() - for dt, script_field, ref_dt_field in (("Custom Script", "script", "dt"), ("Print Format", "html", "doc_type")): + for dt, script_field, ref_dt_field in (("Client Script", "script", "dt"), ("Print Format", "html", "doc_type")): cond1 = " or ".join("""{0} like "%%{1}%%" """.format(script_field, d[0].replace("_", "\\_")) for d in renamed_fields) cond2 = " and standard = 'No'" if dt == "Print Format" else "" diff --git a/erpnext/patches/v7_0/remove_doctypes_and_reports.py b/erpnext/patches/v7_0/remove_doctypes_and_reports.py index 746cae0e1c..2356e2f6ee 100644 --- a/erpnext/patches/v7_0/remove_doctypes_and_reports.py +++ b/erpnext/patches/v7_0/remove_doctypes_and_reports.py @@ -7,7 +7,7 @@ def execute(): where name in('Time Log Batch', 'Time Log Batch Detail', 'Time Log')""") frappe.db.sql("""delete from `tabDocField` where parent in ('Time Log', 'Time Log Batch')""") - frappe.db.sql("""update `tabCustom Script` set dt = 'Timesheet' where dt = 'Time Log'""") + frappe.db.sql("""update `tabClient Script` set dt = 'Timesheet' where dt = 'Time Log'""") for data in frappe.db.sql(""" select label, fieldname from `tabCustom Field` where dt = 'Time Log'""", as_dict=1): custom_field = frappe.get_doc({ diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 2d3bc57900..60aff02b38 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -1103,10 +1103,10 @@ class SalarySlip(TransactionBase): self.calculate_total_for_salary_slip_based_on_timesheet() else: self.total_deduction = 0.0 - if self.earnings: + if hasattr(self, "earnings"): for earning in self.earnings: self.gross_pay += flt(earning.amount, earning.precision("amount")) - if self.deductions: + if hasattr(self, "deductions"): for deduction in self.deductions: self.total_deduction += flt(deduction.amount, deduction.precision("amount")) self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) - flt(self.total_loan_repayment) diff --git a/erpnext/public/build.json b/erpnext/public/build.json index 7326238273..7a3cb838a9 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -61,5 +61,10 @@ "selling/page/point_of_sale/pos_past_order_list.js", "selling/page/point_of_sale/pos_past_order_summary.js", "selling/page/point_of_sale/pos_controller.js" + ], + "js/bank-reconciliation-tool.min.js": [ + "public/js/bank_reconciliation_tool/data_table_manager.js", + "public/js/bank_reconciliation_tool/number_card.js", + "public/js/bank_reconciliation_tool/dialog_manager.js" ] } diff --git a/erpnext/public/images/erpnext-logo.png b/erpnext/public/images/erpnext-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3090727d8ff5f95e49d25278b24b10bf13747f4a GIT binary patch literal 2360 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4Yzkn2Hfk$L90|U1(2s1Lwnj--e zWGoJHcVbv~PUa<$!;&U>cv7h@-A}a#}$5~Go$B+ufw>K9Sh9*k5UVLwOJ(!Va zX^Y;A2EGdl@_l>`i7y#^RP^{9(=r-dj@~ty==pE<-Z0VXbCzc-@3S*_R2nik^f53r zC@?ZGI09YN!~(R06KJv!1A~AH1A~GG1B1haQ3b;&94?q`*kP{E&iDFPT7oB2!_&XD z_5~5k&og@$F%HG3-qv`ucCQqJ+>Ok0zwHd6fknm8%iOovDvb93JGL=j-W!Kx!3@l;vG@68ZvEfg_;k**J@1(rmh2d + "" + + format_currency(value, this.currency) + + "", + }, + { + name: "Withdrawal", + editable: false, + width: 100, + format: (value) => + "" + + format_currency(value, this.currency) + + "", + }, + { + name: "Unallocated Amount", + editable: false, + width: 100, + format: (value) => + "" + + format_currency(value, this.currency) + + "", + }, + { + name: "Reference Number", + editable: false, + width: 140, + }, + { + name: "Actions", + editable: false, + sortable: false, + focusable: false, + dropdown: false, + width: 80, + }, + ]; + } + + format_data(transactions) { + this.transactions = []; + if (transactions[0]) { + this.currency = transactions[0]["currency"]; + } + this.transaction_dt_map = {}; + let length; + transactions.forEach((row) => { + length = this.transactions.push(this.format_row(row)); + this.transaction_dt_map[row["name"]] = length - 1; + }); + } + + format_row(row) { + return [ + row["date"], + row["party_type"], + row["party"], + row["description"], + row["deposit"], + row["withdrawal"], + row["unallocated_amount"], + row["reference_number"], + ` +