From 3c55463b4b86e9715e194d8c8268ee6190a5d30c Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 7 Jan 2021 12:41:44 +0530 Subject: [PATCH 01/93] fix: validation for ldc against supplier and section code --- .../lower_deduction_certificate.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py index e8a8ed8750..d4e7c12362 100644 --- a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py @@ -5,12 +5,16 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import getdate +from frappe.utils import getdate, get_link_to_form from frappe.model.document import Document from erpnext.accounts.utils import get_fiscal_year class LowerDeductionCertificate(Document): def validate(self): + self.validate_dates() + self.validate_supplier_against_section_code() + + def validate_dates(self): if getdate(self.valid_upto) < getdate(self.valid_from): frappe.throw(_("Valid Upto date cannot be before Valid From date")) @@ -24,3 +28,10 @@ class LowerDeductionCertificate(Document): <= fiscal_year.year_end_date): frappe.throw(_("Valid Upto date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) + def validate_supplier_against_section_code(self): + duplicate_certificate = frappe.db.get_value('Lower Deduction Certificate', {'supplier': self.supplier, 'section_code': self.section_code}, ['name']) + if duplicate_certificate: + certificate_link = get_link_to_form('Lower Deduction Certificate', duplicate_certificate) + frappe.throw(_("There is already a Lower Deduction Certificate {0} for Supplier {1} against Section Code {2}") + .format(certificate_link, frappe.bold(self.supplier), frappe.bold(self.section_code))) + From f1107598976d5584beaa3011654e78159b09b618 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Wed, 20 Jan 2021 18:12:48 +0530 Subject: [PATCH 02/93] fix: date validation --- .../lower_deduction_certificate.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py index d4e7c12362..b0a656af30 100644 --- a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py @@ -29,9 +29,19 @@ class LowerDeductionCertificate(Document): frappe.throw(_("Valid Upto date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) def validate_supplier_against_section_code(self): - duplicate_certificate = frappe.db.get_value('Lower Deduction Certificate', {'supplier': self.supplier, 'section_code': self.section_code}, ['name']) - if duplicate_certificate: - certificate_link = get_link_to_form('Lower Deduction Certificate', duplicate_certificate) - frappe.throw(_("There is already a Lower Deduction Certificate {0} for Supplier {1} against Section Code {2}") + duplicate_certificate = frappe.db.get_value('Lower Deduction Certificate', {'supplier': self.supplier, 'section_code': self.section_code}, ['name', 'valid_from', 'valid_upto'], as_dict=True) + if duplicate_certificate and self.are_dates_overlapping(duplicate_certificate): + certificate_link = get_link_to_form('Lower Deduction Certificate', duplicate_certificate.name) + frappe.throw(_("There is already a valid Lower Deduction Certificate {0} for Supplier {1} against Section Code {2} for this time period.") .format(certificate_link, frappe.bold(self.supplier), frappe.bold(self.section_code))) + def are_dates_overlapping(self,duplicate_certificate): + valid_from = duplicate_certificate.valid_from + valid_upto = duplicate_certificate.valid_upto + if valid_from <= getdate(self.valid_from) and getdate(self.valid_from) <= valid_upto: + return True + elif valid_from <= getdate(self.valid_upto) and getdate(self.valid_upto) <= valid_upto: + return True + elif getdate(self.valid_from) <= valid_from and valid_upto <= getdate(self.valid_upto): + return True + return False \ No newline at end of file From 6f718a31f0a04a75018a408e285a3092db10b064 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 27 Jan 2021 16:59:07 +0530 Subject: [PATCH 03/93] 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 04/93] 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 05/93] 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 06/93] 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 07/93] 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 55e7c5644cb17063f2ca79731cc71bebbaa48426 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 28 Jan 2021 12:00:18 +0530 Subject: [PATCH 08/93] fix:rfq contact email set --- .../doctype/request_for_quotation/request_for_quotation.py | 4 ++++ 1 file changed, 4 insertions(+) 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 From 4d9b6066a2de64c231464ed8c33044bbf479af0f Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 28 Jan 2021 18:07:08 +0530 Subject: [PATCH 09/93] 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 10/93] 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 ab88ec208e7a453bfa9cf655fe2762c447ece8ef Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 31 Jan 2021 12:31:46 +0530 Subject: [PATCH 11/93] fix: Error handling in loan reports --- .../report/loan_security_exposure/loan_security_exposure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..8b6ea869a3 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 @@ -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 From 0701f08ab6742a679637b7958acf9d75d1c9166e Mon Sep 17 00:00:00 2001 From: pateljannat Date: Wed, 3 Feb 2021 11:53:14 +0530 Subject: [PATCH 12/93] fix: item_group on update items --- erpnext/controllers/accounts_controller.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 21874fe2ca..919fdb1bc7 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1319,6 +1319,7 @@ def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, child_item.item_code = item.item_code child_item.item_name = item.item_name child_item.description = item.description + child_item.item_group = item.item_group child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_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")) @@ -1342,6 +1343,7 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna child_item.item_code = item.item_code child_item.item_name = item.item_name child_item.description = item.description + child_item.item_group = item.item_group 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")) From 8c3da162baef22ce50ed27fd18035b6de96e2a22 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 4 Feb 2021 19:39:41 +0530 Subject: [PATCH 13/93] feat: introduce parameter group in quality inspection --- .../item_quality_inspection_parameter.json | 11 ++- .../quality_inspection_parameter.json | 13 ++- .../__init__.py | 0 .../quality_inspection_parameter_group.js | 8 ++ .../quality_inspection_parameter_group.json | 82 +++++++++++++++++++ .../quality_inspection_parameter_group.py | 10 +++ ...test_quality_inspection_parameter_group.py | 10 +++ .../quality_inspection_reading.json | 11 ++- 8 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 erpnext/stock/doctype/quality_inspection_parameter_group/__init__.py 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 create mode 100644 erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.py create mode 100644 erpnext/stock/doctype/quality_inspection_parameter_group/test_quality_inspection_parameter_group.py diff --git a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json index 471e6853b5..9b1a47eed6 100644 --- a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json +++ b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json @@ -7,6 +7,7 @@ "engine": "InnoDB", "field_order": [ "specification", + "parameter_group", "value", "numeric", "column_break_3", @@ -75,12 +76,20 @@ "in_list_view": 1, "label": "Numeric", "width": "80px" + }, + { + "fetch_from": "specification.parameter_group", + "fieldname": "parameter_group", + "fieldtype": "Link", + "label": "Parameter Group", + "options": "Quality Inspection Parameter Group", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-02-01 19:18:46.924399", + "modified": "2021-02-04 18:50:02.056173", "modified_by": "Administrator", "module": "Stock", "name": "Item Quality Inspection Parameter", diff --git a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json index 0b5a9b5b3c..6d97601091 100644 --- a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json +++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json @@ -7,24 +7,35 @@ "engine": "InnoDB", "field_order": [ "parameter", + "parameter_group", "description" ], "fields": [ { "fieldname": "parameter", "fieldtype": "Data", + "in_list_view": 1, "label": "Parameter", + "reqd": 1, "unique": 1 }, { "fieldname": "description", "fieldtype": "Text Editor", "label": "Description" + }, + { + "fieldname": "parameter_group", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Parameter Group", + "options": "Quality Inspection Parameter Group", + "reqd": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-12-28 18:06:54.897317", + "modified": "2021-02-04 18:45:12.486344", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection Parameter", diff --git a/erpnext/stock/doctype/quality_inspection_parameter_group/__init__.py b/erpnext/stock/doctype/quality_inspection_parameter_group/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.js b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.js new file mode 100644 index 0000000000..8716a29871 --- /dev/null +++ b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Quality Inspection Parameter Group', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.json b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.json new file mode 100644 index 0000000000..57264741a6 --- /dev/null +++ b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.json @@ -0,0 +1,82 @@ +{ + "actions": [], + "autoname": "field:group_name", + "creation": "2021-02-04 18:44:12.223295", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "group_name" + ], + "fields": [ + { + "fieldname": "group_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Parameter Group Name", + "reqd": 1, + "unique": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-02-04 18:44:12.223295", + "modified_by": "Administrator", + "module": "Stock", + "name": "Quality Inspection Parameter Group", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Quality Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.py b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.py new file mode 100644 index 0000000000..1a3b1a0463 --- /dev/null +++ b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class QualityInspectionParameterGroup(Document): + pass diff --git a/erpnext/stock/doctype/quality_inspection_parameter_group/test_quality_inspection_parameter_group.py b/erpnext/stock/doctype/quality_inspection_parameter_group/test_quality_inspection_parameter_group.py new file mode 100644 index 0000000000..212d4b8c21 --- /dev/null +++ b/erpnext/stock/doctype/quality_inspection_parameter_group/test_quality_inspection_parameter_group.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestQualityInspectionParameterGroup(unittest.TestCase): + pass diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json index 35d58eff58..0eff5a8f00 100644 --- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json +++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json @@ -7,6 +7,7 @@ "engine": "InnoDB", "field_order": [ "specification", + "parameter_group", "status", "value", "numeric", @@ -210,12 +211,20 @@ "fieldtype": "Check", "in_list_view": 1, "label": "Numeric" + }, + { + "fetch_from": "specification.parameter_group", + "fieldname": "parameter_group", + "fieldtype": "Link", + "label": "Parameter Group", + "options": "Quality Inspection Parameter Group", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-02-01 19:46:22.138018", + "modified": "2021-02-04 19:15:37.991221", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection Reading", From 637ddff6e9a24b2f4ae05f22230ba61199a0d2cb Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 9 Feb 2021 16:17:30 +0530 Subject: [PATCH 14/93] fix: code cleanup --- erpnext/controllers/accounts_controller.py | 53 +++++++--------------- 1 file changed, 17 insertions(+), 36 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 919fdb1bc7..d341030aba 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1309,47 +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.item_group = item.item_group - 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.date_fieldname = trans_item.get(date_fieldname) or p_doc.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.item_group = item.item_group - 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 @@ -1413,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): From 1ab95413beacf98f7f7627ee2f8b6a3a2b0930ce Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 9 Feb 2021 17:35:17 +0530 Subject: [PATCH 15/93] fix: conditions simplified --- .../lower_deduction_certificate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py index b0a656af30..ad60db0559 100644 --- a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py @@ -38,9 +38,9 @@ class LowerDeductionCertificate(Document): def are_dates_overlapping(self,duplicate_certificate): valid_from = duplicate_certificate.valid_from valid_upto = duplicate_certificate.valid_upto - if valid_from <= getdate(self.valid_from) and getdate(self.valid_from) <= valid_upto: + if valid_from <= getdate(self.valid_from) <= valid_upto: return True - elif valid_from <= getdate(self.valid_upto) and getdate(self.valid_upto) <= valid_upto: + elif valid_from <= getdate(self.valid_upto) <= valid_upto: return True elif getdate(self.valid_from) <= valid_from and valid_upto <= getdate(self.valid_upto): return True From df6e20887d85500c312e2a1bf478d41e38e1a771 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 11 Feb 2021 11:04:39 +0530 Subject: [PATCH 16/93] fix: update total in words after updating items (#24602) * fix: Update total in words after Updating items Update total in words after Updating items in sales/purchase orders. Port of #24592 Closes ISS-20-21-09425 * test: Add test for total & words after update item Add test for total & words after updating items in sales order. --- erpnext/controllers/accounts_controller.py | 1 + erpnext/selling/doctype/sales_order/test_sales_order.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 21874fe2ca..991eef1d21 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1521,6 +1521,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.flags.ignore_validate_update_after_submit = True parent.set_qty_as_per_stock_uom() parent.calculate_taxes_and_totals() + parent.set_total_in_words() if parent_doctype == "Sales Order": make_packing_list(parent) parent.set_gross_profit() diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index e259367e58..cbfab8204f 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -325,6 +325,9 @@ class TestSalesOrder(unittest.TestCase): create_dn_against_so(so.name, 4) make_sales_invoice(so.name) + prev_total = so.get("base_total") + prev_total_in_words = so.get("base_in_words") + first_item_of_so = so.get("items")[0] trans_item = json.dumps([ {'item_code' : first_item_of_so.item_code, 'rate' : first_item_of_so.rate, \ @@ -340,6 +343,12 @@ class TestSalesOrder(unittest.TestCase): self.assertEqual(so.get("items")[-1].amount, 1400) self.assertEqual(so.status, 'To Deliver and Bill') + updated_total = so.get("base_total") + updated_total_in_words = so.get("base_in_words") + + self.assertEqual(updated_total, prev_total+1400) + self.assertNotEqual(updated_total_in_words, prev_total_in_words) + def test_update_child_removing_item(self): so = make_sales_order(**{ "item_list": [{ From 8609d3143099e4814da44a0c689b899653a710ef Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Thu, 11 Feb 2021 11:05:48 +0530 Subject: [PATCH 17/93] fix: amend cancelled job offer (#24589) --- erpnext/hr/doctype/job_offer/job_offer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/job_offer/job_offer.py b/erpnext/hr/doctype/job_offer/job_offer.py index c397a3f5ca..7e650f7691 100644 --- a/erpnext/hr/doctype/job_offer/job_offer.py +++ b/erpnext/hr/doctype/job_offer/job_offer.py @@ -16,7 +16,7 @@ class JobOffer(Document): def validate(self): self.validate_vacancies() - job_offer = frappe.db.exists("Job Offer",{"job_applicant": self.job_applicant}) + job_offer = frappe.db.exists("Job Offer",{"job_applicant": self.job_applicant, "docstatus": ["!=", 2]}) if job_offer and job_offer != self.name: frappe.throw(_("Job Offer: {0} is already for Job Applicant: {1}").format(frappe.bold(job_offer), frappe.bold(self.job_applicant))) From e3c72a3d718e6d560d8eed0c18609f0723afd6cd Mon Sep 17 00:00:00 2001 From: Anuja Pawar <60467153+Anuja-pawar@users.noreply.github.com> Date: Thu, 11 Feb 2021 11:06:12 +0530 Subject: [PATCH 18/93] fix: fix filters for the report (#24594) --- erpnext/regional/report/irs_1099/irs_1099.py | 22 +++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/erpnext/regional/report/irs_1099/irs_1099.py b/erpnext/regional/report/irs_1099/irs_1099.py index c1c8aedc9f..4e57ff7ea3 100644 --- a/erpnext/regional/report/irs_1099/irs_1099.py +++ b/erpnext/regional/report/irs_1099/irs_1099.py @@ -32,6 +32,10 @@ def execute(filters=None): data = [] columns = get_columns() + conditions = "" + if filters.supplier_group: + conditions += "AND s.supplier_group = %s" %frappe.db.escape(filters.get("supplier_group")) + data = frappe.db.sql(""" SELECT s.supplier_group as "supplier_group", @@ -46,15 +50,17 @@ def execute(filters=None): AND s.irs_1099 = 1 AND gl.fiscal_year = %(fiscal_year)s AND gl.party_type = "Supplier" + AND gl.company = %(company)s + {conditions} + GROUP BY gl.party + ORDER BY - gl.party DESC - """, { - "fiscal_year": filters.fiscal_year, - "supplier_group": filters.supplier_group, - "company": filters.company - }, as_dict=True) + gl.party DESC""".format(conditions=conditions), { + "fiscal_year": filters.fiscal_year, + "company": filters.company + }, as_dict=True) return columns, data @@ -79,13 +85,13 @@ def get_columns(): "fieldname": "tax_id", "label": _("Tax ID"), "fieldtype": "Data", - "width": 120 + "width": 200 }, { "fieldname": "payments", "label": _("Total Payments"), "fieldtype": "Currency", - "width": 120 + "width": 200 } ] From e816ed89e6923bc5d378ccc0440e68aa3b61d414 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Thu, 11 Feb 2021 11:12:13 +0530 Subject: [PATCH 19/93] fix: portal permission issue (#24577) --- .../setup/doctype/customer_group/customer_group.json | 11 ++++++++++- erpnext/setup/doctype/item_group/item_group.json | 11 ++++++++++- erpnext/setup/doctype/territory/territory.json | 11 ++++++++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/erpnext/setup/doctype/customer_group/customer_group.json b/erpnext/setup/doctype/customer_group/customer_group.json index 10f9bd0030..0e2ed9efcf 100644 --- a/erpnext/setup/doctype/customer_group/customer_group.json +++ b/erpnext/setup/doctype/customer_group/customer_group.json @@ -139,7 +139,7 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2020-03-18 18:10:13.048492", + "modified": "2021-02-08 17:01:52.162202", "modified_by": "Administrator", "module": "Setup", "name": "Customer Group", @@ -189,6 +189,15 @@ "permlevel": 1, "read": 1, "role": "Sales Manager" + }, + { + "email": 1, + "export": 1, + "print": 1, + "report": 1, + "role": "Customer", + "select": 1, + "share": 1 } ], "search_fields": "parent_customer_group", diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json index 31624edb49..e835214487 100644 --- a/erpnext/setup/doctype/item_group/item_group.json +++ b/erpnext/setup/doctype/item_group/item_group.json @@ -214,7 +214,7 @@ "is_tree": 1, "links": [], "max_attachments": 3, - "modified": "2020-12-30 12:57:38.876956", + "modified": "2021-02-08 17:02:44.951572", "modified_by": "Administrator", "module": "Setup", "name": "Item Group", @@ -271,6 +271,15 @@ "read": 1, "report": 1, "role": "Accounts User" + }, + { + "email": 1, + "export": 1, + "print": 1, + "report": 1, + "role": "Customer", + "select": 1, + "share": 1 } ], "search_fields": "parent_item_group", diff --git a/erpnext/setup/doctype/territory/territory.json b/erpnext/setup/doctype/territory/territory.json index aa8e0486f5..a25bda054b 100644 --- a/erpnext/setup/doctype/territory/territory.json +++ b/erpnext/setup/doctype/territory/territory.json @@ -123,7 +123,7 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2020-03-18 18:11:36.623555", + "modified": "2021-02-08 17:10:03.767426", "modified_by": "Administrator", "module": "Setup", "name": "Territory", @@ -166,6 +166,15 @@ { "read": 1, "role": "Maintenance User" + }, + { + "email": 1, + "export": 1, + "print": 1, + "report": 1, + "role": "Customer", + "select": 1, + "share": 1 } ], "search_fields": "parent_territory,territory_manager", From bb05acb60dce762269af1a20e786e26d3f892ef6 Mon Sep 17 00:00:00 2001 From: Kanchan Chauhan Date: Thu, 11 Feb 2021 11:18:59 +0530 Subject: [PATCH 20/93] fix(Leave Application): List view fix (#24550) --- erpnext/hr/doctype/leave_application/leave_application_list.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/hr/doctype/leave_application/leave_application_list.js b/erpnext/hr/doctype/leave_application/leave_application_list.js index cbb4b73227..a3c03b1bec 100644 --- a/erpnext/hr/doctype/leave_application/leave_application_list.js +++ b/erpnext/hr/doctype/leave_application/leave_application_list.js @@ -1,5 +1,6 @@ frappe.listview_settings['Leave Application'] = { add_fields: ["leave_type", "employee", "employee_name", "total_leave_days", "from_date", "to_date"], + has_indicator_for_draft: 1, get_indicator: function (doc) { if (doc.status === "Approved") { return [__("Approved"), "green", "status,=,Approved"]; From 30c8873f2446fdd02250cc0b92ef13346f6f7be8 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Thu, 11 Feb 2021 11:46:48 +0530 Subject: [PATCH 21/93] fix: validation for disabled warehouse (#24539) * fix: validation for disabled warehouse * fix: better function name * fix: validation in stock ledger entry --- erpnext/controllers/stock_controller.py | 3 ++- .../stock/doctype/stock_ledger_entry/stock_ledger_entry.py | 3 ++- erpnext/stock/utils.py | 6 +++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 4b5e347970..2ae9dc7102 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -313,7 +313,7 @@ class StockController(AccountsController): return serialized_items def validate_warehouse(self): - from erpnext.stock.utils import validate_warehouse_company + from erpnext.stock.utils import validate_warehouse_company, validate_disabled_warehouse warehouses = list(set([d.warehouse for d in self.get("items") if getattr(d, "warehouse", None)])) @@ -329,6 +329,7 @@ class StockController(AccountsController): warehouses.extend(from_warehouse) for w in warehouses: + validate_disabled_warehouse(w) validate_warehouse_company(w, self.company) def update_billing_percentage(self, update_modified=True): diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index a5c303ccb4..36d09efd1a 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -27,10 +27,11 @@ class StockLedgerEntry(Document): def validate(self): self.flags.ignore_submit_comment = True - from erpnext.stock.utils import validate_warehouse_company + from erpnext.stock.utils import validate_warehouse_company, validate_disabled_warehouse self.validate_mandatory() self.validate_item() self.validate_batch() + validate_disabled_warehouse(self.warehouse) validate_warehouse_company(self.warehouse, self.company) self.scrub_posting_time() self.validate_and_set_fiscal_year() diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 4ea7e4fcd6..0af3d90822 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe, erpnext from frappe import _ import json -from frappe.utils import flt, cstr, nowdate, nowtime +from frappe.utils import flt, cstr, nowdate, nowtime, get_link_to_form from six import string_types @@ -284,6 +284,10 @@ def is_group_warehouse(warehouse): if frappe.db.get_value("Warehouse", warehouse, "is_group"): frappe.throw(_("Group node warehouse is not allowed to select for transactions")) +def validate_disabled_warehouse(warehouse): + if frappe.db.get_value("Warehouse", warehouse, "disabled"): + frappe.throw(_("Disabled Warehouse {0} cannot be used for this transaction.").format(get_link_to_form('Warehouse', warehouse))) + def update_included_uom_in_report(columns, result, include_uom, conversion_factors): if not include_uom or not conversion_factors: return From 15f4672ffac9ad2d77b44abe0996fba4de211042 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 11 Feb 2021 12:11:44 +0530 Subject: [PATCH 22/93] fix: pos closing valdation (#24605) --- .../pos_closing_entry/pos_closing_entry.py | 15 ++++++++++----- .../pos_invoice_merge_log.py | 4 ++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py index edf3d5a574..f5224a269e 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py @@ -20,11 +20,16 @@ class POSClosingEntry(StatusUpdater): self.validate_pos_invoices() def validate_pos_closing(self): - user = frappe.get_all("POS Closing Entry", - filters = { "user": self.user, "docstatus": 1, "pos_profile": self.pos_profile }, - or_filters = { - "period_start_date": ("between", [self.period_start_date, self.period_end_date]), - "period_end_date": ("between", [self.period_start_date, self.period_end_date]) + user = frappe.db.sql(""" + SELECT name FROM `tabPOS Closing Entry` + WHERE + user = %(user)s AND docstatus = 1 AND pos_profile = %(profile)s AND + (period_start_date between %(start)s and %(end)s OR period_end_date between %(start)s and %(end)s) + """, { + 'user': self.user, + 'profile': self.pos_profile, + 'start': self.period_start_date, + 'end': self.period_end_date }) if user: 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 58409cd3c6..c88d67989b 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 @@ -212,8 +212,8 @@ def consolidate_pos_invoices(pos_invoices=[], closing_entry={}): invoice_by_customer = get_invoice_customer_map(invoices) if len(invoices) >= 5 and closing_entry: - enqueue_job(create_merge_logs, invoice_by_customer, closing_entry) closing_entry.set_status(update=True, status='Queued') + enqueue_job(create_merge_logs, invoice_by_customer, closing_entry) else: create_merge_logs(invoice_by_customer, closing_entry) @@ -225,8 +225,8 @@ def unconsolidate_pos_invoices(closing_entry): ) if len(merge_logs) >= 5: - enqueue_job(cancel_merge_logs, merge_logs, closing_entry) closing_entry.set_status(update=True, status='Queued') + enqueue_job(cancel_merge_logs, merge_logs, closing_entry) else: cancel_merge_logs(merge_logs, closing_entry) From e37a5c2646b28fd69e3fe1a00056f1ea283c937e Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 11 Feb 2021 13:10:52 +0530 Subject: [PATCH 23/93] fix: customer_currency referenced before assignment (#24607) --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 8d8babbe75..165294b24c 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -317,13 +317,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"): From aa00eb98987fb26435ce02ed10019ffa32d0ff0b Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 11 Feb 2021 13:23:01 +0530 Subject: [PATCH 24/93] fix: reposting issue with same posting date and posting time (#24570) * fix: reposting issue with same posting date and posting time * Update erpnext/stock/stock_ledger.py Co-authored-by: Nabin Hait --- erpnext/stock/stock_ledger.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 46919c8c8c..95f8c438b3 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -202,8 +202,7 @@ class update_entries_after(object): where item_code = %(item_code)s and warehouse = %(warehouse)s - and voucher_type = %(voucher_type)s - and voucher_no = %(voucher_no)s + and timestamp(posting_date, time_format(posting_time, '%H:%i:%s')) = timestamp(%(posting_date)s, time_format(%(posting_time)s, '%H:%i:%s')) order by creation ASC for update @@ -794,4 +793,4 @@ def get_future_sle_with_negative_qty(args): and qty_after_transaction + {0} < 0 order by timestamp(posting_date, posting_time) asc limit 1 - """.format(args.actual_qty), args, as_dict=1) \ No newline at end of file + """.format(args.actual_qty), args, as_dict=1) From 47f784aa9cbf76d226bc0bca6a37f6e07618079f Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Thu, 11 Feb 2021 13:23:51 +0530 Subject: [PATCH 25/93] fix: Lead not getting linked to call logs (#24593) Co-authored-by: Nabin Hait --- erpnext/crm/doctype/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 From aa235b36e05c8c058407c5f0366e63cf40d56a3e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 11 Feb 2021 13:26:53 +0530 Subject: [PATCH 26/93] Update erpnext/controllers/accounts_controller.py Co-authored-by: Marica --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index d341030aba..db6484a606 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1319,7 +1319,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child 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.date_fieldname = trans_item.get(date_fieldname) or p_doc.date_fieldname + child_item.update({date_fieldname: trans_item.get(date_fieldname) or p_doc.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 From a46a83b476f9dcfc040c5cc9fb09b9883bf947b9 Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Thu, 11 Feb 2021 13:41:10 +0530 Subject: [PATCH 27/93] fix: Assignment Rule Unassign Condition doesn't work (#24551) Co-authored-by: Marica Co-authored-by: Nabin Hait --- erpnext/selling/doctype/customer/customer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index bf8b7fc128..c452594608 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -126,7 +126,9 @@ class Customer(TransactionBase): '''If Customer created from Lead, update lead status to "Converted" update Customer link in Quotation, Opportunity''' if self.lead_name: - frappe.db.set_value('Lead', self.lead_name, 'status', 'Converted', update_modified=False) + lead = frappe.get_doc('Lead', self.lead_name) + lead.status = 'Converted' + lead.save() def create_lead_address_contact(self): if self.lead_name: From 0681a480c2d92e8dabbc0f91b7c39888723fe749 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 11 Feb 2021 14:06:15 +0530 Subject: [PATCH 28/93] feat(pos): mpesa related fixes & additions (#24306) * fix: switching of mode of payments * feat: transaction limit for mpesa integration * feat: resend payment request if one fails * feat: make new request only for failed ones * fix: invalid amount for mpesa request * fix: payment successful message not shown * fix: url and business shortcode for live env * fix: duplicate items validation for pos invoices * fix: pos closing entry queued status * fix: peroid end date for amended pos closing --- .../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 | 56 ++++-- erpnext/controllers/selling_controller.py | 6 +- .../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 ++++++++++++++---- .../page/point_of_sale/pos_item_cart.js | 2 +- .../selling/page/point_of_sale/pos_payment.js | 74 ++++--- 12 files changed, 433 insertions(+), 131 deletions(-) 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 165294b24c..94573f9df3 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -384,22 +384,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/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/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/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index f753b6d5c5..044e80357d 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -477,7 +477,7 @@ erpnext.PointOfSale.ItemCart = class { const taxes = frm.doc.taxes.map(t => { return { description: t.description, rate: t.rate - } + }; }); this.render_taxes(frm.doc.total_taxes_and_charges, taxes); } diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index c9b8ad9afe..bcbac3b4be 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -153,32 +153,23 @@ erpnext.PointOfSale.Payment = class { me.$payment_modes.find(`.${mode}-amount`).css('display', 'none'); me.$payment_modes.find(`.${mode}-name`).css('display', 'inline'); - const doc = me.events.get_frm().doc; me.selected_mode = me[`${mode}_control`]; me.selected_mode && me.selected_mode.$input.get(0).focus(); - const current_value = me.selected_mode ? me.selected_mode.get_value() : undefined; - !current_value && doc.grand_total > doc.paid_amount && me.selected_mode ? - me.selected_mode.set_value(doc.grand_total - doc.paid_amount) : ''; + me.auto_set_remaining_amount(); } }); - frappe.realtime.on("process_phone_payment", function(data) { - frappe.dom.unfreeze(); - cur_frm.reload_doc(); - let message = data["ResultDesc"]; - let title = __("Payment Failed"); + frappe.ui.form.on('POS Invoice', 'contact_mobile', (frm) => { + const contact = frm.doc.contact_mobile; + const request_button = $(this.request_for_payment_field.$input[0]); + if (contact) { + request_button.removeClass('btn-default').addClass('btn-primary'); + } else { + request_button.removeClass('btn-primary').addClass('btn-default'); + } + }); - if (data["ResultCode"] == 0) { - title = __("Payment Received"); - $('.btn.btn-xs.btn-default[data-fieldname=request_for_payment]').html(`Payment Received`); - me.events.submit_invoice(); - } - - frappe.msgprint({ - "message": message, - "title": title - }); - }); + this.setup_listener_for_payments(); this.$payment_modes.on('click', '.shortcut', () => { const value = $(this).attr('data-value'); @@ -224,6 +215,41 @@ erpnext.PointOfSale.Payment = class { }); } + setup_listener_for_payments() { + frappe.realtime.on("process_phone_payment", (data) => { + const doc = this.events.get_frm().doc; + const { response, amount, success, failure_message } = data; + let message, title; + + if (success) { + title = __("Payment Received"); + if (amount >= doc.grand_total) { + frappe.dom.unfreeze(); + message = __("Payment of {0} received successfully.", [format_currency(amount, doc.currency, 0)]); + this.events.submit_invoice(); + cur_frm.reload_doc(); + + } else { + message = __("Payment of {0} received successfully. Waiting for other requests to complete...", [format_currency(amount, doc.currency, 0)]); + } + } else if (failure_message) { + message = failure_message; + title = __("Payment Failed"); + } + + frappe.msgprint({ "message": message, "title": title }); + }); + } + + auto_set_remaining_amount() { + const doc = this.events.get_frm().doc; + const remaining_amount = doc.grand_total - doc.paid_amount; + const current_value = this.selected_mode ? this.selected_mode.get_value() : undefined; + if (!current_value && remaining_amount > 0 && this.selected_mode) { + this.selected_mode.set_value(remaining_amount); + } + } + attach_shortcuts() { const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl'; this.$component.find('.submit-order-btn').attr("title", `${ctrl_label}+Enter`); @@ -333,9 +359,11 @@ erpnext.PointOfSale.Payment = class { fieldtype: 'Currency', placeholder: __('Enter {0} amount.', [p.mode_of_payment]), onchange: function() { - if (this.value || this.value == 0) { - frappe.model.set_value(p.doctype, p.name, 'amount', flt(this.value)) - .then(() => me.update_totals_section()); + const current_value = frappe.model.get_value(p.doctype, p.name, 'amount'); + if (current_value != this.value) { + frappe.model + .set_value(p.doctype, p.name, 'amount', flt(this.value)) + .then(() => me.update_totals_section()) const formatted_currency = format_currency(this.value, currency); me.$payment_modes.find(`.${mode}-amount`).html(formatted_currency); From f25ab6f826b178364cfcfd4c725a220f5095ba8f Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 11 Feb 2021 17:50:57 +0530 Subject: [PATCH 29/93] fix: validate cancellation only if irn generated (#24608) --- erpnext/regional/india/e_invoice/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 5f701f2aa6..e016c81409 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -37,7 +37,7 @@ def validate_einvoice_fields(doc): elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN')) - elif doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled: + elif doc.irn and doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled: frappe.throw(_('You must cancel IRN before cancelling the document.'), title=_('Cancel Not Allowed')) def raise_document_name_too_long_error(): From 861522709012c94232c0fbdc28399ecbe5d4e878 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 11 Feb 2021 18:51:35 +0530 Subject: [PATCH 30/93] fix: Shopping cart breaks without Payment Gateway Account --- .../shopping_cart_settings.json | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json index 702583aead..3691721302 100644 --- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json +++ b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json @@ -26,10 +26,10 @@ "quotation_series", "section_break_8", "enable_checkout", - "payment_success_url", - "column_break_11", "save_quotations_as_draft", - "payment_gateway_account" + "column_break_11", + "payment_gateway_account", + "payment_success_url" ], "fields": [ { @@ -143,10 +143,12 @@ }, { "default": "Orders", + "depends_on": "enable_checkout", "description": "After payment completion redirect user to selected page.", "fieldname": "payment_success_url", "fieldtype": "Select", "label": "Payment Success Url", + "mandatory_depends_on": "enable_checkout", "options": "\nOrders\nInvoices\nMy Account" }, { @@ -154,9 +156,11 @@ "fieldtype": "Column Break" }, { + "depends_on": "enable_checkout", "fieldname": "payment_gateway_account", "fieldtype": "Link", "label": "Payment Gateway Account", + "mandatory_depends_on": "enable_checkout", "options": "Payment Gateway Account" }, { @@ -186,7 +190,7 @@ "idx": 1, "issingle": 1, "links": [], - "modified": "2021-02-01 18:18:54.606535", + "modified": "2021-02-11 18:48:30.433058", "modified_by": "Administrator", "module": "Shopping Cart", "name": "Shopping Cart Settings", From e2d0715cdf0b785c6f9f0c5b00ad4a92c51b54ef Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 11 Feb 2021 19:06:10 +0530 Subject: [PATCH 31/93] fix: Server side validation for missing payment gateway account --- erpnext/shopping_cart/cart.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py index 3c9f84982b..681d161edc 100644 --- a/erpnext/shopping_cart/cart.py +++ b/erpnext/shopping_cart/cart.py @@ -462,6 +462,9 @@ def get_party(user=None): return customer def get_debtors_account(cart_settings): + if not cart_settings.payment_gateway_account: + frappe.throw(_("Payment Gateway Account not set"), _("Mandatory")) + payment_gateway_account_currency = \ frappe.get_doc("Payment Gateway Account", cart_settings.payment_gateway_account).currency From 0a7939277c6c3c697c1f6b9e7c9c10a2e546c835 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Thu, 11 Feb 2021 20:11:06 +0530 Subject: [PATCH 32/93] fix: dynamic links for reports (#24462) * fix: dynamic links for reports * fix: revert changes for payment period report * fix: link doctypes --- .../bank_clearance_summary.py | 54 +++++++++++++++---- .../prospects_engaged_but_not_converted.py | 53 ++++++++++++++---- 2 files changed, 89 insertions(+), 18 deletions(-) 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/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 = [] From ee87484134164cf2360e558fc5249e57252a1eac Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Thu, 11 Feb 2021 20:11:58 +0530 Subject: [PATCH 33/93] fix: calculated discount percentage (#24510) * fix: calculated discount percentage * fix: slider Co-authored-by: Nabin Hait --- erpnext/public/js/controllers/taxes_and_totals.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 22e75780b8..416495ceac 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -4,7 +4,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ setup: function() {}, - apply_pricing_rule_on_item: function(item){ + apply_pricing_rule_on_item: function(item) { let effective_item_rate = item.price_list_rate; let item_rate = item.rate; if (in_list(["Sales Order", "Quotation"], item.parenttype) && item.blanket_order_rate) { @@ -26,6 +26,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ if (item.discount_amount) { item_rate = flt((item.rate_with_margin) - (item.discount_amount), precision('rate', item)); + item.discount_percentage = 100 * flt(item.discount_amount) / flt(item.rate_with_margin); } frappe.model.set_value(item.doctype, item.name, "rate", item_rate); From 7e1dcf911d9552fb84e09158998d1c3c0e26e057 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Thu, 11 Feb 2021 20:19:30 +0530 Subject: [PATCH 34/93] feat: capture Rate of stock UOM in purchase (#24315) * feat: capture Rate of stock UOM in purchase * fix: review changes * added server side code * added stock uom rate in sales transactions * fix: review changes * fix: resolving conflicts * adding patch * fix: removing patch --- .../purchase_invoice_item/purchase_invoice_item.json | 11 ++++++++++- .../sales_invoice_item/sales_invoice_item.json | 11 ++++++++++- .../purchase_order_item/purchase_order_item.json | 11 ++++++++++- erpnext/controllers/stock_controller.py | 6 ++++++ erpnext/public/js/controllers/buying.js | 1 - erpnext/public/js/controllers/transaction.js | 12 ++++++++++-- .../doctype/quotation_item/quotation_item.json | 11 ++++++++++- .../doctype/sales_order_item/sales_order_item.json | 12 ++++++++++-- .../delivery_note_item/delivery_note_item.json | 11 ++++++++++- .../purchase_receipt_item/purchase_receipt_item.json | 11 ++++++++++- 10 files changed, 86 insertions(+), 11 deletions(-) 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/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/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 2ae9dc7102..70e8d8cc78 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() @@ -396,6 +397,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(): diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index a2a723dd77..c96386611b 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -191,7 +191,6 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ item.rejected_qty = flt(item.received_qty - item.qty, precision("rejected_qty", item)); item.received_stock_qty = flt(item.conversion_factor, precision("conversion_factor", item)) * flt(item.received_qty); } - this._super(doc, cdt, cdn); }, diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 1db0f5f29d..e5f9049017 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -40,7 +40,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ cur_frm.cscript.set_gross_profit(item); cur_frm.cscript.calculate_taxes_and_totals(); - + cur_frm.cscript.calculate_stock_uom_rate(frm, cdt, cdn); }); @@ -1122,6 +1122,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }); } + me.calculate_stock_uom_rate(doc, cdt, cdn); }, conversion_factor: function(doc, cdt, cdn, dont_fetch_price_list_rate) { @@ -1142,6 +1143,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ frappe.meta.has_field(doc.doctype, "price_list_currency")) { this.apply_price_list(item, true); } + this.calculate_stock_uom_rate(doc, cdt, cdn); } }, @@ -1162,9 +1164,15 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ qty: function(doc, cdt, cdn) { let item = frappe.get_doc(cdt, cdn); this.conversion_factor(doc, cdt, cdn, true); + this.calculate_stock_uom_rate(doc, cdt, cdn); this.apply_pricing_rule(item, true); }, + calculate_stock_uom_rate: function(doc, cdt, cdn) { + let item = frappe.get_doc(cdt, cdn); + item.stock_uom_rate = flt(item.rate)/flt(item.conversion_factor); + refresh_field("stock_uom_rate", item.name, item.parentfield); + }, service_stop_date: function(frm, cdt, cdn) { var child = locals[cdt][cdn]; @@ -1275,7 +1283,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ this.frm.set_currency_labels(["base_rate", "base_net_rate", "base_price_list_rate", "base_amount", "base_net_amount"], company_currency, "items"); - this.frm.set_currency_labels(["rate", "net_rate", "price_list_rate", "amount", "net_amount"], + this.frm.set_currency_labels(["rate", "net_rate", "price_list_rate", "amount", "net_amount", "stock_uom_rate"], this.frm.doc.currency, "items"); if(this.frm.fields_dict["operations"]) { diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index 59ae7b2323..a6785f709a 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -47,6 +47,7 @@ "base_amount", "base_net_amount", "pricing_rules", + "stock_uom_rate", "is_free_item", "section_break_43", "valuation_rate", @@ -634,12 +635,20 @@ "print_hide": 1, "read_only": 1, "report_hide": 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-05-19 20:48:43.222229", + "modified": "2021-01-30 21:39:40.174551", "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item", diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 159655b74b..37e47a9d41 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -46,6 +46,7 @@ "base_rate", "base_amount", "pricing_rules", + "stock_uom_rate", "is_free_item", "section_break_24", "net_rate", @@ -214,7 +215,6 @@ "fieldtype": "Link", "label": "UOM", "options": "UOM", - "print_hide": 0, "reqd": 1 }, { @@ -780,12 +780,20 @@ "fieldname": "manufacturing_section_section", "fieldtype": "Section Break", "label": "Manufacturing Section" + }, + { + "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-012-07 20:54:32.309460", + "modified": "2021-01-30 21:35:07.617320", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 9de088df0e..17996247c5 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -47,6 +47,7 @@ "base_rate", "base_amount", "pricing_rules", + "stock_uom_rate", "is_free_item", "section_break_25", "net_rate", @@ -743,13 +744,21 @@ "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, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-12-26 17:31:27.029803", + "modified": "2021-01-30 21:42:03.767968", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index e99119202e..8974ad9318 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -48,6 +48,7 @@ "base_rate", "base_amount", "pricing_rules", + "stock_uom_rate", "is_free_item", "section_break_29", "net_rate", @@ -874,6 +875,14 @@ "label": "Received Qty in Stock UOM", "print_hide": 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": "delivery_note_item", "fieldtype": "Data", @@ -886,7 +895,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-12-26 16:50:56.479347", + "modified": "2021-01-30 21:44:06.918515", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", From bb0d8f8038b1b1b73fdf6e711a53d7eb699950e0 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Thu, 11 Feb 2021 20:59:28 +0530 Subject: [PATCH 35/93] Update accounts_controller.py --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 19c97b68d3..12a81c7887 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1319,7 +1319,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child 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.date_fieldname}) + 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 From ad1f2b41f47bc0016d4abe7915f0f88af6cbc197 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 12 Feb 2021 11:29:24 +0530 Subject: [PATCH 36/93] fix: Issues with packing items --- erpnext/public/js/utils/serial_no_batch_selector.js | 9 ++++++--- erpnext/selling/doctype/sales_order/sales_order.js | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 2623c3c1a7..d49a8138fb 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -140,6 +140,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ () => me.update_batch_serial_no_items(), () => { refresh_field("items"); + refresh_field("packed_items"); if (me.callback) { return me.callback(me.item); } @@ -154,7 +155,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ if (this.item.serial_no) { this.dialog.fields_dict.serial_no.set_value(this.item.serial_no); } - + if (this.has_batch && !this.has_serial_no && d.batch_no) { this.frm.doc.items.forEach(data => { if(data.item_code == d.item_code) { @@ -231,7 +232,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ this.map_row_values(row, batch, 'batch_no', 'selected_qty', this.values.warehouse); }); - } + } }, update_serial_no_item() { @@ -250,7 +251,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ filters: { 'name': ["in", selected_serial_nos]}, fields: ["batch_no", "name"] }).then((data) => { - // data = [{batch_no: 'batch-1', name: "SR-001"}, + // data = [{batch_no: 'batch-1', name: "SR-001"}, // {batch_no: 'batch-2', name: "SR-003"}, {batch_no: 'batch-2', name: "SR-004"}] const batch_serial_map = data.reduce((acc, d) => { if (!acc[d['batch_no']]) acc[d['batch_no']] = []; @@ -298,6 +299,8 @@ erpnext.SerialNoBatchSelector = Class.extend({ } else { row.warehouse = values.warehouse || warehouse; } + + this.frm.dirty(); }, update_total_qty: function() { diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index e492377385..e3b41e66fb 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -514,7 +514,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( make_delivery_note: function() { frappe.model.open_mapped_doc({ method: "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note", - frm: me.frm + frm: this.frm }) }, From c44d6b95464c6262cd3082441ea8432194dff8c4 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Fri, 12 Feb 2021 12:24:41 +0530 Subject: [PATCH 37/93] fix: NoneType object has no attribute len() (#24616) --- erpnext/regional/india/e_invoice/utils.py | 64 +++++++++++------------ 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index e016c81409..2043f49825 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -23,7 +23,7 @@ def validate_einvoice_fields(doc): invalid_doctype = doc.doctype != 'Sales Invoice' invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') - no_taxes_applied = len(doc.get('taxes', [])) == 0 + no_taxes_applied = not doc.get('taxes') if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction or no_taxes_applied: return @@ -63,7 +63,7 @@ def get_transaction_details(invoice): elif invoice.gst_category == 'Overseas': supply_type = 'EXPWOP' elif invoice.gst_category == 'Deemed Export': supply_type = 'DEXP' - if not supply_type: + if not supply_type: rr, sez, overseas, export = bold('Registered Regular'), bold('SEZ'), bold('Overseas'), bold('Deemed Export') frappe.throw(_('GST category should be one of {}, {}, {}, {}').format(rr, sez, overseas, export), title=_('Invalid Supply Type')) @@ -128,7 +128,7 @@ def get_gstin_details(gstin): if details: frappe.local.gstin_cache[key] = details return details - + if not details: return GSPConnector.get_gstin_details(gstin) @@ -174,7 +174,7 @@ def get_item_list(invoice): item.serial_no = "" item = update_item_taxes(invoice, item) - + item.total_value = abs( item.taxable_value + item.igst_amount + item.sgst_amount + item.cgst_amount + item.cess_amount + item.cess_nadv_amount + item.other_charges @@ -232,9 +232,9 @@ def get_invoice_value_details(invoice): invoice_value_details.round_off = invoice.base_rounding_adjustment invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total) invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total) - + invoice_value_details = update_invoice_taxes(invoice, invoice_value_details) - + return invoice_value_details def update_invoice_taxes(invoice, invoice_value_details): @@ -251,13 +251,13 @@ def update_invoice_taxes(invoice, invoice_value_details): if t.account_head in gst_accounts.cess_account: # using after discount amt since item also uses after discount amt for cess calc invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount) - + for tax_type in ['igst', 'cgst', 'sgst']: if t.account_head in gst_accounts[f'{tax_type}_account']: invoice_value_details[f'total_{tax_type}_amt'] += abs(t.base_tax_amount_after_discount_amount) else: invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount) - + return invoice_value_details def get_payment_details(invoice): @@ -329,23 +329,23 @@ def make_einvoice(invoice): place_of_supply = get_place_of_supply(invoice, invoice.doctype) or invoice.billing_address_gstin place_of_supply = place_of_supply[:2] buyer_details.update(dict(place_of_supply=place_of_supply)) - + shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({}) if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name: if invoice.gst_category == 'Overseas': shipping_details = get_overseas_address_details(invoice.shipping_address_name) else: shipping_details = get_party_details(invoice.shipping_address_name) - + if invoice.is_pos and invoice.base_paid_amount: payment_details = get_payment_details(invoice) - + if invoice.is_return and invoice.return_against: prev_doc_details = get_return_doc_reference(invoice) - + if invoice.transporter: eway_bill_details = get_eway_bill_details(invoice) - + # not yet implemented dispatch_details = period_details = export_details = frappe._dict({}) @@ -357,7 +357,7 @@ def make_einvoice(invoice): export_details=export_details, eway_bill_details=eway_bill_details ) einvoice = json.loads(einvoice) - + validations = json.loads(read_json('einv_validation')) errors = validate_einvoice(validations, einvoice) if errors: @@ -419,7 +419,7 @@ def validate_einvoice(validations, einvoice, errors=[]): errors.append(_('{} {} should be between {} and {}').format(label, value, minimum, maximum)) if pattern_str and not pattern.match(value): errors.append(field_validation.get('validationMsg')) - + return errors class RequestFailed(Exception): pass @@ -452,19 +452,19 @@ class GSPConnector(): else: credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None return credentials - + def get_seller_gstin(self): gstin = self.invoice.company_gstin or frappe.db.get_value('Address', self.invoice.company_address, 'gstin') if not gstin: frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.')) return gstin - + def get_auth_token(self): if time_diff_in_seconds(self.e_invoice_settings.token_expiry, now_datetime()) < 150.0: self.fetch_auth_token() - + return self.e_invoice_settings.auth_token - + def make_request(self, request_type, url, headers=None, data=None): if request_type == 'post': res = make_post_request(url, headers=headers, data=data) @@ -473,7 +473,7 @@ class GSPConnector(): self.log_request(url, headers, data, res) return res - + def log_request(self, url, headers, data, res): headers.update({ 'password': self.credentials.password }) request_log = frappe.get_doc({ @@ -504,7 +504,7 @@ class GSPConnector(): except Exception: self.log_error(res) self.raise_error(True) - + def get_headers(self): return { 'content-type': 'application/json', @@ -526,7 +526,7 @@ class GSPConnector(): else: self.log_error(res) raise RequestFailed - + except RequestFailed: self.raise_error() @@ -571,7 +571,7 @@ class GSPConnector(): else: raise RequestFailed - + except RequestFailed: errors = self.sanitize_error_message(res.get('message')) self.raise_error(errors=errors) @@ -579,7 +579,7 @@ class GSPConnector(): except Exception: self.log_error(data) self.raise_error(True) - + def get_irn_details(self, irn): headers = self.get_headers() @@ -590,7 +590,7 @@ class GSPConnector(): return res.get('result') else: raise RequestFailed - + except RequestFailed: errors = self.sanitize_error_message(res.get('message')) self.raise_error(errors=errors) @@ -598,7 +598,7 @@ class GSPConnector(): except Exception: self.log_error() self.raise_error(True) - + def cancel_irn(self, irn, reason, remark): headers = self.get_headers() data = json.dumps({ @@ -620,7 +620,7 @@ class GSPConnector(): else: raise RequestFailed - + except RequestFailed: errors = self.sanitize_error_message(res.get('message')) self.raise_error(errors=errors) @@ -669,7 +669,7 @@ class GSPConnector(): except Exception: self.log_error(data) self.raise_error(True) - + def cancel_eway_bill(self, eway_bill, reason, remark): headers = self.get_headers() data = json.dumps({ @@ -701,7 +701,7 @@ class GSPConnector(): except Exception: self.log_error(data) self.raise_error(True) - + def sanitize_error_message(self, message): ''' On validation errors, response message looks something like this: @@ -740,7 +740,7 @@ class GSPConnector(): "Exception:", err_tb ]) frappe.log_error(title=_('E Invoice Request Failed'), message=message) - + def raise_error(self, raise_exception=False, errors=[]): title = _('E Invoice Request Failed') if errors: @@ -753,7 +753,7 @@ class GSPConnector(): raise_exception=raise_exception, indicator='red' ) - + def set_einvoice_data(self, res): enc_signed_invoice = res.get('SignedInvoice') dec_signed_invoice = jwt.decode(enc_signed_invoice, verify=False)['data'] @@ -792,7 +792,7 @@ class GSPConnector(): _file.save() frappe.db.commit() self.invoice.qrcode_image = _file.file_url - + def update_invoice(self): self.invoice.flags.ignore_validate_update_after_submit = True self.invoice.flags.ignore_validate = True From 164ffac4efe8d7a71828a513393b8435eb6babf1 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 11 Feb 2021 11:07:59 +0530 Subject: [PATCH 38/93] fix: Consolidated Financial Statement report not works if child company account not present in parent company (#24580) Fixed conflicts --- .../consolidated_financial_statement.py | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) 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 = [] From fd4e7bdbb11793bf9b809edd2097c9386a0ec8d1 Mon Sep 17 00:00:00 2001 From: Richard Case <64409021+casesolved-co-uk@users.noreply.github.com> Date: Mon, 15 Feb 2021 06:28:27 +0000 Subject: [PATCH 39/93] fix: plaid client version to support latest API (#24531) --- .../doctype/plaid_settings/plaid_connector.py | 4 ++-- requirements.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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/requirements.txt b/requirements.txt index 4511aa54d8..5a352364b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ frappe gocardless-pro==1.11.0 googlemaps==3.1.1 pandas>=1.0.5 -plaid-python==6.0.0 +plaid-python>=7.0.0 pycountry==19.8.18 PyGithub==1.44.1 python-stdnum==1.12 @@ -12,4 +12,4 @@ taxjar==1.9.0 tweepy==3.8.0 Unidecode==1.1.1 WooCommerce==2.1.1 -pycryptodome==3.9.8 \ No newline at end of file +pycryptodome==3.9.8 From 9bf05ad927de697b03e644048ba55b96c2017b2f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 15 Feb 2021 13:32:50 +0530 Subject: [PATCH 40/93] feat: Loans Dashboard --- .../loan_disbursements.json | 29 +++++++ .../dashboard_chart/new_loans/new_loans.json | 30 ++++++++ .../top_10_pledged_loan_securities.json | 29 +++++++ .../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 | 10 +++ .../loan_dashboard/loan_dashboard.json | 57 ++++++++++++++ .../active_loans/active_loans.json | 23 ++++++ .../active_securities/active_securities.json | 23 ++++++ .../applicants_with_unpaid_shortfall.json | 21 +++++ .../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_sanctioned_amount.json | 23 ++++++ .../total_shortfall_amount.json | 23 ++++++ .../applicant_wise_loan_security_exposure.py | 4 +- .../loan_interest_report.py | 2 +- .../loan_security_exposure.py | 2 +- .../loan_management/loan_management.json | 9 ++- 24 files changed, 496 insertions(+), 5 deletions(-) create mode 100644 erpnext/loan_management/dashboard_chart/loan_disbursements/loan_disbursements.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 create mode 100644 erpnext/loan_management/dashboard_chart_source/__init__.py create mode 100644 erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/__init__.py 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/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_sanctioned_amount/total_sanctioned_amount.json create mode 100644 erpnext/loan_management/number_card/total_shortfall_amount/total_shortfall_amount.json 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/new_loans/new_loans.json b/erpnext/loan_management/dashboard_chart/new_loans/new_loans.json new file mode 100644 index 0000000000..6e196d25a2 --- /dev/null +++ b/erpnext/loan_management/dashboard_chart/new_loans/new_loans.json @@ -0,0 +1,30 @@ +{ + "based_on": "creation", + "chart_name": "New Loans", + "chart_type": "Count", + "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-06 17:11:05.154562", + "modified": "2021-02-06 17:28:40.134346", + "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..d48a6ce083 --- /dev/null +++ b/erpnext/loan_management/dashboard_chart/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json @@ -0,0 +1,29 @@ +{ + "based_on": "", + "chart_name": "Top 10 Pledged Loan Securities", + "chart_type": "Custom", + "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, + "modified": "2021-02-07 17:34:25.117983", + "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/loan_management/dashboard_chart_source/__init__.py b/erpnext/loan_management/dashboard_chart_source/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/__init__.py b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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..b481d67491 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -348,3 +348,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/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..2da6bb7f01 --- /dev/null +++ b/erpnext/loan_management/loan_management_dashboard/loan_dashboard/loan_dashboard.json @@ -0,0 +1,57 @@ +{ + "cards": [ + { + "card": "Total Disbursed" + }, + { + "card": "Total Sanctioned Amount" + }, + { + "card": "Active Loans" + }, + { + "card": "Open Loan Applications" + }, + { + "card": "New Loans" + }, + { + "card": "New Loan Applications" + }, + { + "card": "Active Securities" + }, + { + "card": "Applicants With Unpaid Shortfall" + }, + { + "card": "Total Shortfall Amount" + } + ], + "charts": [ + { + "chart": "New Loans", + "width": "Half" + }, + { + "chart": "Loan Disbursements", + "width": "Half" + }, + { + "chart": "Top 10 Pledged Loan Securities", + "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-09 08:07:27.293764", + "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/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_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/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..fa55ec57cd 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')) diff --git a/erpnext/loan_management/workspace/loan_management/loan_management.json b/erpnext/loan_management/workspace/loan_management/loan_management.json index 2e8b5bf5b3..848b1e761d 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-06 17:31:17.593447", "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 From e963362dfd6daa6d2577ad400fe8488c983f809d Mon Sep 17 00:00:00 2001 From: Afshan Date: Mon, 15 Feb 2021 15:00:37 +0530 Subject: [PATCH 41/93] fix: check if customer provided item while setting opening stock --- erpnext/stock/doctype/item/item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index b661570fd7..7b7d2da969 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -177,7 +177,7 @@ class Item(WebsiteGenerator): if not self.valuation_rate and self.standard_rate: self.valuation_rate = self.standard_rate - if not self.valuation_rate: + if not self.valuation_rate and not self.is_customer_provided_item: frappe.throw(_("Valuation Rate is mandatory if Opening Stock entered")) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry From 57c3d373a7197ce8da94c9337a63cebf5002845a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 15 Feb 2021 14:16:00 +0530 Subject: [PATCH 42/93] fix(India): Add GST state code for Ladakh --- erpnext/patches.txt | 1 + .../patches/v12_0/add_state_code_for_ladakh.py | 16 ++++++++++++++++ erpnext/regional/india/__init__.py | 4 +++- erpnext/regional/india/gst_state_code_data.json | 5 +++++ 4 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v12_0/add_state_code_for_ladakh.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 3b7c6ab48e..e1f7250dec 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -752,3 +752,4 @@ 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 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/regional/india/__init__.py b/erpnext/regional/india/__init__.py index d6221a80aa..378b735e07 100644 --- a/erpnext/regional/india/__init__.py +++ b/erpnext/regional/india/__init__.py @@ -20,6 +20,7 @@ states = [ 'Jharkhand', 'Karnataka', 'Kerala', + 'Ladakh', 'Lakshadweep Islands', 'Madhya Pradesh', 'Maharashtra', @@ -59,6 +60,7 @@ state_numbers = { "Jharkhand": "20", "Karnataka": "29", "Kerala": "32", + "Ladakh": "38", "Lakshadweep Islands": "31", "Madhya Pradesh": "23", "Maharashtra": "27", @@ -80,4 +82,4 @@ state_numbers = { "West Bengal": "19", } -number_state_mapping = {v: k for k, v in iteritems(state_numbers)} \ No newline at end of file +number_state_mapping = {v: k for k, v in iteritems(state_numbers)} diff --git a/erpnext/regional/india/gst_state_code_data.json b/erpnext/regional/india/gst_state_code_data.json index ff88e0f9d6..8481c27972 100644 --- a/erpnext/regional/india/gst_state_code_data.json +++ b/erpnext/regional/india/gst_state_code_data.json @@ -168,5 +168,10 @@ "state_number": "37", "state_code": "AD", "state_name": "Andhra Pradesh (New)" + }, + { + "state_number": "38", + "state_code": "LA", + "state_name": "Ladakh" } ] From 535dc4aea2865a76d0564de7322504383d44f61f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 15 Feb 2021 15:54:25 +0530 Subject: [PATCH 43/93] fix: Do not round off per day interest --- .../doctype/loan_interest_accrual/loan_interest_accrual.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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)) From b0735732f960cd3235416568c9227938c9c5388e Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 15 Feb 2021 17:38:40 +0530 Subject: [PATCH 44/93] fix(minor): add erpnext-logo.png --- erpnext/public/images/erpnext-log.png | Bin 0 -> 2360 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 erpnext/public/images/erpnext-log.png diff --git a/erpnext/public/images/erpnext-log.png b/erpnext/public/images/erpnext-log.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 Date: Mon, 15 Feb 2021 17:40:50 +0530 Subject: [PATCH 45/93] fix(typo): erpnext-logo.png --- .../images/{erpnext-log.png => erpnext-logo.png} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename erpnext/public/images/{erpnext-log.png => erpnext-logo.png} (100%) diff --git a/erpnext/public/images/erpnext-log.png b/erpnext/public/images/erpnext-logo.png similarity index 100% rename from erpnext/public/images/erpnext-log.png rename to erpnext/public/images/erpnext-logo.png From 243661b37b1a79763e71d46aaa80148bbd6578a0 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 15 Feb 2021 19:27:49 +0530 Subject: [PATCH 46/93] fix: formatting query args (#24627) --- erpnext/stock/stock_ledger.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 95f8c438b3..8c9c172ebd 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -194,6 +194,8 @@ class update_entries_after(object): self.process_sle(sle) def get_sle_against_current_voucher(self): + self.args['time_format'] = '%H:%i:%s' + return frappe.db.sql(""" select *, timestamp(posting_date, posting_time) as "timestamp" @@ -202,7 +204,7 @@ class update_entries_after(object): where item_code = %(item_code)s and warehouse = %(warehouse)s - and timestamp(posting_date, time_format(posting_time, '%H:%i:%s')) = timestamp(%(posting_date)s, time_format(%(posting_time)s, '%H:%i:%s')) + and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) order by creation ASC for update From d60ff83b6fc42cddea27c85099a322764dd8b115 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 16 Feb 2021 09:12:27 +0530 Subject: [PATCH 47/93] fix: added patch to fix incorrect stock qty and account value (#24628) --- erpnext/accounts/utils.py | 13 ++++++--- erpnext/patches.txt | 1 + .../item_reposting_for_incorrect_sl_and_gl.py | 27 +++++++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 67c7fd2d22..80319056dd 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) @@ -895,7 +895,8 @@ def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for 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) + future_stock_vouchers = get_future_stock_vouchers(posting_date, posting_time, + for_warehouses, for_items, company) gle = get_voucherwise_gl_entries(future_stock_vouchers, posting_date) for voucher_type, voucher_no in future_stock_vouchers: @@ -909,7 +910,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 +923,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 +987,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/patches.txt b/erpnext/patches.txt index e1f7250dec..e5ee551c11 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -753,3 +753,4 @@ 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 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..7c4835a813 --- /dev/null +++ b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py @@ -0,0 +1,27 @@ +import frappe +from frappe import _ +from erpnext.stock.stock_ledger import update_entries_after +from erpnext.accounts.utils import update_gl_entries_after + + +data = frappe.db.sql(''' SELECT name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time + from `tabStock Ledger Entry` where creation > '2020-12-26 12:58:55.903836' and is_cancelled = 0 + order by timestamp(posting_date, posting_time) asc, creation asc''', as_dict=1) + +for index, d in enumerate(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) + +frappe.db.auto_commit_on_many_writes = 1 + +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 \ No newline at end of file From 9a40c1b2e1af132b860452c54aa74f83adc24357 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 16 Feb 2021 10:35:10 +0530 Subject: [PATCH 48/93] fix: patch --- .../item_reposting_for_incorrect_sl_and_gl.py | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) 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 index 7c4835a813..eff0ae4694 100644 --- 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 @@ -4,24 +4,25 @@ from erpnext.stock.stock_ledger import update_entries_after from erpnext.accounts.utils import update_gl_entries_after -data = frappe.db.sql(''' SELECT name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time - from `tabStock Ledger Entry` where creation > '2020-12-26 12:58:55.903836' and is_cancelled = 0 - order by timestamp(posting_date, posting_time) asc, creation asc''', as_dict=1) +def execute(): + data = frappe.db.sql(''' SELECT name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time + from `tabStock Ledger Entry` where creation > '2020-12-26 12:58:55.903836' and is_cancelled = 0 + order by timestamp(posting_date, posting_time) asc, creation asc''', as_dict=1) -for index, d in enumerate(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) + for index, d in enumerate(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) -frappe.db.auto_commit_on_many_writes = 1 + frappe.db.auto_commit_on_many_writes = 1 -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) + 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 \ No newline at end of file + frappe.db.auto_commit_on_many_writes = 0 \ No newline at end of file From 7e8113c7e8b77f62d9af334abe3a2a8c683dbe59 Mon Sep 17 00:00:00 2001 From: prssanna Date: Tue, 16 Feb 2021 11:59:12 +0530 Subject: [PATCH 49/93] fix: broken image style --- erpnext/public/less/hub.less | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/public/less/hub.less b/erpnext/public/less/hub.less index 8cb7a9c1ce..c08121f704 100644 --- a/erpnext/public/less/hub.less +++ b/erpnext/public/less/hub.less @@ -32,7 +32,13 @@ body[data-route*="marketplace"] { } .hub-image-loading, .hub-image-broken { - .img-background(); + content: " "; + display: block; + position: absolute; + left: 0; + height: 100%; + width: 100%; + background-color: var(--bg-light-gray); display: flex; align-items: center; justify-content: center; From 9b178bcd10689cfea8d7ad0a850a09e6f03630e0 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 16 Feb 2021 14:57:00 +0530 Subject: [PATCH 50/93] fix: stock and account balance syncing (#24644) * fix: stock and account balance syncing * fix: stock and account balance syncing * fix: stock and account balance syncing * fix: minor fix --- erpnext/accounts/utils.py | 14 +++++++++----- erpnext/controllers/stock_controller.py | 3 +-- .../clinical_procedure/clinical_procedure.py | 1 + .../clinical_procedure/test_clinical_procedure.py | 5 +++-- .../item_reposting_for_incorrect_sl_and_gl.py | 1 - erpnext/stock/__init__.py | 2 +- erpnext/stock/doctype/batch/test_batch.py | 6 +++--- .../test_landed_cost_voucher.py | 5 ++++- .../repost_item_valuation/repost_item_valuation.py | 2 +- erpnext/stock/doctype/shipment/test_shipment.py | 1 + erpnext/stock/stock_ledger.py | 3 ++- 11 files changed, 26 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 80319056dd..60d1e20fea 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -888,18 +888,22 @@ 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, company) - gle = get_voucherwise_gl_entries(future_stock_vouchers, posting_date) + gle = get_voucherwise_gl_entries(stock_vouchers, posting_date) - for voucher_type, voucher_no in future_stock_vouchers: + 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) expected_gle = voucher_obj.get_gl_entries(warehouse_account) @@ -924,7 +928,7 @@ def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, f values += for_warehouses if company: - condition += " and company = %s " + condition += " and company = %s" values.append(company) for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 70e8d8cc78..ea9659ce01 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -488,13 +488,12 @@ 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(): check_if_stock_and_account_balance_synced(self.posting_date, self.company, self.doctype, self.name) - + def is_reposting_pending(): return frappe.db.exists("Repost Item Valuation", {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) 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/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py index eff0ae4694..f60e0d3036 100644 --- 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 @@ -3,7 +3,6 @@ from frappe import _ from erpnext.stock.stock_ledger import update_entries_after from erpnext.accounts.utils import update_gl_entries_after - def execute(): data = frappe.db.sql(''' SELECT name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time from `tabStock Ledger Entry` where creation > '2020-12-26 12:58:55.903836' and is_cancelled = 0 diff --git a/erpnext/stock/__init__.py b/erpnext/stock/__init__.py index 8d64efe41d..b3ae804b1c 100644 --- a/erpnext/stock/__init__.py +++ b/erpnext/stock/__init__.py @@ -64,7 +64,7 @@ def get_warehouse_account(warehouse, warehouse_account=None): if not account and warehouse.company: account = get_company_default_inventory_account(warehouse.company) - if not account and warehouse.company: + if not account and warehouse.company and not warehouse.is_group: frappe.throw(_("Please set Account in Warehouse {0} or Default Inventory Account in Company {1}") .format(warehouse.name, warehouse.company)) return account diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 97f85bafd9..cbd272df4b 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -298,9 +298,9 @@ class TestBatch(unittest.TestCase): self.assertEqual(details.get('price_list_rate'), 400) def create_batch(item_code, rate, create_item_price_for_batch): - pi = make_purchase_invoice(company="_Test Company with perpetual inventory", - warehouse= "Stores - TCP1", cost_center = "Main - TCP1", update_stock=1, - expense_account ="_Test Account Cost for Goods Sold - TCP1", item_code=item_code) + pi = make_purchase_invoice(company="_Test Company", + warehouse= "Stores - _TC", cost_center = "Main - _TC", update_stock=1, + expense_account ="_Test Account Cost for Goods Sold - _TC", item_code=item_code) batch = frappe.db.get_value('Batch', {'item': item_code, 'reference_name': pi.name}) diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 144101c67d..984ae46c66 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -148,7 +148,6 @@ class TestLandedCostVoucher(unittest.TestCase): def test_landed_cost_voucher_for_odd_numbers (self): - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True) pr.items[0].cost_center = "Main - TCP1" for x in range(2): @@ -208,6 +207,10 @@ class TestLandedCostVoucher(unittest.TestCase): self.assertEqual(pr.items[1].landed_cost_voucher_amount, 100) def test_multi_currency_lcv(self): + from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records, save_new_records + + save_new_records(test_records) + ## Create USD Shipping charges_account usd_shipping = create_account(account_name="Shipping Charges USD", parent_account="Duties and Taxes - TCP1", company="_Test Company with perpetual inventory", diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index ba2c2c6f44..f22c6018d4 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -64,7 +64,7 @@ def repost(doc): message += "
" + "Traceback:
" + traceback frappe.db.set_value(doc.doctype, doc.name, 'error_log', message) - notify_error_to_stock_managers(doc) + notify_error_to_stock_managers(doc, message) doc.set_status('Failed') raise finally: diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py index e1fa207a21..9c3e22f023 100644 --- a/erpnext/stock/doctype/shipment/test_shipment.py +++ b/erpnext/stock/doctype/shipment/test_shipment.py @@ -190,6 +190,7 @@ def create_shipment_company(company_name, abbr): company.abbr = abbr company.default_currency = 'EUR' company.country = 'Germany' + company.enable_perpetual_inventory = 0 company.insert() return company diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 8c9c172ebd..21860b6863 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -204,7 +204,8 @@ class update_entries_after(object): where item_code = %(item_code)s and warehouse = %(warehouse)s - and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) + and voucher_type = %(voucher_type)s + and voucher_no = %(voucher_no)s order by creation ASC for update From 7166b6740ff9541d66f0e1abd59e16aa340556c7 Mon Sep 17 00:00:00 2001 From: prssanna Date: Tue, 16 Feb 2021 16:17:17 +0530 Subject: [PATCH 51/93] fix: duplicate css property --- erpnext/public/less/hub.less | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/public/less/hub.less b/erpnext/public/less/hub.less index c08121f704..29deada8a4 100644 --- a/erpnext/public/less/hub.less +++ b/erpnext/public/less/hub.less @@ -33,7 +33,6 @@ body[data-route*="marketplace"] { .hub-image-loading, .hub-image-broken { content: " "; - display: block; position: absolute; left: 0; height: 100%; From f9d4d9a095b7278753f053120643e86c9f4f782d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 16 Feb 2021 18:45:39 +0530 Subject: [PATCH 52/93] test(selling): add test for cancelling sales order Add failing test to reproduce bug in cancelling sales order with advance payments Related issue: ISS-20-21-09586 --- .../doctype/sales_order/test_sales_order.py | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index cbfab8204f..52a0174798 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -17,6 +17,18 @@ from erpnext.selling.doctype.product_bundle.test_product_bundle import make_prod from erpnext.stock.doctype.item.test_item import make_item class TestSalesOrder(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.unlink_setting = int(frappe.db.get_value("Accounts Settings", "Accounts Settings", + "unlink_advance_payment_on_cancelation_of_order")) + + @classmethod + def tearDownClass(cls) -> None: + # reset config to previous state + frappe.db.set_value("Accounts Settings", "Accounts Settings", + "unlink_advance_payment_on_cancelation_of_order", cls.unlink_setting) + def tearDown(self): frappe.set_user("Administrator") @@ -1049,6 +1061,38 @@ class TestSalesOrder(unittest.TestCase): self.assertRaises(frappe.LinkExistsError, so_doc.cancel) + def test_cancel_sales_order_after_cancel_payment_entry(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry + # make a sales order + so = make_sales_order() + + # disable unlinking of payment entry + frappe.db.set_value("Accounts Settings", "Accounts Settings", + "unlink_advance_payment_on_cancelation_of_order", 0) + + # create a payment entry against sales order + pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Bank - _TC") + pe.reference_no = "1" + pe.reference_date = nowdate() + pe.paid_from_account_currency = so.currency + pe.paid_to_account_currency = so.currency + pe.source_exchange_rate = 1 + pe.target_exchange_rate = 1 + pe.paid_amount = so.grand_total + pe.save(ignore_permissions=True) + pe.submit() + + # Cancel payment entry + po_doc = frappe.get_doc("Payment Entry", pe.name) + po_doc.cancel() + + # Cancel sales order + try: + so_doc = frappe.get_doc('Sales Order', so.name) + so_doc.cancel() + except Exception: + self.fail("Can not cancel sales order with linked cancelled payment entry") + def test_request_for_raw_materials(self): item = make_item("_Test Finished Item", {"is_stock_item": 1, "maintain_stock": 1, @@ -1207,4 +1251,4 @@ def make_sales_order_workflow(): )) workflow.insert(ignore_permissions=True) - return workflow \ No newline at end of file + return workflow From 3f16902b0090c1c967b1809870831525bf059341 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 16 Feb 2021 18:46:32 +0530 Subject: [PATCH 53/93] fix(selling): cancel sales order with cancelled PE Allow cancelling sales order with cancelled payment entry. Ignoring GL entries while cancelling the document is required to cancel it, reverse entries are created by accounts controller. Related issue: ISS-20-21-09586 --- erpnext/selling/doctype/sales_order/sales_order.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 1516dd6d95..e56129170c 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -180,6 +180,7 @@ class SalesOrder(SellingController): update_coupon_code_count(self.coupon_code,'used') def on_cancel(self): + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') super(SalesOrder, self).on_cancel() # Cannot cancel closed SO From d826bee13ac6298cea90157a6a804e58eb24c1ca Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 18 Feb 2021 14:14:21 +0530 Subject: [PATCH 54/93] fix: update qty in future sle (#24649) * fix: update qty in future sle * fix: validate cancellation due to ongoing reposting * fix: process sle against current timestamp --- .../doctype/work_order/test_work_order.py | 10 +-- erpnext/stock/__init__.py | 2 +- erpnext/stock/doctype/bin/bin.py | 13 +-- .../delivery_note/test_delivery_note.py | 5 +- .../purchase_receipt/test_purchase_receipt.py | 9 +- .../repost_item_valuation.py | 3 + .../stock_ledger_entry/stock_ledger_entry.py | 1 + erpnext/stock/stock_ledger.py | 87 ++++++++++++++++--- 8 files changed, 102 insertions(+), 28 deletions(-) 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/stock/__init__.py b/erpnext/stock/__init__.py index b3ae804b1c..9e240cc2b3 100644 --- a/erpnext/stock/__init__.py +++ b/erpnext/stock/__init__.py @@ -70,4 +70,4 @@ def get_warehouse_account(warehouse, warehouse_account=None): return account def get_company_default_inventory_account(company): - return frappe.get_cached_value('Company', company, 'default_inventory_account') + return frappe.get_cached_value('Company', company, 'default_inventory_account') diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 1088b4127d..0514bd2394 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -16,8 +16,9 @@ class Bin(Document): def update_stock(self, args, allow_negative_stock=False, via_landed_cost_voucher=False): '''Called from erpnext.stock.utils.update_bin''' self.update_qty(args) + if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation": - from erpnext.stock.stock_ledger import update_entries_after, validate_negative_qty_in_future_sle + from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle if not args.get("posting_date"): args["posting_date"] = nowdate() @@ -34,11 +35,13 @@ class Bin(Document): "posting_time": args.get("posting_time"), "voucher_type": args.get("voucher_type"), "voucher_no": args.get("voucher_no"), - "sle_id": args.name + "sle_id": args.name, + "creation": args.creation }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) - # Validate negative qty in future transactions - validate_negative_qty_in_future_sle(args) + # update qty in future ale and Validate negative qty + update_qty_in_future_sle(args, allow_negative_stock) + def update_qty(self, args): # update the stock values (for current quantities) @@ -51,7 +54,7 @@ class Bin(Document): self.reserved_qty = flt(self.reserved_qty) + flt(args.get("reserved_qty")) self.indented_qty = flt(self.indented_qty) + flt(args.get("indented_qty")) self.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty")) - + self.set_projected_qty() self.db_update() diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 559f8be0de..d39b22965e 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -489,7 +489,10 @@ class TestDeliveryNote(unittest.TestCase): def test_closed_delivery_note(self): from erpnext.stock.doctype.delivery_note.delivery_note import update_delivery_note_status - dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True) + make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) + + dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', + cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True) dn.submit() diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index ca58ab2823..7741ee7f60 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -94,10 +94,15 @@ class TestPurchaseReceipt(unittest.TestCase): frappe.get_doc('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice').delete() def test_purchase_receipt_no_gl_entry(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company') - existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC"}, "stock_value") + existing_bin_qty, existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC"}, ["actual_qty", "stock_value"]) + + if existing_bin_qty < 0: + make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=abs(existing_bin_qty)) pr = make_purchase_receipt() diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index f22c6018d4..8436acbed2 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -46,6 +46,9 @@ class RepostItemValuation(Document): def repost(doc): try: + if not frappe.db.exists("Repost Item Valuation", doc.name): + return + doc.set_status('In Progress') frappe.db.commit() diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 36d09efd1a..b0e7440e6c 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -38,6 +38,7 @@ class StockLedgerEntry(Document): self.block_transactions_against_group_warehouse() self.validate_with_last_transaction_posting_time() + def on_submit(self): self.check_stock_frozen_date() self.actual_amt_check() diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 21860b6863..e4f5725c68 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -23,6 +23,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc cancel = sl_entries[0].get("is_cancelled") if cancel: + validate_cancellation(sl_entries) set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no')) for sle in sl_entries: @@ -45,6 +46,20 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc args = sle_doc.as_dict() update_bin(args, allow_negative_stock, via_landed_cost_voucher) +def validate_cancellation(args): + if args[0].get("is_cancelled"): + repost_entry = frappe.db.get_value("Repost Item Valuation", { + 'voucher_type': args[0].voucher_type, + 'voucher_no': args[0].voucher_no, + 'docstatus': 1 + }, ['name', 'status'], as_dict=1) + + if repost_entry: + if repost_entry.status == 'In Progress': + frappe.throw(_("Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet.")) + if repost_entry.status == 'Queued': + frappe.delete_doc("Repost Item Valuation", repost_entry.name) + def set_as_cancel(voucher_type, voucher_no): frappe.db.sql("""update `tabStock Ledger Entry` set is_cancelled=1, @@ -74,7 +89,8 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat "item_code": args[i].item_code, "warehouse": args[i].warehouse, "posting_date": args[i].posting_date, - "posting_time": args[i].posting_time + "posting_time": args[i].posting_time, + "creation": args[i].get("creation") }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) for item_wh, new_sle in iteritems(obj.new_items): @@ -86,7 +102,7 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat def get_args_for_voucher(voucher_type, voucher_no): return frappe.db.get_all("Stock Ledger Entry", filters={"voucher_type": voucher_type, "voucher_no": voucher_no}, - fields=["item_code", "warehouse", "posting_date", "posting_time"], + fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"], order_by="creation asc", group_by="item_code, warehouse" ) @@ -117,7 +133,7 @@ class update_entries_after(object): self.item_code = args.get("item_code") if self.args.sle_id: self.args['name'] = self.args.sle_id - + self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company") self.get_precision() self.valuation_method = get_valuation_method(self.item_code) @@ -155,7 +171,7 @@ class update_entries_after(object): """ self.data.setdefault(args.warehouse, frappe._dict()) warehouse_dict = self.data[args.warehouse] - previous_sle = self.get_sle_before_datetime(args) + previous_sle = self.get_previous_sle_of_current_voucher(args) warehouse_dict.previous_sle = previous_sle for key in ("qty_after_transaction", "valuation_rate", "stock_value"): @@ -167,9 +183,35 @@ class update_entries_after(object): "stock_value_difference": 0.0 }) + def get_previous_sle_of_current_voucher(self, args): + """get stock ledger entries filtered by specific posting datetime conditions""" + + args['time_format'] = '%H:%i:%s' + if not args.get("posting_date"): + args["posting_date"] = "1900-01-01" + if not args.get("posting_time"): + args["posting_time"] = "00:00" + + sle = frappe.db.sql(""" + select *, timestamp(posting_date, posting_time) as "timestamp" + from `tabStock Ledger Entry` + where item_code = %(item_code)s + and warehouse = %(warehouse)s + and is_cancelled = 0 + and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) + order by timestamp(posting_date, posting_time) desc, creation desc + limit 1""", args, as_dict=1) + + return sle[0] if sle else frappe._dict() + + def build(self): + from erpnext.controllers.stock_controller import check_if_future_sle_exists + if self.args.get("sle_id"): - self.process_sle_against_current_voucher() + self.process_sle_against_current_timestamp() + if not check_if_future_sle_exists(self.args): + self.update_bin() else: entries_to_fix = self.get_future_entries_to_fix() @@ -182,13 +224,13 @@ class update_entries_after(object): if sle.dependant_sle_voucher_detail_no: entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle) + + self.update_bin() if self.exceptions: self.raise_exceptions() - self.update_bin() - - def process_sle_against_current_voucher(self): + def process_sle_against_current_timestamp(self): sl_entries = self.get_sle_against_current_voucher() for sle in sl_entries: self.process_sle(sle) @@ -204,8 +246,8 @@ class update_entries_after(object): where item_code = %(item_code)s and warehouse = %(warehouse)s - and voucher_type = %(voucher_type)s - and voucher_no = %(voucher_no)s + and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) + order by creation ASC for update @@ -232,7 +274,6 @@ class update_entries_after(object): return entries_to_fix elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data: return entries_to_fix - self.initialize_previous_data(dependant_sle) args = self.data[dependant_sle.warehouse].previous_sle \ @@ -639,7 +680,6 @@ class update_entries_after(object): # update bin for each warehouse for warehouse, data in iteritems(self.data): bin_doc = get_bin(self.item_code, warehouse) - bin_doc.update({ "valuation_rate": data.valuation_rate, "actual_qty": data.qty_after_transaction, @@ -765,6 +805,25 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, return valuation_rate +def update_qty_in_future_sle(args, allow_negative_stock=None): + frappe.db.sql(""" + update `tabStock Ledger Entry` + set qty_after_transaction = qty_after_transaction + {qty} + where + item_code = %(item_code)s + and warehouse = %(warehouse)s + and voucher_no != %(voucher_no)s + and is_cancelled = 0 + and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s) + or ( + timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s) + and creation > %(creation)s + ) + ) + """.format(qty=args.actual_qty), args) + + validate_negative_qty_in_future_sle(args, allow_negative_stock) + def validate_negative_qty_in_future_sle(args, allow_negative_stock=None): allow_negative_stock = allow_negative_stock \ or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) @@ -793,7 +852,7 @@ def get_future_sle_with_negative_qty(args): and voucher_no != %(voucher_no)s and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s) and is_cancelled = 0 - and qty_after_transaction + {0} < 0 + and qty_after_transaction < 0 order by timestamp(posting_date, posting_time) asc limit 1 - """.format(args.actual_qty), args, as_dict=1) + """, args, as_dict=1) \ No newline at end of file From c697cb8b2d9ff1da753ca42f9c10c98a68b22c4d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 11 Feb 2021 18:30:10 +0530 Subject: [PATCH 55/93] fix: validation of job card in stock entry --- .../doctype/job_card/job_card.py | 18 +- .../doctype/job_card_item/job_card_item.json | 455 ++++-------------- .../stock/doctype/stock_entry/stock_entry.py | 3 +- .../stock_entry_detail.json | 14 +- 4 files changed, 136 insertions(+), 354 deletions(-) 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/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index d77b70ff14..9cdc3cfa55 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -163,7 +163,7 @@ class StockEntry(StockController): if self.purpose not in valid_purposes: frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes))) - if self.job_card and self.purpose != 'Material Transfer for Manufacture': + if self.job_card and self.purpose not in ['Material Transfer for Manufacture', 'Repack']: frappe.throw(_("For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry") .format(self.job_card)) @@ -823,6 +823,7 @@ class StockEntry(StockController): if self.job_card: job_doc = frappe.get_doc('Job Card', self.job_card) job_doc.set_transferred_qty(update_status=True) + job_doc.set_transferred_qty_in_job_card(self) if self.work_order: pro_doc = frappe.get_doc("Work Order", self.work_order) diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 988ae92969..864ff488b2 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -69,7 +69,8 @@ "putaway_rule", "column_break_51", "reference_purchase_receipt", - "quality_inspection" + "quality_inspection", + "job_card_item" ], "fields": [ { @@ -532,13 +533,22 @@ "fieldname": "is_finished_item", "fieldtype": "Check", "label": "Is Finished Item" + }, + { + "fieldname": "job_card_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Job Card Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-12-30 15:00:44.489442", + "modified": "2021-02-11 13:47:50.158754", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", From ef5c714de2b01788dae3e4bae688822f7ee6974f Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Thu, 18 Feb 2021 15:52:29 +0530 Subject: [PATCH 56/93] fix: salary slip attribute error (#24455) Co-authored-by: Rucha Mahabal --- erpnext/payroll/doctype/salary_slip/salary_slip.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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) From e7bf87cc841cf44309754d152408ebe6ae5933c9 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 18 Feb 2021 16:04:35 +0530 Subject: [PATCH 57/93] fix: POS return for Serialized Items (#24292) Co-authored-by: Nabin Hait Co-authored-by: Saqib --- .../doctype/pos_invoice/pos_invoice.py | 20 +++++-- .../doctype/pos_invoice/test_pos_invoice.py | 59 +++++++++++++++++++ .../pos_invoice_item/pos_invoice_item.json | 12 +++- .../pos_invoice_merge_log.py | 21 +++---- .../controllers/sales_and_purchase_return.py | 9 ++- 5 files changed, 101 insertions(+), 20 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 94573f9df3..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) 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/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 From d44e60f17c958f3e58196fce6b4c164123db2db5 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Thu, 18 Feb 2021 11:54:37 +0100 Subject: [PATCH 58/93] feat: add Print Language to Lead and Opportunity (#24317) * fix: move "Print Language" to "More Information" * feat: add "Print Language" in "More Information" Co-authored-by: Rucha Mahabal --- erpnext/buying/doctype/supplier/supplier.json | 4 ++-- erpnext/crm/doctype/lead/lead.json | 11 +++++++++-- erpnext/crm/doctype/opportunity/opportunity.json | 9 ++++++++- erpnext/selling/doctype/customer/customer.json | 6 +++--- 4 files changed, 22 insertions(+), 8 deletions(-) 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/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.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/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 557c7151d9..3c0652eb67 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -34,9 +34,8 @@ "companies", "currency_and_price_list", "default_currency", - "default_price_list", "column_break_14", - "language", + "default_price_list", "address_contacts", "address_html", "website", @@ -59,6 +58,7 @@ "column_break_45", "market_segment", "industry", + "language", "is_frozen", "column_break_38", "loyalty_program", @@ -485,7 +485,7 @@ "idx": 363, "image_field": "image", "links": [], - "modified": "2020-03-17 11:03:42.706907", + "modified": "2021-01-06 19:35:25.418017", "modified_by": "Administrator", "module": "Selling", "name": "Customer", From ccc80927f6aa3b072b7cac6d9d548394de6f19ba Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 18 Feb 2021 16:41:10 +0530 Subject: [PATCH 59/93] feat: Department Wise Appointment Type charges (#24572) * feat: Appointment Type Service Items Co-Authored-By: muhammad * fix: set practitioner service item charges mandatory on item selection Co-Authored-By: muhammad * feat: use charges from appointment type during billing * feat: handle appointment charges priority for invoice automation * test: patient appointment auto invoicing scenarios * fix: sider * fix: minor fixes Co-authored-by: muhammad Co-authored-by: Nabin Hait --- .../appointment_type/appointment_type.js | 78 ++++++++++++ .../appointment_type/appointment_type.json | 24 +++- .../appointment_type/appointment_type.py | 49 +++++++- .../appointment_type_service_item/__init__.py | 0 .../appointment_type_service_item.json | 67 +++++++++++ .../appointment_type_service_item.py | 10 ++ .../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 ++++++++++--- 12 files changed, 485 insertions(+), 123 deletions(-) create mode 100644 erpnext/healthcare/doctype/appointment_type_service_item/__init__.py create mode 100644 erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json create mode 100644 erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py 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/healthcare/doctype/appointment_type_service_item/__init__.py b/erpnext/healthcare/doctype/appointment_type_service_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py new file mode 100644 index 0000000000..b2e0e82bad --- /dev/null +++ b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class AppointmentTypeServiceItem(Document): + pass 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') From 4858b40813addf9fe10e0c7a65de30660e8abd9d Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 18 Feb 2021 18:44:29 +0530 Subject: [PATCH 60/93] fix: excluding unidentified accounts from gstr-1 --- erpnext/regional/report/gstr_1/gstr_1.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 96dc3f728d..09b04ff367 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -236,6 +236,7 @@ class Gstr1Report(object): self.cgst_sgst_invoices = [] unidentified_gst_accounts = [] + unidentified_gst_accounts_invoice = [] for parent, account, item_wise_tax_detail, tax_amount in self.tax_details: if account in self.gst_accounts.cess_account: self.invoice_cess.setdefault(parent, tax_amount) @@ -251,6 +252,7 @@ class Gstr1Report(object): if not (cgst_or_sgst or account in self.gst_accounts.igst_account): if "gst" in account.lower() and account not in unidentified_gst_accounts: unidentified_gst_accounts.append(account) + unidentified_gst_accounts_invoice.append(parent) continue for item_code, tax_amounts in item_wise_tax_detail.items(): @@ -273,7 +275,7 @@ class Gstr1Report(object): # Build itemised tax for export invoices where tax table is blank for invoice, items in iteritems(self.invoice_items): - if invoice not in self.items_based_on_tax_rate \ + if invoice not in self.items_based_on_tax_rate and invoice not in unidentified_gst_accounts_invoice \ and frappe.db.get_value(self.doctype, invoice, "export_type") == "Without Payment of Tax": self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) From bc857522ad376fc85d8f24449f7daedca86badd4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 19 Feb 2021 10:13:56 +0530 Subject: [PATCH 61/93] fix: Loan Interest Accrual Chart --- .../loan_interest_accrual.json | 29 +++++++++++++++++++ .../loan_dashboard/loan_dashboard.json | 6 +++- .../loan_management/loan_management.json | 2 +- 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 erpnext/loan_management/dashboard_chart/loan_interest_accrual/loan_interest_accrual.json 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..5b3c0e3dd3 --- /dev/null +++ b/erpnext/loan_management/dashboard_chart/loan_interest_accrual/loan_interest_accrual.json @@ -0,0 +1,29 @@ +{ + "based_on": "posting_date", + "chart_name": "Loan Interest Accrual", + "chart_type": "Sum", + "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, + "modified": "2021-02-18 20:07:10.015299", + "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/loan_management_dashboard/loan_dashboard/loan_dashboard.json b/erpnext/loan_management/loan_management_dashboard/loan_dashboard/loan_dashboard.json index 2da6bb7f01..bd04cfe4aa 100644 --- a/erpnext/loan_management/loan_management_dashboard/loan_dashboard/loan_dashboard.json +++ b/erpnext/loan_management/loan_management_dashboard/loan_dashboard/loan_dashboard.json @@ -40,6 +40,10 @@ { "chart": "Top 10 Pledged Loan Securities", "width": "Half" + }, + { + "chart": "Loan Interest Accrual", + "width": "Half" } ], "creation": "2021-02-06 16:52:43.484752", @@ -49,7 +53,7 @@ "idx": 0, "is_default": 0, "is_standard": 1, - "modified": "2021-02-09 08:07:27.293764", + "modified": "2021-02-18 20:07:10.095401", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Dashboard", diff --git a/erpnext/loan_management/workspace/loan_management/loan_management.json b/erpnext/loan_management/workspace/loan_management/loan_management.json index 848b1e761d..18559dceef 100644 --- a/erpnext/loan_management/workspace/loan_management/loan_management.json +++ b/erpnext/loan_management/workspace/loan_management/loan_management.json @@ -220,7 +220,7 @@ "type": "Link" } ], - "modified": "2021-02-06 17:31:17.593447", + "modified": "2021-02-18 17:31:53.586508", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Management", From 7efbbcf3aa7f36a4ff78e561a8b96abe37ea4ea5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 19 Feb 2021 10:53:58 +0530 Subject: [PATCH 62/93] fix: Do not delete GST Accounts from GST Settings on clearing transactions from Company Master --- erpnext/setup/doctype/company/delete_company_transactions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/delete_company_transactions.py b/erpnext/setup/doctype/company/delete_company_transactions.py index 7a72fe3102..0df4c87f51 100644 --- a/erpnext/setup/doctype/company/delete_company_transactions.py +++ b/erpnext/setup/doctype/company/delete_company_transactions.py @@ -28,7 +28,7 @@ def delete_company_transactions(company_name): "Party Account", "Employee", "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template", "POS Profile", "BOM", "Company", "Bank Account", "Item Tax Template", "Mode Of Payment", - "Item Default", "Customer", "Supplier"): + "Item Default", "Customer", "Supplier", "GST Account"): delete_for_doctype(doctype, company_name) # reset company values From 6a5ef261f20a68d6ab0acba5e614d9506da77cb5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Fri, 19 Feb 2021 14:30:23 +0530 Subject: [PATCH 63/93] feat: Normal rounding for GST Taxes (#24488) * feat: Normal rounding for GST Taxes * fix: Add test case Co-authored-by: Nabin Hait --- erpnext/__init__.py | 2 +- erpnext/controllers/taxes_and_totals.py | 19 ++ erpnext/hooks.py | 1 + .../public/js/controllers/taxes_and_totals.js | 30 +- .../doctype/gst_settings/gst_settings.json | 290 +++++------------- .../gstr_3b_report/test_gstr_3b_report.py | 67 +++- erpnext/regional/india/utils.py | 21 ++ 7 files changed, 198 insertions(+), 232 deletions(-) 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/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/hooks.py b/erpnext/hooks.py index 109d9216e7..347da6b563 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -399,6 +399,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/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 416495ceac..d81321b291 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -2,7 +2,9 @@ // License: GNU General Public License v3. See license.txt erpnext.taxes_and_totals = erpnext.payments.extend({ - setup: function() {}, + setup: function() { + this.fetch_round_off_accounts(); + }, apply_pricing_rule_on_item: function(item) { let effective_item_rate = item.price_list_rate; @@ -152,6 +154,22 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ }); }, + fetch_round_off_accounts: function() { + let me = this; + frappe.flags.round_off_applicable_accounts = []; + + return frappe.call({ + "method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts", + "args": { + "company": me.frm.doc.company, + "account_list": frappe.flags.round_off_applicable_accounts + }, + callback: function(r) { + frappe.flags.round_off_applicable_accounts.push(...r.message); + } + }); + }, + determine_exclusive_rate: function() { var me = this; @@ -372,11 +390,21 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } else if (tax.charge_type == "On Item Quantity") { current_tax_amount = tax_rate * item.qty; } + + current_tax_amount = this.get_final_tax_amount(tax, current_tax_amount); this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount); return current_tax_amount; }, + get_final_tax_amount: function(tax, current_tax_amount) { + if (frappe.flags.round_off_applicable_accounts.includes(tax.account_head)) { + current_tax_amount = Math.round(current_tax_amount); + } + + return current_tax_amount; + }, + set_item_wise_tax: function(item, tax, tax_rate, current_tax_amount) { // store tax breakup for each item let tax_detail = tax.item_wise_tax_detail; diff --git a/erpnext/regional/doctype/gst_settings/gst_settings.json b/erpnext/regional/doctype/gst_settings/gst_settings.json index 98c33ad33b..95b930c4c8 100644 --- a/erpnext/regional/doctype/gst_settings/gst_settings.json +++ b/erpnext/regional/doctype/gst_settings/gst_settings.json @@ -1,222 +1,86 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-06-27 15:09:01.318003", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2017-06-27 15:09:01.318003", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "gst_summary", + "column_break_2", + "round_off_gst_values", + "gstin_email_sent_on", + "section_break_4", + "gst_accounts", + "b2c_limit" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gst_summary", - "fieldtype": "HTML", - "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": "GST Summary", - "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 - }, + "fieldname": "gst_summary", + "fieldtype": "HTML", + "label": "GST Summary", + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 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, - "unique": 0 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gstin_email_sent_on", - "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": "GSTIN Email Sent On", - "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 - }, + "fieldname": "gstin_email_sent_on", + "fieldtype": "Date", + "label": "GSTIN Email Sent On", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 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, - "unique": 0 - }, + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gst_accounts", - "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": "GST Accounts", - "length": 0, - "no_copy": 0, - "options": "GST 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 - }, + "fieldname": "gst_accounts", + "fieldtype": "Table", + "label": "GST Accounts", + "options": "GST Account", + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "250000", - "description": "Set Invoice Value for B2C. B2CL and B2CS calculated based on this invoice value.", - "fieldname": "b2c_limit", - "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": "B2C Limit", - "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 + "default": "250000", + "description": "Set Invoice Value for B2C. B2CL and B2CS calculated based on this invoice value.", + "fieldname": "b2c_limit", + "fieldtype": "Data", + "in_list_view": 1, + "label": "B2C Limit", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "description": "Enabling this option will round off individual GST components in all the Invoices", + "fieldname": "round_off_gst_values", + "fieldtype": "Check", + "label": "Round Off GST Values", + "show_days": 1, + "show_seconds": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2018-02-14 08:14:15.375181", - "modified_by": "Administrator", - "module": "Regional", - "name": "GST Settings", - "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 + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2021-01-28 17:19:47.969260", + "modified_by": "Administrator", + "module": "Regional", + "name": "GST Settings", + "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/regional/doctype/gstr_3b_report/test_gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py index 8174da20cb..023b4ed22b 100644 --- a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py @@ -14,8 +14,20 @@ import json test_dependencies = ["Territory", "Customer Group", "Supplier Group", "Item"] class TestGSTR3BReport(unittest.TestCase): - def test_gstr_3b_report(self): + def setUp(self): + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company GST'") + frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company GST'") + frappe.db.sql("delete from `tabGSTR 3B Report` where company='_Test Company GST'") + + make_company() + make_item("Milk", properties = {"is_nil_exempt": 1, "standard_rate": 0.000000}) + set_account_heads() + make_customers() + make_suppliers() + + def test_gstr_3b_report(self): month_number_mapping = { 1: "January", 2: "February", @@ -31,17 +43,6 @@ class TestGSTR3BReport(unittest.TestCase): 12: "December" } - frappe.set_user("Administrator") - - frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company GST'") - frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company GST'") - frappe.db.sql("delete from `tabGSTR 3B Report` where company='_Test Company GST'") - - make_company() - make_item("Milk", properties = {"is_nil_exempt": 1, "standard_rate": 0.000000}) - set_account_heads() - make_customers() - make_suppliers() make_sales_invoice() create_purchase_invoices() @@ -67,6 +68,42 @@ class TestGSTR3BReport(unittest.TestCase): self.assertEqual(output["itc_elg"]["itc_avl"][4]["samt"], 22.50) self.assertEqual(output["itc_elg"]["itc_avl"][4]["camt"], 22.50) + def test_gst_rounding(self): + gst_settings = frappe.get_doc('GST Settings') + gst_settings.round_off_gst_values = 1 + gst_settings.save() + + current_country = frappe.flags.country + frappe.flags.country = 'India' + + si = create_sales_invoice(company="_Test Company GST", + customer = '_Test GST Customer', + currency = 'INR', + warehouse = 'Finished Goods - _GST', + debit_to = 'Debtors - _GST', + income_account = 'Sales - _GST', + expense_account = 'Cost of Goods Sold - _GST', + cost_center = 'Main - _GST', + rate=216, + do_not_save=1 + ) + + si.append("taxes", { + "charge_type": "On Net Total", + "account_head": "IGST - _GST", + "cost_center": "Main - _GST", + "description": "IGST @ 18.0", + "rate": 18 + }) + + si.save() + # Check for 39 instead of 38.88 + self.assertEqual(si.taxes[0].base_tax_amount_after_discount_amount, 39) + + frappe.flags.country = current_country + gst_settings.round_off_gst_values = 1 + gst_settings.save() + def make_sales_invoice(): si = create_sales_invoice(company="_Test Company GST", customer = '_Test GST Customer', @@ -145,7 +182,6 @@ def make_sales_invoice(): si3.submit() def create_purchase_invoices(): - pi = make_purchase_invoice( company="_Test Company GST", supplier = '_Test Registered Supplier', @@ -193,7 +229,6 @@ def create_purchase_invoices(): pi1.submit() def make_suppliers(): - if not frappe.db.exists("Supplier", "_Test Registered Supplier"): frappe.get_doc({ "supplier_group": "_Test Supplier Group", @@ -257,7 +292,6 @@ def make_suppliers(): address.save() def make_customers(): - if not frappe.db.exists("Customer", "_Test GST Customer"): frappe.get_doc({ "customer_group": "_Test Customer Group", @@ -354,9 +388,9 @@ def make_customers(): address.save() def make_company(): - if frappe.db.exists("Company", "_Test Company GST"): return + company = frappe.new_doc("Company") company.company_name = "_Test Company GST" company.abbr = "_GST" @@ -388,7 +422,6 @@ def make_company(): address.save() def set_account_heads(): - gst_settings = frappe.get_doc("GST Settings") gst_account = frappe.get_all( diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index e89885f380..09bafdd7f2 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -772,3 +772,24 @@ def make_regional_gl_entries(gl_entries, doc): ) return gl_entries + +@frappe.whitelist() +def get_regional_round_off_accounts(company, account_list): + country = frappe.get_cached_value('Company', company, 'country') + + if country != 'India': + return + + if isinstance(account_list, string_types): + account_list = json.loads(account_list) + + if not frappe.db.get_single_value('GST Settings', 'round_off_gst_values'): + return + + gst_accounts = get_gst_accounts(company) + gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ + + gst_accounts.get('igst_account') + + account_list.extend(gst_account_list) + + return account_list \ No newline at end of file From 8c7c682f1704fa51e8e125aafa08fd24212dc8e1 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 19 Feb 2021 20:35:23 +0530 Subject: [PATCH 64/93] chore: make parameter group optional --- .../quality_inspection_parameter.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json index 6d97601091..418b4825f2 100644 --- a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json +++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json @@ -29,13 +29,12 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Parameter Group", - "options": "Quality Inspection Parameter Group", - "reqd": 1 + "options": "Quality Inspection Parameter Group" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-02-04 18:45:12.486344", + "modified": "2021-02-19 20:33:30.657406", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection Parameter", From ab149f196eb200da41a7d0a00165da1b6f55a203 Mon Sep 17 00:00:00 2001 From: Michelle Alva <50285544+michellealva@users.noreply.github.com> Date: Sat, 20 Feb 2021 10:43:47 +0530 Subject: [PATCH 65/93] fix: typo in Leave Balance Report Change Leaves to Leave --- .../report/employee_leave_balance/employee_leave_balance.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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, From 43f126ed1339a46f6a28a2755b6e0f587f0ed8a2 Mon Sep 17 00:00:00 2001 From: Michelle Alva <50285544+michellealva@users.noreply.github.com> Date: Sat, 20 Feb 2021 10:46:22 +0530 Subject: [PATCH 66/93] fix: Typo in Leave Application Change "Leaves" to "Leave" --- .../leave_application_dashboard.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 %} From cbb4fe2cce2993f30c8d269b8904c03bc1a48663 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 21 Feb 2021 20:57:30 +0530 Subject: [PATCH 67/93] fix: Add cards --- .../closed_loans/closed_loans.json | 23 ++++++++++++++++++ .../total_repayment/total_repayment.json | 24 +++++++++++++++++++ .../total_write_off/total_write_off.json | 24 +++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 erpnext/loan_management/number_card/closed_loans/closed_loans.json create mode 100644 erpnext/loan_management/number_card/total_repayment/total_repayment.json create mode 100644 erpnext/loan_management/number_card/total_write_off/total_write_off.json 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/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_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 From e53fd8766991a0ba58066614c66cbf17b2ae26e7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 21 Feb 2021 20:57:56 +0530 Subject: [PATCH 68/93] fix: Rearrange cards --- .../loan_dashboard/loan_dashboard.json | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) 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 index bd04cfe4aa..e060253d34 100644 --- a/erpnext/loan_management/loan_management_dashboard/loan_dashboard/loan_dashboard.json +++ b/erpnext/loan_management/loan_management_dashboard/loan_dashboard/loan_dashboard.json @@ -1,22 +1,25 @@ { "cards": [ { - "card": "Total Disbursed" - }, - { - "card": "Total Sanctioned Amount" + "card": "New Loans" }, { "card": "Active Loans" }, + { + "card": "Closed Loans" + }, + { + "card": "Total Disbursed" + }, { "card": "Open Loan Applications" }, { - "card": "New Loans" + "card": "New Loan Applications" }, { - "card": "New Loan Applications" + "card": "Total Sanctioned Amount" }, { "card": "Active Securities" @@ -26,6 +29,12 @@ }, { "card": "Total Shortfall Amount" + }, + { + "card": "Total Repayment" + }, + { + "card": "Total Write Off" } ], "charts": [ @@ -53,7 +62,7 @@ "idx": 0, "is_default": 0, "is_standard": 1, - "modified": "2021-02-18 20:07:10.095401", + "modified": "2021-02-21 20:53:47.531699", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Dashboard", From e4964a0c6ca1e01c6c68ed5715b067261bfe76ad Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 21 Feb 2021 21:03:42 +0530 Subject: [PATCH 69/93] fix: Update color in charts --- .../loan_interest_accrual/loan_interest_accrual.json | 4 +++- .../loan_management/dashboard_chart/new_loans/new_loans.json | 5 +++-- .../top_10_pledged_loan_securities.json | 4 +++- 3 files changed, 9 insertions(+), 4 deletions(-) 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 index 5b3c0e3dd3..aa0f78a2f6 100644 --- 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 @@ -2,6 +2,7 @@ "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", @@ -12,7 +13,8 @@ "idx": 0, "is_public": 0, "is_standard": 1, - "modified": "2021-02-18 20:07:10.015299", + "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", diff --git a/erpnext/loan_management/dashboard_chart/new_loans/new_loans.json b/erpnext/loan_management/dashboard_chart/new_loans/new_loans.json index 6e196d25a2..35bd43b994 100644 --- a/erpnext/loan_management/dashboard_chart/new_loans/new_loans.json +++ b/erpnext/loan_management/dashboard_chart/new_loans/new_loans.json @@ -2,6 +2,7 @@ "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", @@ -12,8 +13,8 @@ "idx": 0, "is_public": 0, "is_standard": 1, - "last_synced_on": "2021-02-06 17:11:05.154562", - "modified": "2021-02-06 17:28:40.134346", + "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", 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 index d48a6ce083..76c27b062d 100644 --- 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 @@ -2,6 +2,7 @@ "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", @@ -12,7 +13,8 @@ "idx": 0, "is_public": 0, "is_standard": 1, - "modified": "2021-02-07 17:34:25.117983", + "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", From aa46def745f2e45bf80c2769ee8eae6e24089a8a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 21 Feb 2021 21:54:23 +0530 Subject: [PATCH 70/93] fix: Accounting Dimension creation background job timeout --- .../doctype/accounting_dimension/accounting_dimension.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index dd26c4cec2..774c58a820 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) From d780f56ba8f66484b22746958ac1384b8d38bd2d Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 22 Feb 2021 15:12:35 +0530 Subject: [PATCH 71/93] Revert "fix: Error handling in loan reports and precision fixes" --- .../doctype/loan_interest_accrual/loan_interest_accrual.py | 4 +++- .../report/loan_security_exposure/loan_security_exposure.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) 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 7978350adf..7d7992d40a 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,5 +246,7 @@ def get_per_day_interest(principal_amount, rate_of_interest, posting_date=None): if not posting_date: posting_date = getdate() - return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100)) + 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) 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 8b6ea869a3..adc8013c68 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 @@ -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], {}).get('latest_price', 0)) + total_portfolio_value += flt(qty * loan_security_details.get(key[1])['latest_price']) return security_wise_map, total_portfolio_value From fdafb55fba5448b14d286b2ff0ddf7abc62e8f9e Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Mon, 22 Feb 2021 19:25:57 +0530 Subject: [PATCH 72/93] fix: error message when making reverse journal entry (#24665) --- .../accounts/doctype/journal_entry/journal_entry.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 From 14472f6bab275c0339bb0ad4581cc49e5f66a365 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 22 Feb 2021 19:27:23 +0530 Subject: [PATCH 73/93] fix(regional): vehicle no is mandatory for ewaybill generation (#24679) * fix: vehicle no required for e-invoice * fix: ewaybill generation dialog condition --- erpnext/patches.txt | 1 + .../patches/v13_0/update_vehicle_no_reqd_condition.py | 9 +++++++++ erpnext/regional/india/e_invoice/einvoice.js | 1 - 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index e5ee551c11..01d9ad32e7 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -754,3 +754,4 @@ 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.update_vehicle_no_reqd_condition \ No newline at end of file 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..33fef86af8 --- /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', '') \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 9fa94c401f..a756b57eb7 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -188,7 +188,6 @@ const get_ewaybill_fields = (frm) => { 'fieldname': 'vehicle_no', 'label': 'Vehicle No', 'fieldtype': 'Data', - 'depends_on': 'eval:(doc.mode_of_transport === "Road")', 'default': frm.doc.vehicle_no }, { From 8676bedd18aae126fea412adbc7cb98859643b77 Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Mon, 22 Feb 2021 19:28:01 +0530 Subject: [PATCH 74/93] fix: Add phone icon on read only phone fields (#24680) * fix: Add phone icon on read only phone fields * fix: sider fix --- erpnext/public/js/telephony.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/telephony.js b/erpnext/public/js/telephony.js index 6cb1207e79..b66126c2b8 100644 --- a/erpnext/public/js/telephony.js +++ b/erpnext/public/js/telephony.js @@ -1,13 +1,16 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlData.extend( { make_input() { - this._super(); + if (!this.df.read_only) { + this._super(); + } if (this.df.options == 'Phone') { this.setup_phone(); } }, setup_phone() { if (frappe.phone_call.handler) { - this.$wrapper.find('.control-input') + let control = this.df.read_only ? '.control-value' : '.control-input'; + this.$wrapper.find(control) .append(` From bb8cd1cdaac12ea405fb1ed36656a2b535164192 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 22 Feb 2021 19:28:45 +0530 Subject: [PATCH 75/93] feat: Basic validation for indian PAN No. in Supplier (#24687) --- erpnext/hooks.py | 3 +++ erpnext/regional/india/utils.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 347da6b563..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'] }, diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 09bafdd7f2..5f078632d6 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -55,6 +55,14 @@ def validate_gstin_for_india(doc, method): frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.") .format(doc.gst_state_number)) +def validate_pan_for_india(doc, method): + if doc.get('country') != 'India': + return + + p = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}") + if not p.match(doc.pan): + frappe.throw(_("Invalid PAN No. The input you've entered doesn't match the format of PAN.")) + def validate_tax_category(doc, method): if doc.get('gst_state') and frappe.db.get_value('Tax Category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state}): if doc.is_inter_state: From 98a0feab896b8b98d5cc697b1774ed4a965dad5a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 22 Feb 2021 19:30:02 +0530 Subject: [PATCH 76/93] fix(india): escape for special characters in JSON (#24695) JSON does not accept special whitespace characters like tab, carriage return, line feed Ref: https://www.ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf Related issue: ISS-20-21-09811 --- erpnext/regional/india/e_invoice/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 2043f49825..322fa183f4 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -160,7 +160,7 @@ def get_item_list(invoice): item.update(d.as_dict()) item.sr_no = d.idx - item.description = d.item_name.replace('"', '\\"') + item.description = json.dumps(d.item_name)[1:-1] item.qty = abs(item.qty) item.discount_amount = 0 From 0ca74ce04bb6c54bf458cc395cceab49e784b36c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 22 Feb 2021 19:32:51 +0530 Subject: [PATCH 77/93] fix: Fixes related to client script renaming (#24698) --- erpnext/patches/v11_0/refactor_autoname_naming.py | 2 +- ...eplace_renamed_fields_in_custom_scripts_and_print_formats.py | 2 +- erpnext/patches/v7_0/remove_doctypes_and_reports.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/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({ From 58399f652470254af0cbc743a0c8fe7f586c12df Mon Sep 17 00:00:00 2001 From: Shridhar Patil Date: Mon, 22 Feb 2021 19:37:38 +0530 Subject: [PATCH 78/93] fix: next follow up date should be always greater then current time (#24308) --- erpnext/crm/doctype/opportunity/opportunity.js | 6 ++++++ 1 file changed, 6 insertions(+) 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"); From 69a6bed10621db660325eb5b3a4b36d5d5c56a77 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 22 Feb 2021 20:03:23 +0530 Subject: [PATCH 79/93] fix: Replaced spaces with tabs --- erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py b/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py index 33fef86af8..c26cddbe4e 100644 --- a/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py +++ b/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py @@ -1,9 +1,9 @@ import frappe def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + 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', '') \ No newline at end of file + if frappe.db.exists('Custom Field', { 'fieldname': 'vehicle_no' }): + frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'mandatory_depends_on', '') From 866cf70d3ac2b8a17a9016e8cc00dca881461a61 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 22 Feb 2021 21:35:00 +0530 Subject: [PATCH 80/93] fix: validate pan --- erpnext/regional/india/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 5f078632d6..cb30605291 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -56,7 +56,7 @@ def validate_gstin_for_india(doc, method): .format(doc.gst_state_number)) def validate_pan_for_india(doc, method): - if doc.get('country') != 'India': + if doc.get('country') != 'India' or not doc.pan: return p = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}") From 07f40596bce6c6703b6f151be09161b7d61213ce Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Mon, 22 Feb 2021 22:19:47 +0530 Subject: [PATCH 81/93] feat(Bank Reconciliation): Redesign (#24273) Co-authored-by: Nabin Hait --- .../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 - .../__init__.py | 0 .../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 - .../__init__.py | 0 ...nk_statement_transaction_invoice_item.json | 365 -------- ...bank_statement_transaction_invoice_item.py | 10 - .../__init__.py | 0 ...nk_statement_transaction_payment_item.json | 494 ----------- ...bank_statement_transaction_payment_item.py | 10 - .../__init__.py | 0 .../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 +- .../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 - .../doctype/plaid_settings/plaid_settings.py | 4 +- erpnext/patches.txt | 3 +- .../v11_1/update_bank_transaction_status.py | 23 +- ...delete_old_bank_reconciliation_doctypes.py | 26 + erpnext/public/build.json | 5 + .../data_table_manager.js | 220 +++++ .../dialog_manager.js | 594 +++++++++++++ .../bank_reconciliation_tool/number_card.js | 75 ++ 61 files changed, 2922 insertions(+), 5018 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/__init__.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/__init__.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_invoice_item/bank_statement_transaction_invoice_item.py delete mode 100644 erpnext/accounts/doctype/bank_statement_transaction_payment_item/__init__.py 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_payment_item/bank_statement_transaction_payment_item.py delete mode 100644 erpnext/accounts/doctype/bank_statement_transaction_settings/__init__.py 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 create mode 100644 erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py 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 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/__init__.py b/erpnext/accounts/doctype/bank_statement_transaction_entry/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 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/__init__.py b/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 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_invoice_item/bank_statement_transaction_invoice_item.py b/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.py deleted file mode 100644 index cb1b15815f..0000000000 --- a/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_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 BankStatementTransactionInvoiceItem(Document): - pass diff --git a/erpnext/accounts/doctype/bank_statement_transaction_payment_item/__init__.py b/erpnext/accounts/doctype/bank_statement_transaction_payment_item/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 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_payment_item/bank_statement_transaction_payment_item.py b/erpnext/accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_item.py deleted file mode 100644 index 9840c0dbe3..0000000000 --- a/erpnext/accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_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 BankStatementTransactionPaymentItem(Document): - pass diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings/__init__.py b/erpnext/accounts/doctype/bank_statement_transaction_settings/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 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/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 @@ -
    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/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/patches.txt b/erpnext/patches.txt index 01d9ad32e7..ba31feeefc 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -754,4 +754,5 @@ 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.update_vehicle_no_reqd_condition \ No newline at end of file +erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes +erpnext.patches.v13_0.update_vehicle_no_reqd_condition 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/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/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/js/bank_reconciliation_tool/data_table_manager.js b/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js new file mode 100644 index 0000000000..5bb58faf2f --- /dev/null +++ b/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js @@ -0,0 +1,220 @@ +frappe.provide("erpnext.accounts.bank_reconciliation"); + +erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager { + constructor(opts) { + Object.assign(this, opts); + this.dialog_manager = new erpnext.accounts.bank_reconciliation.DialogManager( + this.company, + this.bank_account + ); + this.make_dt(); + } + + make_dt() { + var me = this; + frappe.call({ + method: + "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_bank_transactions", + args: { + bank_account: this.bank_account, + }, + callback: function (response) { + me.format_data(response.message); + me.get_dt_columns(); + me.get_datatable(); + me.set_listeners(); + }, + }); + } + + get_dt_columns() { + this.columns = [ + { + name: "Date", + editable: false, + width: 100, + }, + + { + name: "Party Type", + editable: false, + width: 95, + }, + { + name: "Party", + editable: false, + width: 100, + }, + { + name: "Description", + editable: false, + width: 350, + }, + { + name: "Deposit", + editable: false, + width: 100, + format: (value) => + "" + + 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"], + ` + +
    +
    +
    ${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"], + ` +