From 40f7dd4e774ac56776e7b7a42061a1fa0c4d074f Mon Sep 17 00:00:00 2001 From: Khushal Trivedi Date: Tue, 26 Nov 2019 18:31:40 +0530 Subject: [PATCH 001/102] fix: date validation on inpatient record, else condition removing on clinical prcd templ which is not req --- .../clinical_procedure_template.py | 5 +++-- .../doctype/inpatient_record/inpatient_record.py | 11 ++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py index 141329b3db..7cec362200 100644 --- a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py +++ b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py @@ -63,10 +63,11 @@ def updating_rate(self): item_code=%s""",(self.template, self.rate, self.item)) def create_item_from_template(doc): + disabled = 1 + if(doc.is_billable == 1): disabled = 0 - else: - disabled = 1 + #insert item item = frappe.get_doc({ "doctype": "Item", diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py index c107cd7335..835b38bedf 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import today, now_datetime +from frappe.utils import today, now_datetime, getdate from frappe.model.document import Document from frappe.desk.reportview import get_match_cond @@ -15,11 +15,20 @@ class InpatientRecord(Document): frappe.db.set_value("Patient", self.patient, "inpatient_record", self.name) def validate(self): + self.validate_dates() self.validate_already_scheduled_or_admitted() if self.status == "Discharged": frappe.db.set_value("Patient", self.patient, "inpatient_status", None) frappe.db.set_value("Patient", self.patient, "inpatient_record", None) + def validate_dates(self): + if (getdate(self.scheduled_date) < getdate(today())) or \ + (getdate(self.admitted_datetime) < getdate(today())): + frappe.throw(_("Scheduled and Admitted dates can not be less than today")) + if (getdate(self.expected_discharge) < getdate(self.scheduled_date)) or \ + (getdate(self.discharge_date) < getdate(self.scheduled_date)): + frappe.throw(_("Expected and Discharge dates cannot be less than Admission Schedule date")) + def validate_already_scheduled_or_admitted(self): query = """ select name, status From 213e071b214893ac951f313a84ffada18a145358 Mon Sep 17 00:00:00 2001 From: Khushal Trivedi Date: Mon, 2 Dec 2019 15:08:52 +0530 Subject: [PATCH 002/102] fix:Pricing Rule error AttributeError: 'str' object has no attribute 'get' #19770 --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 430dce7ddb..e81d186c73 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -33,9 +33,9 @@ class PricingRule(Document): if not self.margin_type: self.margin_rate_or_amount = 0.0 def validate_duplicate_apply_on(self): + print("##############",self.apply_on) field = apply_on_dict.get(self.apply_on) - values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field)] - + values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field) if field] if len(values) != len(set(values)): frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on)) From db8911b05d79c98b10d1ff6b0dd723216961dc3d Mon Sep 17 00:00:00 2001 From: Khushal Trivedi Date: Mon, 2 Dec 2019 15:14:11 +0530 Subject: [PATCH 003/102] fix:Pricing Rule error AttributeError: 'str' object has no attribute 'get' #19770 --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index e81d186c73..e871d98af6 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -33,7 +33,6 @@ class PricingRule(Document): if not self.margin_type: self.margin_rate_or_amount = 0.0 def validate_duplicate_apply_on(self): - print("##############",self.apply_on) field = apply_on_dict.get(self.apply_on) values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field) if field] if len(values) != len(set(values)): From 8af51e1f900ce49361b5766ef0468ab8e7c16433 Mon Sep 17 00:00:00 2001 From: Khushal Trivedi Date: Tue, 3 Dec 2019 19:28:16 +0530 Subject: [PATCH 004/102] fix: joining and relieving Date can be on same date as valid use case --- erpnext/hr/doctype/employee/employee.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index 2f88e1e363..6f4e0eaac8 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -152,7 +152,7 @@ class Employee(NestedSet): elif self.date_of_retirement and self.date_of_joining and (getdate(self.date_of_retirement) <= getdate(self.date_of_joining)): throw(_("Date Of Retirement must be greater than Date of Joining")) - elif self.relieving_date and self.date_of_joining and (getdate(self.relieving_date) <= getdate(self.date_of_joining)): + elif self.relieving_date and self.date_of_joining and (getdate(self.relieving_date) < getdate(self.date_of_joining)): throw(_("Relieving Date must be greater than Date of Joining")) elif self.contract_end_date and self.date_of_joining and (getdate(self.contract_end_date) <= getdate(self.date_of_joining)): From d86cefe1a509ec8b06544ae5298f5bf73b92f3b0 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Sat, 7 Dec 2019 13:10:44 +0100 Subject: [PATCH 005/102] first draft --- .../verified/de_kontenplan_SKR03_gnucash.json | 364 ++++++++++++++++++ 1 file changed, 364 insertions(+) create mode 100644 erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json new file mode 100644 index 0000000000..b8325e17e3 --- /dev/null +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json @@ -0,0 +1,364 @@ +{ + "country_code": "de", + "name": "SKR03 GnuCash", + "tree": { + "Aktiva": { + "is_group": 1, + "root_type": "Asset", + "Anlage- u. Kapitalkonten 0": { + "is_group": 1, + "EDV-Software": { + "account_number": "0027", + "account_type": "Fixed Asset" + }, + "Gesch\u00e4ftsausstattung": { + "account_number": "0410", + "account_type": "Fixed Asset" + }, + "B\u00fcroeinrichtung": { + "account_number": "0420", + "account_type": "Fixed Asset" + }, + "Darlehen": { + "account_number": "0565" + }, + "Maschinen": { + "account_number": "0210", + "account_type": "Fixed Asset" + }, + "Betriebsausstattung": { + "account_number": "0400", + "account_type": "Fixed Asset" + }, + "Ladeneinrichtung": { + "account_number": "0430", + "account_type": "Fixed Asset" + } + }, + "Finanzkonten 1": { + "is_group": 1, + "Postbank": { + "account_number": "1100", + "account_type": "Bank" + }, + "Bankkonto": { + "account_number": "1200", + "account_type": "Bank" + }, + "Durchlaufende Posten": { + "account_number": "1590" + }, + "Gewinnermittlung \u00a74/3 nicht Ergebniswirksam": { + "account_number": "1371" + }, + "Abziehbare VSt. 7%": { + "account_number": "1571" + }, + "Abziehbare VSt. 19%": { + "account_number": "1576" + }, + "Abziehbare VStr. nach \u00a713b UStG 19%": { + "account_number": "1577" + }, + "Ford. a. Lieferungen und Leistungen": { + "account_number": "1400", + "account_type": "Receivable" + } + }, + "Wareneingangs- u. Bestandskonten 3": { + "is_group": 1, + "Leistungen \u00a713b UStG 19% Vorsteuer, 19% Umsatzsteuer": { + "account_number": "3120" + }, + "Wareneingang VSt. 19%": { + "account_number": "3400" + } + } + }, + "Passiva": { + "is_group": 1, + "root_type": "Liability", + "Umsatzsteuer": { + "is_group": 1, + "Umsatzsteuer 7%": { + "account_number": "1771" + }, + "Umsatzsteuer 19%": { + "account_number": "1776" + }, + "Umsatzsteuer-Vorauszahlung": { + "account_number": "1780" + }, + "Umsatzsteuer-Vorauszahlung 1/11": { + "account_number": "1781" + }, + "Umsatzsteuer \u00a7 13b UStG 19%": { + "account_number": "1787" + }, + "Umsatzsteuer Vorjahr": { + "account_number": "1790" + }, + "Umsatzsteuer fr\u00fchere Jahre": { + "account_number": "1791" + } + }, + "Verbindlichkeiten": { + "is_group": 1, + "Verblk. aus Lieferungen u. Leistungen": { + "account_number": "1600", + "account_type": "Payable" + } + } + }, + "Erl\u00f6se u. Ertr\u00e4ge 2/8": { + "is_group": 1, + "root_type": "Income", + "Erl\u00f6skonten 8": { + "is_group": 1, + "Erl\u00f6se USt. 19%": { + "account_number": "8400", + "account_type": "Income Account" + }, + "Erl\u00f6se USt. 7%": { + "account_number": "8300", + "account_type": "Income Account" + } + }, + "Ertragskonten 2": { + "is_group": 1, + "sonstige Zinsen und \u00e4hnliche Ertr\u00e4ge": { + "account_number": "2650", + "account_type": "Income Account" + }, + "Au\u00dferordentliche Ertr\u00e4ge": { + "account_number": "2500", + "account_type": "Income Account" + }, + "Sonstige Ertr\u00e4ge": { + "account_number": "2700", + "account_type": "Income Account" + } + } + }, + "Aufwendungen 2/4": { + "is_group": 1, + "root_type": "Expense", + "Abschreibungen": { + "is_group": 1, + "Sofortabschreibung GWG": { + "account_number": "4855", + "account_type": "Expense Account" + } + }, + "Kfz-Kosten": { + "is_group": 1, + "Kfz-Steuer": { + "account_number": "4510", + "account_type": "Expense Account" + }, + "Kfz-Versicherungen": { + "account_number": "4520", + "account_type": "Expense Account" + }, + "laufende Kfz-Betriebskosten": { + "account_number": "4530", + "account_type": "Expense Account" + }, + "Kfz-Reparaturen": { + "account_number": "4540", + "account_type": "Expense Account" + }, + "Fremdfahrzeuge": { + "account_number": "4570", + "account_type": "Expense Account" + }, + "sonstige Kfz-Kosten": { + "account_number": "4580", + "account_type": "Expense Account" + } + }, + "Personalkosten": { + "is_group": 1, + "Geh\u00e4lter": { + "account_number": "4120", + "account_type": "Expense Account" + }, + "gesetzliche soziale Aufwendungen": { + "account_number": "4130", + "account_type": "Expense Account" + }, + "Aufwendungen f\u00fcr Altersvorsorge": { + "account_number": "4165", + "account_type": "Expense Account" + }, + "Verm\u00f6genswirksame Leistungen": { + "account_number": "4170", + "account_type": "Expense Account" + }, + "Aushilfsl\u00f6hne": { + "account_number": "4190", + "account_type": "Expense Account" + } + }, + "Raumkosten": { + "is_group": 1, + "Miete und Nebenkosten": { + "account_number": "4210", + "account_type": "Expense Account" + }, + "Gas, Wasser, Strom (Verwaltung, Vertrieb)": { + "account_number": "4240", + "account_type": "Expense Account" + }, + "Reinigung": { + "account_number": "4250", + "account_type": "Expense Account" + } + }, + "Reparatur/Instandhaltung": { + "is_group": 1, + "Reparatur u. Instandh. von Anlagen/Maschinen u. Betriebs- u. Gesch\u00e4ftsausst.": { + "account_number": "4805", + "account_type": "Expense Account" + } + }, + "Versicherungsbeitr\u00e4ge": { + "is_group": 1, + "Versicherungen": { + "account_number": "4360", + "account_type": "Expense Account" + }, + "Beitr\u00e4ge": { + "account_number": "4380", + "account_type": "Expense Account" + }, + "sonstige Ausgaben": { + "account_number": "4390", + "account_type": "Expense Account" + }, + "steuerlich abzugsf\u00e4hige Versp\u00e4tungszuschl\u00e4ge und Zwangsgelder": { + "account_number": "4396", + "account_type": "Expense Account" + } + }, + "Werbe-/Reisekosten": { + "is_group": 1, + "Werbekosten": { + "account_number": "4610", + "account_type": "Expense Account" + }, + "Aufmerksamkeiten": { + "account_number": "4653", + "account_type": "Expense Account" + }, + "nicht abzugsf\u00e4hige Betriebsausg. aus Werbe-, Repr\u00e4s.- u. Reisekosten": { + "account_number": "4665", + "account_type": "Expense Account" + }, + "Reisekosten Unternehmer": { + "account_number": "4670", + "account_type": "Expense Account" + } + }, + "verschiedene Kosten": { + "is_group": 1, + "Porto": { + "account_number": "4910", + "account_type": "Expense Account" + }, + "Telekom": { + "account_number": "4920", + "account_type": "Expense Account" + }, + "Mobilfunk D2": { + "account_number": "4921", + "account_type": "Expense Account" + }, + "Internet": { + "account_number": "4922", + "account_type": "Expense Account" + }, + "B\u00fcrobedarf": { + "account_number": "4930", + "account_type": "Expense Account" + }, + "Zeitschriften, B\u00fccher": { + "account_number": "4940", + "account_type": "Expense Account" + }, + "Fortbildungskosten": { + "account_number": "4945", + "account_type": "Expense Account" + }, + "Buchf\u00fchrungskosten": { + "account_number": "4955", + "account_type": "Expense Account" + }, + "Abschlu\u00df- u. Pr\u00fcfungskosten": { + "account_number": "4957", + "account_type": "Expense Account" + }, + "Nebenkosten des Geldverkehrs": { + "account_number": "4970", + "account_type": "Expense Account" + }, + "Werkzeuge und Kleinger\u00e4te": { + "account_number": "4985", + "account_type": "Expense Account" + } + }, + "Zinsaufwendungen": { + "is_group": 1, + "Zinsaufwendungen f\u00fcr kurzfristige Verbindlichkeiten": { + "account_number": "2110", + "account_type": "Expense Account" + }, + "Zinsaufwendungen f\u00fcr KFZ Finanzierung": { + "account_number": "2121", + "account_type": "Expense Account" + } + } + }, + "Anfangsbestand 9": { + "is_group": 1, + "root_type": "Equity", + "Saldenvortragskonten": { + "is_group": 1, + "Saldenvortrag Sachkonten": { + "account_number": "9000" + }, + "Saldenvortr\u00e4ge Debitoren": { + "account_number": "9008" + }, + "Saldenvortr\u00e4ge Kreditoren": { + "account_number": "9009" + } + } + }, + "Privatkonten 1": { + "is_group": 1, + "root_type": "Equity", + "Privatentnahmen/-einlagen": { + "is_group": 1, + "Privatentnahme allgemein": { + "account_number": "1800" + }, + "Privatsteuern": { + "account_number": "1810" + }, + "Sonderausgaben beschr\u00e4nkt abzugsf\u00e4hig": { + "account_number": "1820" + }, + "Sonderausgaben unbeschr\u00e4nkt abzugsf\u00e4hig": { + "account_number": "1830" + }, + "Au\u00dfergew\u00f6hnliche Belastungen": { + "account_number": "1850" + }, + "Privateinlagen": { + "account_number": "1890" + } + } + } + } +} From 05d708b51cc4c5925917609de0515cd7967e81eb Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Sat, 7 Dec 2019 14:21:22 +0100 Subject: [PATCH 006/102] add some missing accounts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aktive Rechnungsabgrenzung Passive Rechnungsabgrenzung Kasse Verblk. aus Lohn und Gehalt Erlöse --- .../verified/de_kontenplan_SKR03_gnucash.json | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json index b8325e17e3..688fb4b619 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json @@ -33,10 +33,20 @@ "Ladeneinrichtung": { "account_number": "0430", "account_type": "Fixed Asset" - } + }, + "Aktive Rechnungsabgrenzung": { + "account_number": "0980" + }, + "Passive Rechnungsabgrenzung": { + "account_number": "0990" + } }, "Finanzkonten 1": { - "is_group": 1, + "is_group": 1, + "Kasse": { + "account_number": "1100", + "account_type": "Cash" + }, "Postbank": { "account_number": "1100", "account_type": "Bank" @@ -107,14 +117,22 @@ "Verblk. aus Lieferungen u. Leistungen": { "account_number": "1600", "account_type": "Payable" - } + }, + "Verblk. aus Lohn und Gehalt": { + "account_number": "1740", + "account_type": "Payable" + } } }, "Erl\u00f6se u. Ertr\u00e4ge 2/8": { "is_group": 1, "root_type": "Income", "Erl\u00f6skonten 8": { - "is_group": 1, + "is_group": 1, + "Erl\u00f6se": { + "account_number": "8200", + "account_type": "Income Account" + }, "Erl\u00f6se USt. 19%": { "account_number": "8400", "account_type": "Income Account" From af9c5fda9ef0a08eea98504e67fb782839cfe347 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Sat, 7 Dec 2019 14:22:51 +0100 Subject: [PATCH 007/102] rename CoA --- .../chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json index 688fb4b619..cd580c20f5 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json @@ -1,6 +1,6 @@ { "country_code": "de", - "name": "SKR03 GnuCash", + "name": "SKR03 mit Kontonummern", "tree": { "Aktiva": { "is_group": 1, From 9f0699f7e310df03c0a6417feb671855f8ca7112 Mon Sep 17 00:00:00 2001 From: Khushal Trivedi Date: Mon, 9 Dec 2019 18:02:06 +0530 Subject: [PATCH 008/102] fix-education: date of birth validation --- erpnext/education/doctype/student/student.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/education/doctype/student/student.py b/erpnext/education/doctype/student/student.py index 76825cec1b..8e4b4e16f9 100644 --- a/erpnext/education/doctype/student/student.py +++ b/erpnext/education/doctype/student/student.py @@ -5,12 +5,14 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document +from frappe.utils import getdate,today from frappe import _ from frappe.desk.form.linked_with import get_linked_doctypes from erpnext.education.utils import check_content_completion, check_quiz_completion class Student(Document): def validate(self): self.title = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name])) + self.validate_dates() if self.student_applicant: self.check_unique() @@ -19,6 +21,10 @@ class Student(Document): if frappe.get_value("Student", self.name, "title") != self.title: self.update_student_name_in_linked_doctype() + def validate_dates(self): + if self.date_of_birth and getdate(self.date_of_birth) >= getdate(today()): + frappe.throw(_("Date of Birth cannot be greater than today.")) + def update_student_name_in_linked_doctype(self): linked_doctypes = get_linked_doctypes("Student") for d in linked_doctypes: From e89d521848d91dac8d11b8eccabdd3c2f2c39196 Mon Sep 17 00:00:00 2001 From: Khushal Trivedi Date: Wed, 11 Dec 2019 14:02:07 +0530 Subject: [PATCH 009/102] fix:Sibling child table filtering for duplacacy on student form --- erpnext/education/doctype/student/student.js | 21 +++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/erpnext/education/doctype/student/student.js b/erpnext/education/doctype/student/student.js index b6e741c4da..66df6b977e 100644 --- a/erpnext/education/doctype/student/student.js +++ b/erpnext/education/doctype/student/student.js @@ -31,8 +31,9 @@ frappe.ui.form.on('Student', { frappe.ui.form.on('Student Guardian', { guardians_add: function(frm){ frm.fields_dict['guardians'].grid.get_field('guardian').get_query = function(doc){ - var guardian_list = []; + let guardian_list = []; if(!doc.__islocal) guardian_list.push(doc.guardian); + $.each(doc.guardians, function(idx, val){ if (val.guardian) guardian_list.push(val.guardian); }); @@ -40,3 +41,21 @@ frappe.ui.form.on('Student Guardian', { }; } }); + + +frappe.ui.form.on('Student Sibling', { + siblings_add: function(frm){ + frm.fields_dict['siblings'].grid.get_field('student').get_query = function(doc){ + let sibling_list = [frm.doc.name]; + if(!doc.__islocal) sibling_list.push(doc.student); + + $.each(doc.siblings, function(idx, val){ + if (val.student && val.studying_in_same_institute == 'YES') { + sibling_list.push(val.student); + } + + }); + return { filters: [['Student', 'name', 'not in', sibling_list]] }; + }; + } +}); \ No newline at end of file From 4c24fb1efc6a2e3111f8a7d7e03fa1717dc73aa8 Mon Sep 17 00:00:00 2001 From: Khushal Trivedi Date: Wed, 11 Dec 2019 15:16:56 +0530 Subject: [PATCH 010/102] fix:Sibling child table filtering for duplacacy on student form --- erpnext/education/doctype/student/student.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/education/doctype/student/student.js b/erpnext/education/doctype/student/student.js index 66df6b977e..ea01b5ded2 100644 --- a/erpnext/education/doctype/student/student.js +++ b/erpnext/education/doctype/student/student.js @@ -33,7 +33,6 @@ frappe.ui.form.on('Student Guardian', { frm.fields_dict['guardians'].grid.get_field('guardian').get_query = function(doc){ let guardian_list = []; if(!doc.__islocal) guardian_list.push(doc.guardian); - $.each(doc.guardians, function(idx, val){ if (val.guardian) guardian_list.push(val.guardian); }); @@ -48,12 +47,10 @@ frappe.ui.form.on('Student Sibling', { frm.fields_dict['siblings'].grid.get_field('student').get_query = function(doc){ let sibling_list = [frm.doc.name]; if(!doc.__islocal) sibling_list.push(doc.student); - $.each(doc.siblings, function(idx, val){ if (val.student && val.studying_in_same_institute == 'YES') { sibling_list.push(val.student); - } - + } }); return { filters: [['Student', 'name', 'not in', sibling_list]] }; }; From 03e770782a30bb6ffa9ba010f2d642c1573aece3 Mon Sep 17 00:00:00 2001 From: Khushal Trivedi Date: Wed, 11 Dec 2019 15:24:39 +0530 Subject: [PATCH 011/102] fix:Sibling child table filtering for duplacacy on student form --- erpnext/education/doctype/student/student.js | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/education/doctype/student/student.js b/erpnext/education/doctype/student/student.js index ea01b5ded2..1936dcbd3e 100644 --- a/erpnext/education/doctype/student/student.js +++ b/erpnext/education/doctype/student/student.js @@ -46,7 +46,6 @@ frappe.ui.form.on('Student Sibling', { siblings_add: function(frm){ frm.fields_dict['siblings'].grid.get_field('student').get_query = function(doc){ let sibling_list = [frm.doc.name]; - if(!doc.__islocal) sibling_list.push(doc.student); $.each(doc.siblings, function(idx, val){ if (val.student && val.studying_in_same_institute == 'YES') { sibling_list.push(val.student); From ee9aea4febee1dfa0113809053df526395ea02fe Mon Sep 17 00:00:00 2001 From: Khushal Trivedi Date: Tue, 24 Dec 2019 13:37:10 +0530 Subject: [PATCH 012/102] fix: date validation on student form, instructor duplicacy fix on student grp, instructor with same employee id fix --- erpnext/education/doctype/instructor/instructor.py | 9 +++++++++ erpnext/education/doctype/student/student.py | 3 +++ .../education/doctype/student_group/student_group.js | 12 ++++++++++++ 3 files changed, 24 insertions(+) diff --git a/erpnext/education/doctype/instructor/instructor.py b/erpnext/education/doctype/instructor/instructor.py index 0756b5f01a..058d476f5b 100644 --- a/erpnext/education/doctype/instructor/instructor.py +++ b/erpnext/education/doctype/instructor/instructor.py @@ -22,3 +22,12 @@ class Instructor(Document): self.name = self.employee elif naming_method == 'Full Name': self.name = self.instructor_name + + def validate(self): + self.validate_duplicate_employee() + + def validate_duplicate_employee(self): + if self.employee and frappe.db.get_value("Instructor", {'employee': self.employee}, 'name'): + frappe.throw(_("Employee ID is linked with another instructor")) + + diff --git a/erpnext/education/doctype/student/student.py b/erpnext/education/doctype/student/student.py index 8e4b4e16f9..99c4c0e908 100644 --- a/erpnext/education/doctype/student/student.py +++ b/erpnext/education/doctype/student/student.py @@ -25,6 +25,9 @@ class Student(Document): if self.date_of_birth and getdate(self.date_of_birth) >= getdate(today()): frappe.throw(_("Date of Birth cannot be greater than today.")) + if self.joining_date and self.date_of_leaving and getdate(self.joining_date) > getdate(self.date_of_leaving): + frappe.throw(_("Joining Date can not be greater than Leaving Date")) + def update_student_name_in_linked_doctype(self): linked_doctypes = get_linked_doctypes("Student") for d in linked_doctypes: diff --git a/erpnext/education/doctype/student_group/student_group.js b/erpnext/education/doctype/student_group/student_group.js index c29c134843..372e190af9 100644 --- a/erpnext/education/doctype/student_group/student_group.js +++ b/erpnext/education/doctype/student_group/student_group.js @@ -122,3 +122,15 @@ frappe.ui.form.on("Student Group", { } } }); + +frappe.ui.form.on('Student Group Instructor', { + instructors_add: function(frm){ + frm.fields_dict['instructors'].grid.get_field('instructor').get_query = function(doc){ + let instructor_list = []; + $.each(doc.instructors, function(idx, val){ + instructor_list.push(val.instructor); + }); + return { filters: [['Instructor', 'name', 'not in', instructor_list]] }; + }; + } +}); \ No newline at end of file From e57f1c995d1e0c35788eb1b4d437cb5b03d96f04 Mon Sep 17 00:00:00 2001 From: Khushal Trivedi Date: Tue, 24 Dec 2019 14:02:31 +0530 Subject: [PATCH 013/102] fix: date validation on student form, instructor duplicacy fix on student grp, instructor with same employee id fix --- erpnext/education/doctype/student_group/student_group.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/education/doctype/student_group/student_group.js b/erpnext/education/doctype/student_group/student_group.js index 372e190af9..4165ce0f2e 100644 --- a/erpnext/education/doctype/student_group/student_group.js +++ b/erpnext/education/doctype/student_group/student_group.js @@ -128,7 +128,7 @@ frappe.ui.form.on('Student Group Instructor', { frm.fields_dict['instructors'].grid.get_field('instructor').get_query = function(doc){ let instructor_list = []; $.each(doc.instructors, function(idx, val){ - instructor_list.push(val.instructor); + instructor_list.push(val.instructor); }); return { filters: [['Instructor', 'name', 'not in', instructor_list]] }; }; From 0d9c151d9f61d03e57f815d99158e1b90c9dca5e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 24 Dec 2019 18:06:49 +0530 Subject: [PATCH 014/102] fix: Exclude current record while validating duplicate employee --- erpnext/education/doctype/instructor/instructor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/education/doctype/instructor/instructor.py b/erpnext/education/doctype/instructor/instructor.py index 058d476f5b..28df2fcdc1 100644 --- a/erpnext/education/doctype/instructor/instructor.py +++ b/erpnext/education/doctype/instructor/instructor.py @@ -27,7 +27,7 @@ class Instructor(Document): self.validate_duplicate_employee() def validate_duplicate_employee(self): - if self.employee and frappe.db.get_value("Instructor", {'employee': self.employee}, 'name'): + if self.employee and frappe.db.get_value("Instructor", {'employee': self.employee, 'name': ['!=', self.name]}, 'name'): frappe.throw(_("Employee ID is linked with another instructor")) From 5d6fada4315bd00496b6290878309a1a6c44b598 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Wed, 1 Jan 2020 22:55:54 +0100 Subject: [PATCH 015/102] feat: add accounts and account types --- .../verified/de_kontenplan_SKR03_gnucash.json | 293 +++++++++++++----- 1 file changed, 208 insertions(+), 85 deletions(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json index cd580c20f5..b106e6f3ec 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json @@ -4,8 +4,8 @@ "tree": { "Aktiva": { "is_group": 1, - "root_type": "Asset", - "Anlage- u. Kapitalkonten 0": { + "root_type": "Asset", + "A - Anlagevermögen": { "is_group": 1, "EDV-Software": { "account_number": "0027", @@ -34,95 +34,184 @@ "account_number": "0430", "account_type": "Fixed Asset" }, + "Accumulated Depreciation": { + "account_type": "Accumulated Depreciation" + } + }, + "B - Umlaufvermögen": { + "is_group": 1, + "I. Vorräte": { + "is_group": 1, + "Roh-, Hilfs- und Betriebsstoffe (Bestand)": { + "account_number": "3970", + "account_type": "Stock" + }, + "Waren (Bestand)": { + "account_number": "3980", + "account_type": "Stock" + } + }, + "II. Forderungen und sonstige Vermögensgegenstände": { + "is_group": 1, + "Ford. a. Lieferungen und Leistungen": { + "account_number": "1400", + "account_type": "Receivable" + }, + "Durchlaufende Posten": { + "account_number": "1590" + }, + "Gewinnermittlung \u00a74/3 nicht Ergebniswirksam": { + "account_number": "1371" + }, + "Abziehbare VSt. 7%": { + "account_number": "1571" + }, + "Abziehbare VSt. 19%": { + "account_number": "1576" + }, + "Abziehbare VStr. nach \u00a713b UStG 19%": { + "account_number": "1577" + }, + "Leistungen \u00a713b UStG 19% Vorsteuer, 19% Umsatzsteuer": { + "account_number": "3120" + } + }, + "III. Wertpapiere": { + "is_group": 1 + }, + "IV. Kassenbestand, Bundesbankguthaben, Guthaben bei Kreditinstituten und Schecks.": { + "is_group": 1, + "Kasse": { + "account_number": "1100", + "account_type": "Cash" + }, + "Postbank": { + "account_number": "1100", + "account_type": "Bank" + }, + "Bankkonto": { + "account_number": "1200", + "account_type": "Bank" + } + } + }, + "C - Rechnungsabgrenzungsposten": { + "is_group": 1, "Aktive Rechnungsabgrenzung": { "account_number": "0980" - }, - "Passive Rechnungsabgrenzung": { - "account_number": "0990" } - }, - "Finanzkonten 1": { + }, + "D - Aktive latente Steuern": { "is_group": 1, - "Kasse": { - "account_number": "1100", - "account_type": "Cash" - }, - "Postbank": { - "account_number": "1100", - "account_type": "Bank" - }, - "Bankkonto": { - "account_number": "1200", - "account_type": "Bank" - }, - "Durchlaufende Posten": { - "account_number": "1590" - }, - "Gewinnermittlung \u00a74/3 nicht Ergebniswirksam": { - "account_number": "1371" - }, - "Abziehbare VSt. 7%": { - "account_number": "1571" - }, - "Abziehbare VSt. 19%": { - "account_number": "1576" - }, - "Abziehbare VStr. nach \u00a713b UStG 19%": { - "account_number": "1577" - }, - "Ford. a. Lieferungen und Leistungen": { - "account_number": "1400", - "account_type": "Receivable" - } - }, - "Wareneingangs- u. Bestandskonten 3": { - "is_group": 1, - "Leistungen \u00a713b UStG 19% Vorsteuer, 19% Umsatzsteuer": { - "account_number": "3120" - }, - "Wareneingang VSt. 19%": { - "account_number": "3400" - } - } + "Aktive latente Steuern": { + "account_number": "0983" + } + }, + "E - Aktiver Unterschiedsbetrag aus der Vermögensverrechnung": { + "is_group": 1 + } }, "Passiva": { "is_group": 1, - "root_type": "Liability", - "Umsatzsteuer": { - "is_group": 1, - "Umsatzsteuer 7%": { - "account_number": "1771" - }, - "Umsatzsteuer 19%": { - "account_number": "1776" - }, - "Umsatzsteuer-Vorauszahlung": { - "account_number": "1780" - }, - "Umsatzsteuer-Vorauszahlung 1/11": { - "account_number": "1781" - }, - "Umsatzsteuer \u00a7 13b UStG 19%": { - "account_number": "1787" - }, - "Umsatzsteuer Vorjahr": { - "account_number": "1790" - }, - "Umsatzsteuer fr\u00fchere Jahre": { - "account_number": "1791" - } - }, - "Verbindlichkeiten": { - "is_group": 1, - "Verblk. aus Lieferungen u. Leistungen": { - "account_number": "1600", - "account_type": "Payable" + "root_type": "Liability", + "A. Eigenkapital": { + "is_group": 1, + "I. Gezeichnetes Kapital": { + "is_group": 1 }, - "Verblk. aus Lohn und Gehalt": { - "account_number": "1740", - "account_type": "Payable" + "II. Kapitalrücklage": { + "is_group": 1 + }, + "III. Gewinnrücklagen": { + "is_group": 1 + }, + "IV. Gewinnvortrag/Verlustvortrag": { + "is_group": 1 + }, + "V. Jahresüberschuß/Jahresfehlbetrag": { + "is_group": 1 } - } + }, + "B. Rückstellungen": { + "is_group": 1, + "I. Rückstellungen für Pensionen und ähnliche Verpflichtungen": { + "is_group": 1 + }, + "II. Steuerrückstellungen": { + "is_group": 1 + }, + "III. sonstige Rückstellungen": { + "is_group": 1 + } + }, + "C. Verbindlichkeiten": { + "is_group": 1, + "I. Anleihen": { + "is_group": 1 + }, + "II. Verbindlichkeiten gegenüber Kreditinstituten": { + "is_group": 1 + }, + "III. Erhaltene Anzahlungen auf Bestellungen": { + "is_group": 1 + }, + "IV. Verbindlichkeiten aus Lieferungen und Leistungen": { + "is_group": 1, + "Verbindlichkeiten aus Lieferungen u. Leistungen": { + "account_number": "1600", + "account_type": "Payable" + } + }, + "V. Verbindlichkeiten aus der Annahme gezogener Wechsel und der Ausstellung eigener Wechsel": { + "is_group": 1 + }, + "VI. Verbindlichkeiten gegenüber verbundenen Unternehmen": { + "is_group": 1 + }, + "VII. Verbindlichkeiten gegenüber Unternehmen, mit denen ein Beteiligungsverhältnis besteht": { + "is_group": 1 + }, + "VIII. sonstige Verbindlichkeiten": { + "is_group": 1, + "Verbindlichkeiten aus Lohn und Gehalt": { + "account_number": "1740", + "account_type": "Payable" + }, + "Umsatzsteuer": { + "is_group": 1, + "Umsatzsteuer 7%": { + "account_number": "1771" + }, + "Umsatzsteuer 19%": { + "account_number": "1776" + }, + "Umsatzsteuer-Vorauszahlung": { + "account_number": "1780" + }, + "Umsatzsteuer-Vorauszahlung 1/11": { + "account_number": "1781" + }, + "Umsatzsteuer \u00a7 13b UStG 19%": { + "account_number": "1787" + }, + "Umsatzsteuer Vorjahr": { + "account_number": "1790" + }, + "Umsatzsteuer fr\u00fchere Jahre": { + "account_number": "1791" + } + } + } + }, + "D. Rechnungsabgrenzungsposten": { + "is_group": 1, + "Passive Rechnungsabgrenzung": { + "account_number": "0990" + } + }, + "E. Passive latente Steuern": { + "is_group": 1 + } }, "Erl\u00f6se u. Ertr\u00e4ge 2/8": { "is_group": 1, @@ -160,13 +249,47 @@ }, "Aufwendungen 2/4": { "is_group": 1, - "root_type": "Expense", + "root_type": "Expense", + "Wareneingang": { + "account_number": "3200" + }, + "Herstellungskosten": { + "account_number": "4996", + "account_type": "Cost of Goods Sold" + }, + "Verluste aus dem Abgang von Gegenständen des Anlagevermögens": { + "account_number": "2320", + "account_type": "Stock Adjustment" + }, + "Verwaltungskosten": { + "account_number": "4997", + "account_type": "Expenses Included In Valuation" + }, + "Vertriebskosten": { + "account_number": "4998", + "account_type": "Expenses Included In Valuation" + }, + "Gegenkonto 4996-4998": { + "account_number": "4999" + }, "Abschreibungen": { "is_group": 1, - "Sofortabschreibung GWG": { + "Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": { + "account_number": "4830", + "account_type": "Depreciation" + }, + "Abschreibungen auf Gebäude": { + "account_number": "4831", + "account_type": "Depreciation" + }, + "Abschreibungen auf Kfz": { + "account_number": "4832", + "account_type": "Depreciation" + }, + "Sofortabschreibung GWG": { "account_number": "4855", "account_type": "Expense Account" - } + } }, "Kfz-Kosten": { "is_group": 1, From 3496104ebe8c1d04cb9300699fdc57e1c62c5be2 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 28 Jan 2020 13:11:49 +0530 Subject: [PATCH 016/102] fix: odometer value was not syncing properly --- erpnext/hr/doctype/vehicle_log/vehicle_log.js | 38 +- .../hr/doctype/vehicle_log/vehicle_log.json | 842 ++++-------------- erpnext/hr/doctype/vehicle_log/vehicle_log.py | 18 +- 3 files changed, 201 insertions(+), 697 deletions(-) diff --git a/erpnext/hr/doctype/vehicle_log/vehicle_log.js b/erpnext/hr/doctype/vehicle_log/vehicle_log.js index 7694cfed7c..4c192a0234 100644 --- a/erpnext/hr/doctype/vehicle_log/vehicle_log.js +++ b/erpnext/hr/doctype/vehicle_log/vehicle_log.js @@ -2,19 +2,10 @@ // For license information, please see license.txt frappe.ui.form.on("Vehicle Log", { - refresh: function(frm,cdt,cdn) { - var vehicle_log=frappe.model.get_doc(cdt,cdn); - if (vehicle_log.license_plate) { - frappe.call({ - method: "erpnext.hr.doctype.vehicle_log.vehicle_log.get_make_model", - args: { - license_plate: vehicle_log.license_plate - }, - callback: function(r) { - frappe.model.set_value(cdt, cdn, ("model"), r.message[0]); - frappe.model.set_value(cdt, cdn, ("make"), r.message[1]); - } - }) + refresh: function(frm) { + + if(frm.doc.license_plate && frm.doc.__islocal){ + frm.events.set_vehicle_details(frm) } if(frm.doc.docstatus == 1) { @@ -25,6 +16,27 @@ frappe.ui.form.on("Vehicle Log", { } }, + license_plate: function(frm){ + if(frm.doc.license_plate){ + frm.events.set_vehicle_details(frm) + } + }, + + set_vehicle_details: function(frm){ + frappe.call({ + method: "erpnext.hr.doctype.vehicle_log.vehicle_log.get_make_model", + args: { + license_plate: frm.doc.license_plate + }, + callback: function(r) { + frappe.model.set_value(cur_frm.doctype, cur_frm.docname, "model", r.message[0]); + frappe.model.set_value(cur_frm.doctype, cur_frm.docname, "make", r.message[1]); + frappe.model.set_value(cur_frm.doctype, cur_frm.docname, "last_odometer", r.message[2]); + frappe.model.set_value(cur_frm.doctype, cur_frm.docname, "employee", r.message[3]); + } + }); + }, + expense_claim: function(frm){ frappe.call({ method: "erpnext.hr.doctype.vehicle_log.vehicle_log.make_expense_claim", diff --git a/erpnext/hr/doctype/vehicle_log/vehicle_log.json b/erpnext/hr/doctype/vehicle_log/vehicle_log.json index cde39e7ee4..52effffc06 100644 --- a/erpnext/hr/doctype/vehicle_log/vehicle_log.json +++ b/erpnext/hr/doctype/vehicle_log/vehicle_log.json @@ -1,706 +1,192 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "naming_series:", - "beta": 0, - "creation": "2016-09-03 14:14:51.788550", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, + "actions": [], + "autoname": "naming_series:", + "creation": "2016-09-03 14:14:51.788550", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "vehicle_section", + "naming_series", + "license_plate", + "employee", + "column_break_4", + "column_break_7", + "model", + "make", + "odometer_reading", + "date", + "odometer", + "column_break_12", + "last_odometer", + "refuelling_details", + "fuel_qty", + "price", + "column_break_15", + "supplier", + "invoice", + "service_details", + "service_detail", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "vehicle_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, - "length": 0, - "no_copy": 0, - "options": "fa fa-user", - "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": "vehicle_section", + "fieldtype": "Section Break", + "options": "fa fa-user" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "naming_series", - "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": "Series", - "length": 0, - "no_copy": 1, - "options": "HR-VLOG-.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 - }, + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "no_copy": 1, + "options": "HR-VLOG-.YYYY.-", + "print_hide": 1, + "reqd": 1, + "set_only_once": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "license_plate", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "License Plate", - "length": 0, - "no_copy": 0, - "options": "Vehicle", - "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 - }, + "fieldname": "license_plate", + "fieldtype": "Link", + "in_global_search": 1, + "in_list_view": 1, + "label": "License Plate", + "options": "Vehicle", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "employee", - "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": 1, - "label": "Employee", - "length": 0, - "no_copy": 0, - "options": "Employee", - "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 - }, + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, { - "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 - }, + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 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 - }, + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "model", - "fieldtype": "Read Only", - "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": "Model", - "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": "model", + "fieldtype": "Read Only", + "label": "Model" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "make", - "fieldtype": "Read Only", - "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": "Make", - "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": "make", + "fieldtype": "Read Only", + "label": "Make" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "odometer_reading", - "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": "Odometer Reading", - "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": "odometer_reading", + "fieldtype": "Section Break", + "label": "Odometer Reading" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "date", + "fieldtype": "Date", + "label": "Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "odometer", - "fieldtype": "Int", - "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": "Odometer", - "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 - }, + "fieldname": "odometer", + "fieldtype": "Int", + "label": "Current Odometer value ", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "refuelling_details", - "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": "Refuelling Details", - "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 - }, + "collapsible": 1, + "fieldname": "refuelling_details", + "fieldtype": "Section Break", + "label": "Refuelling Details" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "fuel_qty", - "fieldtype": "Float", - "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": "Fuel 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": "fuel_qty", + "fieldtype": "Float", + "label": "Fuel Qty" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "price", - "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": "Fuel Price", - "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": "price", + "fieldtype": "Currency", + "label": "Fuel Price" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_15", - "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_15", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "supplier", - "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": "Supplier", - "length": 0, - "no_copy": 0, - "options": "Supplier", - "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": "supplier", + "fieldtype": "Link", + "label": "Supplier", + "options": "Supplier" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "invoice", - "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": "Invoice Ref", - "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": "invoice", + "fieldtype": "Data", + "label": "Invoice Ref" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "service_details", - "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": "Service Details", - "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 - }, + "collapsible": 1, + "fieldname": "service_details", + "fieldtype": "Section Break", + "label": "Service Details" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "service_detail", - "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": "Service Detail", - "length": 0, - "no_copy": 0, - "options": "Vehicle Service", - "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": "service_detail", + "fieldtype": "Table", + "label": "Service Detail", + "options": "Vehicle Service" + }, { - "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": "Vehicle Log", - "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 + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Vehicle Log", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "last_odometer", + "fieldtype": "Int", + "label": "last Odometer Value ", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" } - ], - "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-08-21 14:44:51.131186", - "modified_by": "Administrator", - "module": "HR", - "name": "Vehicle Log", - "name_case": "", - "owner": "Administrator", + ], + "is_submittable": 1, + "links": [], + "modified": "2020-01-28 12:43:34.419647", + "modified_by": "Administrator", + "module": "HR", + "name": "Vehicle Log", + "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": "Fleet Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Fleet Manager", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/vehicle_log/vehicle_log.py b/erpnext/hr/doctype/vehicle_log/vehicle_log.py index df633611be..0dbf1ceb96 100644 --- a/erpnext/hr/doctype/vehicle_log/vehicle_log.py +++ b/erpnext/hr/doctype/vehicle_log/vehicle_log.py @@ -11,22 +11,28 @@ from frappe.model.document import Document class VehicleLog(Document): def validate(self): - last_odometer=frappe.db.get_value("Vehicle", self.license_plate, "last_odometer") - if flt(self.odometer) < flt(last_odometer): - frappe.throw(_("Current Odometer reading entered should be greater than initial Vehicle Odometer {0}").format(last_odometer)) + if flt(self.odometer) < flt(self.last_odometer): + frappe.throw(_("Current Odometer reading entered should be greater than initial Vehicle Odometer {0}").format(self.last_odometer)) for service_detail in self.service_detail: if (service_detail.service_item or service_detail.type or service_detail.frequency or service_detail.expense_amount): if not (service_detail.service_item and service_detail.type and service_detail.frequency and service_detail.expense_amount): frappe.throw(_("Service Item,Type,frequency and expense amount are required")) def on_submit(self): - frappe.db.sql("update `tabVehicle` set last_odometer=%s where license_plate=%s", - (self.odometer, self.license_plate)) + print("I am here") + frappe.db.set_value("Vehicle", self.license_plate, "last_odometer", self.odometer) + + def on_cancel(self): + print("sel"*10, self.last_odometer, self.odometer) + distance_travelled = self.odometer - self.last_odometer + if(distance_travelled > 0): + updated_odometer_value = int(frappe.db.get_value("Vehicle", self.license_plate, "last_odometer")) - distance_travelled + frappe.db.set_value("Vehicle", self.license_plate, "last_odometer", updated_odometer_value) @frappe.whitelist() def get_make_model(license_plate): vehicle=frappe.get_doc("Vehicle",license_plate) - return (vehicle.make,vehicle.model) + return (vehicle.make,vehicle.model,vehicle.last_odometer,vehicle.employee) @frappe.whitelist() def make_expense_claim(docname): From 3716434218a9a56b34564ad7d085137cd6b64660 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Wed, 29 Jan 2020 22:21:19 +0100 Subject: [PATCH 017/102] fix: add missing account types --- .../verified/de_kontenplan_SKR03_gnucash.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json index b106e6f3ec..6f0ca0e760 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json @@ -173,6 +173,14 @@ }, "VIII. sonstige Verbindlichkeiten": { "is_group": 1, + "Sonstige Verbindlichkeiten": { + "account_number": "1700", + "account_type": "Asset Received But Not Billed" + }, + "Sonstige Verbindlichkeiten (1 bis 5 Jahre)": { + "account_number": "1702", + "account_type": "Stock Received But Not Billed" + }, "Verbindlichkeiten aus Lohn und Gehalt": { "account_number": "1740", "account_type": "Payable" @@ -253,6 +261,10 @@ "Wareneingang": { "account_number": "3200" }, + "Bezugsnebenkosten": { + "account_number": "3800", + "account_type": "Expenses Included In Asset Valuation" + }, "Herstellungskosten": { "account_number": "4996", "account_type": "Cost of Goods Sold" @@ -276,7 +288,7 @@ "is_group": 1, "Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": { "account_number": "4830", - "account_type": "Depreciation" + "account_type": "Accumulated Depreciation" }, "Abschreibungen auf Gebäude": { "account_number": "4831", From e21cd5e7f13dbc55b453138d5fde6e198c8bc5f4 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Wed, 29 Jan 2020 23:39:55 +0100 Subject: [PATCH 018/102] fix: account number for "Kasse" --- .../chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json index 6f0ca0e760..e676182255 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json @@ -82,7 +82,7 @@ "IV. Kassenbestand, Bundesbankguthaben, Guthaben bei Kreditinstituten und Schecks.": { "is_group": 1, "Kasse": { - "account_number": "1100", + "account_number": "1000", "account_type": "Cash" }, "Postbank": { From eacc1bc2dc9363e8622d60a7b24bba990066bbbe Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 5 Feb 2020 12:45:43 +0530 Subject: [PATCH 019/102] fix: requested changes --- erpnext/hr/doctype/vehicle_log/vehicle_log.js | 14 +++++++------- erpnext/hr/doctype/vehicle_log/vehicle_log.py | 11 ++++++++--- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/erpnext/hr/doctype/vehicle_log/vehicle_log.js b/erpnext/hr/doctype/vehicle_log/vehicle_log.js index 4c192a0234..bdb37d2b73 100644 --- a/erpnext/hr/doctype/vehicle_log/vehicle_log.js +++ b/erpnext/hr/doctype/vehicle_log/vehicle_log.js @@ -5,32 +5,32 @@ frappe.ui.form.on("Vehicle Log", { refresh: function(frm) { if(frm.doc.license_plate && frm.doc.__islocal){ - frm.events.set_vehicle_details(frm) + frm.events.set_vehicle_details(frm); } if(frm.doc.docstatus == 1) { frm.add_custom_button(__('Expense Claim'), function() { - frm.events.expense_claim(frm) + frm.events.expense_claim(frm); }, __('Create')); frm.page.set_inner_btn_group_as_primary(__('Create')); } }, - license_plate: function(frm){ + license_plate: function(frm) { if(frm.doc.license_plate){ - frm.events.set_vehicle_details(frm) + frm.events.set_vehicle_details(frm); } }, - set_vehicle_details: function(frm){ + set_vehicle_details: function(frm) { frappe.call({ method: "erpnext.hr.doctype.vehicle_log.vehicle_log.get_make_model", args: { license_plate: frm.doc.license_plate }, callback: function(r) { - frappe.model.set_value(cur_frm.doctype, cur_frm.docname, "model", r.message[0]); - frappe.model.set_value(cur_frm.doctype, cur_frm.docname, "make", r.message[1]); + frappe.model.set_value(cur_frm.doctype, cur_frm.docname, "make", r.message[0]); + frappe.model.set_value(cur_frm.doctype, cur_frm.docname, "model", r.message[1]); frappe.model.set_value(cur_frm.doctype, cur_frm.docname, "last_odometer", r.message[2]); frappe.model.set_value(cur_frm.doctype, cur_frm.docname, "employee", r.message[3]); } diff --git a/erpnext/hr/doctype/vehicle_log/vehicle_log.py b/erpnext/hr/doctype/vehicle_log/vehicle_log.py index 0dbf1ceb96..dfdfc0d720 100644 --- a/erpnext/hr/doctype/vehicle_log/vehicle_log.py +++ b/erpnext/hr/doctype/vehicle_log/vehicle_log.py @@ -18,12 +18,17 @@ class VehicleLog(Document): if not (service_detail.service_item and service_detail.type and service_detail.frequency and service_detail.expense_amount): frappe.throw(_("Service Item,Type,frequency and expense amount are required")) + def before_save(self): + model_details = get_make_model(self.license_plate) + self.make = model_details[0] + self.model = model_details[1] + self.last_odometer = model_details[2] + self.employee = model_details[3] + def on_submit(self): - print("I am here") frappe.db.set_value("Vehicle", self.license_plate, "last_odometer", self.odometer) def on_cancel(self): - print("sel"*10, self.last_odometer, self.odometer) distance_travelled = self.odometer - self.last_odometer if(distance_travelled > 0): updated_odometer_value = int(frappe.db.get_value("Vehicle", self.license_plate, "last_odometer")) - distance_travelled @@ -32,7 +37,7 @@ class VehicleLog(Document): @frappe.whitelist() def get_make_model(license_plate): vehicle=frappe.get_doc("Vehicle",license_plate) - return (vehicle.make,vehicle.model,vehicle.last_odometer,vehicle.employee) + return (vehicle.make, vehicle.model, vehicle.last_odometer, vehicle.employee) @frappe.whitelist() def make_expense_claim(docname): From 94e14257fba5d759106999cd32895540e6d98ab9 Mon Sep 17 00:00:00 2001 From: Er-Naren719 <49683121+Er-Naren719@users.noreply.github.com> Date: Mon, 10 Feb 2020 13:46:47 +0530 Subject: [PATCH 020/102] feat: column width increased for view attachments field (#314) --- erpnext/projects/doctype/project_user/project_user.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/doctype/project_user/project_user.json b/erpnext/projects/doctype/project_user/project_user.json index f0a70dd1df..2f452cc2d7 100644 --- a/erpnext/projects/doctype/project_user/project_user.json +++ b/erpnext/projects/doctype/project_user/project_user.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2016-03-25 02:52:19.283003", "doctype": "DocType", "editable_grid": 1, @@ -46,6 +47,7 @@ "fetch_from": "user.full_name", "fieldname": "full_name", "fieldtype": "Read Only", + "in_list_view": 1, "label": "Full Name" }, { @@ -55,7 +57,7 @@ "label": "Welcome email sent" }, { - "columns": 1, + "columns": 2, "default": "0", "fieldname": "view_attachments", "fieldtype": "Check", @@ -74,7 +76,8 @@ } ], "istable": 1, - "modified": "2019-07-15 19:37:26.942294", + "links": [], + "modified": "2020-02-09 23:26:50.321417", "modified_by": "Administrator", "module": "Projects", "name": "Project User", From 0fac1f94292b3099e5ceb53b985f379a5aa2963f Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Wed, 12 Feb 2020 13:59:48 +0100 Subject: [PATCH 021/102] Add groups for account types "Bank" and "Cash" --- .../verified/de_kontenplan_SKR03_gnucash.json | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json index e676182255..89465eedf0 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json @@ -82,16 +82,25 @@ "IV. Kassenbestand, Bundesbankguthaben, Guthaben bei Kreditinstituten und Schecks.": { "is_group": 1, "Kasse": { - "account_number": "1000", - "account_type": "Cash" + "account_type": "Cash", + "is_group": 1, + "Kasse": { + "is_group": 1, + "account_number": "1000", + "account_type": "Cash" + } }, - "Postbank": { - "account_number": "1100", - "account_type": "Bank" - }, - "Bankkonto": { - "account_number": "1200", - "account_type": "Bank" + "Bank": { + "is_group": 1, + "account_type": "Bank", + "Postbank": { + "account_number": "1100", + "account_type": "Bank" + }, + "Bankkonto": { + "account_number": "1200", + "account_type": "Bank" + } } } }, From 4fb897728bbb3e19e277512b5fbe3f2f5c3b75e4 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Thu, 13 Feb 2020 17:02:01 +0100 Subject: [PATCH 022/102] EXTF must be in quotes --- erpnext/regional/report/datev/datev.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index bd70639ef2..0b0bb76f25 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -282,7 +282,7 @@ def get_header(filters, csv_class): # A = DATEV format # DTVF = created by DATEV software, # EXTF = created by other software - "EXTF", + '"EXTF"', # B = version of the DATEV format # 141 = 1.41, # 510 = 5.10, From 7d2777870ead35aba2e16d0f02183d4fe191f538 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Thu, 13 Feb 2020 17:05:37 +0100 Subject: [PATCH 023/102] bump version number --- erpnext/regional/report/datev/datev.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 0b0bb76f25..abbc56b5f5 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -287,7 +287,7 @@ def get_header(filters, csv_class): # 141 = 1.41, # 510 = 5.10, # 720 = 7.20 - "510", + "700", csv_class.DATA_CATEGORY, csv_class.FORMAT_NAME, # E = Format version (regarding format name) From 0070805bbbe939c6216d30d4eebbe3f0ed893fb4 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Thu, 13 Feb 2020 17:06:06 +0100 Subject: [PATCH 024/102] add format version (data type) --- erpnext/regional/report/datev/datev.py | 2 +- erpnext/regional/report/datev/datev_constants.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index abbc56b5f5..a3cc180004 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -291,7 +291,7 @@ def get_header(filters, csv_class): csv_class.DATA_CATEGORY, csv_class.FORMAT_NAME, # E = Format version (regarding format name) - "", + csv_class.FORMAT_VERSION, # F = Generated on datetime.datetime.now().strftime("%Y%m%d"), # G = Imported on -- stays empty diff --git a/erpnext/regional/report/datev/datev_constants.py b/erpnext/regional/report/datev/datev_constants.py index 1c9bd23ee1..1ca480c376 100644 --- a/erpnext/regional/report/datev/datev_constants.py +++ b/erpnext/regional/report/datev/datev_constants.py @@ -499,14 +499,17 @@ class FormatName(): class Transactions(): DATA_CATEGORY = DataCategory.TRANSACTIONS FORMAT_NAME = FormatName.TRANSACTIONS + FORMAT_VERSION = "9" COLUMNS = TRANSACTION_COLUMNS class DebtorsCreditors(): DATA_CATEGORY = DataCategory.DEBTORS_CREDITORS FORMAT_NAME = FormatName.DEBTORS_CREDITORS + FORMAT_VERSION = "5" COLUMNS = DEBTOR_CREDITOR_COLUMNS class AccountNames(): DATA_CATEGORY = DataCategory.ACCOUNT_NAMES FORMAT_NAME = FormatName.ACCOUNT_NAMES + FORMAT_VERSION = "2" COLUMNS = ACCOUNT_NAME_COLUMNS From 46cf20825b07af61f4f20704e0b7820d2b6fb64e Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Thu, 13 Feb 2020 17:09:22 +0100 Subject: [PATCH 025/102] generated on is datetime, not date --- erpnext/regional/report/datev/datev.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index a3cc180004..fe2add9bf4 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -293,7 +293,7 @@ def get_header(filters, csv_class): # E = Format version (regarding format name) csv_class.FORMAT_VERSION, # F = Generated on - datetime.datetime.now().strftime("%Y%m%d"), + datetime.datetime.now().strftime("%Y%m%d%H%M%S"), # G = Imported on -- stays empty "", # H = Origin (SV = other (?), RE = KARE) From 8818850174dc45a83bae1a18010b492e22b3b753 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Thu, 13 Feb 2020 17:13:15 +0100 Subject: [PATCH 026/102] consutant number and client number are mandatory --- erpnext/regional/report/datev/datev.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index fe2add9bf4..bf6ac20e7e 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -303,10 +303,10 @@ def get_header(filters, csv_class): # J = Imported by -- stays empty "", # K = Tax consultant number (Beraternummer) - frappe.get_value("DATEV Settings", filters.get("company"), "consultant_number") or "", + frappe.get_value("DATEV Settings", filters.get("company"), "consultant_number"), "", # L = Tax client number (Mandantennummer) - frappe.get_value("DATEV Settings", filters.get("company"), "client_number") or "", + frappe.get_value("DATEV Settings", filters.get("company"), "client_number"), "", # M = Start of the fiscal year (Wirtschaftsjahresbeginn) frappe.utils.formatdate(frappe.defaults.get_user_default("year_start_date"), "yyyyMMdd"), From b555ed0cba85b10d3713d0e5d2044510133b9d73 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Thu, 13 Feb 2020 20:05:27 +0100 Subject: [PATCH 027/102] update header accoding to "DATEV Format v7.0" --- erpnext/regional/report/datev/datev.py | 73 +++++++++++++++++--------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index bf6ac20e7e..05d817718b 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -274,66 +274,89 @@ def get_datev_csv(data, filters, csv_class): if not six.PY2: data = data.encode('latin_1') - return header + b'\r\n' + data + # 1st Row: Header with meta data + # 2nd Row: Data heading (Überschrift der Nutzdaten) + # 3rd Row: – n: Data (Nutzdaten) + return header + b'\r\n\r\n' + data def get_header(filters, csv_class): + coa = frappe.get_value("Company", filters.get("company"), "chart_of_accounts") + coa_used = "SKR04" if "SKR04" in coa else ("SKR03" if "SKR03" in coa else "") + header = [ - # A = DATEV format - # DTVF = created by DATEV software, - # EXTF = created by other software + # DATEV format + # "DTVF" = created by DATEV software, + # "EXTF" = created by other software '"EXTF"', - # B = version of the DATEV format + # version of the DATEV format # 141 = 1.41, # 510 = 5.10, # 720 = 7.20 - "700", + '700', csv_class.DATA_CATEGORY, csv_class.FORMAT_NAME, - # E = Format version (regarding format name) + # Format version (regarding format name) csv_class.FORMAT_VERSION, - # F = Generated on + # Generated on datetime.datetime.now().strftime("%Y%m%d%H%M%S"), - # G = Imported on -- stays empty - "", - # H = Origin (SV = other (?), RE = KARE) - "SV", + # Imported on -- stays empty + '', + # Origin. Any two symbols, will be replaced by "SV" on import. + '"EN"', # I = Exported by - frappe.session.user, + '"%s"' % frappe.session.user, # J = Imported by -- stays empty - "", + '', # K = Tax consultant number (Beraternummer) frappe.get_value("DATEV Settings", filters.get("company"), "consultant_number"), - "", # L = Tax client number (Mandantennummer) frappe.get_value("DATEV Settings", filters.get("company"), "client_number"), - "", # M = Start of the fiscal year (Wirtschaftsjahresbeginn) frappe.utils.formatdate(frappe.defaults.get_user_default("year_start_date"), "yyyyMMdd"), # N = Length of account numbers (Sachkontenlänge) - "4", + # minimum of 4, 5 if debtors/creditors are included + '5', # O = Transaction batch start date (YYYYMMDD) frappe.utils.formatdate(filters.get('from_date'), "yyyyMMdd"), # P = Transaction batch end date (YYYYMMDD) frappe.utils.formatdate(filters.get('to_date'), "yyyyMMdd"), # Q = Description (for example, "January - February 2019 Transactions") - "{} - {} {}".format( + '"{} - {} {}"'.format( frappe.utils.formatdate(filters.get('from_date'), "MMMM yyyy"), frappe.utils.formatdate(filters.get('to_date'), "MMMM yyyy"), csv_class.FORMAT_NAME ), # R = Diktatkürzel - "", + '', # S = Buchungstyp - # 1 = Transaction batch (Buchungsstapel), + # 1 = Transaction batch (Finanzbuchführung), # 2 = Annual financial statement (Jahresabschluss) - "1" if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "", + '1' if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', # T = Rechnungslegungszweck - "", + '', # U = Festschreibung - "", - # V = Kontoführungs-Währungskennzeichen des Geldkontos - frappe.get_value("Company", filters.get("company"), "default_currency") + '', + # V = Default currency, for example, "EUR" + '"%s"' % frappe.get_value("Company", filters.get("company"), "default_currency"), + # reserviert + '', + # Derivatskennzeichen + '', + # reserviert + '', + # reserviert + '', + # SKR + '"%s"' % coa_used, + # Branchen-Lösungs-ID + '', + # reserviert + '', + # reserviert + '', + # Anwendungsinformation (Verarbeitungskennzeichen der abgebenden Anwendung) + '' ] return header From 96b66dfae6e4bcca6de5efbac07da54d2fe62a75 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Thu, 13 Feb 2020 20:37:37 +0100 Subject: [PATCH 028/102] update TRANSACTION_COLUMNS according to "DATEV Format v7.0" --- erpnext/regional/report/datev/datev.py | 3 +- .../regional/report/datev/datev_constants.py | 93 +++++++++++++------ 2 files changed, 64 insertions(+), 32 deletions(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 05d817718b..ddc973ddae 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -315,8 +315,7 @@ def get_header(filters, csv_class): # M = Start of the fiscal year (Wirtschaftsjahresbeginn) frappe.utils.formatdate(frappe.defaults.get_user_default("year_start_date"), "yyyyMMdd"), # N = Length of account numbers (Sachkontenlänge) - # minimum of 4, 5 if debtors/creditors are included - '5', + '4', # O = Transaction batch start date (YYYYMMDD) frappe.utils.formatdate(filters.get('from_date'), "yyyyMMdd"), # P = Transaction batch end date (YYYYMMDD) diff --git a/erpnext/regional/report/datev/datev_constants.py b/erpnext/regional/report/datev/datev_constants.py index 1ca480c376..501f2cea17 100644 --- a/erpnext/regional/report/datev/datev_constants.py +++ b/erpnext/regional/report/datev/datev_constants.py @@ -13,24 +13,27 @@ TRANSACTION_COLUMNS = [ "Basis-Umsatz", "WKZ Basis-Umsatz", # Konto/Gegenkonto - "Kontonummer", + "Konto", "Gegenkonto (ohne BU-Schlüssel)", "BU-Schlüssel", # Datum "Belegdatum", - # Belegfelder + # Rechnungs- / Belegnummer "Belegfeld 1", + # z.B. Fälligkeitsdatum Format: TTMMJJ "Belegfeld 2", - # Weitere Felder + # Skonto-Betrag / -Abzug (Der Wert 0 ist unzulässig) "Skonto", + # Beschreibung des Buchungssatzes "Buchungstext", - # OPOS-Informationen + # Mahn- / Zahl-Sperre (1 = Postensperre) "Postensperre", "Diverse Adressnummer", "Geschäftspartnerbank", "Sachverhalt", + # Keine Mahnzinsen "Zinssperre", - # Digitaler Beleg + # Link auf den Buchungsbeleg (Programmkürzel + GUID) "Beleglink", # Beleginfo "Beleginfo - Art 1", @@ -49,22 +52,30 @@ TRANSACTION_COLUMNS = [ "Beleginfo - Inhalt 7", "Beleginfo - Art 8", "Beleginfo - Inhalt 8", - # Kostenrechnung - "Kost 1 - Kostenstelle", - "Kost 2 - Kostenstelle", - "Kost-Menge", - # Steuerrechnung - "EU-Land u. UStID", + # Zuordnung des Geschäftsvorfalls für die Kostenrechnung + "KOST1 - Kostenstelle", + "KOST2 - Kostenstelle", + "KOST-Menge", + # USt-ID-Nummer (Beispiel: DE133546770) + "EU-Mitgliedstaat u. USt-IdNr.", + # Der im EU-Bestimmungsland gültige Steuersatz "EU-Steuersatz", + # I = Ist-Versteuerung, + # K = keine Umsatzsteuerrechnung + # P = Pauschalierung (z. B. für Land- und Forstwirtschaft), + # S = Soll-Versteuerung "Abw. Versteuerungsart", - # L+L Sachverhalt + # Sachverhalte gem. § 13b Abs. 1 Satz 1 Nrn. 1.-5. UStG "Sachverhalt L+L", + # Steuersatz / Funktion zum L+L-Sachverhalt (Beispiel: Wert 190 für 19%) "Funktionsergänzung L+L", - # Funktion Steuerschlüssel 49 + # Bei Verwendung des BU-Schlüssels 49 für „andere Steuersätze“ muss der + # steuerliche Sachverhalt mitgegeben werden "BU 49 Hauptfunktionstyp", "BU 49 Hauptfunktionsnummer", "BU 49 Funktionsergänzung", - # Zusatzinformationen + # Zusatzinformationen, besitzen den Charakter eines Notizzettels und können + # frei erfasst werden. "Zusatzinformation - Art 1", "Zusatzinformation - Inhalt 1", "Zusatzinformation - Art 2", @@ -105,54 +116,76 @@ TRANSACTION_COLUMNS = [ "Zusatzinformation - Inhalt 19", "Zusatzinformation - Art 20", "Zusatzinformation - Inhalt 20", - # Mengenfelder LuF + # Wirkt sich nur bei Sachverhalt mit SKR 14 Land- und Forstwirtschaft aus, + # für andere SKR werden die Felder beim Import / Export überlesen bzw. + # leer exportiert. "Stück", "Gewicht", - # Forderungsart + # 1 = Lastschrift + # 2 = Mahnung + # 3 = Zahlung "Zahlweise", "Forderungsart", + # JJJJ "Veranlagungsjahr", + # TTMMJJJJ "Zugeordnete Fälligkeit", - # Weitere Felder + # 1 = Einkauf von Waren + # 2 = Erwerb von Roh-Hilfs- und Betriebsstoffen "Skontotyp", - # Anzahlungen + # Allgemeine Bezeichnung, des Auftrags / Projekts. "Auftragsnummer", + # AA = Angeforderte Anzahlung / Abschlagsrechnung + # AG = Erhaltene Anzahlung (Geldeingang) + # AV = Erhaltene Anzahlung (Verbindlichkeit) + # SR = Schlussrechnung + # SU = Schlussrechnung (Umbuchung) + # SG = Schlussrechnung (Geldeingang) + # SO = Sonstige "Buchungstyp", "USt-Schlüssel (Anzahlungen)", - "EU-Land (Anzahlungen)", + "EU-Mitgliedstaat (Anzahlungen)", "Sachverhalt L+L (Anzahlungen)", "EU-Steuersatz (Anzahlungen)", "Erlöskonto (Anzahlungen)", - # Stapelinformationen + # Wird beim Import durch SV (Stapelverarbeitung) ersetzt. "Herkunft-Kz", - # Technische Identifikation - "Buchungs GUID", - # Kostenrechnung - "Kost-Datum", - # OPOS-Informationen + # Wird von DATEV verwendet. + "Leerfeld", + # Format TTMMJJJJ + "KOST-Datum", + # Vom Zahlungsempfänger individuell vergebenes Kennzeichen eines Mandats + # (z.B. Rechnungs- oder Kundennummer). "SEPA-Mandatsreferenz", + # 1 = Skontosperre + # 0 = Keine Skontosperre "Skontosperre", # Gesellschafter und Sonderbilanzsachverhalt "Gesellschaftername", + # Amtliche Nummer aus der Feststellungserklärung "Beteiligtennummer", "Identifikationsnummer", "Zeichnernummer", - # OPOS-Informationen + # Format TTMMJJJJ "Postensperre bis", # Gesellschafter und Sonderbilanzsachverhalt "Bezeichnung SoBil-Sachverhalt", "Kennzeichen SoBil-Buchung", - # Stapelinformationen + # 0 = keine Festschreibung + # 1 = Festschreibung "Festschreibung", - # Datum + # Format TTMMJJJJ "Leistungsdatum", + # Format TTMMJJJJ "Datum Zuord. Steuerperiode", - # OPOS-Informationen + # OPOS-Informationen, Format TTMMJJJJ "Fälligkeit", - # Konto/Gegenkonto + # G oder 1 = Generalumkehr + # 0 = keine Generalumkehr "Generalumkehr (GU)", # Steuersatz für Steuerschlüssel "Steuersatz", + # Beispiel: DE für Deutschland "Land" ] From 3bacdf1f4b3436697d90c11a71914cfcf7122d71 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Thu, 13 Feb 2020 20:40:15 +0100 Subject: [PATCH 029/102] quote format name --- erpnext/regional/report/datev/datev.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index ddc973ddae..a2ff442af4 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -295,7 +295,7 @@ def get_header(filters, csv_class): # 720 = 7.20 '700', csv_class.DATA_CATEGORY, - csv_class.FORMAT_NAME, + '"%s"' % csv_class.FORMAT_NAME, # Format version (regarding format name) csv_class.FORMAT_VERSION, # Generated on From 54717fa993a2490cba50672d9fdaadb0962de3df Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Thu, 13 Feb 2020 20:58:44 +0100 Subject: [PATCH 030/102] update cloumn names --- erpnext/regional/report/datev/datev.py | 9 ++++---- .../regional/report/datev/datev_constants.py | 21 +++++++------------ 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index a2ff442af4..d4f480196c 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -72,17 +72,16 @@ def get_transactions(filters, as_dict=1): case gl.debit when 0 then 'H' else 'S' end as 'Soll/Haben-Kennzeichen', /* account number or, if empty, party account number */ - coalesce(acc.account_number, acc_pa.account_number) as 'Kontonummer', + coalesce(acc.account_number, acc_pa.account_number) as 'Konto', /* against number or, if empty, party against number */ coalesce(acc_against.account_number, acc_against_pa.account_number) as 'Gegenkonto (ohne BU-Schlüssel)', gl.posting_date as 'Belegdatum', + gl.voucher_no as 'Belegfeld 1', gl.remarks as 'Buchungstext', - gl.voucher_type as 'Beleginfo - Art 1', - gl.voucher_no as 'Beleginfo - Inhalt 1', - gl.against_voucher_type as 'Beleginfo - Art 2', - gl.against_voucher as 'Beleginfo - Inhalt 2' + gl.against_voucher_type as 'Beleginfo - Art 1', + gl.against_voucher as 'Beleginfo - Inhalt 1' FROM `tabGL Entry` gl diff --git a/erpnext/regional/report/datev/datev_constants.py b/erpnext/regional/report/datev/datev_constants.py index 501f2cea17..a4cd5fc10e 100644 --- a/erpnext/regional/report/datev/datev_constants.py +++ b/erpnext/regional/report/datev/datev_constants.py @@ -472,8 +472,8 @@ QUERY_REPORT_COLUMNS = [ "fieldtype": "Data", }, { - "label": "Kontonummer", - "fieldname": "Kontonummer", + "label": "Konto", + "fieldname": "Konto", "fieldtype": "Data", }, { @@ -486,6 +486,11 @@ QUERY_REPORT_COLUMNS = [ "fieldname": "Belegdatum", "fieldtype": "Date", }, + { + "label": "Belegfeld 1", + "fieldname": "Belegfeld 1", + "fieldtype": "Data", + }, { "label": "Buchungstext", "fieldname": "Buchungstext", @@ -493,21 +498,11 @@ QUERY_REPORT_COLUMNS = [ }, { "label": "Beleginfo - Art 1", - "fieldname": "Beleginfo - Art 1", - "fieldtype": "Data", - }, - { - "label": "Beleginfo - Inhalt 1", - "fieldname": "Beleginfo - Inhalt 1", - "fieldtype": "Data", - }, - { - "label": "Beleginfo - Art 2", "fieldname": "Beleginfo - Art 2", "fieldtype": "Data", }, { - "label": "Beleginfo - Inhalt 2", + "label": "Beleginfo - Inhalt 1", "fieldname": "Beleginfo - Inhalt 2", "fieldtype": "Data", } From 772394b95aef049852361ab1b62712c40a1a576a Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Thu, 13 Feb 2020 20:58:59 +0100 Subject: [PATCH 031/102] fix header --- erpnext/regional/report/datev/datev.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index d4f480196c..7ceaf50134 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -239,8 +239,6 @@ def get_datev_csv(data, filters, csv_class): filters -- dict csv_class -- defines DATA_CATEGORY, FORMAT_NAME and COLUMNS """ - header = get_header(filters, csv_class) - empty_df = pd.DataFrame(columns=csv_class.COLUMNS) data_df = pd.DataFrame.from_records(data) @@ -252,7 +250,6 @@ def get_datev_csv(data, filters, csv_class): if csv_class.DATA_CATEGORY == DataCategory.ACCOUNT_NAMES: result['Sprach-ID'] = 'de-DE' - header = ';'.join(header).encode('latin_1') data = result.to_csv( # Reason for str(';'): https://github.com/pandas-dev/pandas/issues/6035 sep=str(';'), @@ -273,10 +270,13 @@ def get_datev_csv(data, filters, csv_class): if not six.PY2: data = data.encode('latin_1') + header = get_header(filters, csv_class) + header = ';'.join(header).encode('latin_1') + # 1st Row: Header with meta data - # 2nd Row: Data heading (Überschrift der Nutzdaten) - # 3rd Row: – n: Data (Nutzdaten) - return header + b'\r\n\r\n' + data + # 2nd Row: Data heading (Überschrift der Nutzdaten), included in `data` here. + # 3rd - nth Row: Data (Nutzdaten) + return header + b'\r\n' + data def get_header(filters, csv_class): From ad645afeb2b20c11222f5b0b55ac15c088ade718 Mon Sep 17 00:00:00 2001 From: Yugandhara Date: Sat, 15 Feb 2020 10:56:41 +0530 Subject: [PATCH 032/102] fix: Changes done to get valid customer and employee list on payment entry form --- .../accounts/doctype/payment_entry/payment_entry.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 2192b7bf98..968fb60571 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -253,6 +253,19 @@ frappe.ui.form.on('Payment Entry', { frappe.throw(__("Party can only be one of "+ party_types.join(", "))); } + frm.set_query("party", function() { + if(frm.doc.party_type == 'Employee'){ + return { + query: "erpnext.controllers.queries.employee_query" + } + } + else if(frm.doc.party_type == 'Customer'){ + return { + query: "erpnext.controllers.queries.customer_query" + } + } + }); + if(frm.doc.party) { $.each(["party", "party_balance", "paid_from", "paid_to", "paid_from_account_currency", "paid_from_account_balance", From 21d662f96ddb382f4582e34aa90f8da7b012f09b Mon Sep 17 00:00:00 2001 From: Anuradha Kalaskar Date: Sat, 15 Feb 2020 13:17:57 +0530 Subject: [PATCH 033/102] fix: to date cannot be less than from date validation on student leave application --- .../student_leave_application.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/education/doctype/student_leave_application/student_leave_application.py b/erpnext/education/doctype/student_leave_application/student_leave_application.py index b3e71a2b08..410f0cca3f 100644 --- a/erpnext/education/doctype/student_leave_application/student_leave_application.py +++ b/erpnext/education/doctype/student_leave_application/student_leave_application.py @@ -7,9 +7,11 @@ import frappe from frappe import _ from frappe.utils import get_link_to_form from frappe.model.document import Document +from frappe import throw, _ class StudentLeaveApplication(Document): def validate(self): + self.validate_dates() self.validate_duplicate() def validate_duplicate(self): @@ -29,4 +31,8 @@ class StudentLeaveApplication(Document): if data: link = get_link_to_form("Student Leave Application", data[0].name) frappe.throw(_("Leave application {0} already exists against the student {1}") - .format(link, self.student)) \ No newline at end of file + .format(link, self.student)) + + def validate_dates(self): + if self.to_date < self.from_date : + throw(_("To Date cannot be less than From Date")) \ No newline at end of file From c224cf761f7e4bb0b47e4696fcbaddfba1c0303f Mon Sep 17 00:00:00 2001 From: "pratik.m" Date: Sat, 15 Feb 2020 13:22:17 +0530 Subject: [PATCH 034/102] fix: restricted duplicate topics on Course doctype (#20594) --- erpnext/education/doctype/course/course.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/erpnext/education/doctype/course/course.js b/erpnext/education/doctype/course/course.js index e31ba72686..69329896e0 100644 --- a/erpnext/education/doctype/course/course.js +++ b/erpnext/education/doctype/course/course.js @@ -6,21 +6,21 @@ frappe.ui.form.on("Course", "refresh", function(frm) { } frappe.set_route("List", "Program"); }); - + frm.add_custom_button(__("Student Group"), function() { frappe.route_options = { course: frm.doc.name } frappe.set_route("List", "Student Group"); }); - + frm.add_custom_button(__("Course Schedule"), function() { frappe.route_options = { course: frm.doc.name } frappe.set_route("List", "Course Schedule"); }); - + frm.add_custom_button(__("Assessment Plan"), function() { frappe.route_options = { course: frm.doc.name @@ -36,4 +36,17 @@ frappe.ui.form.on("Course", "refresh", function(frm) { } } }); -}); \ No newline at end of file +}); + +frappe.ui.form.on('Course Topic', { + topics_add: function(frm){ + frm.fields_dict['topics'].grid.get_field('topic').get_query = function(doc){ + var topics_list = []; + if(!doc.__islocal) topics_list.push(doc.name); + $.each(doc.topics, function(idx, val){ + if (val.topic) topics_list.push(val.topic); + }); + return { filters: [['Topic', 'name', 'not in', topics_list]] }; + }; + } +}); From 21b1a00f613dc96cffc2bf5abd7d4e7349070645 Mon Sep 17 00:00:00 2001 From: anil pise Date: Sat, 15 Feb 2020 15:23:15 +0530 Subject: [PATCH 035/102] Fix:Child account which is not a group account is treating as Parent Account while creating New Department. #20599 --- erpnext/hr/doctype/department/department.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/hr/doctype/department/department.js b/erpnext/hr/doctype/department/department.js index 963f3615cc..4185446d12 100644 --- a/erpnext/hr/doctype/department/department.js +++ b/erpnext/hr/doctype/department/department.js @@ -2,12 +2,18 @@ // For license information, please see license.txt frappe.ui.form.on('Department', { + onload: function(frm) { + frm.set_query("parent_department", function(){ + return { "filters": [["Department", "is_group", "=", 1]]} + }); + }, refresh: function(frm) { // read-only for root department if(!frm.doc.parent_department && !frm.is_new()) { frm.set_read_only(); frm.set_intro(__("This is a root department and cannot be edited.")); } + }, validate: function(frm) { if(frm.doc.name=="All Departments") { From dc39b4ba130024f7c246e2110bd9b93a29ca8360 Mon Sep 17 00:00:00 2001 From: Khushal Trivedi Date: Sat, 15 Feb 2020 15:38:16 +0530 Subject: [PATCH 036/102] fix: validation on max group strength student group form --- erpnext/education/doctype/student_group/student_group.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/education/doctype/student_group/student_group.py b/erpnext/education/doctype/student_group/student_group.py index aa542ddfbf..9ec94384d5 100644 --- a/erpnext/education/doctype/student_group/student_group.py +++ b/erpnext/education/doctype/student_group/student_group.py @@ -26,6 +26,8 @@ class StudentGroup(Document): frappe.throw(_("Please select Program")) def validate_strength(self): + if self.max_strength < 0: + frappe.throw(_("""Cannot enroll less than 0 students for this student group.""")) if self.max_strength and len(self.students) > self.max_strength: frappe.throw(_("""Cannot enroll more than {0} students for this student group.""").format(self.max_strength)) From 7c474130ea98789227e02537c65cc5b397fcd47d Mon Sep 17 00:00:00 2001 From: anil pise Date: Sat, 15 Feb 2020 15:58:57 +0530 Subject: [PATCH 037/102] fix: Removed Missing semicolon and Mixed spaces and tabs and Trailing spaces not allowed. --- erpnext/hr/doctype/department/department.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/department/department.js b/erpnext/hr/doctype/department/department.js index 4185446d12..7db8cfbd60 100644 --- a/erpnext/hr/doctype/department/department.js +++ b/erpnext/hr/doctype/department/department.js @@ -4,8 +4,8 @@ frappe.ui.form.on('Department', { onload: function(frm) { frm.set_query("parent_department", function(){ - return { "filters": [["Department", "is_group", "=", 1]]} - }); + return {"filters": [["Department", "is_group", "=", 1]]}; + }); }, refresh: function(frm) { // read-only for root department @@ -13,7 +13,6 @@ frappe.ui.form.on('Department', { frm.set_read_only(); frm.set_intro(__("This is a root department and cannot be edited.")); } - }, validate: function(frm) { if(frm.doc.name=="All Departments") { From af2b39891e097bf1db55e549410c331377c92595 Mon Sep 17 00:00:00 2001 From: Yugandhara Date: Sat, 15 Feb 2020 16:01:41 +0530 Subject: [PATCH 038/102] fix: Changes done to get valid sales order list on sales order child table of production plan --- .../doctype/production_plan/production_plan.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 2b168d1d76..4e92ee2d0a 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -296,3 +296,11 @@ frappe.ui.form.on("Material Request Plan Item", { } } }); + +cur_frm.fields_dict['sales_orders'].grid.get_field("sales_order").get_query = function(doc, cdt, cdn) { + return{ + filters: [ + ['Sales Order','docstatus', '=' ,1] + ] + } +}; \ No newline at end of file From e997168a2364f9350441d1aac7e03c293556c77f Mon Sep 17 00:00:00 2001 From: Yugandhara Date: Sat, 15 Feb 2020 16:14:46 +0530 Subject: [PATCH 039/102] fix: unused variable cdn removed --- .../manufacturing/doctype/production_plan/production_plan.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 4e92ee2d0a..d400d59a13 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -297,7 +297,7 @@ frappe.ui.form.on("Material Request Plan Item", { } }); -cur_frm.fields_dict['sales_orders'].grid.get_field("sales_order").get_query = function(doc, cdt, cdn) { +cur_frm.fields_dict['sales_orders'].grid.get_field("sales_order").get_query = function(doc) { return{ filters: [ ['Sales Order','docstatus', '=' ,1] From 3a6ca13b4479e313f5c473444eb34308c9236e01 Mon Sep 17 00:00:00 2001 From: jsukrut Date: Sat, 15 Feb 2020 16:18:17 +0530 Subject: [PATCH 040/102] fix:Mandatory field property failed to work based on the selection of field 'Group Based on' on 'Student Group' Page #20605 --- .../education/doctype/student_group/student_group.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/education/doctype/student_group/student_group.js b/erpnext/education/doctype/student_group/student_group.js index 4165ce0f2e..d7822487ec 100644 --- a/erpnext/education/doctype/student_group/student_group.js +++ b/erpnext/education/doctype/student_group/student_group.js @@ -70,6 +70,16 @@ frappe.ui.form.on("Student Group", { group_based_on: function(frm) { if (frm.doc.group_based_on == "Batch") { frm.doc.course = null; + frm.set_df_property('program', 'reqd', 1); + frm.set_df_property('course', 'reqd', 0); + } + else if (frm.doc.group_based_on == "Course") { + frm.set_df_property('program', 'reqd', 0); + frm.set_df_property('course', 'reqd', 1); + } + else if (frm.doc.group_based_on == "Activity") { + frm.set_df_property('program', 'reqd', 0); + frm.set_df_property('course', 'reqd', 0); } }, From ce1f6a73e22588b7e274af5d4d302f7d64deb59c Mon Sep 17 00:00:00 2001 From: Yugandhara Date: Sat, 15 Feb 2020 16:31:53 +0530 Subject: [PATCH 041/102] fix: Changes done to get valid sales order list on production plan form(#20609) --- .../manufacturing/doctype/production_plan/production_plan.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index d400d59a13..96e5cd57c3 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -297,7 +297,7 @@ frappe.ui.form.on("Material Request Plan Item", { } }); -cur_frm.fields_dict['sales_orders'].grid.get_field("sales_order").get_query = function(doc) { +cur_frm.fields_dict['sales_orders'].grid.get_field("sales_order").get_query = function() { return{ filters: [ ['Sales Order','docstatus', '=' ,1] From c3a23ddbe3111d2f68a6876fb755e90ddb0e1a39 Mon Sep 17 00:00:00 2001 From: jsukrut Date: Sat, 15 Feb 2020 16:39:51 +0530 Subject: [PATCH 042/102] fix: restricted duplicate courses on Program Enrollment doctype (#20611) --- .../program_enrollment/program_enrollment.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment.js b/erpnext/education/doctype/program_enrollment/program_enrollment.js index d35f41a54b..e3b3e9fdd6 100644 --- a/erpnext/education/doctype/program_enrollment/program_enrollment.js +++ b/erpnext/education/doctype/program_enrollment/program_enrollment.js @@ -81,3 +81,16 @@ frappe.ui.form.on("Program Enrollment", { }) } }); + +frappe.ui.form.on('Program Enrollment Course', { + courses_add: function(frm){ + frm.fields_dict['courses'].grid.get_field('course').get_query = function(doc){ + var course_list = []; + if(!doc.__islocal) course_list.push(doc.name); + $.each(doc.courses, function(idx, val){ + if (val.course) course_list.push(val.course); + }); + return { filters: [['Course', 'name', 'not in', course_list]] }; + }; + } +}); \ No newline at end of file From c0fd30a7ec63cba49efd5fb282f991a78c8b82ec Mon Sep 17 00:00:00 2001 From: jsukrut Date: Sat, 15 Feb 2020 17:33:52 +0530 Subject: [PATCH 043/102] fix: Indentation --- erpnext/education/doctype/student_group/student_group.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/education/doctype/student_group/student_group.js b/erpnext/education/doctype/student_group/student_group.js index d7822487ec..13724409ba 100644 --- a/erpnext/education/doctype/student_group/student_group.js +++ b/erpnext/education/doctype/student_group/student_group.js @@ -74,8 +74,8 @@ frappe.ui.form.on("Student Group", { frm.set_df_property('course', 'reqd', 0); } else if (frm.doc.group_based_on == "Course") { - frm.set_df_property('program', 'reqd', 0); - frm.set_df_property('course', 'reqd', 1); + frm.set_df_property('program', 'reqd', 0); + frm.set_df_property('course', 'reqd', 1); } else if (frm.doc.group_based_on == "Activity") { frm.set_df_property('program', 'reqd', 0); From 1118ee07a8f56337cce2dd2ca892f204a54b8028 Mon Sep 17 00:00:00 2001 From: indictrans Date: Sat, 15 Feb 2020 17:37:39 +0530 Subject: [PATCH 044/102] fix:Future date of birth accepted at Sibling Detail section in Student form #20619 --- erpnext/education/doctype/student/student.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/education/doctype/student/student.py b/erpnext/education/doctype/student/student.py index 99c4c0e908..d7d35ca122 100644 --- a/erpnext/education/doctype/student/student.py +++ b/erpnext/education/doctype/student/student.py @@ -22,6 +22,10 @@ class Student(Document): self.update_student_name_in_linked_doctype() def validate_dates(self): + for sibling in self.siblings: + if sibling.date_of_birth and getdate(sibling.date_of_birth) >= getdate(today()): + frappe.throw(_("Sibling Date of Birth cannot be greater than today at row #{0} ").format(sibling.idx)) + if self.date_of_birth and getdate(self.date_of_birth) >= getdate(today()): frappe.throw(_("Date of Birth cannot be greater than today.")) From ad5da823e12e6f3cfd2a67e33a16343622a7d327 Mon Sep 17 00:00:00 2001 From: indictrans Date: Sun, 16 Feb 2020 22:59:41 +0530 Subject: [PATCH 045/102] fix:Future date of birth accepted at Sibling Detail section in Student form #20619 --- erpnext/education/doctype/student/student.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/education/doctype/student/student.py b/erpnext/education/doctype/student/student.py index d7d35ca122..1a870a3cbe 100644 --- a/erpnext/education/doctype/student/student.py +++ b/erpnext/education/doctype/student/student.py @@ -23,8 +23,8 @@ class Student(Document): def validate_dates(self): for sibling in self.siblings: - if sibling.date_of_birth and getdate(sibling.date_of_birth) >= getdate(today()): - frappe.throw(_("Sibling Date of Birth cannot be greater than today at row #{0} ").format(sibling.idx)) + if sibling.date_of_birth and getdate(sibling.date_of_birth) >= getdate(): + frappe.throw(_("Row {0}:Sibling Date of Birth cannot be greater than today").format(sibling.idx)) if self.date_of_birth and getdate(self.date_of_birth) >= getdate(today()): frappe.throw(_("Date of Birth cannot be greater than today.")) From b3712158aef85ae298e3af5e0c54f50f02190ca5 Mon Sep 17 00:00:00 2001 From: P-Froggy <60393001+P-Froggy@users.noreply.github.com> Date: Sun, 16 Feb 2020 19:10:53 +0100 Subject: [PATCH 046/102] Bank accounts reference in supplier dashboard Adds a reference to linked bank accounts in supplier dashboard, which was previously missing. --- erpnext/buying/doctype/supplier/supplier_dashboard.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/supplier/supplier_dashboard.py b/erpnext/buying/doctype/supplier/supplier_dashboard.py index 887a093736..b3b294d570 100644 --- a/erpnext/buying/doctype/supplier/supplier_dashboard.py +++ b/erpnext/buying/doctype/supplier/supplier_dashboard.py @@ -9,7 +9,8 @@ def get_data(): 'heatmap_message': _('This is based on transactions against this Supplier. See timeline below for details'), 'fieldname': 'supplier', 'non_standard_fieldnames': { - 'Payment Entry': 'party_name' + 'Payment Entry': 'party_name', + 'Bank Account': 'party' }, 'transactions': [ { @@ -24,6 +25,10 @@ def get_data(): 'label': _('Payments'), 'items': ['Payment Entry'] }, + { + 'label': _('Bank'), + 'items': ['Bank Account'] + }, { 'label': _('Pricing'), 'items': ['Pricing Rule'] From c14f49c079a63caf4f781a6b206754fa53c64d17 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 17 Feb 2020 10:50:05 +0530 Subject: [PATCH 047/102] fix: filters for quality_procedure tree --- .../doctype/quality_procedure/quality_procedure_tree.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure_tree.js b/erpnext/quality_management/doctype/quality_procedure/quality_procedure_tree.js index dbdbbab392..6df6f656aa 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure_tree.js +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure_tree.js @@ -4,7 +4,7 @@ frappe.treeview_settings["Quality Procedure"] = { add_tree_node: 'erpnext.quality_management.doctype.quality_procedure.quality_procedure.add_node', filters: [ { - fieldname: "quality_procedure", + fieldname: "parent_quality_procedure", fieldtype: "Link", options: "Quality Procedure", label: __("Quality Procedure"), From 7a46aae57a613e65cba3d7ba8a6dc5ce65cfd9f0 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 17 Feb 2020 12:20:25 +0530 Subject: [PATCH 048/102] fix: updated package.json * fixed devDependencies typo which possibly caused https://discuss.erpnext.com/t/bin-sh-1-snyk-not-found-when-bench-update/58035 which was introduced in https://github.com/frappe/erpnext/pull/20563 * Added missing fields for package meta data --- package.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 13fcc0f98d..1b2dc9efcf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,21 @@ { - "devdependencies": { + "name": "erpnext", + "description": "Open Source ERP System powered by the Frappe Framework", + "repository": { + "type": "git", + "url": "git+https://github.com/frappe/erpnext.git" + }, + "homepage": "https://erpnext.com", + "author": "Frappe Technologies Pvt. Ltd.", + "license": "GPL-3.0", + "bugs": { + "url": "https://github.com/frappe/erpnext/issues" + }, + "devDependencies": { "snyk": "^1.290.1" }, + "dependencies": { + }, "scripts": { "snyk-protect": "snyk protect", "prepare": "yarn run snyk-protect" From 23e728612f3175e8ebb4d9ae5be3d646bd080ad8 Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Mon, 17 Feb 2020 14:15:20 +0530 Subject: [PATCH 049/102] fix: disabled mode of payments fetches in sales invoices --- .../doctype/mode_of_payment/mode_of_payment.py | 13 +++++++++++++ erpnext/accounts/doctype/sales_invoice/pos.py | 7 +++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py index fcf94ce98a..d54a47e3c9 100644 --- a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py +++ b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py @@ -11,6 +11,7 @@ class ModeofPayment(Document): def validate(self): self.validate_accounts() self.validate_repeating_companies() + self.validate_pos_mode_of_payment() def validate_repeating_companies(self): """Error when Same Company is entered multiple times in accounts""" @@ -27,3 +28,15 @@ class ModeofPayment(Document): if frappe.db.get_value("Account", entry.default_account, "company") != entry.company: frappe.throw(_("Account {0} does not match with Company {1} in Mode of Account: {2}") .format(entry.default_account, entry.company, self.name)) + + def validate_pos_mode_of_payment(self): + if not self.enabled: + pos_profiles = frappe.db.sql("""SELECT sip.parent FROM `tabSales Invoice Payment` sip + WHERE sip.parenttype = 'POS Profile' and sip.mode_of_payment = %s""", (self.name)) + pos_profiles = list(map(lambda x: x[0], pos_profiles)) + + if pos_profiles: + message = "POS Profile " + frappe.bold(", ".join(pos_profiles)) + " contains \ + Mode of Payment " + frappe.bold(str(self.name)) + ". Please remove them to disable this mode." + frappe.throw(_(message), title="Not Allowed") + diff --git a/erpnext/accounts/doctype/sales_invoice/pos.py b/erpnext/accounts/doctype/sales_invoice/pos.py index a48d224489..749816f781 100755 --- a/erpnext/accounts/doctype/sales_invoice/pos.py +++ b/erpnext/accounts/doctype/sales_invoice/pos.py @@ -152,8 +152,11 @@ def update_multi_mode_option(doc, pos_profile): def get_mode_of_payment(doc): - return frappe.db.sql(""" select mpa.default_account, mpa.parent, mp.type as type from `tabMode of Payment Account` mpa, \ - `tabMode of Payment` mp where mpa.parent = mp.name and mpa.company = %(company)s""", {'company': doc.company}, as_dict=1) + return frappe.db.sql(""" + select mpa.default_account, mpa.parent, mp.type as type + from `tabMode of Payment Account` mpa,`tabMode of Payment` mp + where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""", + {'company': doc.company}, as_dict=1) def update_tax_table(doc): From 68fa65edd171f9ec44d0cae5c030ded70f07a492 Mon Sep 17 00:00:00 2001 From: indictrans Date: Mon, 17 Feb 2020 14:55:56 +0530 Subject: [PATCH 050/102] fix: Throw message --- erpnext/education/doctype/student/student.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/education/doctype/student/student.py b/erpnext/education/doctype/student/student.py index 45e848b55e..81fa7f7fae 100644 --- a/erpnext/education/doctype/student/student.py +++ b/erpnext/education/doctype/student/student.py @@ -24,7 +24,7 @@ class Student(Document): def validate_dates(self): for sibling in self.siblings: if sibling.date_of_birth and getdate(sibling.date_of_birth) >= getdate(): - frappe.throw(_("Row {0}:Sibling Date of Birth cannot be greater than today").format(sibling.idx)) + frappe.throw(_("Row {0}:Sibling Date of Birth cannot be greater than today.").format(sibling.idx)) if self.date_of_birth and getdate(self.date_of_birth) >= getdate(today()): frappe.throw(_("Date of Birth cannot be greater than today.")) From db002708958dc88b09b05c1d7deea16687a1b345 Mon Sep 17 00:00:00 2001 From: Marica Date: Mon, 17 Feb 2020 15:58:08 +0530 Subject: [PATCH 051/102] feat: Added popup to 'Get Items from Open Material Requests' in Purchase Order (#20371) * feat: Added popup to 'Get Items from Open Material Requests' in Purchase Order * fix: Query with filters, UX enhancements and cleanup --- .../doctype/purchase_order/purchase_order.js | 12 ++- .../purchase_order/purchase_order.json | 3 +- erpnext/public/js/utils.js | 17 +++- .../material_request/material_request.py | 81 +++++++++++-------- 4 files changed, 75 insertions(+), 38 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 7b1f1354d7..a3264a4c0f 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -180,10 +180,20 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( get_items_from_open_material_requests: function() { erpnext.utils.map_current_doc({ method: "erpnext.stock.doctype.material_request.material_request.make_purchase_order_based_on_supplier", + args: { + supplier: this.frm.doc.supplier + }, + source_doctype: "Material Request", source_name: this.frm.doc.supplier, + target: this.frm, + setters: { + company: me.frm.doc.company + }, get_query_filters: { docstatus: ["!=", 2], - } + supplier: this.frm.doc.supplier + }, + get_query_method: "erpnext.stock.doctype.material_request.material_request.get_material_requests_based_on_supplier" }); }, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index d82e128735..4d83690391 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -12,8 +12,8 @@ "supplier", "get_items_from_open_material_requests", "supplier_name", - "company", "column_break1", + "company", "transaction_date", "schedule_date", "order_confirmation_no", @@ -170,6 +170,7 @@ "search_index": 1 }, { + "description": "Fetch items based on Default Supplier.", "depends_on": "eval:doc.supplier && doc.docstatus===0 && (!(doc.items && doc.items.length) || (doc.items.length==1 && !doc.items[0].item_code))", "fieldname": "get_items_from_open_material_requests", "fieldtype": "Button", diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 3f444f8387..35dc8427aa 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -514,9 +514,18 @@ erpnext.utils.update_child_items = function(opts) { } erpnext.utils.map_current_doc = function(opts) { - if(opts.get_query_filters) { - opts.get_query = function() { - return {filters: opts.get_query_filters}; + let query_args = {}; + if (opts.get_query_filters) { + query_args.filters = opts.get_query_filters; + } + + if (opts.get_query_method) { + query_args.query = opts.get_query_method; + } + + if (query_args.filters || query_args.query) { + opts.get_query = () => { + return query_args; } } var _map = function() { @@ -582,7 +591,7 @@ erpnext.utils.map_current_doc = function(opts) { "method": opts.method, "source_names": opts.source_name, "target_doc": cur_frm.doc, - 'args': opts.args + "args": opts.args }, callback: function(r) { if(!r.exc) { diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 941f904102..4542847016 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import frappe +import json from frappe.utils import cstr, flt, getdate, new_line_sep, nowdate, add_days from frappe import msgprint, _ @@ -329,17 +330,13 @@ def make_request_for_quotation(source_name, target_doc=None): return doclist @frappe.whitelist() -def make_purchase_order_based_on_supplier(source_name, target_doc=None): - if target_doc: - if isinstance(target_doc, string_types): - import json - target_doc = frappe.get_doc(json.loads(target_doc)) - target_doc.set("items", []) +def make_purchase_order_based_on_supplier(source_name, target_doc=None, args=None): + mr = source_name - material_requests, supplier_items = get_material_requests_based_on_supplier(source_name) + supplier_items = get_items_based_on_default_supplier(args.get("supplier")) def postprocess(source, target_doc): - target_doc.supplier = source_name + target_doc.supplier = args.get("supplier") if getdate(target_doc.schedule_date) < getdate(nowdate()): target_doc.schedule_date = None target_doc.set("items", [d for d in target_doc.get("items") @@ -347,44 +344,64 @@ def make_purchase_order_based_on_supplier(source_name, target_doc=None): set_missing_values(source, target_doc) - for mr in material_requests: - target_doc = get_mapped_doc("Material Request", mr, { - "Material Request": { - "doctype": "Purchase Order", - }, - "Material Request Item": { - "doctype": "Purchase Order Item", - "field_map": [ - ["name", "material_request_item"], - ["parent", "material_request"], - ["uom", "stock_uom"], - ["uom", "uom"] - ], - "postprocess": update_item, - "condition": lambda doc: doc.ordered_qty < doc.qty - } - }, target_doc, postprocess) + target_doc = get_mapped_doc("Material Request", mr, { + "Material Request": { + "doctype": "Purchase Order", + }, + "Material Request Item": { + "doctype": "Purchase Order Item", + "field_map": [ + ["name", "material_request_item"], + ["parent", "material_request"], + ["uom", "stock_uom"], + ["uom", "uom"] + ], + "postprocess": update_item, + "condition": lambda doc: doc.ordered_qty < doc.qty + } + }, target_doc, postprocess) return target_doc -def get_material_requests_based_on_supplier(supplier): +@frappe.whitelist() +def get_items_based_on_default_supplier(supplier): supplier_items = [d.parent for d in frappe.db.get_all("Item Default", - {"default_supplier": supplier}, 'parent')] + {"default_supplier": supplier, "parenttype": "Item"}, 'parent')] + + return supplier_items + +@frappe.whitelist() +def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, page_len, filters): + conditions = "" + if txt: + conditions += "and mr.name like '%%"+txt+"%%' " + + if filters.get("transaction_date"): + date = filters.get("transaction_date")[1] + conditions += "and mr.transaction_date between '{0}' and '{1}' ".format(date[0], date[1]) + + supplier = filters.get("supplier") + supplier_items = get_items_based_on_default_supplier(supplier) + if not supplier_items: frappe.throw(_("{0} is not the default supplier for any items.").format(supplier)) - material_requests = frappe.db.sql_list("""select distinct mr.name + material_requests = frappe.db.sql("""select distinct mr.name, transaction_date,company from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item where mr.name = mr_item.parent - and mr_item.item_code in (%s) + and mr_item.item_code in ({0}) and mr.material_request_type = 'Purchase' and mr.per_ordered < 99.99 and mr.docstatus = 1 and mr.status != 'Stopped' - order by mr_item.item_code ASC""" % ', '.join(['%s']*len(supplier_items)), - tuple(supplier_items)) + and mr.company = '{1}' + {2} + order by mr_item.item_code ASC + limit {3} offset {4} """ \ + .format(', '.join(['%s']*len(supplier_items)), filters.get("company"), conditions, page_len, start), + tuple(supplier_items), as_dict=1) - return material_requests, supplier_items + return material_requests def get_default_supplier_query(doctype, txt, searchfield, start, page_len, filters): doc = frappe.get_doc("Material Request", filters.get("doc")) From 0047fcf7adb0c6f5c8a0a4b8c236890095bb11e6 Mon Sep 17 00:00:00 2001 From: abhijitkumbharIND Date: Mon, 17 Feb 2020 18:08:09 +0530 Subject: [PATCH 052/102] fix: HSN/SAC Error while creating a new item and not selecting the field. #20590 --- erpnext/stock/doctype/item/item.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index dd3248baf2..735f35f36f 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -137,7 +137,7 @@ frappe.ui.form.on("Item", { }, gst_hsn_code: function(frm) { - if(!frm.doc.taxes || !frm.doc.taxes.length) { + if((!frm.doc.taxes || !frm.doc.taxes.length) && frm.doc.gst_hsn_code) { frappe.db.get_doc("GST HSN Code", frm.doc.gst_hsn_code).then(hsn_doc => { $.each(hsn_doc.taxes || [], function(i, tax) { let a = frappe.model.add_child(cur_frm.doc, 'Item Tax', 'taxes'); From e14398381952f6fe5df111273ad42a7eb2e7ff9d Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 18 Feb 2020 10:51:54 +0530 Subject: [PATCH 053/102] fix: Stock Quantity not calculated on client side in Material Request Items. --- erpnext/public/js/controllers/transaction.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 51ab48a3ab..497f9c579d 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -941,15 +941,19 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }, conversion_factor: function(doc, cdt, cdn, dont_fetch_price_list_rate) { - if(doc.doctype != 'Material Request' && frappe.meta.get_docfield(cdt, "stock_qty", cdn)) { + if(frappe.meta.get_docfield(cdt, "stock_qty", cdn)) { var item = frappe.get_doc(cdt, cdn); frappe.model.round_floats_in(item, ["qty", "conversion_factor"]); item.stock_qty = flt(item.qty * item.conversion_factor, precision("stock_qty", item)); - item.total_weight = flt(item.stock_qty * item.weight_per_unit); refresh_field("stock_qty", item.name, item.parentfield); - refresh_field("total_weight", item.name, item.parentfield); this.toggle_conversion_factor(item); - this.calculate_net_weight(); + + if(doc.doctype != "Material Request") { + item.total_weight = flt(item.stock_qty * item.weight_per_unit); + refresh_field("total_weight", item.name, item.parentfield); + this.calculate_net_weight(); + } + if (!dont_fetch_price_list_rate && frappe.meta.has_field(doc.doctype, "price_list_currency")) { this.apply_price_list(item, true); From 93e8fc183335c90e8c65a3ec4301d200a76fa360 Mon Sep 17 00:00:00 2001 From: Anuradha Kalaskar Date: Tue, 18 Feb 2020 11:32:35 +0530 Subject: [PATCH 054/102] fix: date of birth cannot be greater than today on student application form --- .../education/doctype/student_applicant/student_applicant.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/education/doctype/student_applicant/student_applicant.py b/erpnext/education/doctype/student_applicant/student_applicant.py index 6d0957c502..ab947807dd 100644 --- a/erpnext/education/doctype/student_applicant/student_applicant.py +++ b/erpnext/education/doctype/student_applicant/student_applicant.py @@ -29,10 +29,15 @@ class StudentApplicant(Document): set_name_by_naming_series(self) def validate(self): + self.validate_dates() self.title = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name])) if self.student_admission and self.program and self.date_of_birth: self.validation_from_student_admission() + def validate_dates(self): + if self.date_of_birth and getdate(self.date_of_birth) >= getdate(): + frappe.throw(_("Date of Birth cannot be greater than today.")) + def on_update_after_submit(self): student = frappe.get_list("Student", filters= {"student_applicant": self.name}) if student: From 15ff6a594ba6554648a17b86accd4a132c438e2f Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 18 Feb 2020 12:28:41 +0530 Subject: [PATCH 055/102] feat: Inter warehouse stock transfer on valuation rate with taxation (#20083) * fix: Validation for target warehouse * feat: Get items on valuation rate in delivery note on Internal transfers * fix: Create Inter company purchase receipt from delivery note * feat: Inter company stock transfer on valuation rate with taxation * fix: Add from warehouse in purchase invoice * fix: Use get_value instead of get_cached_value * fix: Get incoming rate instead of valuation rate * fix: GL entry for from warehouse in purchase receipt * fix: GL Entries fixes in purchase invoice * fix: Address and tax fetching fixes * fix: Add test case for stock transfer via purchase receipt * fix: Code cleanup, added validations and test cases * fix: Validation for supplier warehouse * fix: Test Cases * fix: Change validation condition * fix: Address fixes while creating Purchase Receipt from delivery note * fix: Set taxes while creating Purchase Receipt from Delivery Note * fix: Update set_missing_value function Co-authored-by: Nabin Hait --- .../purchase_invoice/purchase_invoice.json | 10 +- .../purchase_invoice/purchase_invoice.py | 67 +- .../purchase_invoice_item.json | 11 +- .../doctype/sales_invoice/sales_invoice.json | 9 + .../doctype/sales_invoice/sales_invoice.py | 27 +- erpnext/accounts/party.py | 3 +- erpnext/buying/doctype/supplier/supplier.json | 1996 ++++------------- erpnext/controllers/buying_controller.py | 28 + erpnext/controllers/selling_controller.py | 7 +- erpnext/controllers/stock_controller.py | 16 +- erpnext/patches.txt | 1 + .../v12_0/add_eway_bill_in_delivery_note.py | 19 + .../public/js/controllers/taxes_and_totals.js | 1 + erpnext/public/js/controllers/transaction.js | 42 +- .../gstr_3b_report/test_gstr_3b_report.py | 2 + erpnext/regional/india/setup.py | 13 +- erpnext/regional/india/taxes.js | 3 + erpnext/regional/india/utils.py | 34 +- .../selling/doctype/customer/customer.json | 3 +- .../doctype/delivery_note/delivery_note.js | 9 + .../doctype/delivery_note/delivery_note.json | 18 +- .../doctype/delivery_note/delivery_note.py | 75 + .../doctype/delivery_note/regional/india.js | 19 + .../purchase_receipt/purchase_receipt.js | 13 +- .../purchase_receipt/purchase_receipt.json | 17 + .../purchase_receipt/purchase_receipt.py | 28 +- .../purchase_receipt/test_purchase_receipt.py | 77 + .../purchase_receipt_item.json | 14 +- erpnext/stock/get_item_details.py | 1 + 29 files changed, 887 insertions(+), 1676 deletions(-) create mode 100644 erpnext/patches/v12_0/add_eway_bill_in_delivery_note.py diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 7725994c6b..3cd988ccd2 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -149,6 +149,7 @@ "column_break_63", "status", "inter_company_invoice_reference", + "is_internal_supplier", "remarks", "subscription_section", "from_date", @@ -418,7 +419,6 @@ "fieldname": "contact_email", "fieldtype": "Small Text", "label": "Contact Email", - "options": "Email", "print_hide": 1, "read_only": 1 }, @@ -1284,6 +1284,14 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "default": "0", + "fetch_from": "supplier.is_internal_supplier", + "fieldname": "is_internal_supplier", + "fieldtype": "Check", + "label": "Is Internal Supplier", + "read_only": 1 } ], "icon": "fa fa-file-text", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 4002d7e55a..80be2c803f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -224,7 +224,7 @@ class PurchaseInvoice(BuyingController): for item in self.get("items"): # in case of auto inventory accounting, # expense account is always "Stock Received But Not Billed" for a stock item - # except epening entry, drop-ship entry and fixed asset items + # except opening entry, drop-ship entry and fixed asset items if item.item_code: asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category") @@ -233,10 +233,22 @@ class PurchaseInvoice(BuyingController): and (not item.po_detail or not frappe.db.get_value("Purchase Order Item", item.po_detail, "delivered_by_supplier")): - if self.update_stock: + if self.update_stock and (not item.from_warehouse): item.expense_account = warehouse_account[item.warehouse]["account"] else: - item.expense_account = stock_not_billed_account + # check if 'Stock Received But Not Billed' account is credited in Purchase receipt or not + if item.purchase_receipt: + negative_expense_booked_in_pr = frappe.db.sql("""select name from `tabGL Entry` + where voucher_type='Purchase Receipt' and voucher_no=%s and account = %s""", + (item.purchase_receipt, stock_not_billed_account)) + + if negative_expense_booked_in_pr: + item.expense_account = stock_not_billed_account + else: + # If no purchase receipt present then book expense in 'Stock Received But Not Billed' + # This is done in cases when Purchase Invoice is created before Purchase Receipt + item.expense_account = stock_not_billed_account + elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category): item.expense_account = get_asset_category_account('fixed_asset_account', item=item.item_code, company = self.company) @@ -467,16 +479,47 @@ class PurchaseInvoice(BuyingController): warehouse_debit_amount = self.make_stock_adjustment_entry(gl_entries, item, voucher_wise_stock_value, account_currency) - gl_entries.append( - self.get_gl_dict({ - "account": item.expense_account, - "against": self.supplier, - "debit": warehouse_debit_amount, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + if item.from_warehouse: + + gl_entries.append(self.get_gl_dict({ + "account": warehouse_account[item.warehouse]['account'], + "against": warehouse_account[item.from_warehouse]["account"], "cost_center": item.cost_center, - "project": item.project - }, account_currency, item=item) - ) + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "debit": warehouse_debit_amount, + }, warehouse_account[item.warehouse]["account_currency"], item=item)) + + # Intentionally passed negative debit amount to avoid incorrect GL Entry validation + gl_entries.append(self.get_gl_dict({ + "account": warehouse_account[item.from_warehouse]['account'], + "against": warehouse_account[item.warehouse]["account"], + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "debit": -1 * flt(item.base_net_amount, item.precision("base_net_amount")), + }, warehouse_account[item.from_warehouse]["account_currency"], item=item)) + + gl_entries.append( + self.get_gl_dict({ + "account": item.expense_account, + "against": self.supplier, + "debit": flt(item.base_net_amount, item.precision("base_net_amount")), + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "cost_center": item.cost_center, + "project": item.project + }, account_currency, item=item) + ) + + else: + gl_entries.append( + self.get_gl_dict({ + "account": item.expense_account, + "against": self.supplier, + "debit": warehouse_debit_amount, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "cost_center": item.cost_center, + "project": item.project + }, account_currency, item=item) + ) # Amount added through landed-cost-voucher if landed_cost_entries: 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 acb0398b5c..28208ce1c2 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -63,6 +63,7 @@ "warehouse_section", "warehouse", "rejected_warehouse", + "from_warehouse", "quality_inspection", "batch_no", "col_br_wh", @@ -762,16 +763,22 @@ "fetch_from": "item_code.asset_category", "fieldname": "asset_category", "fieldtype": "Data", - "in_preview": 1, "label": "Asset Category", "options": "Asset Category", "read_only": 1 + }, + { + "fieldname": "from_warehouse", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Supplier Warehouse", + "options": "Warehouse" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2019-12-04 12:23:17.046413", + "modified": "2020-01-13 16:04:14.200462", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 52a0f4e081..e239f9143d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -153,6 +153,7 @@ "select_print_heading", "more_information", "inter_company_invoice_reference", + "is_internal_customer", "customer_group", "campaign", "is_discounted", @@ -1563,6 +1564,14 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "default": "0", + "fetch_from": "customer.is_internal_customer", + "fieldname": "is_internal_customer", + "fieldtype": "Check", + "label": "Is Internal Customer", + "read_only": 1 } ], "icon": "fa fa-file-text", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index d8344ea6a6..8b4923fd4f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1426,23 +1426,42 @@ def set_account_for_mode_of_payment(self): data.account = get_bank_cash_account(data.mode_of_payment, self.company).get("account") def get_inter_company_details(doc, doctype): - if doctype in ["Sales Invoice", "Sales Order"]: - party = frappe.db.get_value("Supplier", {"disabled": 0, "is_internal_supplier": 1, "represents_company": doc.company}, "name") + if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]: + parties = frappe.db.get_all("Supplier", fields=["name"], filters={"disabled": 0, "is_internal_supplier": 1, "represents_company": doc.company}) company = frappe.get_cached_value("Customer", doc.customer, "represents_company") + + party = get_internal_party(parties, "Supplier", doc) else: - party = frappe.db.get_value("Customer", {"disabled": 0, "is_internal_customer": 1, "represents_company": doc.company}, "name") + parties = frappe.db.get_all("Customer", fields=["name"], filters={"disabled": 0, "is_internal_customer": 1, "represents_company": doc.company}) company = frappe.get_cached_value("Supplier", doc.supplier, "represents_company") + party = get_internal_party(parties, "Customer", doc) + return { "party": party, "company": company } +def get_internal_party(parties, link_doctype, doc): + if len(parties) == 1: + party = parties[0].name + else: + # If more than one Internal Supplier/Customer, get supplier/customer on basis of address + if doc.get('company_address') or doc.get('shipping_address'): + party = frappe.db.get_value("Dynamic Link", {"parent": doc.get('company_address') or doc.get('shipping_address'), + "parenttype": "Address", "link_doctype": link_doctype}, "link_name") + + if not party: + party = parties[0].name + else: + party = parties[0].name + + return party def validate_inter_company_transaction(doc, doctype): details = get_inter_company_details(doc, doctype) - price_list = doc.selling_price_list if doctype in ["Sales Invoice", "Sales Order"] else doc.buying_price_list + price_list = doc.selling_price_list if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"] else doc.buying_price_list valid_price_list = frappe.db.get_value("Price List", {"name": price_list, "buying": 1, "selling": 1}) if not valid_price_list: frappe.throw(_("Selected Price List should have buying and selling fields checked.")) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 156f2181b8..422ace64f5 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -35,8 +35,7 @@ def get_party_details(party=None, account=None, party_type="Customer", company=N def _get_party_details(party=None, account=None, party_type="Customer", company=None, posting_date=None, bill_date=None, price_list=None, currency=None, doctype=None, ignore_permissions=False, - fetch_payment_terms_template=True, party_address=None, company_address=None,shipping_address=None, pos_profile=None): - + fetch_payment_terms_template=True, party_address=None, company_address=None, shipping_address=None, pos_profile=None): party_details = frappe._dict(set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype)) party = party_details[party_type.lower()] diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index b5f0366c8e..1ab171ae37 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -1,1683 +1,439 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 1, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "naming_series:", - "beta": 0, - "creation": "2013-01-10 16:34:11", - "custom": 0, - "description": "Supplier of Goods or Services.", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, - "engine": "InnoDB", + "actions": [], + "allow_events_in_timeline": 1, + "allow_import": 1, + "allow_rename": 1, + "autoname": "naming_series:", + "creation": "2013-01-10 16:34:11", + "description": "Supplier of Goods or Services.", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "basic_info", + "naming_series", + "supplier_name", + "country", + "default_bank_account", + "tax_id", + "tax_category", + "tax_withholding_category", + "is_transporter", + "is_internal_supplier", + "represents_company", + "image", + "column_break0", + "supplier_group", + "supplier_type", + "pan", + "language", + "disabled", + "warn_rfqs", + "warn_pos", + "prevent_rfqs", + "prevent_pos", + "allowed_to_transact_section", + "companies", + "section_break_7", + "default_currency", + "column_break_10", + "default_price_list", + "section_credit_limit", + "payment_terms", + "cb_21", + "on_hold", + "hold_type", + "release_date", + "address_contacts", + "address_html", + "column_break1", + "contact_html", + "default_payable_accounts", + "accounts", + "default_tax_withholding_config", + "column_break2", + "website", + "supplier_details", + "column_break_30", + "is_frozen" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "basic_info", - "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": "Name and Type", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "options": "fa fa-user", - "permlevel": 0, - "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": "basic_info", + "fieldtype": "Section Break", + "label": "Name and Type", + "oldfieldtype": "Section Break", + "options": "fa fa-user" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "naming_series", - "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": "Series", - "length": 0, - "no_copy": 1, - "oldfieldname": "naming_series", - "oldfieldtype": "Select", - "options": "SUP-.YYYY.-", - "permlevel": 0, - "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": 1, - "translatable": 0, - "unique": 0 - }, + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "no_copy": 1, + "oldfieldname": "naming_series", + "oldfieldtype": "Select", + "options": "SUP-.YYYY.-", + "set_only_once": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "fieldname": "supplier_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Supplier Name", - "length": 0, - "no_copy": 1, - "oldfieldname": "supplier_name", - "oldfieldtype": "Data", - "permlevel": 0, - "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 - }, + "bold": 1, + "fieldname": "supplier_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Supplier Name", + "no_copy": 1, + "oldfieldname": "supplier_name", + "oldfieldtype": "Data", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "country", - "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": "Country", - "length": 0, - "no_copy": 0, - "options": "Country", - "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": "country", + "fieldtype": "Link", + "label": "Country", + "options": "Country" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_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": 0, - "label": "Default 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 - }, + "fieldname": "default_bank_account", + "fieldtype": "Link", + "label": "Default Bank Account", + "options": "Bank Account" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "tax_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": "Tax ID", - "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": "tax_id", + "fieldtype": "Data", + "label": "Tax ID" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "tax_category", - "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": "Tax Category", - "length": 0, - "no_copy": 0, - "options": "Tax Category", - "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": "tax_category", + "fieldtype": "Link", + "label": "Tax Category", + "options": "Tax Category" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "tax_withholding_category", - "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": "Tax Withholding Category", - "length": 0, - "no_copy": 0, - "options": "Tax Withholding Category", - "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": "tax_withholding_category", + "fieldtype": "Link", + "label": "Tax Withholding Category", + "options": "Tax Withholding Category" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "is_transporter", - "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": "Is Transporter", - "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": "is_transporter", + "fieldtype": "Check", + "label": "Is Transporter" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fieldname": "is_internal_supplier", - "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": "Is Internal Supplier", - "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": "is_internal_supplier", + "fieldtype": "Check", + "label": "Is Internal Supplier" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "is_internal_supplier", - "fieldname": "represents_company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Represents Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "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": 1 - }, + "depends_on": "is_internal_supplier", + "fieldname": "represents_company", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Represents Company", + "options": "Company" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "image", - "fieldtype": "Attach Image", - "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": "Image", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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": "image", + "fieldtype": "Attach Image", + "hidden": 1, + "label": "Image", + "no_copy": 1, + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break0", - "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, - "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_break0", + "fieldtype": "Column Break", "width": "50%" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "supplier_group", - "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": 1, - "label": "Supplier Group", - "length": 0, - "no_copy": 0, - "oldfieldname": "supplier_type", - "oldfieldtype": "Link", - "options": "Supplier Group", - "permlevel": 0, - "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 - }, + "fieldname": "supplier_group", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Supplier Group", + "oldfieldname": "supplier_type", + "oldfieldtype": "Link", + "options": "Supplier Group", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Company", - "fieldname": "supplier_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": "Supplier Type", - "length": 0, - "no_copy": 0, - "options": "Company\nIndividual", - "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 - }, + "default": "Company", + "fieldname": "supplier_type", + "fieldtype": "Select", + "label": "Supplier Type", + "options": "Company\nIndividual", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "pan", - "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": "PAN", - "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": "pan", + "fieldtype": "Data", + "label": "PAN" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "language", - "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": "Print Language", - "length": 0, - "no_copy": 0, - "options": "Language", - "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": "language", + "fieldtype": "Link", + "label": "Print Language", + "options": "Language" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "default": "0", - "fieldname": "disabled", - "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": "Disabled", - "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 - }, + "bold": 1, + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "warn_rfqs", - "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": "Warn RFQs", - "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 - }, + "default": "0", + "fieldname": "warn_rfqs", + "fieldtype": "Check", + "label": "Warn RFQs", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "warn_pos", - "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": "Warn POs", - "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 - }, + "default": "0", + "fieldname": "warn_pos", + "fieldtype": "Check", + "label": "Warn POs", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "prevent_rfqs", - "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": "Prevent RFQs", - "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 - }, + "default": "0", + "fieldname": "prevent_rfqs", + "fieldtype": "Check", + "label": "Prevent RFQs", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "prevent_pos", - "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": "Prevent POs", - "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 - }, + "default": "0", + "fieldname": "prevent_pos", + "fieldtype": "Check", + "label": "Prevent POs", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "represents_company", - "fieldname": "allowed_to_transact_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": "Allowed To Transact With", - "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 - }, + "depends_on": "represents_company", + "fieldname": "allowed_to_transact_section", + "fieldtype": "Section Break", + "label": "Allowed To Transact With" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "represents_company", - "fieldname": "companies", - "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": "Allowed To Transact With", - "length": 0, - "no_copy": 0, - "options": "Allowed To Transact With", - "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 - }, + "depends_on": "represents_company", + "fieldname": "companies", + "fieldtype": "Table", + "label": "Allowed To Transact With", + "options": "Allowed To Transact With" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "section_break_7", - "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": "Currency and Price List", - "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 - }, + "collapsible": 1, + "fieldname": "section_break_7", + "fieldtype": "Section Break", + "label": "Currency and Price List" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_currency", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Billing Currency", - "length": 0, - "no_copy": 1, - "options": "Currency", - "permlevel": 0, - "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": "default_currency", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Billing Currency", + "no_copy": 1, + "options": "Currency" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_price_list", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Price List", - "length": 0, - "no_copy": 0, - "options": "Price List", - "permlevel": 0, - "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": "default_price_list", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Price List", + "options": "Price List" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "section_credit_limit", - "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": "Credit 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": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "collapsible": 1, + "fieldname": "section_credit_limit", + "fieldtype": "Section Break", + "label": "Credit Limit" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "payment_terms", - "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": "Default Payment Terms Template", - "length": 0, - "no_copy": 0, - "options": "Payment Terms Template", - "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": "payment_terms", + "fieldtype": "Link", + "label": "Default Payment Terms Template", + "options": "Payment Terms Template" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "cb_21", - "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": "cb_21", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fieldname": "on_hold", - "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": "Block Supplier", - "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": "on_hold", + "fieldtype": "Check", + "label": "Block Supplier" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.on_hold", - "fieldname": "hold_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": "Hold Type", - "length": 0, - "no_copy": 0, - "options": "\nAll\nInvoices\nPayments", - "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 - }, + "depends_on": "eval:doc.on_hold", + "fieldname": "hold_type", + "fieldtype": "Select", + "label": "Hold Type", + "options": "\nAll\nInvoices\nPayments" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.on_hold", - "description": "Leave blank if the Supplier is blocked indefinitely", - "fieldname": "release_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": "Release 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 - }, + "depends_on": "eval:doc.on_hold", + "description": "Leave blank if the Supplier is blocked indefinitely", + "fieldname": "release_date", + "fieldtype": "Date", + "label": "Release Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:!doc.__islocal", - "fieldname": "address_contacts", - "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": "Address and Contacts", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Column Break", - "options": "fa fa-map-marker", - "permlevel": 0, - "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 - }, + "depends_on": "eval:!doc.__islocal", + "fieldname": "address_contacts", + "fieldtype": "Section Break", + "label": "Address and Contacts", + "oldfieldtype": "Column Break", + "options": "fa fa-map-marker" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address_html", - "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": "Address HTML", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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": "address_html", + "fieldtype": "HTML", + "label": "Address HTML", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break1", - "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, - "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_break1", + "fieldtype": "Column Break", "width": "50%" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "contact_html", - "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": "Contact HTML", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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": "contact_html", + "fieldtype": "HTML", + "label": "Contact HTML", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "collapsible_depends_on": "accounts", - "columns": 0, - "fieldname": "default_payable_accounts", - "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": "Default Payable Accounts", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "collapsible": 1, + "collapsible_depends_on": "accounts", + "fieldname": "default_payable_accounts", + "fieldtype": "Section Break", + "label": "Default Payable Accounts" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "description": "Mention if non-standard payable account", - "fieldname": "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": "Accounts", - "length": 0, - "no_copy": 0, - "options": "Party Account", - "permlevel": 0, - "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 - }, + "description": "Mention if non-standard payable account", + "fieldname": "accounts", + "fieldtype": "Table", + "label": "Accounts", + "options": "Party Account" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "default_tax_withholding_config", - "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": "Default Tax Withholding Config", - "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 - }, + "collapsible": 1, + "fieldname": "default_tax_withholding_config", + "fieldtype": "Section Break", + "label": "Default Tax Withholding Config" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "collapsible_depends_on": "supplier_details", - "columns": 0, - "fieldname": "column_break2", - "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": "More Information", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, + "collapsible": 1, + "collapsible_depends_on": "supplier_details", + "fieldname": "column_break2", + "fieldtype": "Section Break", + "label": "More Information", "width": "50%" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "website", - "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": "Website", - "length": 0, - "no_copy": 0, - "oldfieldname": "website", - "oldfieldtype": "Data", - "permlevel": 0, - "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": "website", + "fieldtype": "Data", + "label": "Website", + "oldfieldname": "website", + "oldfieldtype": "Data" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Statutory info and other general information about your Supplier", - "fieldname": "supplier_details", - "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": "Supplier Details", - "length": 0, - "no_copy": 0, - "oldfieldname": "supplier_details", - "oldfieldtype": "Code", - "permlevel": 0, - "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 - }, + "description": "Statutory info and other general information about your Supplier", + "fieldname": "supplier_details", + "fieldtype": "Text", + "label": "Supplier Details", + "oldfieldname": "supplier_details", + "oldfieldtype": "Code" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_30", - "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_30", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "is_frozen", - "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": "Is Frozen", - "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": "is_frozen", + "fieldtype": "Check", + "label": "Is Frozen" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-user", - "idx": 370, - "image_field": "image", - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-01-17 13:58:08.597793", - "modified_by": "Administrator", - "module": "Buying", - "name": "Supplier", - "name_case": "Title Case", - "owner": "Administrator", + ], + "icon": "fa fa-user", + "idx": 370, + "image_field": "image", + "links": [], + "modified": "2019-12-19 18:17:16.614567", + "modified_by": "Administrator", + "module": "Buying", + "name": "Supplier", + "name_case": "Title Case", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Purchase User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase User" + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Purchase Manager", - "set_user_permissions": 0, - "share": 0, - "submit": 0, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase Manager", "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Purchase Master Manager", - "set_user_permissions": 1, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase Master Manager", + "set_user_permissions": 1, + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Stock User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "read": 1, + "role": "Stock User" + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Stock Manager", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager" + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Accounts User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "read": 1, + "role": "Accounts User" + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager" } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "supplier_name, supplier_group", - "show_name_in_global_search": 1, - "sort_order": "ASC", - "title_field": "supplier_name", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} + ], + "quick_entry": 1, + "search_fields": "supplier_name, supplier_group", + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "ASC", + "title_field": "supplier_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 69caabd724..8d3db8d534 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -43,6 +43,7 @@ class BuyingController(StockController): self.set_qty_as_per_stock_uom() self.validate_stock_or_nonstock_items() self.validate_warehouse() + self.validate_from_warehouse() self.set_supplier_address() if self.doctype=="Purchase Invoice": @@ -115,6 +116,14 @@ class BuyingController(StockController): if not d.cost_center and lc_voucher_data and lc_voucher_data[0][1]: d.db_set('cost_center', lc_voucher_data[0][1]) + def validate_from_warehouse(self): + for item in self.get('items'): + if item.get('from_warehouse') and (item.get('from_warehouse') == item.get('warehouse')): + frappe.throw(_("Row #{0}: Accepted Warehouse and Supplier Warehouse cannot be same").format(item.idx)) + + if item.get('from_warehouse') and self.get('is_subcontracted') == 'Yes': + frappe.throw(_("Row #{0}: Cannot select Supplier Warehouse while suppling raw materials to subcontractor").format(item.idx)) + def set_supplier_address(self): address_dict = { 'supplier_address': 'address_display', @@ -521,6 +530,16 @@ class BuyingController(StockController): pr_qty = flt(d.qty) * flt(d.conversion_factor) if pr_qty: + + if d.from_warehouse and ((not cint(self.is_return) and self.docstatus==1) + or (cint(self.is_return) and self.docstatus==2)): + from_warehouse_sle = self.get_sl_entries(d, { + "actual_qty": -1 * pr_qty, + "warehouse": d.from_warehouse + }) + + sl_entries.append(from_warehouse_sle) + sle = self.get_sl_entries(d, { "actual_qty": flt(pr_qty), "serial_no": cstr(d.serial_no).strip() @@ -541,6 +560,15 @@ class BuyingController(StockController): }) sl_entries.append(sle) + if d.from_warehouse and ((not cint(self.is_return) and self.docstatus==2) + or (cint(self.is_return) and self.docstatus==1)): + from_warehouse_sle = self.get_sl_entries(d, { + "actual_qty": -1 * pr_qty, + "warehouse": d.from_warehouse + }) + + sl_entries.append(from_warehouse_sle) + if flt(d.rejected_qty) != 0: sl_entries.append(self.get_sl_entries(d, { "warehouse": d.rejected_warehouse, diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 9a9f3d1d31..2b21ee8aa4 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -180,7 +180,7 @@ class SellingController(StockController): last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"]) last_purchase_rate_in_sales_uom = last_purchase_rate / (it.conversion_factor or 1) - if flt(it.base_rate) < flt(last_purchase_rate_in_sales_uom): + if flt(it.base_rate) < flt(last_purchase_rate_in_sales_uom) and not self.get('is_internal_customer'): throw_message(it.item_name, last_purchase_rate_in_sales_uom, "last purchase rate") last_valuation_rate = frappe.db.sql(""" @@ -190,7 +190,8 @@ class SellingController(StockController): """, (it.item_code, it.warehouse)) if last_valuation_rate: last_valuation_rate_in_sales_uom = last_valuation_rate[0][0] / (it.conversion_factor or 1) - if is_stock_item and flt(it.base_rate) < flt(last_valuation_rate_in_sales_uom): + if is_stock_item and flt(it.base_rate) < flt(last_valuation_rate_in_sales_uom) \ + and not self.get('is_internal_customer'): throw_message(it.name, last_valuation_rate_in_sales_uom, "valuation rate") @@ -300,7 +301,7 @@ class SellingController(StockController): d.conversion_factor = get_conversion_factor(d.item_code, d.uom).get("conversion_factor") or 1.0 return_rate = 0 if cint(self.is_return) and self.return_against and self.docstatus==1: - return_rate = self.get_incoming_rate_for_sales_return(d.item_code, self.return_against) + return_rate = self.get_incoming_rate_for_return(d.item_code, self.return_against) # On cancellation or if return entry submission, make stock ledger entry for # target warehouse first, to update serial no values properly diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 14ee23b112..57b4ddd0fb 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -72,7 +72,7 @@ class StockController(AccountsController): if sle_list: for sle in sle_list: if warehouse_account.get(sle.warehouse): - # from warehouse account + # from warehouse account/ target warehouse account self.check_expense_account(item_row) @@ -96,7 +96,7 @@ class StockController(AccountsController): "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No", }, warehouse_account[sle.warehouse]["account_currency"], item=item_row)) - # to target warehouse / expense account + # expense account gl_list.append(self.get_gl_dict({ "account": item_row.expense_account, "against": warehouse_account[sle.warehouse]["account"], @@ -288,7 +288,7 @@ class StockController(AccountsController): return serialized_items - def get_incoming_rate_for_sales_return(self, item_code, against_document): + def get_incoming_rate_for_return(self, item_code, against_document): incoming_rate = 0.0 if against_document and item_code: incoming_rate = frappe.db.sql("""select abs(stock_value_difference / actual_qty) @@ -306,6 +306,16 @@ class StockController(AccountsController): warehouses = list(set([d.warehouse for d in self.get("items") if getattr(d, "warehouse", None)])) + target_warehouses = list(set([d.target_warehouse for d in + self.get("items") if getattr(d, "target_warehouse", None)])) + + warehouses.extend(target_warehouses) + + from_warehouse = list(set([d.from_warehouse for d in + self.get("items") if getattr(d, "from_warehouse", None)])) + + warehouses.extend(from_warehouse) + for w in warehouses: validate_warehouse_company(w, self.company) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index a8938406f2..09890c59a2 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -652,6 +652,7 @@ erpnext.patches.v12_0.set_production_capacity_in_workstation erpnext.patches.v12_0.set_employee_preferred_emails erpnext.patches.v12_0.set_against_blanket_order_in_sales_and_purchase_order erpnext.patches.v12_0.set_cost_center_in_child_table_of_expense_claim +erpnext.patches.v12_0.add_eway_bill_in_delivery_note erpnext.patches.v12_0.set_lead_title_field erpnext.patches.v12_0.set_permission_einvoicing erpnext.patches.v12_0.set_published_in_hub_tracked_item diff --git a/erpnext/patches/v12_0/add_eway_bill_in_delivery_note.py b/erpnext/patches/v12_0/add_eway_bill_in_delivery_note.py new file mode 100644 index 0000000000..bb4b0380f8 --- /dev/null +++ b/erpnext/patches/v12_0/add_eway_bill_in_delivery_note.py @@ -0,0 +1,19 @@ +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_field + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + + if not company: + return + + create_custom_field('Delivery Note', { + 'fieldname': 'ewaybill', + 'label': 'E-Way Bill No.', + 'fieldtype': 'Data', + 'depends_on': 'eval:(doc.docstatus === 1)', + 'allow_on_submit': 1, + 'insert_after': 'customer_name_in_arabic', + 'translatable': 0, + 'owner': 'Administrator' + }) \ No newline at end of file diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 28fb649025..a51c2f0954 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -3,6 +3,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ setup: function() {}, + apply_pricing_rule_on_item: function(item){ let effective_item_rate = item.price_list_rate; if (item.parenttype === "Sales Order" && item.blanket_order_rate) { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 51ab48a3ab..9146479b1e 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -490,7 +490,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ cost_center: item.cost_center, tax_category: me.frm.doc.tax_category, item_tax_template: item.item_tax_template, - child_docname: item.name, + child_docname: item.name } }, @@ -504,7 +504,20 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ me.apply_product_discount(d.free_item_data); } }, - () => me.frm.script_manager.trigger("price_list_rate", cdt, cdn), + () => { + // for internal customer instead of pricing rule directly apply valuation rate on item + if (me.frm.doc.is_internal_customer || me.frm.doc.is_internal_supplier) { + me.get_incoming_rate(item, me.frm.posting_date, me.frm.posting_time, + me.frm.doc.doctype, me.frm.doc.company); + } else { + me.frm.script_manager.trigger("price_list_rate", cdt, cdn); + } + }, + () => { + if (me.frm.doc.is_internal_customer || me.frm.doc.is_internal_supplier) { + me.calculate_taxes_and_totals(); + } + }, () => me.toggle_conversion_factor(item), () => { if(show_batch_dialog && !frappe.flags.hide_serial_batch_dialog) { @@ -528,6 +541,31 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }, + get_incoming_rate: function(item, posting_date, posting_time, voucher_type, company) { + + let item_args = { + 'item_code': item.item_code, + 'warehouse': in_list('Purchase Receipt', 'Purchase Invoice') ? item.from_warehouse : item.warehouse, + 'posting_date': posting_date, + 'posting_time': posting_time, + 'qty': item.qty * item.conversion_factor, + 'serial_no': item.serial_no, + 'voucher_type': voucher_type, + 'company': company, + 'allow_zero_valuation_rate': item.allow_zero_valuation_rate + } + + frappe.call({ + method: 'erpnext.stock.utils.get_incoming_rate', + args: { + args: item_args + }, + callback: function(r) { + frappe.model.set_value(item.doctype, item.name, 'rate', r.message); + } + }); + }, + add_taxes_from_item_tax_template: function(item_tax_map) { let me = this; 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 fa6fb706e9..8174da20cb 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 @@ -152,6 +152,7 @@ def create_purchase_invoices(): currency = 'INR', warehouse = 'Finished Goods - _GST', cost_center = 'Main - _GST', + expense_account = 'Cost of Goods Sold - _GST', do_not_save=1, ) @@ -181,6 +182,7 @@ def create_purchase_invoices(): currency = 'INR', warehouse = 'Finished Goods - _GST', cost_center = 'Main - _GST', + expense_account = 'Cost of Goods Sold - _GST', item = "Milk", do_not_save=1 ) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index cabfde40ef..970a831e0e 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -245,7 +245,16 @@ def make_custom_fields(update=True): 'insert_after': 'lr_date', 'print_hide': 1, 'translatable': 0 - } + }, + { + 'fieldname': 'ewaybill', + 'label': 'E-Way Bill No.', + 'fieldtype': 'Data', + 'depends_on': 'eval:(doc.docstatus === 1)', + 'allow_on_submit': 1, + 'insert_after': 'customer_name_in_arabic', + 'translatable': 0, + } ] si_ewaybill_fields = [ @@ -361,7 +370,7 @@ def make_custom_fields(update=True): }, { 'fieldname': 'ewaybill', - 'label': 'e-Way Bill No.', + 'label': 'E-Way Bill No.', 'fieldtype': 'Data', 'depends_on': 'eval:(doc.docstatus === 1)', 'allow_on_submit': 1, diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js index 1e59032db1..4d36cff1e6 100644 --- a/erpnext/regional/india/taxes.js +++ b/erpnext/regional/india/taxes.js @@ -32,6 +32,9 @@ erpnext.setup_auto_gst_taxation = (doctype) => { callback: function(r) { if(r.message) { frm.set_value('taxes_and_charges', r.message.taxes_and_charges); + } else if (frm.doc.is_internal_supplier || frm.doc.is_internal_customer) { + frm.set_value('taxes_and_charges', ''); + frm.set_value('taxes', []); } } }); diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 266affb6d0..f37b0e4cc9 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -141,18 +141,24 @@ def get_place_of_supply(party_details, doctype): address_name = party_details.shipping_address or party_details.supplier_address if address_name: - address = frappe.db.get_value("Address", address_name, ["gst_state", "gst_state_number"], as_dict=1) + address = frappe.db.get_value("Address", address_name, ["gst_state", "gst_state_number", "gstin"], as_dict=1) if address and address.gst_state and address.gst_state_number: + party_details.gstin = address.gstin return cstr(address.gst_state_number) + "-" + cstr(address.gst_state) @frappe.whitelist() def get_regional_address_details(party_details, doctype, company, return_taxes=None): - if isinstance(party_details, string_types): party_details = json.loads(party_details) party_details = frappe._dict(party_details) party_details.place_of_supply = get_place_of_supply(party_details, doctype) + + if is_internal_transfer(party_details, doctype): + party_details.taxes_and_charges = '' + party_details.taxes = '' + return + if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): master_doctype = "Sales Taxes and Charges Template" @@ -167,7 +173,6 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): master_doctype = "Purchase Taxes and Charges Template" - get_tax_template_for_sez(party_details, master_doctype, company, 'Supplier') get_tax_template_based_on_category(master_doctype, company, party_details) @@ -196,6 +201,17 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N if return_taxes: return party_details +def is_internal_transfer(party_details, doctype): + if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): + destination_gstin = party_details.company_gstin + elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): + destination_gstin = party_details.supplier_gstin + + if party_details.gstin == destination_gstin: + return True + else: + False + def get_tax_template_based_on_category(master_doctype, company, party_details): if not party_details.get('tax_category'): return @@ -218,7 +234,6 @@ def get_tax_template(master_doctype, company, is_inter_state, state_code): (not default_tax and not tax_category.gst_state): default_tax = frappe.db.get_value(master_doctype, {'disabled': 0, 'tax_category': tax_category.name}, 'name') - return default_tax def get_tax_template_for_sez(party_details, master_doctype, company, party_type): @@ -357,16 +372,13 @@ def calculate_hra_exemption_for_period(doc): return exemptions def get_ewb_data(dt, dn): - if dt != 'Sales Invoice': - frappe.throw(_('E-Way Bill JSON can only be generated from Sales Invoice')) - dn = dn.split(',') ewaybills = [] for doc_name in dn: doc = frappe.get_doc(dt, doc_name) - validate_sales_invoice(doc) + validate_doc(doc) data = frappe._dict({ "transporterId": "", @@ -376,7 +388,9 @@ def get_ewb_data(dt, dn): data.userGstin = data.fromGstin = doc.company_gstin data.supplyType = 'O' - if doc.gst_category in ['Registered Regular', 'SEZ']: + if dt == 'Delivery Note': + data.subSupplyType = 1 + elif doc.gst_category in ['Registered Regular', 'SEZ']: data.subSupplyType = 1 elif doc.gst_category in ['Overseas', 'Deemed Export']: data.subSupplyType = 3 @@ -535,7 +549,7 @@ def get_item_list(data, doc): return data -def validate_sales_invoice(doc): +def validate_doc(doc): if doc.docstatus != 1: frappe.throw(_('E-Way Bill JSON can only be generated from submitted document')) diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 89ce325a84..df563ee8ff 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -208,8 +208,7 @@ "fieldtype": "Link", "ignore_user_permissions": 1, "label": "Represents Company", - "options": "Company", - "unique": 1 + "options": "Company" }, { "depends_on": "represents_company", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 67e8bd2441..f8608d8ac0 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -92,6 +92,15 @@ frappe.ui.form.on("Delivery Note", { }, __('Create')); frm.page.set_inner_btn_group_as_primary(__('Create')); } + + if (frm.doc.docstatus === 1 && frm.doc.is_internal_customer && !frm.doc.inter_company_reference) { + frm.add_custom_button(__('Purchase Receipt'), function() { + frappe.model.open_mapped_doc({ + method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_inter_company_purchase_receipt', + frm: frm, + }) + }, __('Create')); + } }, to_warehouse: function(frm) { diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 86200ba26b..6f9d83d674 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -115,6 +115,8 @@ "campaign", "source", "column_break5", + "is_internal_customer", + "inter_company_reference", "per_billed", "customer_group", "territory", @@ -1234,13 +1236,27 @@ { "fieldname": "section_break_18", "fieldtype": "Section Break" + }, + { + "default": "0", + "fetch_from": "customer.is_internal_customer", + "fieldname": "is_internal_customer", + "fieldtype": "Check", + "label": "Is Internal Customer", + "read_only": 1 + }, + { + "fieldname": "inter_company_reference", + "fieldtype": "Link", + "label": "Inter Company Reference", + "options": "Purchase Receipt" } ], "icon": "fa fa-truck", "idx": 146, "is_submittable": 1, "links": [], - "modified": "2019-12-30 19:17:13.122644", + "modified": "2019-12-31 19:17:13.122644", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 013d50a06d..ea94c7b1ad 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -14,6 +14,7 @@ from frappe.desk.notifications import clear_doctype_notifications from frappe.model.mapper import get_mapped_doc from frappe.model.utils import get_fetch_values from frappe.utils import cint, flt +from erpnext.controllers.accounts_controller import get_taxes_and_charges form_grid_templates = { "items": "templates/form_grid/item_grid.html" @@ -587,3 +588,77 @@ def make_sales_return(source_name, target_doc=None): def update_delivery_note_status(docname, status): dn = frappe.get_doc("Delivery Note", docname) dn.update_status(status) + +@frappe.whitelist() +def make_inter_company_purchase_receipt(source_name, target_doc=None): + return make_inter_company_transaction("Delivery Note", source_name, target_doc) + +def make_inter_company_transaction(doctype, source_name, target_doc=None): + from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_transaction, get_inter_company_details + + if doctype == 'Delivery Note': + source_doc = frappe.get_doc(doctype, source_name) + target_doctype = "Purchase Receipt" + source_document_warehouse_field = 'target_warehouse' + target_document_warehouse_field = 'from_warehouse' + else: + source_doc = frappe.get_doc(doctype, source_name) + target_doctype = 'Delivery Note' + source_document_warehouse_field = 'from_warehouse' + target_document_warehouse_field = 'target_warehouse' + + validate_inter_company_transaction(source_doc, doctype) + details = get_inter_company_details(source_doc, doctype) + + def set_missing_values(source, target): + target.run_method("set_missing_values") + + if target.doctype == 'Purchase Receipt': + master_doctype = 'Purchase Taxes and Charges Template' + else: + master_doctype = 'Sales Taxes and Charges Template' + + if not target.get('taxes') and target.get('taxes_and_charges'): + for tax in get_taxes_and_charges(master_doctype, target.get('taxes_and_charges')): + target.append('taxes', tax) + + def update_details(source_doc, target_doc, source_parent): + target_doc.inter_company_invoice_reference = source_doc.name + if target_doc.doctype == 'Purchase Receipt': + target_doc.company = details.get("company") + target_doc.supplier = details.get("party") + target_doc.supplier_address = source_doc.company_address + target_doc.shipping_address = source_doc.shipping_address_name or source_doc.customer_address + target_doc.buying_price_list = source_doc.selling_price_list + target_doc.is_internal_supplier = 1 + target_doc.inter_company_reference = source_doc.name + else: + target_doc.company = details.get("company") + target_doc.customer = details.get("party") + target_doc.company_address = source_doc.supplier_address + target_doc.shipping_address_name = source_doc.shipping_address + target_doc.selling_price_list = source_doc.buying_price_list + target_doc.is_internal_customer = 1 + target_doc.inter_company_reference = source_doc.name + + doclist = get_mapped_doc(doctype, source_name, { + doctype: { + "doctype": target_doctype, + "postprocess": update_details, + "field_no_map": [ + "taxes_and_charges" + ] + }, + doctype +" Item": { + "doctype": target_doctype + " Item", + "field_map": { + source_document_warehouse_field: target_document_warehouse_field + }, + "field_no_map": [ + "warehouse" + ] + } + + }, target_doc, set_missing_values) + + return doclist diff --git a/erpnext/stock/doctype/delivery_note/regional/india.js b/erpnext/stock/doctype/delivery_note/regional/india.js index 22f4716ea5..0c1ca5caaa 100644 --- a/erpnext/stock/doctype/delivery_note/regional/india.js +++ b/erpnext/stock/doctype/delivery_note/regional/india.js @@ -2,3 +2,22 @@ erpnext.setup_auto_gst_taxation('Delivery Note'); +frappe.ui.form.on('Delivery Note', { + refresh: function(frm) { + if(frm.doc.docstatus == 1 && !frm.is_dirty() && !frm.doc.ewaybill) { + frm.add_custom_button('E-Way Bill JSON', () => { + var w = window.open( + frappe.urllib.get_full_url( + "/api/method/erpnext.regional.india.utils.generate_ewb_json?" + + "dt=" + encodeURIComponent(frm.doc.doctype) + + "&dn=" + encodeURIComponent(frm.doc.name) + ) + ); + if (!w) { + frappe.msgprint(__("Please enable pop-ups")); return; + } + }, __("Create")); + } + } +}) + diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 6b5e40e628..f3020e04ff 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -22,7 +22,7 @@ frappe.ui.form.on("Purchase Receipt", { frappe.set_route("Form", lcv.doctype, lcv.name); }, } - + frm.custom_make_buttons = { 'Stock Entry': 'Return', 'Purchase Invoice': 'Invoice' @@ -40,7 +40,7 @@ frappe.ui.form.on("Purchase Receipt", { filters: {'company': frm.doc.company } } }); - + }, onload: function(frm) { erpnext.queries.setup_queries(frm, "Warehouse", function() { @@ -62,6 +62,15 @@ frappe.ui.form.on("Purchase Receipt", { }, __('Create')); frm.page.set_inner_btn_group_as_primary(__('Create')); } + + if (frm.doc.docstatus === 1 && frm.doc.is_internal_supplier && !frm.doc.inter_company_reference) { + frm.add_custom_button(__('Delivery Note'), function() { + frappe.model.open_mapped_doc({ + method: 'erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_inter_company_delivery_note', + frm: cur_frm, + }) + }, __('Create')); + } }, company: function(frm) { diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 63ef7ca8d8..35446ecb1f 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -106,6 +106,8 @@ "range", "column_break4", "per_billed", + "is_internal_supplier", + "inter_company_reference", "subscription_detail", "auto_repeat", "printing_settings", @@ -1053,6 +1055,21 @@ "oldfieldtype": "Date", "print_width": "100px", "width": "100px" + }, + { + "default": "0", + "fetch_from": "supplier.is_internal_supplier", + "fieldname": "is_internal_supplier", + "fieldtype": "Check", + "label": "Is Internal Supplier", + "read_only": 1 + }, + { + "fieldname": "inter_company_reference", + "fieldtype": "Link", + "label": "Inter Company Reference", + "options": "Delivery Note", + "read_only": 1 } ], "icon": "fa fa-truck", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index fb123b9c1f..3b43690658 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -17,6 +17,7 @@ from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from six import iteritems +from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction form_grid_templates = { "items": "templates/form_grid/item_grid.html" @@ -223,6 +224,7 @@ class PurchaseReceipt(BuyingController): if not stock_value_diff: continue + gl_entries.append(self.get_gl_dict({ "account": warehouse_account[d.warehouse]["account"], "against": stock_rbnb, @@ -231,17 +233,23 @@ class PurchaseReceipt(BuyingController): "debit": stock_value_diff }, warehouse_account[d.warehouse]["account_currency"], item=d)) - # stock received but not billed - stock_rbnb_currency = get_account_currency(stock_rbnb) + # GL Entry for from warehouse or Stock Received but not billed + # Intentionally passed negative debit amount to avoid incorrect GL Entry validation + credit_currency = get_account_currency(warehouse_account[d.from_warehouse]['account']) \ + if d.from_warehouse else get_account_currency(stock_rbnb) + + credit_amount = flt(d.base_net_amount, d.precision("base_net_amount")) \ + if credit_currency == self.company_currency else flt(d.net_amount, d.precision("net_amount")) + gl_entries.append(self.get_gl_dict({ - "account": stock_rbnb, + "account": warehouse_account[d.from_warehouse]['account'] \ + if d.from_warehouse else stock_rbnb, "against": warehouse_account[d.warehouse]["account"], "cost_center": d.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(d.base_net_amount, d.precision("base_net_amount")), - "credit_in_account_currency": flt(d.base_net_amount, d.precision("base_net_amount")) \ - if stock_rbnb_currency==self.company_currency else flt(d.net_amount, d.precision("net_amount")) - }, stock_rbnb_currency, item=d)) + "debit": -1 * flt(d.base_net_amount, d.precision("base_net_amount")), + "debit_in_account_currency": -1 * credit_amount + }, credit_currency, item=d)) negative_expense_to_be_booked += flt(d.item_tax_amount) @@ -287,7 +295,7 @@ class PurchaseReceipt(BuyingController): "remarks": self.get("remarks") or _("Accounting Entry for Stock"), "debit": divisional_loss, "project": d.project - }, stock_rbnb_currency, item=d)) + }, credit_currency, item=d)) elif d.warehouse not in warehouse_with_no_account or \ d.rejected_warehouse not in warehouse_with_no_account: @@ -610,6 +618,10 @@ def make_stock_entry(source_name,target_doc=None): return doclist +@frappe.whitelist() +def make_inter_company_delivery_note(source_name, target_doc=None): + return make_inter_company_transaction("Purchase Receipt", source_name, target_doc) + def get_item_account_wise_additional_cost(purchase_document): landed_cost_vouchers = frappe.get_all("Landed Cost Purchase Receipt", fields=["parent"], filters = {"receipt_document": purchase_document, "docstatus": 1}) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 6113ee66d9..253d5f04c7 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -450,6 +450,83 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEquals(pi2.items[0].qty, 2) self.assertEquals(pi2.items[1].qty, 1) + def test_stock_transfer_from_purchase_receipt(self): + set_perpetual_inventory(1) + pr = make_purchase_receipt(do_not_save=1) + pr.supplier_warehouse = '' + pr.items[0].from_warehouse = '_Test Warehouse 2 - _TC' + + pr.submit() + + gl_entries = get_gl_entries('Purchase Receipt', pr.name) + sl_entries = get_sl_entries('Purchase Receipt', pr.name) + + self.assertFalse(gl_entries) + + expected_sle = { + '_Test Warehouse 2 - _TC': -5, + '_Test Warehouse - _TC': 5 + } + + for sle in sl_entries: + self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty) + + set_perpetual_inventory(0) + + def test_stock_transfer_from_purchase_receipt_with_valuation(self): + set_perpetual_inventory(1) + warehouse = frappe.get_doc('Warehouse', '_Test Warehouse 2 - _TC') + warehouse.account = '_Test Account Stock In Hand - _TC' + warehouse.save() + + pr = make_purchase_receipt(do_not_save=1) + pr.items[0].from_warehouse = '_Test Warehouse 2 - _TC' + pr.supplier_warehouse = '' + + + pr.append('taxes', { + 'charge_type': 'On Net Total', + 'account_head': '_Test Account Shipping Charges - _TC', + 'category': 'Valuation and Total', + 'cost_center': 'Main - _TC', + 'description': 'Test', + 'rate': 9 + }) + + pr.submit() + + gl_entries = get_gl_entries('Purchase Receipt', pr.name) + sl_entries = get_sl_entries('Purchase Receipt', pr.name) + + expected_gle = [ + ['Stock In Hand - _TC', 272.5, 0.0], + ['_Test Account Stock In Hand - _TC', 0.0, 250.0], + ['_Test Account Shipping Charges - _TC', 0.0, 22.5] + ] + + expected_sle = { + '_Test Warehouse 2 - _TC': -5, + '_Test Warehouse - _TC': 5 + } + + for sle in sl_entries: + self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty) + + for i, gle in enumerate(gl_entries): + self.assertEqual(gle.account, expected_gle[i][0]) + self.assertEqual(gle.debit, expected_gle[i][1]) + self.assertEqual(gle.credit, expected_gle[i][2]) + + warehouse.account = '' + warehouse.save() + set_perpetual_inventory(0) + + +def get_sl_entries(voucher_type, voucher_no): + return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference + from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s + order by posting_time desc""", (voucher_type, voucher_no), as_dict=1) + def get_gl_entries(voucher_type, voucher_no): return frappe.db.sql("""select account, debit, credit, cost_center from `tabGL Entry` where voucher_type=%s and voucher_no=%s 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 16ec8db335..bfb7577a63 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-05-24 19:29:10", "doctype": "DocType", @@ -67,6 +68,7 @@ "warehouse_and_reference", "warehouse", "rejected_warehouse", + "from_warehouse", "purchase_order", "material_request", "column_break_40", @@ -815,15 +817,23 @@ "fetch_from": "item_code.asset_category", "fieldname": "asset_category", "fieldtype": "Link", - "in_preview": 1, "label": "Asset Category", "options": "Asset Category", "read_only": 1 + }, + { + "fieldname": "from_warehouse", + "fieldtype": "Link", + "hidden": 1, + "ignore_user_permissions": 1, + "label": "Supplier Warehouse", + "options": "Warehouse" } ], "idx": 1, "istable": 1, - "modified": "2019-10-14 16:03:25.499557", + "links": [], + "modified": "2020-01-13 16:03:34.879827", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index d0efaa228c..53964f24c4 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -973,6 +973,7 @@ def get_default_bom(item_code=None): if bom: return bom +@frappe.whitelist() def get_valuation_rate(item_code, company, warehouse=None): item = get_item_defaults(item_code, company) item_group = get_item_group_defaults(item_code, company) From 0ce9e0cc1f40c458a60f33d42f84674a9b7aec82 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 18 Feb 2020 16:07:34 +0530 Subject: [PATCH 056/102] feat: Group by AR/AP report (#20573) * feat: Group by AR/AP report * fix: Do not consider total row in charts * fix: Subtotal row for last party --- .../accounts_payable/accounts_payable.js | 15 ++++ .../accounts_receivable.js | 16 +++- .../accounts_receivable.py | 75 ++++++++++++++++--- 3 files changed, 96 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index b1f427ca7f..df700ec9d3 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -100,6 +100,11 @@ frappe.query_reports["Accounts Payable"] = { "fieldtype": "Link", "options": "Supplier Group" }, + { + "fieldname": "group_by_party", + "label": __("Group By Supplier"), + "fieldtype": "Check" + }, { "fieldname":"based_on_payment_terms", "label": __("Based On Payment Terms"), @@ -112,6 +117,16 @@ frappe.query_reports["Accounts Payable"] = { "hidden": 1 } ], + + "formatter": function(value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + if (data && data.bold) { + value = value.bold(); + + } + return value; + }, + onload: function(report) { report.page.add_inner_button(__("Accounts Payable Summary"), function() { var filters = report.get_values(); diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index 9b4dda2f69..5d0154f597 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -87,7 +87,7 @@ frappe.query_reports["Accounts Receivable"] = { frappe.query_report.set_filter_value('payment_terms', value["payment_terms"]); }); - frappe.db.get_value('Customer Credit Limit', {'parent': customer, 'company': company}, + frappe.db.get_value('Customer Credit Limit', {'parent': customer, 'company': company}, ["credit_limit"], function(value) { if (value) { frappe.query_report.set_filter_value('credit_limit', value["credit_limit"]); @@ -131,6 +131,11 @@ frappe.query_reports["Accounts Receivable"] = { "fieldtype": "Link", "options": "Sales Person" }, + { + "fieldname": "group_by_party", + "label": __("Group By Customer"), + "fieldtype": "Check" + }, { "fieldname":"based_on_payment_terms", "label": __("Based On Payment Terms"), @@ -177,6 +182,15 @@ frappe.query_reports["Accounts Receivable"] = { } ], + "formatter": function(value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + if (data && data.bold) { + value = value.bold(); + + } + return value; + }, + onload: function(report) { report.page.add_inner_button(__("Accounts Receivable Summary"), function() { var filters = report.get_values(); diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index f82146a1df..0438f6d09e 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -46,7 +46,7 @@ class ReceivablePayableReport(object): self.get_columns() self.get_data() self.get_chart_data() - return self.columns, self.data, None, self.chart + return self.columns, self.data, None, self.chart, None, self.skip_total_row def set_defaults(self): if not self.filters.get("company"): @@ -57,6 +57,12 @@ class ReceivablePayableReport(object): self.party_type = self.filters.party_type self.party_details = {} self.invoices = set() + self.skip_total_row = 0 + + if self.filters.get('group_by_party'): + self.previous_party='' + self.total_row_map = {} + self.skip_total_row = 1 def get_data(self): self.get_gl_entries() @@ -102,6 +108,12 @@ class ReceivablePayableReport(object): ) self.get_invoices(gle) + if self.filters.get('group_by_party'): + self.init_subtotal_row(gle.party) + + if self.filters.get('group_by_party'): + self.init_subtotal_row('Total') + def get_invoices(self, gle): if gle.voucher_type in ('Sales Invoice', 'Purchase Invoice'): if self.filters.get("sales_person"): @@ -111,6 +123,20 @@ class ReceivablePayableReport(object): else: self.invoices.add(gle.voucher_no) + def init_subtotal_row(self, party): + if not self.total_row_map.get(party): + self.total_row_map.setdefault(party, { + 'party': party, + 'bold': 1 + }) + + for field in self.get_currency_fields(): + self.total_row_map[party][field] = 0.0 + + def get_currency_fields(self): + return ['invoiced', 'paid', 'credit_note', 'outstanding', 'range1', + 'range2', 'range3', 'range4', 'range5'] + def update_voucher_balance(self, gle): # get the row where this balance needs to be updated # if its a payment, it will return the linked invoice or will be considered as advance @@ -135,6 +161,18 @@ class ReceivablePayableReport(object): # advance / unlinked payment or other adjustment row.paid -= gle_balance + def update_sub_total_row(self, row, party): + total_row = self.total_row_map.get(party) + + for field in self.get_currency_fields(): + total_row[field] += row.get(field, 0.0) + + def append_subtotal_row(self, party): + sub_total_row = self.total_row_map.get(party) + self.data.append(sub_total_row) + self.data.append({}) + self.update_sub_total_row(sub_total_row, 'Total') + def get_voucher_balance(self, gle): if self.filters.get("sales_person"): against_voucher = gle.against_voucher or gle.voucher_no @@ -192,11 +230,22 @@ class ReceivablePayableReport(object): else: self.append_row(row) + if self.filters.get('group_by_party'): + self.append_subtotal_row(self.previous_party) + self.data.append(self.total_row_map.get('Total')) + def append_row(self, row): self.allocate_future_payments(row) self.set_invoice_details(row) self.set_party_details(row) self.set_ageing(row) + + if self.filters.get('group_by_party'): + self.update_sub_total_row(row, row.party) + if self.previous_party and (self.previous_party != row.party): + self.append_subtotal_row(self.previous_party) + self.previous_party = row.party + self.data.append(row) def set_invoice_details(self, row): @@ -503,6 +552,7 @@ class ReceivablePayableReport(object): # get all the GL entries filtered by the given filters conditions, values = self.prepare_conditions() + order_by = self.get_order_by_condition() if self.filters.get(scrub(self.party_type)): select_fields = "debit_in_account_currency as debit, credit_in_account_currency as credit" @@ -520,9 +570,8 @@ class ReceivablePayableReport(object): and party_type=%s and (party is not null and party != '') and posting_date <= %s - {1} - order by posting_date, party""" - .format(select_fields, conditions), values, as_dict=True) + {1} {2}""" + .format(select_fields, conditions, order_by), values, as_dict=True) def get_sales_invoices_or_customers_based_on_sales_person(self): if self.filters.get("sales_person"): @@ -557,6 +606,12 @@ class ReceivablePayableReport(object): return " and ".join(conditions), values + def get_order_by_condition(self): + if self.filters.get('group_by_party'): + return "order by party, posting_date" + else: + return "order by posting_date, party" + def add_common_filters(self, conditions, values, party_type_field): if self.filters.company: conditions.append("company=%s") @@ -736,11 +791,13 @@ class ReceivablePayableReport(object): def get_chart_data(self): rows = [] for row in self.data: - values = [row.range1, row.range2, row.range3, row.range4, row.range5] - precision = cint(frappe.db.get_default("float_precision")) or 2 - rows.append({ - 'values': [flt(val, precision) for val in values] - }) + row = frappe._dict(row) + if not cint(row.bold): + values = [row.range1, row.range2, row.range3, row.range4, row.range5] + precision = cint(frappe.db.get_default("float_precision")) or 2 + rows.append({ + 'values': [flt(val, precision) for val in values] + }) self.chart = { "data": { From 7cda218af36a78c7af4d370be1ff709ff8bb661d Mon Sep 17 00:00:00 2001 From: racitup Date: Mon, 20 Jan 2020 18:56:35 +0000 Subject: [PATCH 057/102] fix: pymysql.err.InternalError about t2.bank_account_no due to removal of field from Journal Entry Account table: #20343 --- .../doctype/bank_reconciliation/bank_reconciliation.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.py b/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.py index 90cdf834c5..3613a1ef69 100644 --- a/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.py +++ b/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.py @@ -21,10 +21,6 @@ class BankReconciliation(Document): if not self.include_reconciled_entries: condition = " and (clearance_date is null or clearance_date='0000-00-00')" - account_cond = "" - if self.bank_account_no: - account_cond = " and t2.bank_account_no = {0}".format(frappe.db.escape(self.bank_account_no)) - journal_entries = frappe.db.sql(""" select "Journal Entry" as payment_document, t1.name as payment_entry, @@ -36,10 +32,10 @@ class BankReconciliation(Document): where t2.parent = t1.name and t2.account = %s and t1.docstatus=1 and t1.posting_date >= %s and t1.posting_date <= %s - and ifnull(t1.is_opening, 'No') = 'No' {0} {1} + and ifnull(t1.is_opening, 'No') = 'No' {0} group by t2.account, t1.name order by t1.posting_date ASC, t1.name DESC - """.format(condition, account_cond), (self.bank_account, self.from_date, self.to_date), as_dict=1) + """.format(condition), (self.bank_account, self.from_date, self.to_date), as_dict=1) if self.bank_account_no: condition = " and bank_account = %(bank_account_no)s" From 1548e20b7858663f4e68de4d4079cd05bfe5787e Mon Sep 17 00:00:00 2001 From: racitup Date: Mon, 20 Jan 2020 19:41:08 +0000 Subject: [PATCH 058/102] fix: KeyError about bank_account_no due to non-existent field: #20343 --- .../doctype/bank_reconciliation/bank_reconciliation.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.py b/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.py index 3613a1ef69..d5ba49abd0 100644 --- a/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.py +++ b/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.py @@ -68,11 +68,10 @@ class BankReconciliation(Document): from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account where sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name - and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s {0} + and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s order by si.posting_date ASC, si.name DESC - """.format(condition), - {"account":self.bank_account, "from":self.from_date, "to":self.to_date}, as_dict=1) + """, {"account":self.bank_account, "from":self.from_date, "to":self.to_date}, as_dict=1) entries = sorted(list(payment_entries)+list(journal_entries+list(pos_entries)), key=lambda k: k['posting_date'] or getdate(nowdate())) From 9a47c3dc8c918cd98180e4362caddd21a7fa6ec1 Mon Sep 17 00:00:00 2001 From: racitup Date: Tue, 21 Jan 2020 00:47:10 +0000 Subject: [PATCH 059/102] fix: Plaid automatic_synchronization TypeError on filter & add info log message #20343 --- .../doctype/plaid_settings/plaid_settings.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index 92111337fd..be18541983 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -133,8 +133,11 @@ def sync_transactions(bank, bank_account): try: transactions = get_transactions(bank=bank, bank_account=bank_account, start_date=start_date, end_date=end_date) + result = [] if transactions: + frappe.logger().info("Plaid is adding {} Bank Transactions from '{}' between {} and {}".format( + len(transactions), bank_account, start_date, end_date)) for transaction in transactions: result.append(new_bank_transaction(transaction)) @@ -201,7 +204,7 @@ def automatic_synchronization(): settings = frappe.get_doc("Plaid Settings", "Plaid Settings") if settings.enabled == 1 and settings.automatic_sync == 1: - plaid_accounts = frappe.get_all("Bank Account", filter={"integration_id": ["!=", ""]}, fields=["name", "bank"]) + plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"]) for plaid_account in plaid_accounts: frappe.enqueue("erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions", bank=plaid_account.bank, bank_account=plaid_account.name) From 3c5bb80b5a4fb741a5a75db479d3afc7b38f9259 Mon Sep 17 00:00:00 2001 From: racitup Date: Tue, 21 Jan 2020 15:07:47 +0000 Subject: [PATCH 060/102] fix: Plaid transaction import order, transaction_id duplicate check, added transaction category tags --- .../doctype/plaid_settings/plaid_settings.py | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index be18541983..7083950c56 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -10,6 +10,7 @@ from frappe.model.document import Document from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_connector import PlaidConnector from frappe.utils import getdate, formatdate, today, add_months +from frappe.desk.doctype.tag.tag import add_tag class PlaidSettings(Document): pass @@ -135,11 +136,11 @@ def sync_transactions(bank, bank_account): transactions = get_transactions(bank=bank, bank_account=bank_account, start_date=start_date, end_date=end_date) result = [] - if transactions: - frappe.logger().info("Plaid is adding {} Bank Transactions from '{}' between {} and {}".format( - len(transactions), bank_account, start_date, end_date)) - for transaction in transactions: - result.append(new_bank_transaction(transaction)) + for transaction in reversed(transactions): + result += new_bank_transaction(transaction) + + frappe.logger().info("Plaid added {} new Bank Transactions from '{}' between {} and {}".format( + len(result), bank_account, start_date, end_date)) frappe.db.set_value("Bank Account", bank_account, "last_integration_date", getdate(end_date)) @@ -178,6 +179,13 @@ def new_bank_transaction(transaction): status = "Pending" if transaction["pending"] == "True" else "Settled" + try: + tags = [] + tags += transaction["category"] + tags += ["Plaid Cat. {}".format(transaction["category_id"])] + except KeyError: + pass + if not frappe.db.exists("Bank Transaction", dict(transaction_id=transaction["transaction_id"])): try: new_transaction = frappe.get_doc({ @@ -188,11 +196,16 @@ def new_bank_transaction(transaction): "debit": debit, "credit": credit, "currency": transaction["iso_currency_code"], + "transaction_id": transaction["transaction_id"], + "reference_number": transaction["payment_meta"]["reference_number"], "description": transaction["name"] }) new_transaction.insert() new_transaction.submit() + for tag in tags: + add_tag(tag, "Bank Transaction", new_transaction.name) + result.append(new_transaction.name) except Exception: From 719678957a21b14da9055c3718be4e0f1126e411 Mon Sep 17 00:00:00 2001 From: racitup Date: Wed, 22 Jan 2020 04:32:15 +0000 Subject: [PATCH 061/102] fix: Bank Reconciliation Bank Account and Bank Account No field names --- .../bank_reconciliation.js | 6 +++--- .../bank_reconciliation.json | 18 +++++++--------- .../bank_reconciliation.py | 21 ++++++++----------- .../rename_bank_reconciliation_fields.py | 16 ++++++++++++++ 4 files changed, 36 insertions(+), 25 deletions(-) create mode 100644 erpnext/patches/v12_0/rename_bank_reconciliation_fields.py diff --git a/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.js b/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.js index 7c94455b9b..19fadbf6de 100644 --- a/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.js +++ b/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.js @@ -3,16 +3,16 @@ frappe.ui.form.on("Bank Reconciliation", { setup: function(frm) { - frm.add_fetch("bank_account", "account_currency", "account_currency"); + frm.add_fetch("account", "account_currency", "account_currency"); }, onload: function(frm) { let default_bank_account = frappe.defaults.get_user_default("Company")? locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: ""; - frm.set_value("bank_account", default_bank_account); + frm.set_value("account", default_bank_account); - frm.set_query("bank_account", function() { + frm.set_query("account", function() { return { "filters": { "account_type": ["in",["Bank","Cash"]], diff --git a/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.json b/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.json index e2f967dacc..b85ef3e9c4 100644 --- a/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.json +++ b/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.json @@ -19,10 +19,9 @@ "bold": 0, "collapsible": 0, "columns": 0, - "description": "Select account head of the bank where cheque was deposited.", - "fetch_from": "bank_account_no.account", + "fetch_from": "bank_account.account", "fetch_if_empty": 1, - "fieldname": "bank_account", + "fieldname": "account", "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, @@ -31,7 +30,7 @@ "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 0, - "label": "Bank Account", + "label": "Account", "length": 0, "no_copy": 0, "options": "Account", @@ -164,7 +163,6 @@ "length": 0, "no_copy": 0, "permlevel": 0, - "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 0, @@ -183,8 +181,9 @@ "bold": 0, "collapsible": 0, "columns": 0, + "description": "Select the Bank Account to reconcile.", "fetch_if_empty": 0, - "fieldname": "bank_account_no", + "fieldname": "bank_account", "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, @@ -193,12 +192,11 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Bank Account No", + "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, @@ -450,7 +448,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2019-04-09 18:41:06.110453", + "modified": "2020-01-22 00:00:00.000000", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Reconciliation", @@ -483,4 +481,4 @@ "track_changes": 0, "track_seen": 0, "track_views": 0 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.py b/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.py index d5ba49abd0..804bcd8bdf 100644 --- a/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.py +++ b/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.py @@ -30,15 +30,12 @@ class BankReconciliation(Document): from `tabJournal Entry` t1, `tabJournal Entry Account` t2 where - t2.parent = t1.name and t2.account = %s and t1.docstatus=1 - and t1.posting_date >= %s and t1.posting_date <= %s - and ifnull(t1.is_opening, 'No') = 'No' {0} + t2.parent = t1.name and t2.account = %(account)s and t1.docstatus=1 + and t1.posting_date >= %(from)s and t1.posting_date <= %(to)s + and ifnull(t1.is_opening, 'No') = 'No' %(condition)s group by t2.account, t1.name order by t1.posting_date ASC, t1.name DESC - """.format(condition), (self.bank_account, self.from_date, self.to_date), as_dict=1) - - if self.bank_account_no: - condition = " and bank_account = %(bank_account_no)s" + """, {"condition":condition, "account":self.account, "from":self.from_date, "to":self.to_date}, as_dict=1) payment_entries = frappe.db.sql(""" select @@ -51,12 +48,12 @@ class BankReconciliation(Document): from `tabPayment Entry` where (paid_from=%(account)s or paid_to=%(account)s) and docstatus=1 - and posting_date >= %(from)s and posting_date <= %(to)s {0} + and posting_date >= %(from)s and posting_date <= %(to)s + and bank_account = %(bank_account)s order by posting_date ASC, name DESC - """.format(condition), - {"account":self.bank_account, "from":self.from_date, - "to":self.to_date, "bank_account_no": self.bank_account_no}, as_dict=1) + """, {"account":self.account, "from":self.from_date, + "to":self.to_date, "bank_account": self.bank_account}, as_dict=1) pos_entries = [] if self.include_pos_transactions: @@ -71,7 +68,7 @@ class BankReconciliation(Document): and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s order by si.posting_date ASC, si.name DESC - """, {"account":self.bank_account, "from":self.from_date, "to":self.to_date}, as_dict=1) + """, {"account":self.account, "from":self.from_date, "to":self.to_date}, as_dict=1) entries = sorted(list(payment_entries)+list(journal_entries+list(pos_entries)), key=lambda k: k['posting_date'] or getdate(nowdate())) diff --git a/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py b/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py new file mode 100644 index 0000000000..8918b9df7a --- /dev/null +++ b/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py @@ -0,0 +1,16 @@ +import frappe + +def _rename_single_field(**kwargs): + count = frappe.db.sql("SELECT COUNT(*) FROM tabSingles WHERE doctype='{doctype}' AND field='{new_name}';".format(**kwargs))[0][0] + if count == 0: + frappe.db.sql("UPDATE tabSingles SET field='{new_name}' WHERE doctype='{doctype}' AND field='{old_name}';".format(**kwargs)) + +def execute(): + BR = "Bank Reconciliation" + AC = "account" + BA = "bank_account" + BAN = "bank_account_no" + + _rename_single_field(doctype = BR, old_name = BA , new_name = AC) + _rename_single_field(doctype = BR, old_name = BAN, new_name = BA) + frappe.reload_doc("Accounts", "doctype", BR) From 547a0bb909608144f6cc7da1480016cd0d908c5b Mon Sep 17 00:00:00 2001 From: racitup Date: Wed, 22 Jan 2020 04:41:41 +0000 Subject: [PATCH 062/102] fix: attempt at pymysql InternalError 1054 about clearance_date in field list when removing payments from bank transactions --- .../bank_transaction_payments.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction_payments/bank_transaction_payments.json b/erpnext/accounts/doctype/bank_transaction_payments/bank_transaction_payments.json index a75e866997..ab3f60d32c 100644 --- a/erpnext/accounts/doctype/bank_transaction_payments/bank_transaction_payments.json +++ b/erpnext/accounts/doctype/bank_transaction_payments/bank_transaction_payments.json @@ -110,6 +110,15 @@ "set_only_once": 0, "translatable": 0, "unique": 0 + }, + { + "depends_on": "eval:doc.docstatus==1", + "fieldname": "clearance_date", + "fieldtype": "Date", + "label": "Clearance Date", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "has_web_view": 0, @@ -122,7 +131,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2018-12-06 10:57:02.635141", + "modified": "2020-01-22 00:00:00.000000", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Transaction Payments", @@ -138,4 +147,4 @@ "track_changes": 1, "track_seen": 0, "track_views": 0 -} \ No newline at end of file +} From 805c3efa59a6444d801e530f813606a51c72ebbf Mon Sep 17 00:00:00 2001 From: racitup Date: Fri, 24 Jan 2020 23:40:16 +0000 Subject: [PATCH 063/102] fix: manually added patch for bank reconciliatio fields since develop branch was in a different place --- erpnext/patches.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index a8938406f2..824b2fee4b 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -656,4 +656,5 @@ erpnext.patches.v12_0.set_lead_title_field erpnext.patches.v12_0.set_permission_einvoicing erpnext.patches.v12_0.set_published_in_hub_tracked_item erpnext.patches.v12_0.set_job_offer_applicant_email -erpnext.patches.v12_0.create_irs_1099_field_united_states \ No newline at end of file +erpnext.patches.v12_0.create_irs_1099_field_united_states +erpnext.patches.v12_0.rename_bank_reconciliation_fields # 2020-01-22 From f2fb96398614b8935fc42a9029e5c0eed6201433 Mon Sep 17 00:00:00 2001 From: racitup Date: Thu, 30 Jan 2020 14:48:42 +0000 Subject: [PATCH 064/102] Ignore codacy SQL injection warning (internal code only) and add copyright notice --- erpnext/patches/v12_0/rename_bank_reconciliation_fields.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py b/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py index 8918b9df7a..01b1dde607 100644 --- a/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py +++ b/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py @@ -1,9 +1,12 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + import frappe def _rename_single_field(**kwargs): - count = frappe.db.sql("SELECT COUNT(*) FROM tabSingles WHERE doctype='{doctype}' AND field='{new_name}';".format(**kwargs))[0][0] + count = frappe.db.sql("SELECT COUNT(*) FROM tabSingles WHERE doctype='{doctype}' AND field='{new_name}';".format(**kwargs))[0][0] #nosec if count == 0: - frappe.db.sql("UPDATE tabSingles SET field='{new_name}' WHERE doctype='{doctype}' AND field='{old_name}';".format(**kwargs)) + frappe.db.sql("UPDATE tabSingles SET field='{new_name}' WHERE doctype='{doctype}' AND field='{old_name}';".format(**kwargs)) #nosec def execute(): BR = "Bank Reconciliation" From 16445305d5fe05799c28a8e15b25a99dccc952bb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 7 Feb 2020 15:25:43 +0530 Subject: [PATCH 065/102] fix: Styling and minor fixes --- .../bank_reconciliation/bank_reconciliation.py | 6 +++--- .../doctype/plaid_settings/plaid_settings.json | 7 ++++--- .../v12_0/rename_bank_reconciliation_fields.py | 11 +++-------- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.py b/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.py index 804bcd8bdf..52bbe3327a 100644 --- a/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.py +++ b/erpnext/accounts/doctype/bank_reconciliation/bank_reconciliation.py @@ -35,7 +35,7 @@ class BankReconciliation(Document): and ifnull(t1.is_opening, 'No') = 'No' %(condition)s group by t2.account, t1.name order by t1.posting_date ASC, t1.name DESC - """, {"condition":condition, "account":self.account, "from":self.from_date, "to":self.to_date}, as_dict=1) + """, {"condition":condition, "account": self.account, "from": self.from_date, "to": self.to_date}, as_dict=1) payment_entries = frappe.db.sql(""" select @@ -52,8 +52,8 @@ class BankReconciliation(Document): and bank_account = %(bank_account)s order by posting_date ASC, name DESC - """, {"account":self.account, "from":self.from_date, - "to":self.to_date, "bank_account": self.bank_account}, as_dict=1) + """, {"account": self.account, "from":self.from_date, + "to": self.to_date, "bank_account": self.bank_account}, as_dict=1) pos_entries = [] if self.include_pos_transactions: diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json index df77ad8bc9..d8203d7390 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json @@ -49,9 +49,10 @@ }, { "fieldname": "plaid_env", - "fieldtype": "Data", + "fieldtype": "Select", "in_list_view": 1, - "label": "Plaid Environment" + "label": "Plaid Environment", + "options": "sandbox\ndevelopment\nproduction" }, { "fieldname": "column_break_2", @@ -69,7 +70,7 @@ ], "issingle": 1, "links": [], - "modified": "2020-01-05 10:00:22.137832", + "modified": "2020-02-07 15:21:11.616231", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "Plaid Settings", diff --git a/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py b/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py index 01b1dde607..caeda8ae61 100644 --- a/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py +++ b/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py @@ -9,11 +9,6 @@ def _rename_single_field(**kwargs): frappe.db.sql("UPDATE tabSingles SET field='{new_name}' WHERE doctype='{doctype}' AND field='{old_name}';".format(**kwargs)) #nosec def execute(): - BR = "Bank Reconciliation" - AC = "account" - BA = "bank_account" - BAN = "bank_account_no" - - _rename_single_field(doctype = BR, old_name = BA , new_name = AC) - _rename_single_field(doctype = BR, old_name = BAN, new_name = BA) - frappe.reload_doc("Accounts", "doctype", BR) + _rename_single_field(doctype = "Bank Reconciliation", old_name = "bank_account" , new_name = "account") + _rename_single_field(doctype = "Bank Reconciliation", old_name = "bank_account_no", new_name = "bank_account") + frappe.reload_doc("Accounts", "doctype", "Bank Reconciliation") From 8773dc3ffeee8b3e3d258d0b34a842bd328a88b7 Mon Sep 17 00:00:00 2001 From: Khushal Trivedi Date: Tue, 18 Feb 2020 19:37:21 +0530 Subject: [PATCH 066/102] fix: student max gropu cannot be zero or less than zero --- erpnext/education/doctype/student_group/student_group.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/education/doctype/student_group/student_group.py b/erpnext/education/doctype/student_group/student_group.py index 9ec94384d5..c92c69b6b4 100644 --- a/erpnext/education/doctype/student_group/student_group.py +++ b/erpnext/education/doctype/student_group/student_group.py @@ -26,8 +26,8 @@ class StudentGroup(Document): frappe.throw(_("Please select Program")) def validate_strength(self): - if self.max_strength < 0: - frappe.throw(_("""Cannot enroll less than 0 students for this student group.""")) + if self.max_strength <= 0: + frappe.throw(_("""Cannot enroll less than or equal to 0 students for this student group.""")) if self.max_strength and len(self.students) > self.max_strength: frappe.throw(_("""Cannot enroll more than {0} students for this student group.""").format(self.max_strength)) From bde13b98e0718bdcd0e74d78cba190472a8202cd Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 18 Feb 2020 21:35:59 +0530 Subject: [PATCH 067/102] fix: Set Query on warehouse fields in Stock Settings --- erpnext/stock/doctype/stock_settings/stock_settings.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.js b/erpnext/stock/doctype/stock_settings/stock_settings.js index 49ce3d8ef7..755c430718 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.js +++ b/erpnext/stock/doctype/stock_settings/stock_settings.js @@ -3,6 +3,15 @@ frappe.ui.form.on('Stock Settings', { refresh: function(frm) { + let filters = function() { + return { + filters : { + is_group : 0 + } + }; + } + frm.set_query("default_warehouse", filters); + frm.set_query("sample_retention_warehouse", filters); } }); From 120f75026f945e1297fa689c1e18a152cf506cfb Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 19 Feb 2020 11:05:58 +0530 Subject: [PATCH 068/102] fix: Server side validation for Warehouses --- erpnext/stock/doctype/stock_settings/stock_settings.js | 6 +++--- erpnext/stock/doctype/stock_settings/stock_settings.py | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.js b/erpnext/stock/doctype/stock_settings/stock_settings.js index 755c430718..cc0e2cfc42 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.js +++ b/erpnext/stock/doctype/stock_settings/stock_settings.js @@ -5,11 +5,11 @@ frappe.ui.form.on('Stock Settings', { refresh: function(frm) { let filters = function() { return { - filters : { + filters : { is_group : 0 - } + } }; - } + }; frm.set_query("default_warehouse", filters); frm.set_query("sample_retention_warehouse", filters); diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 65de2e58d3..93b5eee75c 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -30,9 +30,17 @@ class StockSettings(Document): frappe.make_property_setter({'fieldname': name, 'property': 'hidden', 'value': 0 if self.show_barcode_field else 1}) + self.validate_warehouses() self.cant_change_valuation_method() self.validate_clean_description_html() + def validate_warehouses(self): + warehouse_fields = ["default_warehouse", "sample_retention_warehouse"] + for field in warehouse_fields: + if frappe.db.get_value("Warehouse", self.get(field), "is_group"): + frappe.throw(_("Group Warehouses cannot be used in transactions. Please change the value of {0}") \ + .format(frappe.bold(self.meta.get_field(field).label)), title =_("Incorrect Warehouse")) + def cant_change_valuation_method(self): db_valuation_method = frappe.db.get_single_value("Stock Settings", "valuation_method") From e5b59c781f56e32884812f220ae428e825c5fb91 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Wed, 19 Feb 2020 13:08:18 +0530 Subject: [PATCH 069/102] fix: show priority in Issue list view --- erpnext/support/doctype/issue/issue.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 222554bda1..53af80cb5e 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "naming_series:", @@ -119,6 +120,7 @@ "default": "Medium", "fieldname": "priority", "fieldtype": "Link", + "in_list_view": 1, "in_standard_filter": 1, "label": "Priority", "options": "Issue Priority" @@ -363,8 +365,9 @@ ], "icon": "fa fa-ticket", "idx": 7, - "modified": "2019-09-11 09:03:57.465623", - "modified_by": "himanshu@erpnext.com", + "links": [], + "modified": "2020-02-18 21:26:35.636013", + "modified_by": "Administrator", "module": "Support", "name": "Issue", "owner": "Administrator", From 1a11cb5a07ef701c298f2c95256effb7ec7ab0bf Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 19 Feb 2020 16:46:00 +0530 Subject: [PATCH 070/102] test: syncing of odometer value --- .../doctype/vehicle_log/test_vehicle_log.py | 60 ++++++++++++------- erpnext/hr/doctype/vehicle_log/vehicle_log.py | 2 +- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py index 35400b0ca8..3770da73fc 100644 --- a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py +++ b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py @@ -8,25 +8,9 @@ import unittest from frappe.utils import nowdate,flt, cstr,random_string # test_records = frappe.get_test_records('Vehicle Log') class TestVehicleLog(unittest.TestCase): - def test_make_vehicle_log(self): - license_plate=random_string(10).upper() + def test_make_vehicle_log_and_syncing_of_odometer_value(self): employee_id=frappe.db.sql("""select name from `tabEmployee` order by modified desc limit 1""")[0][0] - vehicle = frappe.get_doc({ - "doctype": "Vehicle", - "license_plate": cstr(license_plate), - "make": "Maruti", - "model": "PCM", - "last_odometer":5000, - "acquisition_date":frappe.utils.nowdate(), - "location": "Mumbai", - "chassis_no": "1234ABCD", - "uom": "Litre", - "vehicle_value":frappe.utils.flt(500000) - }) - try: - vehicle.insert() - except frappe.DuplicateEntryError: - pass + license_plate = get_vehicle(employee_id) vehicle_log = frappe.get_doc({ "doctype": "Vehicle Log", "license_plate": cstr(license_plate), @@ -36,5 +20,41 @@ class TestVehicleLog(unittest.TestCase): "fuel_qty":frappe.utils.flt(50), "price": frappe.utils.flt(500) }) - vehicle_log.insert() - vehicle_log.submit() \ No newline at end of file + vehicle_log.save() + vehicle_log.submit() + + #checking value of vehicle odometer value on submit. + vehicle = frappe.get_doc("Vehicle", license_plate) + self.assertEqual(vehicle.last_odometer, vehicle_log.odometer) + + #checking value vehicle odometer on vehicle log cancellation. + last_odometer = vehicle_log.last_odometer + current_odometer = vehicle_log.odometer + distance_travelled = current_odometer - last_odometer + + vehicle_log.cancel() + vehicle.reload() + + self.assertEqual(vehicle.last_odometer, current_odometer - distance_travelled) + + +def get_vehicle(employee_id): + license_plate=random_string(10).upper() + vehicle = frappe.get_doc({ + "doctype": "Vehicle", + "license_plate": cstr(license_plate), + "make": "Maruti", + "model": "PCM", + "employee": employee_id, + "last_odometer":5000, + "acquisition_date":frappe.utils.nowdate(), + "location": "Mumbai", + "chassis_no": "1234ABCD", + "uom": "Litre", + "vehicle_value":frappe.utils.flt(500000) + }) + try: + vehicle.insert() + except frappe.DuplicateEntryError: + pass + return license_plate \ No newline at end of file diff --git a/erpnext/hr/doctype/vehicle_log/vehicle_log.py b/erpnext/hr/doctype/vehicle_log/vehicle_log.py index dfdfc0d720..12cc1dd03a 100644 --- a/erpnext/hr/doctype/vehicle_log/vehicle_log.py +++ b/erpnext/hr/doctype/vehicle_log/vehicle_log.py @@ -18,7 +18,7 @@ class VehicleLog(Document): if not (service_detail.service_item and service_detail.type and service_detail.frequency and service_detail.expense_amount): frappe.throw(_("Service Item,Type,frequency and expense amount are required")) - def before_save(self): + def before_insert(self): model_details = get_make_model(self.license_plate) self.make = model_details[0] self.model = model_details[1] From df1ba3eec79e28f438ed40d3849603278e119ed9 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Wed, 19 Feb 2020 19:33:27 +0530 Subject: [PATCH 071/102] chore: Add github action to automate backporting --- .github/workflows/backport.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/backport.yml diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 0000000000..7c6b8432b8 --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,16 @@ +name: Backport +on: + pull_request: + types: + - closed + - labeled + +jobs: + backport: + runs-on: ubuntu-18.04 + name: Backport + steps: + - name: Backport + uses: tibdex/backport@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From 56837bc09d81c8fcbb51bf428937091ba00841a7 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 20 Feb 2020 12:23:08 +0530 Subject: [PATCH 072/102] fix: check for available stock in product bundle's website warehouse (#20681) --- erpnext/utilities/product.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py index 1c0d4c38c7..c23c1f7096 100644 --- a/erpnext/utilities/product.py +++ b/erpnext/utilities/product.py @@ -129,6 +129,7 @@ def get_non_stock_item_status(item_code, item_warehouse_field): #if item belongs to product bundle, check if bundle items are in stock if frappe.db.exists("Product Bundle", item_code): items = frappe.get_doc("Product Bundle", item_code).get_all_children() - return all([ get_qty_in_stock(d.item_code, item_warehouse_field).in_stock for d in items ]) + bundle_warehouse = frappe.db.get_value('Item', item_code, item_warehouse_field) + return all([ get_qty_in_stock(d.item_code, item_warehouse_field, bundle_warehouse).in_stock for d in items ]) else: return 1 From 2d7e024f00107c14dd846b190eeced720c7eccb2 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Thu, 20 Feb 2020 12:24:29 +0530 Subject: [PATCH 073/102] fix: use system language to translate strings (#20673) --- .../connectors/woocommerce_connection.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py index 28c2ab9e54..4422d23e38 100644 --- a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py +++ b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py @@ -47,11 +47,12 @@ def _order(*args, **kwargs): return "success" if event == "created": + sys_lang = frappe.get_single("System Settings").language or 'en' raw_billing_data = order.get("billing") customer_name = raw_billing_data.get("first_name") + " " + raw_billing_data.get("last_name") link_customer_and_address(raw_billing_data, customer_name) - link_items(order.get("line_items"), woocommerce_settings) - create_sales_order(order, woocommerce_settings, customer_name) + link_items(order.get("line_items"), woocommerce_settings, sys_lang) + create_sales_order(order, woocommerce_settings, customer_name, sys_lang) def link_customer_and_address(raw_billing_data, customer_name): customer_woo_com_email = raw_billing_data.get("email") @@ -100,7 +101,7 @@ def link_customer_and_address(raw_billing_data, customer_name): frappe.rename_doc("Address", old_address_title, new_address_title) -def link_items(items_list, woocommerce_settings): +def link_items(items_list, woocommerce_settings, sys_lang): for item_data in items_list: item_woo_com_id = item_data.get("product_id") @@ -112,14 +113,14 @@ def link_items(items_list, woocommerce_settings): item = frappe.new_doc("Item") item.item_name = item_data.get("name") - item.item_code = _("woocommerce - {0}").format(item_data.get("product_id")) + item.item_code = _("woocommerce - {0}", sys_lang).format(item_data.get("product_id")) item.woocommerce_id = item_data.get("product_id") - item.item_group = _("WooCommerce Products") - item.stock_uom = woocommerce_settings.uom or _("Nos") + item.item_group = _("WooCommerce Products", sys_lang) + item.stock_uom = woocommerce_settings.uom or _("Nos", sys_lang) item.flags.ignore_mandatory = True item.save() -def create_sales_order(order, woocommerce_settings, customer_name): +def create_sales_order(order, woocommerce_settings, customer_name, sys_lang): new_sales_order = frappe.new_doc("Sales Order") new_sales_order.customer = customer_name @@ -133,14 +134,14 @@ def create_sales_order(order, woocommerce_settings, customer_name): new_sales_order.company = woocommerce_settings.company - set_items_in_sales_order(new_sales_order, woocommerce_settings, order) + set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_lang) new_sales_order.flags.ignore_mandatory = True new_sales_order.insert() new_sales_order.submit() frappe.db.commit() -def set_items_in_sales_order(new_sales_order, woocommerce_settings, order): +def set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_lang): company_abbr = frappe.db.get_value('Company', woocommerce_settings.company, 'abbr') for item in order.get("line_items"): @@ -154,10 +155,10 @@ def set_items_in_sales_order(new_sales_order, woocommerce_settings, order): "item_name": found_item.item_name, "description": found_item.item_name, "delivery_date": new_sales_order.delivery_date, - "uom": woocommerce_settings.uom or _("Nos"), + "uom": woocommerce_settings.uom or _("Nos", sys_lang), "qty": item.get("quantity"), "rate": item.get("price"), - "warehouse": woocommerce_settings.warehouse or _("Stores - {0}").format(company_abbr) + "warehouse": woocommerce_settings.warehouse or _("Stores - {0}", sys_lang).format(company_abbr) }) add_tax_details(new_sales_order, ordered_items_tax, "Ordered Item tax", woocommerce_settings.tax_account) From f3acb6b79ffc4b80a4e6ea64d886404d08614fb2 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Thu, 20 Feb 2020 12:24:45 +0530 Subject: [PATCH 074/102] fix: use system language to translate strings (#20682) From cfe2db6d776d007728c70bb44eb7cf9cbe4cb16c Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 20 Feb 2020 12:29:44 +0530 Subject: [PATCH 075/102] fix: mandatory on hold comment for purchase invoice (#20666) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 80be2c803f..a68c36846d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1058,7 +1058,7 @@ def unblock_invoice(name): @frappe.whitelist() -def block_invoice(name, hold_comment, release_date): +def block_invoice(name, release_date, hold_comment=None): if frappe.db.exists('Purchase Invoice', name): pi = frappe.get_doc('Purchase Invoice', name) pi.block_invoice(hold_comment, release_date) From ca7f53b4eaae616261a6c639cb992ccdf4f27771 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 20 Feb 2020 12:32:05 +0530 Subject: [PATCH 076/102] chore: SINV set_status remove redundant function calls (#20660) * chore: SINV set_status remove redundant function calls * Update erpnext/accounts/doctype/sales_invoice/sales_invoice.py Co-Authored-By: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- .../accounts/doctype/sales_invoice/sales_invoice.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 8b4923fd4f..ad3640cb72 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1243,25 +1243,28 @@ class SalesInvoice(SellingController): precision = self.precision("outstanding_amount") outstanding_amount = flt(self.outstanding_amount, precision) + due_date = getdate(self.due_date) + nowdate = getdate() + discountng_status = self.get_discounting_status() if not status: if self.docstatus == 2: status = "Cancelled" elif self.docstatus == 1: - if outstanding_amount > 0 and getdate(self.due_date) < getdate(nowdate()) and self.is_discounted and self.get_discounting_status()=='Disbursed': + if outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discountng_status=='Disbursed': self.status = "Overdue and Discounted" - elif outstanding_amount > 0 and getdate(self.due_date) < getdate(nowdate()): + elif outstanding_amount > 0 and due_date < nowdate: self.status = "Overdue" - elif outstanding_amount > 0 and getdate(self.due_date) >= getdate(nowdate()) and self.is_discounted and self.get_discounting_status()=='Disbursed': + elif outstanding_amount > 0 and due_date >= nowdate and self.is_discounted and discountng_status=='Disbursed': self.status = "Unpaid and Discounted" - elif outstanding_amount > 0 and getdate(self.due_date) >= getdate(nowdate()): + elif outstanding_amount > 0 and due_date >= nowdate: self.status = "Unpaid" #Check if outstanding amount is 0 due to credit note issued against invoice elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Sales Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}): self.status = "Credit Note Issued" elif self.is_return == 1: self.status = "Return" - elif outstanding_amount <=0: + elif outstanding_amount<=0: self.status = "Paid" else: self.status = "Submitted" From 45ea2bfc004eed852274790cf9ddc5bc8eb0b575 Mon Sep 17 00:00:00 2001 From: Mathieu Brunot Date: Thu, 20 Feb 2020 08:19:54 +0100 Subject: [PATCH 077/102] chore(ci-coverage): Pin coverage 4.5.4 #20646 (#20647) * chore(ci-coverage): Pin coveralls 4.5.4 #20646 Signed-off-by: mathieu.brunot * chore: Pin coverage Signed-off-by: mathieu.brunot --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 365eb67f3d..213445b806 100644 --- a/.travis.yml +++ b/.travis.yml @@ -77,5 +77,6 @@ install: - bench --site test_site reinstall --yes after_script: + - pip install coverage==4.5.4 - pip install python-coveralls - coveralls -b apps/erpnext -d ../../sites/.coverage From 51cf0eb1592e6ca774c2bfa4f4717f5f4efdde17 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Thu, 20 Feb 2020 12:52:56 +0530 Subject: [PATCH 078/102] fix: changed field type which was affecting filters (#20671) --- erpnext/hr/doctype/attendance/attendance.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/hr/doctype/attendance/attendance.json b/erpnext/hr/doctype/attendance/attendance.json index ab2dc4a90f..eaca9f6ebe 100644 --- a/erpnext/hr/doctype/attendance/attendance.json +++ b/erpnext/hr/doctype/attendance/attendance.json @@ -58,11 +58,12 @@ { "fetch_from": "employee.employee_name", "fieldname": "employee_name", - "fieldtype": "Read Only", + "fieldtype": "Data", "in_global_search": 1, "label": "Employee Name", "oldfieldname": "employee_name", - "oldfieldtype": "Data" + "oldfieldtype": "Data", + "read_only": 1 }, { "depends_on": "working_hours", @@ -174,8 +175,7 @@ "icon": "fa fa-ok", "idx": 1, "is_submittable": 1, - "links": [], - "modified": "2020-01-27 20:25:29.572281", + "modified": "2020-02-19 14:25:32.945842", "modified_by": "Administrator", "module": "HR", "name": "Attendance", From 60713139656f7ba6e018b391c668c261e54a9275 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 20 Feb 2020 12:57:21 +0530 Subject: [PATCH 079/102] fix: document creation via onboarding slide (#20644) * fix: defaults not set in doc created via Onboarding Slide * fix: default company not set in Supplier --- erpnext/buying/doctype/supplier/supplier.py | 6 ++++-- erpnext/selling/doctype/customer/customer.py | 5 ++++- erpnext/stock/doctype/item/item.py | 10 ++++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 62a04f37b1..df143eefa0 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -58,7 +58,9 @@ class Supplier(TransactionBase): frappe.db.set(self, "supplier_name", newdn) def create_onboarding_docs(self, args): - defaults = frappe.defaults.get_defaults() + company = frappe.defaults.get_defaults().get('company') or \ + frappe.db.get_single_value('Global Defaults', 'default_company') + for i in range(1, args.get('max_count')): supplier = args.get('supplier_name_' + str(i)) if supplier: @@ -67,7 +69,7 @@ class Supplier(TransactionBase): 'doctype': self.doctype, 'supplier_name': supplier, 'supplier_group': _('Local'), - 'company': defaults.get('company') + 'company': company }).insert() if args.get('supplier_email_' + str(i)): diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 7f2fe60f59..9261289773 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -213,6 +213,9 @@ class Customer(TransactionBase): def create_onboarding_docs(self, args): defaults = frappe.defaults.get_defaults() + company = defaults.get('company') or \ + frappe.db.get_single_value('Global Defaults', 'default_company') + for i in range(1, args.get('max_count')): customer = args.get('customer_name_' + str(i)) if customer: @@ -223,7 +226,7 @@ class Customer(TransactionBase): 'customer_type': 'Company', 'customer_group': _('Commercial'), 'territory': defaults.get('country'), - 'company': defaults.get('company') + 'company': company }).insert() if args.get('customer_email_' + str(i)): diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index d036a0a1fb..a2a913a73f 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -877,14 +877,16 @@ class Item(WebsiteGenerator): frappe.msgprint(msg=_("You have to enable auto re-order in Stock Settings to maintain re-order levels."), title=_("Enable Auto Re-Order"), indicator="orange") def create_onboarding_docs(self, args): - defaults = frappe.defaults.get_defaults() + company = frappe.defaults.get_defaults().get('company') or \ + frappe.db.get_single_value('Global Defaults', 'default_company') + for i in range(1, args.get('max_count')): item = args.get('item_' + str(i)) if item: default_warehouse = '' default_warehouse = frappe.db.get_value('Warehouse', filters={ 'warehouse_name': _('Finished Goods'), - 'company': defaults.get('company_name') + 'company': company }) try: @@ -901,7 +903,7 @@ class Item(WebsiteGenerator): 'stock_uom': _(args.get('item_uom_' + str(i))), 'item_defaults': [{ 'default_warehouse': default_warehouse, - 'company': defaults.get('company_name') + 'company': company }] }).insert() @@ -909,7 +911,7 @@ class Item(WebsiteGenerator): pass else: if args.get('item_price_' + str(i)): - item_price = flt(args.get('tem_price_' + str(i))) + item_price = flt(args.get('item_price_' + str(i))) price_list_name = frappe.db.get_value('Price List', {'selling': 1}) make_item_price(item, price_list_name, item_price) From 6f6e3a940bcaae2729268698d98f092dd6231e4b Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Thu, 20 Feb 2020 12:58:54 +0530 Subject: [PATCH 080/102] fix: return null for attribute (#20674) --- .../doctype/amazon_mws_settings/amazon_methods.py | 6 ++++++ .../doctype/amazon_mws_settings/amazon_mws_settings.py | 5 ++--- .../doctype/amazon_mws_settings/xml_utils.py | 5 ++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py index 2f39dc596b..3bc8db5e78 100644 --- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py @@ -165,6 +165,9 @@ def create_item_code(amazon_item_json, sku): return item.name def create_manufacturer(amazon_item_json): + if not amazon_item_json.Product.AttributeSets.ItemAttributes.Manufacturer: + return None + existing_manufacturer = frappe.db.get_value("Manufacturer", filters={"short_name":amazon_item_json.Product.AttributeSets.ItemAttributes.Manufacturer}) @@ -177,6 +180,9 @@ def create_manufacturer(amazon_item_json): return existing_manufacturer def create_brand(amazon_item_json): + if not amazon_item_json.Product.AttributeSets.ItemAttributes.Brand: + return None + existing_brand = frappe.db.get_value("Brand", filters={"brand":amazon_item_json.Product.AttributeSets.ItemAttributes.Brand}) if not existing_brand: diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.py index 249a73f9fb..c222afbb6c 100644 --- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.py +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.py @@ -7,7 +7,6 @@ import frappe from frappe.model.document import Document import dateutil from frappe.custom.doctype.custom_field.custom_field import create_custom_fields -from erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods import get_products_details, get_orders class AmazonMWSSettings(Document): def validate(self): @@ -19,12 +18,12 @@ class AmazonMWSSettings(Document): def get_products_details(self): if self.enable_amazon == 1: - get_products_details() + frappe.enqueue('erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods.get_products_details') def get_order_details(self): if self.enable_amazon == 1: after_date = dateutil.parser.parse(self.after_date).strftime("%Y-%m-%d") - get_orders(after_date = after_date) + frappe.enqueue('erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods.get_orders', after_date=after_date) def schedule_get_order_details(): mws_settings = frappe.get_doc("Amazon MWS Settings") diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/xml_utils.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/xml_utils.py index 58db669411..a25a29f9e5 100644 --- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/xml_utils.py +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/xml_utils.py @@ -33,7 +33,10 @@ class object_dict(dict): def __getattr__(self, item): - d = self.__getitem__(item) + try: + d = self.__getitem__(item) + except KeyError: + return None if isinstance(d, dict) and 'value' in d and len(d) == 1: return d['value'] From 395b2b15b2b4f9692159ff8e5e16b97ec6e6ffb5 Mon Sep 17 00:00:00 2001 From: Rohan Date: Thu, 20 Feb 2020 12:59:32 +0530 Subject: [PATCH 081/102] fix: apply url encoding to project names (#20642) --- erpnext/templates/includes/projects/project_row.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/templates/includes/projects/project_row.html b/erpnext/templates/includes/projects/project_row.html index 55b02e2004..73c83ef560 100644 --- a/erpnext/templates/includes/projects/project_row.html +++ b/erpnext/templates/includes/projects/project_row.html @@ -1,6 +1,6 @@ {% if doc.status=="Open" %}
- +
From a0021969ad4a403443a6103b94ff1e55aae791a6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Thu, 20 Feb 2020 13:22:28 +0530 Subject: [PATCH 082/102] fix: Validation message --- erpnext/education/doctype/student_group/student_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/education/doctype/student_group/student_group.py b/erpnext/education/doctype/student_group/student_group.py index c92c69b6b4..2925469312 100644 --- a/erpnext/education/doctype/student_group/student_group.py +++ b/erpnext/education/doctype/student_group/student_group.py @@ -27,7 +27,7 @@ class StudentGroup(Document): def validate_strength(self): if self.max_strength <= 0: - frappe.throw(_("""Cannot enroll less than or equal to 0 students for this student group.""")) + frappe.throw(_("""Max strength must be greater than zero.""")) if self.max_strength and len(self.students) > self.max_strength: frappe.throw(_("""Cannot enroll more than {0} students for this student group.""").format(self.max_strength)) From 5d5f5b4f8ea432746b7ab58bafee22240b6703bf Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Thu, 20 Feb 2020 13:25:55 +0530 Subject: [PATCH 083/102] fix(HR): skip earned leaves check for max leaves set to zero or less (#20535) * fix: skip earned leaves check for max leaves set to zero or less * test: earned leaves creation --- .../leave_application/test_leave_application.py | 10 +++++++++- erpnext/hr/utils.py | 4 +++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index b6216424de..6e909c3f01 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -409,7 +409,7 @@ class TestLeaveApplication(unittest.TestCase): self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8)), 21) - def test_earned_leave(self): + def test_earned_leaves_creation(self): leave_period = get_leave_period() employee = get_employee() leave_type = 'Test Earned Leave Type' @@ -437,6 +437,14 @@ class TestLeaveApplication(unittest.TestCase): i += 1 self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6) + # validate earned leaves creation without maximum leaves + frappe.db.set_value('Leave Type', leave_type, 'max_leaves_allowed', 0) + i = 0 + while(i<6): + allocate_earned_leaves() + i += 1 + self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9) + # test to not consider current leave in leave balance while submitting def test_current_leave_on_submit(self): employee = get_employee() diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index c3e8d27557..ef276001c5 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -316,7 +316,9 @@ def allocate_earned_leaves(): allocation = frappe.get_doc('Leave Allocation', allocation.name) new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves) - new_allocation = new_allocation if new_allocation <= e_leave_type.max_leaves_allowed else e_leave_type.max_leaves_allowed + + if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0: + new_allocation = e_leave_type.max_leaves_allowed if new_allocation == allocation.total_leaves_allocated: continue From a29c87cfda48a7754ee97deb18fef98911e0437c Mon Sep 17 00:00:00 2001 From: Rohan Date: Thu, 20 Feb 2020 13:28:03 +0530 Subject: [PATCH 084/102] feat: remove unused route creation variable (#20558) --- erpnext/public/js/conf.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/erpnext/public/js/conf.js b/erpnext/public/js/conf.js index 0f88f13350..615f6a43f9 100644 --- a/erpnext/public/js/conf.js +++ b/erpnext/public/js/conf.js @@ -29,19 +29,6 @@ $(document).bind('toolbar_setup', function() { }); - - -// doctypes created via tree -$.extend(frappe.create_routes, { - "Customer Group": "Tree/Customer Group", - "Territory": "Tree/Territory", - "Item Group": "Tree/Item Group", - "Sales Person": "Tree/Sales Person", - "Account": "Tree/Account", - "Cost Center": "Tree/Cost Center", - "Department": "Tree/Department", -}); - // preferred modules for breadcrumbs $.extend(frappe.breadcrumbs.preferred, { "Item Group": "Stock", From 3f0c01199dc931746ddc2e10df9169b3492a0c46 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 20 Feb 2020 15:29:09 +0530 Subject: [PATCH 085/102] fix: Additional salary can be created only for active employee --- erpnext/hr/doctype/additional_salary/additional_salary.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/additional_salary/additional_salary.js b/erpnext/hr/doctype/additional_salary/additional_salary.js index d0f64ab51b..18f6b8b52d 100644 --- a/erpnext/hr/doctype/additional_salary/additional_salary.js +++ b/erpnext/hr/doctype/additional_salary/additional_salary.js @@ -8,7 +8,8 @@ frappe.ui.form.on('Additional Salary', { frm.set_query("employee", function() { return { filters: { - company: frm.doc.company + company: frm.doc.company, + status: "Active" } }; }); From 3218730a167e0e0cce0d9943baf1bbd65c741a4e Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 20 Feb 2020 16:14:44 +0530 Subject: [PATCH 086/102] fix: set query on change in opportunity_from value --- erpnext/crm/doctype/opportunity/opportunity.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index c9b0433fad..0c9ba495c7 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -59,6 +59,7 @@ frappe.ui.form.on("Opportunity", { contact_person: erpnext.utils.get_contact_details, opportunity_from: function(frm) { + frm.trigger('setup_queries'); frm.toggle_reqd("party_name", frm.doc.opportunity_from); frm.trigger("set_dynamic_field_label"); }, From 6e53afd947974b576cdb059847b596ac36fc8ed1 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Thu, 20 Feb 2020 23:33:35 +0530 Subject: [PATCH 087/102] fix: Travis (#20688) * fix: Travis * fix: Update test records Co-authored-by: Chinmay Pai --- .../education/doctype/student_group/student_group.py | 4 ++-- .../doctype/student_group/test_records.json | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/erpnext/education/doctype/student_group/student_group.py b/erpnext/education/doctype/student_group/student_group.py index 2925469312..8b61c899bc 100644 --- a/erpnext/education/doctype/student_group/student_group.py +++ b/erpnext/education/doctype/student_group/student_group.py @@ -26,8 +26,8 @@ class StudentGroup(Document): frappe.throw(_("Please select Program")) def validate_strength(self): - if self.max_strength <= 0: - frappe.throw(_("""Max strength must be greater than zero.""")) + if cint(self.max_strength) < 0: + frappe.throw(_("""Max strength cannot be less than zero.""")) if self.max_strength and len(self.students) > self.max_strength: frappe.throw(_("""Cannot enroll more than {0} students for this student group.""").format(self.max_strength)) diff --git a/erpnext/education/doctype/student_group/test_records.json b/erpnext/education/doctype/student_group/test_records.json index c36c9b297c..4c4e042b15 100644 --- a/erpnext/education/doctype/student_group/test_records.json +++ b/erpnext/education/doctype/student_group/test_records.json @@ -5,26 +5,30 @@ "program": "_TP1", "batch": "_Batch 1", "academic_year": "2014-2015", - "academic_term": "2014-2015 (_Test Academic Term)" + "academic_term": "2014-2015 (_Test Academic Term)", + "max_strength": 0 }, { "student_group_name": "Course-TC101-2014-2015 (_Test Academic Term)", "group_based_on": "Course", "course": "TC101", "academic_year": "2014-2015", - "academic_term": "2014-2015 (_Test Academic Term)" + "academic_term": "2014-2015 (_Test Academic Term)", + "max_strength": 0 }, { "student_group_name": "Course-TC102-2014-2015 (_Test Academic Term)", "group_based_on": "Course", "course": "TC102", "academic_year": "2014-2015", - "academic_term": "2014-2015 (_Test Academic Term)" + "academic_term": "2014-2015 (_Test Academic Term)", + "max_strength": 0 }, { "student_group_name": "Activity-2014-2015 (_Test Academic Term)", "group_based_on": "Activity", "academic_year": "2014-2015", - "academic_term": "2014-2015 (_Test Academic Term)" + "academic_term": "2014-2015 (_Test Academic Term)", + "max_strength": 0 } ] \ No newline at end of file From aaea4e60b6996bb9c63671e689643317a8ef09d5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sun, 23 Feb 2020 20:59:05 +0530 Subject: [PATCH 088/102] fix: Validation condition --- erpnext/education/doctype/student/student.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/education/doctype/student/student.py b/erpnext/education/doctype/student/student.py index 81fa7f7fae..6b545d99be 100644 --- a/erpnext/education/doctype/student/student.py +++ b/erpnext/education/doctype/student/student.py @@ -23,7 +23,7 @@ class Student(Document): def validate_dates(self): for sibling in self.siblings: - if sibling.date_of_birth and getdate(sibling.date_of_birth) >= getdate(): + if sibling.date_of_birth and getdate(sibling.date_of_birth) > getdate(): frappe.throw(_("Row {0}:Sibling Date of Birth cannot be greater than today.").format(sibling.idx)) if self.date_of_birth and getdate(self.date_of_birth) >= getdate(today()): From ebdf80ddb5740d662a006357bc620030c164e331 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 24 Feb 2020 18:45:47 +0530 Subject: [PATCH 089/102] fix: account dashboard not working --- .../account_balance_timeline.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py index 43acded3a9..a9b3d7c4cc 100644 --- a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py +++ b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe, json from frappe import _ -from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day, formatdate +from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day, formatdate, get_link_to_form from erpnext.accounts.report.general_ledger.general_ledger import execute from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_period_ending @@ -30,8 +30,13 @@ def get(chart_name = None, chart = None, no_cache = None, from_date = None, to_d account = filters.get("account") company = filters.get("company") - if not account and chart: - frappe.throw(_("Account is not set for the dashboard chart {0}").format(chart)) + if not account and chart_name: + frappe.throw(_("Account is not set for the dashboard chart {0}") + .format(get_link_to_form("Dashboard Chart", chart_name))) + + if not frappe.db.exists("Account", account) and chart_name: + frappe.throw(_("Account {0} does not exists in the dashboard chart {1}") + .format(account, get_link_to_form("Dashboard Chart", chart_name))) if not to_date: to_date = nowdate() From 0ebace5da2dbb93928d633e739778d4e7a0b6f73 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 25 Feb 2020 13:21:16 +0530 Subject: [PATCH 090/102] fix: Update paid amount for pos return (#20543) * fix: Paid amount updation for pos return * fix: Remove console * fix: Styling * fix: get default mode of payment from POS profile * fix: Add test cases * fix: Codacy --- .../doctype/pos_profile/test_pos_profile.py | 28 ++++----- .../doctype/sales_invoice/sales_invoice.py | 2 +- .../sales_invoice/test_sales_invoice.py | 58 +++++++++++++++++++ erpnext/controllers/taxes_and_totals.py | 28 ++++++++- .../public/js/controllers/taxes_and_totals.js | 50 ++++++++++++++-- 5 files changed, 145 insertions(+), 21 deletions(-) diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py index 58f12162d1..64d347de84 100644 --- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py @@ -29,27 +29,29 @@ class TestPOSProfile(unittest.TestCase): frappe.db.sql("delete from `tabPOS Profile`") -def make_pos_profile(): +def make_pos_profile(**args): frappe.db.sql("delete from `tabPOS Profile`") + args = frappe._dict(args) + pos_profile = frappe.get_doc({ - "company": "_Test Company", - "cost_center": "_Test Cost Center - _TC", - "currency": "INR", + "company": args.company or "_Test Company", + "cost_center": args.cost_center or "_Test Cost Center - _TC", + "currency": args.currency or "INR", "doctype": "POS Profile", - "expense_account": "_Test Account Cost for Goods Sold - _TC", - "income_account": "Sales - _TC", - "name": "_Test POS Profile", + "expense_account": args.expense_account or "_Test Account Cost for Goods Sold - _TC", + "income_account": args.income_account or "Sales - _TC", + "name": args.name or "_Test POS Profile", "naming_series": "_T-POS Profile-", - "selling_price_list": "_Test Price List", - "territory": "_Test Territory", + "selling_price_list": args.selling_price_list or "_Test Price List", + "territory": args.territory or "_Test Territory", "customer_group": frappe.db.get_value('Customer Group', {'is_group': 0}, 'name'), - "warehouse": "_Test Warehouse - _TC", - "write_off_account": "_Test Write Off - _TC", - "write_off_cost_center": "_Test Write Off Cost Center - _TC" + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "write_off_account": args.write_off_account or "_Test Write Off - _TC", + "write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC" }) - if not frappe.db.exists("POS Profile", "_Test POS Profile"): + if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"): pos_profile.insert() return pos_profile diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index ad3640cb72..f5dd6e78d5 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -420,7 +420,7 @@ class SalesInvoice(SellingController): if pos: self.allow_print_before_pay = pos.allow_print_before_pay - + if not for_validate: self.tax_category = pos.get("tax_category") diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index a2a47b3a19..e48e6c95a3 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -705,6 +705,64 @@ class TestSalesInvoice(unittest.TestCase): self.pos_gl_entry(si, pos, 50) + def test_pos_returns_without_repayment(self): + pos_profile = make_pos_profile() + + pos = create_sales_invoice(qty = 10, do_not_save=True) + pos.is_pos = 1 + pos.pos_profile = pos_profile.name + + pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500}) + pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500}) + pos.insert() + pos.submit() + + pos_return = create_sales_invoice(is_return=1, + return_against=pos.name, qty=-5, do_not_save=True) + + pos_return.is_pos = 1 + pos_return.pos_profile = pos_profile.name + + pos_return.insert() + pos_return.submit() + + self.assertFalse(pos_return.is_pos) + self.assertFalse(pos_return.get('payments')) + + def test_pos_returns_with_repayment(self): + pos_profile = make_pos_profile() + + pos_profile.append('payments', { + 'default': 1, + 'mode_of_payment': 'Cash', + 'amount': 0.0 + }) + + pos_profile.save() + + pos = create_sales_invoice(qty = 10, do_not_save=True) + + pos.is_pos = 1 + pos.pos_profile = pos_profile.name + + pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500}) + pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500}) + pos.insert() + pos.submit() + + pos_return = create_sales_invoice(is_return=1, + return_against=pos.name, qty=-5, do_not_save=True) + + pos_return.is_pos = 1 + pos_return.pos_profile = pos_profile.name + pos_return.insert() + pos_return.submit() + + self.assertEqual(pos_return.get('payments')[0].amount, -500) + pos_profile.payments = [] + pos_profile.save() + + def test_pos_change_amount(self): make_pos_profile() diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index b52a07dbdf..95e661aa22 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -514,7 +514,7 @@ class calculate_taxes_and_totals(object): if self.doc.doctype == "Sales Invoice": self.calculate_paid_amount() - if self.doc.is_return and self.doc.return_against: return + if self.doc.is_return and self.doc.return_against and not self.doc.get('is_pos'): return self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"]) self._set_in_company_currency(self.doc, ['write_off_amount']) @@ -532,7 +532,7 @@ class calculate_taxes_and_totals(object): self.doc.round_floats_in(self.doc, ["paid_amount"]) change_amount = 0 - if self.doc.doctype == "Sales Invoice": + if self.doc.doctype == "Sales Invoice" and not self.doc.get('is_return'): self.calculate_write_off_amount() self.calculate_change_amount() change_amount = self.doc.change_amount \ @@ -544,6 +544,9 @@ class calculate_taxes_and_totals(object): self.doc.outstanding_amount = flt(total_amount_to_pay - flt(paid_amount) + flt(change_amount), self.doc.precision("outstanding_amount")) + if self.doc.doctype == 'Sales Invoice' and self.doc.get('is_pos') and self.doc.get('is_return'): + self.update_paid_amount_for_return(total_amount_to_pay) + def calculate_paid_amount(self): paid_amount = base_paid_amount = 0.0 @@ -614,6 +617,27 @@ class calculate_taxes_and_totals(object): def set_item_wise_tax_breakup(self): self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc) + def update_paid_amount_for_return(self, total_amount_to_pay): + default_mode_of_payment = frappe.db.get_value('Sales Invoice Payment', + {'parent': self.doc.pos_profile, 'default': 1}, + ['mode_of_payment', 'type', 'account'], as_dict=1) + + self.doc.payments = [] + + if default_mode_of_payment: + self.doc.append('payments', { + 'mode_of_payment': default_mode_of_payment.mode_of_payment, + 'type': default_mode_of_payment.type, + 'account': default_mode_of_payment.account, + 'amount': total_amount_to_pay + }) + else: + self.doc.is_pos = 0 + self.doc.pos_profile = '' + + self.calculate_paid_amount() + + def get_itemised_tax_breakup_html(doc): if not doc.taxes: return diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index a51c2f0954..9a5b750e8c 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -39,6 +39,11 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ this.calculate_total_advance(update_paid_amount); } + if (this.frm.doc.doctype == "Sales Invoice" && this.frm.doc.is_pos && + this.frm.doc.is_return) { + this.update_paid_amount_for_return(); + } + // Sales person's commission if(in_list(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"], this.frm.doc.doctype)) { this.calculate_commission(); @@ -635,23 +640,58 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } }, - set_default_payment: function(total_amount_to_pay, update_paid_amount){ + update_paid_amount_for_return: function() { + var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; + + if(this.frm.doc.party_account_currency == this.frm.doc.currency) { + var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance + - this.frm.doc.write_off_amount), precision("grand_total")); + } else { + var total_amount_to_pay = flt( + (flt(grand_total*this.frm.doc.conversion_rate, precision("grand_total")) + - this.frm.doc.total_advance - this.frm.doc.base_write_off_amount), + precision("base_grand_total") + ); + } + + frappe.db.get_value('Sales Invoice Payment', {'parent': this.frm.doc.pos_profile, 'default': 1}, + ['mode_of_payment', 'account', 'type'], (value) => { + if (this.frm.is_dirty()) { + frappe.model.clear_table(this.frm.doc, 'payments'); + if (value) { + let row = frappe.model.add_child(this.frm.doc, 'Sales Invoice Payment', 'payments'); + row.mode_of_payment = value.mode_of_payment; + row.type = value.type; + row.account = value.account; + row.default = 1; + row.amount = total_amount_to_pay; + } else { + this.frm.set_value('is_pos', 1); + } + this.frm.refresh_fields(); + } + }, 'Sales Invoice'); + + this.calculate_paid_amount(); + }, + + set_default_payment: function(total_amount_to_pay, update_paid_amount) { var me = this; var payment_status = true; - if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)){ - $.each(this.frm.doc['payments'] || [], function(index, data){ + if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) { + $.each(this.frm.doc['payments'] || [], function(index, data) { if(data.default && payment_status && total_amount_to_pay > 0) { data.base_amount = flt(total_amount_to_pay, precision("base_amount")); data.amount = flt(total_amount_to_pay / me.frm.doc.conversion_rate, precision("amount")); payment_status = false; - }else if(me.frm.doc.paid_amount){ + } else if(me.frm.doc.paid_amount) { data.amount = 0.0; } }); } }, - calculate_paid_amount: function(){ + calculate_paid_amount: function() { var me = this; var paid_amount = 0.0; var base_paid_amount = 0.0; From 75d9d79fac84352fc604560fe368415bea2d0d00 Mon Sep 17 00:00:00 2001 From: Andy Zhu Date: Wed, 26 Feb 2020 10:49:48 +1300 Subject: [PATCH 091/102] fix: missing assign item's expense_account As in dict object "ret", missing assign item's expense_account will cause it then fetches the default stock_adjustment_account in 'Company'. --- erpnext/stock/doctype/stock_entry/stock_entry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index a8d1e79eff..8b072c66ee 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -757,7 +757,8 @@ class StockEntry(StockController): 'serial_no' : '', 'has_serial_no' : item.has_serial_no, 'has_batch_no' : item.has_batch_no, - 'sample_quantity' : item.sample_quantity + 'sample_quantity' : item.sample_quantity, + 'expense_account' : item.expense_account }) # update uom From d1d0a50a70cb2698018ea5f26c25700b79017438 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Wed, 26 Feb 2020 10:44:24 +0530 Subject: [PATCH 092/102] feat: Loan management and accounting (#19035) * fix: Create Loan Management module * fix: Move loan doctype for hr module to loan management * fix: Add loan dashboard * fix: Move loan application form hr module to loan management * fix: Move Loan Type from hr to loan management * fix: Move salary slip loan from hr to loan management * fix: Create loan security pledge doctype * fix: Create loan security type doctype * fix: Create Loan security doctype * fix: Add customer in loan_common.js * fix: Import patch for loan * fix: Add security pledging in loan doctype * fix: Add loan application pledge doctype * fix: Add logic for security pledgeing in loan * fix: Add logic for security pledging in loan application * fix: Rename Loan security doctype to Loan Security Pledge * fix: Add doctype for loan security * fix: Fixes in loan and loan application * fix: Add doctypes for loan repayment * fix: Fixes in loan and loan application * fix: Move loan repayment report from HR module to loan management * fix: Create doctype for loan security price * fix: Create doctype for loan security shortfall * fix: Make fields in Loan shortfall readonly * fix: Make loan type submittable * fix: Add daily job for loan security revaluation in hooks.py * fix: Add loan management module * fix: Doctype for loan disbursement Entry * fix: GL entry fix for loan disbursement * fix: Add company currency options in Loan related doctypes * fix: Changes in Loan Doctypes * fix: Allow miltiple loan securities in Loan Security Pledge * fix: Add proposed pledges in Loan Application * fix: Add test cases for loan * fix: Interest Accrual Entry for loans * fix: Remove loan from journal entry * fix: Update triggers in loan_common.js * fix: Accrual Entries for loan Interest * fix: Proposed Pledges for loan application * fix: Update items in loan management module * fix: Allow multiple disbursements against a loan * fix: Add loan security code in loan security master * fix: Allow multiple securities in a single pledge * fix: Spelling and label fixes * fix: Replace date in loan security price with datetime * fix: Add dashboard to loan master * fix: Move Repayment Schedule from HR to Loan Management * fix: Add back jobs for loan accrual entries * fix: Multiple fixes in loan * fix: Multiple fixes in loan application * fix: Loan Reapayment for term loans * fix: Interest Accural Entries for term loans * fix: Changes in Loan Doctypes * fix: Add test case for term loan repayment * fix: Add custom button to update loan security price and trigger shortfall * fix: Usability fixes in Loan management * fix: Multiple usablity and doctype fixes * fix: Muliple bug and usability fixes in loan * fix: Test case fixes for loan repayment from Salary * fix: Codacy fixes * fix: Test Case fixes * fix: Minor fix in validate_repayment_method * fix: Test case and codacy fixes * fix: Provide consistent naming series to loan doctypes * fix: Loan Application fix * fix: Loan Application Dashboard * fix: Add doctypes to process loan interest accural and loan security price * fix: Provision to make loan topup * fix: Pledge and unpledge statuses and doctype for loan securit unpledge * fix: Multiple fixes in Loan Cycle * fix: Add missing dashboards to loan doctypes * fix: Add Loan Manager role to loan doctypes * fix: Loan Process fixes * fix: Loan Security Unpledge fixes * fix: UX, List and dashboards fixes * fix: Minor fixes * fix: Minor fixes * fix: Status on additional loan security * fix: Codacy fixes * fix: Multiple fixes in loan * fix: Rename Process Loan Security Price to Process Loan Security Shortfall * fix: Loan Repayment and Closure report * fix: Loan Security Status report and minor fixes * fix: Multiple UX fixes * fix: Test Case fixes and UX fixes * fix: Currency symbol fixes in Salary Slip Loan * fix: Make loan account details read only * fix: Changes in loan security price updation * fix: Sanctioned Loan Amount doctype * fix: Updates in process loan interest accrual * fix: Pass loan doc instead of loan name * fix: Fixes in process loan interest accrual * fix: Add missing semicolon * fix: Test case * fix: Test case * fix: Sandbox method to get proposed pledges * fix: Sandbox method * fix: Nonetype fix in loan sanction limit * fix: GL entry fixes * fix: Update maximum loan amount on loan security pledging * fix: Round off loan amounts * fix: Loan Security unpledging * fix: Interest amount for loan closure * fix: Due date for loan repayments * fix: API fixes for loan disbursement method * fix: Disable quick entry for sanctioned loan amount * fix: Add misssing translations for validations and codecleanup * fix: Translation fixes and code cleanup * fix: Test Cases * fix: Loan Interest Accrual test case --- .../doctype/journal_entry/journal_entry.py | 14 - erpnext/config/desktop.py | 9 + erpnext/config/loan_management.py | 107 +++ erpnext/hooks.py | 7 +- erpnext/hr/doctype/loan/loan.js | 229 ----- erpnext/hr/doctype/loan/loan.py | 240 ----- erpnext/hr/doctype/loan/loan_dashboard.py | 26 - erpnext/hr/doctype/loan/test_loan.js | 79 -- erpnext/hr/doctype/loan/test_loan.py | 71 -- .../loan_application/loan_application.js | 42 - .../loan_application/loan_application.json | 840 ------------------ .../loan_application/loan_application.py | 70 -- .../loan_application/test_loan_application.js | 68 -- erpnext/hr/doctype/loan_type/loan_type.js | 7 - erpnext/hr/doctype/loan_type/loan_type.json | 259 ------ .../doctype/loan_type/loan_type_dashboard.py | 12 - .../hr/doctype/loan_type/test_loan_type.js | 31 - .../hr/doctype/payroll_entry/payroll_entry.py | 37 - .../payroll_entry/test_payroll_entry.py | 38 +- erpnext/hr/doctype/salary_slip/salary_slip.py | 65 +- .../doctype/salary_slip/test_salary_slip.py | 69 +- .../salary_slip_loan/salary_slip_loan.json | 263 ------ .../salary_structure/test_salary_structure.py | 15 +- .../report/loan_repayment/loan_repayment.js | 9 - .../report/loan_repayment/loan_repayment.json | 28 - .../report/loan_repayment/loan_repayment.py | 95 -- .../loan => loan_management}/__init__.py | 0 .../doctype}/__init__.py | 0 .../doctype/loan}/__init__.py | 0 erpnext/loan_management/doctype/loan/loan.js | 190 ++++ .../doctype/loan/loan.json | 118 ++- erpnext/loan_management/doctype/loan/loan.py | 254 ++++++ .../doctype/loan/loan_dashboard.py | 19 + .../loan_management/doctype/loan/test_loan.py | 559 ++++++++++++ .../doctype/loan_application}/__init__.py | 0 .../loan_application/loan_application.js | 127 +++ .../loan_application/loan_application.json | 278 ++++++ .../loan_application/loan_application.py | 206 +++++ .../loan_application_dashboard.py | 2 +- .../loan_application/test_loan_application.py | 40 +- .../doctype/loan_disbursement}/__init__.py | 0 .../loan_disbursement/loan_disbursement.js | 17 + .../loan_disbursement/loan_disbursement.json | 165 ++++ .../loan_disbursement/loan_disbursement.py | 114 +++ .../test_loan_disbursement.py | 75 ++ .../loan_interest_accrual}/__init__.py | 0 .../loan_interest_accrual.js | 10 + .../loan_interest_accrual.json | 182 ++++ .../loan_interest_accrual.py | 180 ++++ .../test_loan_interest_accrual.py | 61 ++ .../doctype/loan_repayment/__init__.py | 0 .../doctype/loan_repayment/loan_repayment.js | 64 ++ .../loan_repayment/loan_repayment.json | 267 ++++++ .../doctype/loan_repayment/loan_repayment.py | 315 +++++++ .../loan_repayment/test_loan_repayment.py | 10 + .../doctype/loan_security/__init__.py | 0 .../doctype/loan_security/loan_security.js | 8 + .../doctype/loan_security/loan_security.json | 95 ++ .../doctype/loan_security/loan_security.py | 10 + .../loan_security/loan_security_dashboard.py | 15 + .../loan_security/test_loan_security.py | 10 + .../doctype/loan_security_pledge/__init__.py | 0 .../loan_security_pledge.js | 40 + .../loan_security_pledge.json | 176 ++++ .../loan_security_pledge.py | 51 ++ .../loan_security_pledge_list.js | 15 + .../test_loan_security_pledge.py | 10 + .../doctype/loan_security_price/__init__.py | 0 .../loan_security_price.js | 8 + .../loan_security_price.json | 117 +++ .../loan_security_price.py | 51 ++ .../test_loan_security_price.py | 10 + .../loan_security_shortfall/__init__.py | 0 .../loan_security_shortfall.js | 25 + .../loan_security_shortfall.json | 126 +++ .../loan_security_shortfall.py | 94 ++ .../test_loan_security_shortfall.py | 10 + .../doctype/loan_security_type/__init__.py | 0 .../loan_security_type/loan_security_type.js | 8 + .../loan_security_type.json | 73 ++ .../loan_security_type/loan_security_type.py | 10 + .../loan_security_type_dashboard.py | 15 + .../test_loan_security_type.py | 10 + .../loan_security_unpledge/__init__.py | 0 .../loan_security_unpledge.js | 13 + .../loan_security_unpledge.json | 159 ++++ .../loan_security_unpledge.py | 85 ++ .../loan_security_unpledge_list.js | 14 + .../test_loan_security_unpledge.py | 10 + .../doctype/loan_type/__init__.py | 0 .../doctype/loan_type/loan_type.js | 30 + .../doctype/loan_type/loan_type.json | 170 ++++ .../doctype/loan_type/loan_type.py | 21 + .../doctype/loan_type/loan_type_dashboard.py | 15 + .../doctype/loan_type/test_loan_type.py | 6 +- .../doctype/pledge/__init__.py | 0 .../loan_management/doctype/pledge/pledge.js | 8 + .../doctype/pledge/pledge.json | 99 +++ .../doctype/pledge/pledge.py} | 6 +- .../doctype/pledge/test_pledge.py | 10 + .../process_loan_interest_accrual/__init__.py | 0 .../process_loan_interest_accrual.js | 8 + .../process_loan_interest_accrual.json | 81 ++ .../process_loan_interest_accrual.py | 29 + ...process_loan_interest_accrual_dashboard.py | 12 + .../test_process_loan_interest_accrual.py | 10 + .../__init__.py | 0 .../process_loan_security_shortfall.js | 8 + .../process_loan_security_shortfall.json | 67 ++ .../process_loan_security_shortfall.py | 17 + ...ocess_loan_security_shortfall_dashboard.py | 12 + .../test_process_loan_security_shortfall.py | 10 + .../doctype/proposed_pledge/__init__.py | 0 .../proposed_pledge/proposed_pledge.json | 70 ++ .../proposed_pledge/proposed_pledge.py | 10 + .../doctype/repayment_schedule/__init__.py | 0 .../repayment_schedule.json | 13 +- .../repayment_schedule/repayment_schedule.py | 4 +- .../doctype/salary_slip_loan/__init__.py | 0 .../salary_slip_loan/salary_slip_loan.json | 96 ++ .../salary_slip_loan/salary_slip_loan.py | 4 +- .../sanctioned_loan_amount/__init__.py | 0 .../sanctioned_loan_amount.js | 8 + .../sanctioned_loan_amount.json | 88 ++ .../sanctioned_loan_amount.py | 16 + .../test_sanctioned_loan_amount.py | 10 + .../doctype/unpledge/__init__.py | 0 .../doctype/unpledge/unpledge.json | 84 ++ .../doctype/unpledge/unpledge.py | 10 + .../{hr => loan_management}/loan_common.js | 23 +- erpnext/loan_management/report/__init__.py | 0 .../loan_repayment_and_closure/__init__.py | 0 .../loan_repayment_and_closure.js | 41 + .../loan_repayment_and_closure.json | 27 + .../loan_repayment_and_closure.py | 129 +++ .../report/loan_security_status/__init__.py | 0 .../loan_security_status.js | 46 + .../loan_security_status.json | 27 + .../loan_security_status.py | 135 +++ erpnext/modules.txt | 3 +- erpnext/patches.txt | 2 +- .../v11_0/rename_employee_loan_to_loan.py | 28 - 142 files changed, 6168 insertions(+), 2576 deletions(-) create mode 100644 erpnext/config/loan_management.py delete mode 100644 erpnext/hr/doctype/loan/loan.js delete mode 100644 erpnext/hr/doctype/loan/loan.py delete mode 100644 erpnext/hr/doctype/loan/loan_dashboard.py delete mode 100644 erpnext/hr/doctype/loan/test_loan.js delete mode 100644 erpnext/hr/doctype/loan/test_loan.py delete mode 100644 erpnext/hr/doctype/loan_application/loan_application.js delete mode 100644 erpnext/hr/doctype/loan_application/loan_application.json delete mode 100644 erpnext/hr/doctype/loan_application/loan_application.py delete mode 100644 erpnext/hr/doctype/loan_application/test_loan_application.js delete mode 100644 erpnext/hr/doctype/loan_type/loan_type.js delete mode 100644 erpnext/hr/doctype/loan_type/loan_type.json delete mode 100644 erpnext/hr/doctype/loan_type/loan_type_dashboard.py delete mode 100644 erpnext/hr/doctype/loan_type/test_loan_type.js delete mode 100644 erpnext/hr/doctype/salary_slip_loan/salary_slip_loan.json delete mode 100644 erpnext/hr/report/loan_repayment/loan_repayment.js delete mode 100644 erpnext/hr/report/loan_repayment/loan_repayment.json delete mode 100644 erpnext/hr/report/loan_repayment/loan_repayment.py rename erpnext/{hr/doctype/loan => loan_management}/__init__.py (100%) rename erpnext/{hr/doctype/loan_application => loan_management/doctype}/__init__.py (100%) rename erpnext/{hr/doctype/loan_type => loan_management/doctype/loan}/__init__.py (100%) create mode 100644 erpnext/loan_management/doctype/loan/loan.js rename erpnext/{hr => loan_management}/doctype/loan/loan.json (69%) create mode 100644 erpnext/loan_management/doctype/loan/loan.py create mode 100644 erpnext/loan_management/doctype/loan/loan_dashboard.py create mode 100644 erpnext/loan_management/doctype/loan/test_loan.py rename erpnext/{hr/doctype/repayment_schedule => loan_management/doctype/loan_application}/__init__.py (100%) create mode 100644 erpnext/loan_management/doctype/loan_application/loan_application.js create mode 100644 erpnext/loan_management/doctype/loan_application/loan_application.json create mode 100644 erpnext/loan_management/doctype/loan_application/loan_application.py rename erpnext/{hr => loan_management}/doctype/loan_application/loan_application_dashboard.py (77%) rename erpnext/{hr => loan_management}/doctype/loan_application/test_loan_application.py (53%) rename erpnext/{hr/doctype/salary_slip_loan => loan_management/doctype/loan_disbursement}/__init__.py (100%) create mode 100644 erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.js create mode 100644 erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json create mode 100644 erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py create mode 100644 erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py rename erpnext/{hr/report/loan_repayment => loan_management/doctype/loan_interest_accrual}/__init__.py (100%) create mode 100644 erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.js create mode 100644 erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json create mode 100644 erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py create mode 100644 erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py create mode 100644 erpnext/loan_management/doctype/loan_repayment/__init__.py create mode 100644 erpnext/loan_management/doctype/loan_repayment/loan_repayment.js create mode 100644 erpnext/loan_management/doctype/loan_repayment/loan_repayment.json create mode 100644 erpnext/loan_management/doctype/loan_repayment/loan_repayment.py create mode 100644 erpnext/loan_management/doctype/loan_repayment/test_loan_repayment.py create mode 100644 erpnext/loan_management/doctype/loan_security/__init__.py create mode 100644 erpnext/loan_management/doctype/loan_security/loan_security.js create mode 100644 erpnext/loan_management/doctype/loan_security/loan_security.json create mode 100644 erpnext/loan_management/doctype/loan_security/loan_security.py create mode 100644 erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py create mode 100644 erpnext/loan_management/doctype/loan_security/test_loan_security.py create mode 100644 erpnext/loan_management/doctype/loan_security_pledge/__init__.py create mode 100644 erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js create mode 100644 erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json create mode 100644 erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py create mode 100644 erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge_list.js create mode 100644 erpnext/loan_management/doctype/loan_security_pledge/test_loan_security_pledge.py create mode 100644 erpnext/loan_management/doctype/loan_security_price/__init__.py create mode 100644 erpnext/loan_management/doctype/loan_security_price/loan_security_price.js create mode 100644 erpnext/loan_management/doctype/loan_security_price/loan_security_price.json create mode 100644 erpnext/loan_management/doctype/loan_security_price/loan_security_price.py create mode 100644 erpnext/loan_management/doctype/loan_security_price/test_loan_security_price.py create mode 100644 erpnext/loan_management/doctype/loan_security_shortfall/__init__.py create mode 100644 erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.js create mode 100644 erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json create mode 100644 erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py create mode 100644 erpnext/loan_management/doctype/loan_security_shortfall/test_loan_security_shortfall.py create mode 100644 erpnext/loan_management/doctype/loan_security_type/__init__.py create mode 100644 erpnext/loan_management/doctype/loan_security_type/loan_security_type.js create mode 100644 erpnext/loan_management/doctype/loan_security_type/loan_security_type.json create mode 100644 erpnext/loan_management/doctype/loan_security_type/loan_security_type.py create mode 100644 erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py create mode 100644 erpnext/loan_management/doctype/loan_security_type/test_loan_security_type.py create mode 100644 erpnext/loan_management/doctype/loan_security_unpledge/__init__.py create mode 100644 erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js create mode 100644 erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json create mode 100644 erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py create mode 100644 erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge_list.js create mode 100644 erpnext/loan_management/doctype/loan_security_unpledge/test_loan_security_unpledge.py create mode 100644 erpnext/loan_management/doctype/loan_type/__init__.py create mode 100644 erpnext/loan_management/doctype/loan_type/loan_type.js create mode 100644 erpnext/loan_management/doctype/loan_type/loan_type.json create mode 100644 erpnext/loan_management/doctype/loan_type/loan_type.py create mode 100644 erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py rename erpnext/{hr => loan_management}/doctype/loan_type/test_loan_type.py (53%) create mode 100644 erpnext/loan_management/doctype/pledge/__init__.py create mode 100644 erpnext/loan_management/doctype/pledge/pledge.js create mode 100644 erpnext/loan_management/doctype/pledge/pledge.json rename erpnext/{hr/doctype/loan_type/loan_type.py => loan_management/doctype/pledge/pledge.py} (62%) create mode 100644 erpnext/loan_management/doctype/pledge/test_pledge.py create mode 100644 erpnext/loan_management/doctype/process_loan_interest_accrual/__init__.py create mode 100644 erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.js create mode 100644 erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json create mode 100644 erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py create mode 100644 erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py create mode 100644 erpnext/loan_management/doctype/process_loan_interest_accrual/test_process_loan_interest_accrual.py create mode 100644 erpnext/loan_management/doctype/process_loan_security_shortfall/__init__.py create mode 100644 erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.js create mode 100644 erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json create mode 100644 erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py create mode 100644 erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py create mode 100644 erpnext/loan_management/doctype/process_loan_security_shortfall/test_process_loan_security_shortfall.py create mode 100644 erpnext/loan_management/doctype/proposed_pledge/__init__.py create mode 100644 erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json create mode 100644 erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.py create mode 100644 erpnext/loan_management/doctype/repayment_schedule/__init__.py rename erpnext/{hr => loan_management}/doctype/repayment_schedule/repayment_schedule.json (89%) rename erpnext/{hr => loan_management}/doctype/repayment_schedule/repayment_schedule.py (71%) create mode 100644 erpnext/loan_management/doctype/salary_slip_loan/__init__.py create mode 100644 erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json rename erpnext/{hr => loan_management}/doctype/salary_slip_loan/salary_slip_loan.py (71%) create mode 100644 erpnext/loan_management/doctype/sanctioned_loan_amount/__init__.py create mode 100644 erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.js create mode 100644 erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.json create mode 100644 erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py create mode 100644 erpnext/loan_management/doctype/sanctioned_loan_amount/test_sanctioned_loan_amount.py create mode 100644 erpnext/loan_management/doctype/unpledge/__init__.py create mode 100644 erpnext/loan_management/doctype/unpledge/unpledge.json create mode 100644 erpnext/loan_management/doctype/unpledge/unpledge.py rename erpnext/{hr => loan_management}/loan_common.js (56%) create mode 100644 erpnext/loan_management/report/__init__.py create mode 100644 erpnext/loan_management/report/loan_repayment_and_closure/__init__.py create mode 100644 erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.js create mode 100644 erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.json create mode 100644 erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py create mode 100644 erpnext/loan_management/report/loan_security_status/__init__.py create mode 100644 erpnext/loan_management/report/loan_security_status/loan_security_status.js create mode 100644 erpnext/loan_management/report/loan_security_status/loan_security_status.json create mode 100644 erpnext/loan_management/report/loan_security_status/loan_security_status.py delete mode 100644 erpnext/patches/v11_0/rename_employee_loan_to_loan.py diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 458e4a2526..4491f7a225 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -9,7 +9,6 @@ from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.utils import get_balance_on, get_account_currency from erpnext.accounts.party import get_party_account from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount -from erpnext.hr.doctype.loan.loan import update_disbursement_status, update_total_amount_paid from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import get_party_account_based_on_invoice_discounting from six import string_types, iteritems @@ -50,7 +49,6 @@ class JournalEntry(AccountsController): self.make_gl_entries() self.update_advance_paid() self.update_expense_claim() - self.update_loan() self.update_inter_company_jv() self.update_invoice_discounting() @@ -62,7 +60,6 @@ class JournalEntry(AccountsController): self.make_gl_entries(1) self.update_advance_paid() self.update_expense_claim() - self.update_loan() self.unlink_advance_entry_reference() self.unlink_asset_reference() self.unlink_inter_company_jv() @@ -597,17 +594,6 @@ class JournalEntry(AccountsController): doc = frappe.get_doc("Expense Claim", d.reference_name) update_reimbursed_amount(doc) - def update_loan(self): - if self.paid_loan: - paid_loan = json.loads(self.paid_loan) - value = 1 if self.docstatus < 2 else 0 - for name in paid_loan: - frappe.db.set_value("Repayment Schedule", name, "paid", value) - for d in self.accounts: - if d.reference_type=="Loan" and flt(d.debit) > 0: - doc = frappe.get_doc("Loan", d.reference_name) - update_disbursement_status(doc) - update_total_amount_paid(doc) def validate_expense_claim(self): for d in self.accounts: diff --git a/erpnext/config/desktop.py b/erpnext/config/desktop.py index 95f6f7c510..ce7c245a63 100644 --- a/erpnext/config/desktop.py +++ b/erpnext/config/desktop.py @@ -80,6 +80,15 @@ def get_data(): "type": "module", "description": "Sales pipeline, leads, opportunities and customers." }, + { + "module_name": "Loan Management", + "category": "Modules", + "label": _("Loan Management"), + "color": "#EF4DB6", + "icon": "octicon octicon-repo", + "type": "module", + "description": "Loan Management for Customer and Employees" + }, { "module_name": "Support", "category": "Modules", diff --git a/erpnext/config/loan_management.py b/erpnext/config/loan_management.py new file mode 100644 index 0000000000..a84f13abab --- /dev/null +++ b/erpnext/config/loan_management.py @@ -0,0 +1,107 @@ +from __future__ import unicode_literals +from frappe import _ +import frappe + + +def get_data(): + return [ + { + "label": _("Loan"), + "items": [ + { + "type": "doctype", + "name": "Loan Type", + "description": _("Loan Type for interest and penalty rates"), + }, + { + "type": "doctype", + "name": "Loan Application", + "description": _("Loan Applications from customers and employees."), + }, + { + "type": "doctype", + "name": "Loan", + "description": _("Loans provided to customers and employees."), + }, + + ] + }, + { + "label": _("Loan Security"), + "items": [ + { + "type": "doctype", + "name": "Loan Security Type", + }, + { + "type": "doctype", + "name": "Loan Security Price", + }, + { + "type": "doctype", + "name": "Loan Security", + }, + { + "type": "doctype", + "name": "Loan Security Pledge", + }, + { + "type": "doctype", + "name": "Loan Security Unpledge", + }, + { + "type": "doctype", + "name": "Loan Security Shortfall", + }, + ] + }, + { + "label": _("Disbursement and Repayment"), + "items": [ + { + "type": "doctype", + "name": "Loan Disbursement", + }, + { + "type": "doctype", + "name": "Loan Repayment", + }, + { + "type": "doctype", + "name": "Loan Interest Accrual" + } + ] + }, + { + "label": _("Loan Processes"), + "items": [ + { + "type": "doctype", + "name": "Process Loan Security Shortfall", + }, + { + "type": "doctype", + "name": "Process Loan Interest Accrual", + } + ] + }, + { + "label": _("Reports"), + "items": [ + { + "type": "report", + "is_query_report": True, + "name": "Loan Repayment and Closure", + "route": "#query-report/Loan Repayment and Closure", + "doctype": "Loan Repayment", + }, + { + "type": "report", + "is_query_report": True, + "name": "Loan Security Status", + "route": "#query-report/Loan Security Status", + "doctype": "Loan Security Pledge", + } + ] + } + ] \ No newline at end of file diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 774c917f68..54f1a1e452 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -314,12 +314,15 @@ scheduler_events = { "erpnext.setup.doctype.email_digest.email_digest.send", "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms", "erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation", - "erpnext.hr.utils.generate_leave_encashment" + "erpnext.hr.utils.generate_leave_encashment", + "erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall.check_for_ltv_shortfall", + "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.make_accrual_interest_entry_for_term_loans" ], "monthly_long": [ "erpnext.accounts.deferred_revenue.convert_deferred_revenue_to_income", "erpnext.accounts.deferred_revenue.convert_deferred_expense_to_expense", - "erpnext.hr.utils.allocate_earned_leaves" + "erpnext.hr.utils.allocate_earned_leaves", + "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual" ] } diff --git a/erpnext/hr/doctype/loan/loan.js b/erpnext/hr/doctype/loan/loan.js deleted file mode 100644 index 3f5c30c475..0000000000 --- a/erpnext/hr/doctype/loan/loan.js +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -{% include 'erpnext/hr/loan_common.js' %}; - -frappe.ui.form.on('Loan', { - onload: function (frm) { - frm.set_query("loan_application", function () { - return { - "filters": { - "applicant": frm.doc.applicant, - "docstatus": 1, - "status": "Approved" - } - }; - }); - - frm.set_query("interest_income_account", function () { - return { - "filters": { - "company": frm.doc.company, - "root_type": "Income", - "is_group": 0 - } - }; - }); - - $.each(["payment_account", "loan_account"], function (i, field) { - frm.set_query(field, function () { - return { - "filters": { - "company": frm.doc.company, - "root_type": "Asset", - "is_group": 0 - } - }; - }); - }) - }, - - refresh: function (frm) { - if (frm.doc.docstatus == 1) { - if (frm.doc.status == "Sanctioned") { - frm.add_custom_button(__('Create Disbursement Entry'), function() { - frm.trigger("make_jv"); - }).addClass("btn-primary"); - } else if (frm.doc.status == "Disbursed" && frm.doc.repayment_start_date && (frm.doc.applicant_type == 'Member' || frm.doc.repay_from_salary == 0)) { - frm.add_custom_button(__('Create Repayment Entry'), function() { - frm.trigger("make_repayment_entry"); - }).addClass("btn-primary"); - } - } - frm.trigger("toggle_fields"); - }, - - make_jv: function (frm) { - frappe.call({ - args: { - "loan": frm.doc.name, - "company": frm.doc.company, - "loan_account": frm.doc.loan_account, - "applicant_type": frm.doc.applicant_type, - "applicant": frm.doc.applicant, - "loan_amount": frm.doc.loan_amount, - "payment_account": frm.doc.payment_account - }, - method: "erpnext.hr.doctype.loan.loan.make_jv_entry", - callback: function (r) { - if (r.message) - var doc = frappe.model.sync(r.message)[0]; - frappe.set_route("Form", doc.doctype, doc.name); - } - }) - }, - make_repayment_entry: function(frm) { - var repayment_schedule = $.map(frm.doc.repayment_schedule, function(d) { return d.paid ? d.payment_date : false; }); - if(repayment_schedule.length >= 1){ - frm.repayment_data = []; - frm.show_dialog = 1; - let title = ""; - let fields = [ - {fieldtype:'Section Break', label: __('Repayment Schedule')}, - {fieldname: 'payments', fieldtype: 'Table', - fields: [ - { - fieldtype:'Data', - fieldname:'payment_date', - label: __('Date'), - read_only:1, - in_list_view: 1, - columns: 2 - }, - { - fieldtype:'Currency', - fieldname:'principal_amount', - label: __('Principal Amount'), - read_only:1, - in_list_view: 1, - columns: 3 - }, - { - fieldtype:'Currency', - fieldname:'interest_amount', - label: __('Interest'), - read_only:1, - in_list_view: 1, - columns: 2 - }, - { - fieldtype:'Currency', - read_only:1, - fieldname:'total_payment', - label: __('Total Payment'), - in_list_view: 1, - columns: 3 - }, - ], - data: frm.repayment_data, - get_data: function() { - return frm.repayment_data; - } - } - ] - - var dialog = new frappe.ui.Dialog({ - title: title, fields: fields, - }); - if (frm.doc['repayment_schedule']) { - frm.doc['repayment_schedule'].forEach((payment, index) => { - if (payment.paid == 0 && payment.payment_date <= frappe.datetime.now_date()) { - frm.repayment_data.push ({ - 'id': index, - 'name': payment.name, - 'payment_date': payment.payment_date, - 'principal_amount': payment.principal_amount, - 'interest_amount': payment.interest_amount, - 'total_payment': payment.total_payment - }); - dialog.fields_dict.payments.grid.refresh(); - $(dialog.wrapper.find(".grid-buttons")).hide(); - $(`.octicon.octicon-triangle-down`).hide(); - } - - }) - } - - dialog.show() - dialog.set_primary_action(__('Create Repayment Entry'), function() { - frm.values = dialog.get_values(); - if(frm.values) { - _make_repayment_entry(frm, dialog.fields_dict.payments.grid.get_selected_children()); - dialog.hide() - } - }); - } - - dialog.get_close_btn().on('click', () => { - dialog.hide(); - }); - }, - - mode_of_payment: function (frm) { - if (frm.doc.mode_of_payment && frm.doc.company) { - frappe.call({ - method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.get_bank_cash_account", - args: { - "mode_of_payment": frm.doc.mode_of_payment, - "company": frm.doc.company - }, - callback: function (r, rt) { - if (r.message) { - frm.set_value("payment_account", r.message.account); - } - } - }); - } - }, - - loan_application: function (frm) { - if(frm.doc.loan_application){ - return frappe.call({ - method: "erpnext.hr.doctype.loan.loan.get_loan_application", - args: { - "loan_application": frm.doc.loan_application - }, - callback: function (r) { - if (!r.exc && r.message) { - frm.set_value("loan_type", r.message.loan_type); - frm.set_value("loan_amount", r.message.loan_amount); - frm.set_value("repayment_method", r.message.repayment_method); - frm.set_value("monthly_repayment_amount", r.message.repayment_amount); - frm.set_value("repayment_periods", r.message.repayment_periods); - frm.set_value("rate_of_interest", r.message.rate_of_interest); - } - } - }); - } - }, - - repayment_method: function (frm) { - frm.trigger("toggle_fields") - }, - - toggle_fields: function (frm) { - frm.toggle_enable("monthly_repayment_amount", frm.doc.repayment_method == "Repay Fixed Amount per Period") - frm.toggle_enable("repayment_periods", frm.doc.repayment_method == "Repay Over Number of Periods") - } -}); - -var _make_repayment_entry = function(frm, payment_rows) { - frappe.call({ - method:"erpnext.hr.doctype.loan.loan.make_repayment_entry", - args: { - payment_rows: payment_rows, - "loan": frm.doc.name, - "company": frm.doc.company, - "loan_account": frm.doc.loan_account, - "applicant_type": frm.doc.applicant_type, - "applicant": frm.doc.applicant, - "payment_account": frm.doc.payment_account, - "interest_income_account": frm.doc.interest_income_account - }, - callback: function(r) { - if (r.message) - var doc = frappe.model.sync(r.message)[0]; - frappe.set_route("Form", doc.doctype, doc.name, {'payment_rows': payment_rows}); - } - }); -} \ No newline at end of file diff --git a/erpnext/hr/doctype/loan/loan.py b/erpnext/hr/doctype/loan/loan.py deleted file mode 100644 index a803863124..0000000000 --- a/erpnext/hr/doctype/loan/loan.py +++ /dev/null @@ -1,240 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe, math, json -import erpnext -from frappe import _ -from frappe.utils import flt, rounded, add_months, nowdate, getdate -from erpnext.controllers.accounts_controller import AccountsController - -class Loan(AccountsController): - def validate(self): - validate_repayment_method(self.repayment_method, self.loan_amount, self.monthly_repayment_amount, self.repayment_periods) - self.set_missing_fields() - self.make_repayment_schedule() - self.set_repayment_period() - self.calculate_totals() - - def set_missing_fields(self): - if not self.company: - self.company = erpnext.get_default_company() - - if not self.posting_date: - self.posting_date = nowdate() - - if self.loan_type and not self.rate_of_interest: - self.rate_of_interest = frappe.db.get_value("Loan Type", self.loan_type, "rate_of_interest") - - if self.repayment_method == "Repay Over Number of Periods": - self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods) - - if self.status == "Repaid/Closed": - self.total_amount_paid = self.total_payment - - - def make_jv_entry(self): - self.check_permission('write') - journal_entry = frappe.new_doc('Journal Entry') - journal_entry.voucher_type = 'Bank Entry' - journal_entry.user_remark = _('Against Loan: {0}').format(self.name) - journal_entry.company = self.company - journal_entry.posting_date = nowdate() - - account_amt_list = [] - - account_amt_list.append({ - "account": self.loan_account, - "party_type": self.applicant_type, - "party": self.applicant, - "debit_in_account_currency": self.loan_amount, - "reference_type": "Loan", - "reference_name": self.name, - }) - account_amt_list.append({ - "account": self.payment_account, - "credit_in_account_currency": self.loan_amount, - "reference_type": "Loan", - "reference_name": self.name, - }) - journal_entry.set("accounts", account_amt_list) - return journal_entry.as_dict() - - def make_repayment_schedule(self): - self.repayment_schedule = [] - payment_date = self.repayment_start_date - balance_amount = self.loan_amount - while(balance_amount > 0): - interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100)) - principal_amount = self.monthly_repayment_amount - interest_amount - balance_amount = rounded(balance_amount + interest_amount - self.monthly_repayment_amount) - - if balance_amount < 0: - principal_amount += balance_amount - balance_amount = 0.0 - - total_payment = principal_amount + interest_amount - self.append("repayment_schedule", { - "payment_date": payment_date, - "principal_amount": principal_amount, - "interest_amount": interest_amount, - "total_payment": total_payment, - "balance_loan_amount": balance_amount - }) - next_payment_date = add_months(payment_date, 1) - payment_date = next_payment_date - - def set_repayment_period(self): - if self.repayment_method == "Repay Fixed Amount per Period": - repayment_periods = len(self.repayment_schedule) - - self.repayment_periods = repayment_periods - - def calculate_totals(self): - self.total_payment = 0 - self.total_interest_payable = 0 - self.total_amount_paid = 0 - for data in self.repayment_schedule: - self.total_payment += data.total_payment - self.total_interest_payable +=data.interest_amount - if data.paid: - self.total_amount_paid += data.total_payment - -def update_total_amount_paid(doc): - total_amount_paid = 0 - for data in doc.repayment_schedule: - if data.paid: - total_amount_paid += data.total_payment - frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid) - -def update_disbursement_status(doc): - disbursement = frappe.db.sql(""" - select posting_date, ifnull(sum(credit_in_account_currency), 0) as disbursed_amount - from `tabGL Entry` - where account = %s and against_voucher_type = 'Loan' and against_voucher = %s - """, (doc.payment_account, doc.name), as_dict=1)[0] - - disbursement_date = None - if not disbursement or disbursement.disbursed_amount == 0: - status = "Sanctioned" - elif disbursement.disbursed_amount == doc.loan_amount: - disbursement_date = disbursement.posting_date - status = "Disbursed" - elif disbursement.disbursed_amount > doc.loan_amount: - frappe.throw(_("Disbursed Amount cannot be greater than Loan Amount {0}").format(doc.loan_amount)) - - if status == 'Disbursed' and getdate(disbursement_date) > getdate(frappe.db.get_value("Loan", doc.name, "repayment_start_date")): - frappe.throw(_("Disbursement Date cannot be after Loan Repayment Start Date")) - - frappe.db.sql(""" - update `tabLoan` - set status = %s, disbursement_date = %s - where name = %s - """, (status, disbursement_date, doc.name)) - -def validate_repayment_method(repayment_method, loan_amount, monthly_repayment_amount, repayment_periods): - if repayment_method == "Repay Over Number of Periods" and not repayment_periods: - frappe.throw(_("Please enter Repayment Periods")) - - if repayment_method == "Repay Fixed Amount per Period": - if not monthly_repayment_amount: - frappe.throw(_("Please enter repayment Amount")) - if monthly_repayment_amount > loan_amount: - frappe.throw(_("Monthly Repayment Amount cannot be greater than Loan Amount")) - -def get_monthly_repayment_amount(repayment_method, loan_amount, rate_of_interest, repayment_periods): - if rate_of_interest: - monthly_interest_rate = flt(rate_of_interest) / (12 *100) - monthly_repayment_amount = math.ceil((loan_amount * monthly_interest_rate * - (1 + monthly_interest_rate)**repayment_periods) \ - / ((1 + monthly_interest_rate)**repayment_periods - 1)) - else: - monthly_repayment_amount = math.ceil(flt(loan_amount) / repayment_periods) - return monthly_repayment_amount - -@frappe.whitelist() -def get_loan_application(loan_application): - loan = frappe.get_doc("Loan Application", loan_application) - if loan: - return loan.as_dict() - -@frappe.whitelist() -def make_repayment_entry(payment_rows, loan, company, loan_account, applicant_type, applicant, \ - payment_account=None, interest_income_account=None): - - if isinstance(payment_rows, frappe.string_types): - payment_rows_list = json.loads(payment_rows) - else: - frappe.throw(_("No repayments available for Journal Entry")) - - if payment_rows_list: - row_name = list(set(d["name"] for d in payment_rows_list)) - else: - frappe.throw(_("No repayments selected for Journal Entry")) - total_payment = 0 - principal_amount = 0 - interest_amount = 0 - for d in payment_rows_list: - total_payment += d["total_payment"] - principal_amount += d["principal_amount"] - interest_amount += d["interest_amount"] - - journal_entry = frappe.new_doc('Journal Entry') - journal_entry.voucher_type = 'Bank Entry' - journal_entry.user_remark = _('Against Loan: {0}').format(loan) - journal_entry.company = company - journal_entry.posting_date = nowdate() - journal_entry.paid_loan = json.dumps(row_name) - account_amt_list = [] - - account_amt_list.append({ - "account": payment_account, - "debit_in_account_currency": total_payment, - "reference_type": "Loan", - "reference_name": loan, - }) - account_amt_list.append({ - "account": loan_account, - "credit_in_account_currency": principal_amount, - "party_type": applicant_type, - "party": applicant, - "reference_type": "Loan", - "reference_name": loan, - }) - account_amt_list.append({ - "account": interest_income_account, - "credit_in_account_currency": interest_amount, - "reference_type": "Loan", - "reference_name": loan, - }) - journal_entry.set("accounts", account_amt_list) - - return journal_entry.as_dict() - -@frappe.whitelist() -def make_jv_entry(loan, company, loan_account, applicant_type, applicant, loan_amount,payment_account=None): - - journal_entry = frappe.new_doc('Journal Entry') - journal_entry.voucher_type = 'Bank Entry' - journal_entry.user_remark = _('Against Loan: {0}').format(loan) - journal_entry.company = company - journal_entry.posting_date = nowdate() - account_amt_list = [] - - account_amt_list.append({ - "account": loan_account, - "debit_in_account_currency": loan_amount, - "party_type": applicant_type, - "party": applicant, - "reference_type": "Loan", - "reference_name": loan, - }) - account_amt_list.append({ - "account": payment_account, - "credit_in_account_currency": loan_amount, - "reference_type": "Loan", - "reference_name": loan, - }) - journal_entry.set("accounts", account_amt_list) - return journal_entry.as_dict() diff --git a/erpnext/hr/doctype/loan/loan_dashboard.py b/erpnext/hr/doctype/loan/loan_dashboard.py deleted file mode 100644 index 7256d9424a..0000000000 --- a/erpnext/hr/doctype/loan/loan_dashboard.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import unicode_literals -from frappe import _ - -def get_data(): - return { - 'fieldname': 'applicant', - 'non_standard_fieldnames': { - 'Journal Entry': 'reference_name', - 'Salary Slip': 'employee' - }, - 'transactions': [ - { - 'label': _('Applicant'), - 'items': ['Loan Application'] - }, - - { - 'label': _('Account'), - 'items': ['Journal Entry'] - }, - { - 'label': _('Employee'), - 'items': ['Salary Slip'] - } - ] - } \ No newline at end of file diff --git a/erpnext/hr/doctype/loan/test_loan.js b/erpnext/hr/doctype/loan/test_loan.js deleted file mode 100644 index 28d30c9832..0000000000 --- a/erpnext/hr/doctype/loan/test_loan.js +++ /dev/null @@ -1,79 +0,0 @@ - -QUnit.test("Test Loan [HR]", function(assert) { - assert.expect(8); - let done = assert.async(); - let employee_name; - - // To create a loan and check principal,interest and balance amount - let loan_creation = (ename,lname) => { - return frappe.run_serially([ - () => frappe.db.get_value('Employee', {'employee_name': ename}, 'name'), - (r) => { - employee_name = r.message.name; - }, - () => frappe.db.get_value('Loan Application', {'loan_type': lname}, 'name'), - (r) => { - // Creating loan for an employee - return frappe.tests.make('Loan', [ - { company: 'For Testing'}, - { posting_date: '2017-08-26'}, - { applicant: employee_name}, - { loan_application: r.message.name}, - { disbursement_date: '2018-08-26'}, - { mode_of_payment: 'Cash'}, - { loan_account: 'Temporary Opening - FT'}, - { interest_income_account: 'Service - FT'} - ]); - }, - () => frappe.timeout(3), - () => frappe.click_button('Submit'), - () => frappe.timeout(1), - () => frappe.click_button('Yes'), - () => frappe.timeout(3), - - // Checking if all the amounts are correctly calculated - () => { - assert.ok(cur_frm.get_field('applicant_name').value=='Test Employee 1'&& - (cur_frm.get_field('status').value=='Sanctioned'), - 'Loan Sanctioned for correct employee'); - - assert.equal(7270, - cur_frm.get_doc('repayment_schedule').repayment_schedule[0].principal_amount, - 'Principal amount for first instalment is correctly calculated'); - - assert.equal(2333, - cur_frm.get_doc('repayment_schedule').repayment_schedule[0].interest_amount, - 'Interest amount for first instalment is correctly calculated'); - - assert.equal(192730, - cur_frm.get_doc('repayment_schedule').repayment_schedule[0].balance_loan_amount, - 'Balance amount after first instalment is correctly calculated'); - - assert.equal(9479, - cur_frm.get_doc('repayment_schedule').repayment_schedule[23].principal_amount, - 'Principal amount for last instalment is correctly calculated'); - - assert.equal(111, - cur_frm.get_doc('repayment_schedule').repayment_schedule[23].interest_amount, - 'Interest amount for last instalment is correctly calculated'); - - assert.equal(0, - cur_frm.get_doc('repayment_schedule').repayment_schedule[23].balance_loan_amount, - 'Balance amount after last instalment is correctly calculated'); - - }, - () => frappe.set_route('List','Loan','List'), - () => frappe.timeout(2), - - // Checking the submission of Loan - () => { - assert.ok(cur_list.data[0].docstatus==1,'Loan sanctioned and submitted successfully'); - }, - ]); - }; - frappe.run_serially([ - // Creating loan - () => loan_creation('Test Employee 1','Test Loan'), - () => done() - ]); -}); diff --git a/erpnext/hr/doctype/loan/test_loan.py b/erpnext/hr/doctype/loan/test_loan.py deleted file mode 100644 index 740e429510..0000000000 --- a/erpnext/hr/doctype/loan/test_loan.py +++ /dev/null @@ -1,71 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals - -import frappe -import erpnext -import unittest -from frappe.utils import nowdate, add_days -from erpnext.hr.doctype.salary_structure.test_salary_structure import make_employee - -class TestLoan(unittest.TestCase): - def setUp(self): - create_loan_type("Personal Loan", 500000, 8.4) - self.applicant = make_employee("robert_loan@loan.com") - create_loan(self.applicant, "Personal Loan", 280000, "Repay Over Number of Periods", 20) - - def test_loan(self): - loan = frappe.get_doc("Loan", {"applicant":self.applicant}) - self.assertEquals(loan.monthly_repayment_amount, 15052) - self.assertEquals(loan.total_interest_payable, 21034) - self.assertEquals(loan.total_payment, 301034) - - schedule = loan.repayment_schedule - - self.assertEqual(len(schedule), 20) - - for idx, principal_amount, interest_amount, balance_loan_amount in [[3, 13369, 1683, 227079], [19, 14941, 105, 0], [17, 14740, 312, 29785]]: - self.assertEqual(schedule[idx].principal_amount, principal_amount) - self.assertEqual(schedule[idx].interest_amount, interest_amount) - self.assertEqual(schedule[idx].balance_loan_amount, balance_loan_amount) - - loan.repayment_method = "Repay Fixed Amount per Period" - loan.monthly_repayment_amount = 14000 - loan.save() - - self.assertEquals(len(loan.repayment_schedule), 22) - self.assertEquals(loan.total_interest_payable, 22712) - self.assertEquals(loan.total_payment, 302712) - -def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest): - if not frappe.db.exists("Loan Type", loan_name): - frappe.get_doc({ - "doctype": "Loan Type", - "loan_name": loan_name, - "maximum_loan_amount": maximum_loan_amount, - "rate_of_interest": rate_of_interest - }).insert() - -def create_loan(applicant, loan_type, loan_amount, repayment_method, repayment_periods): - create_loan_type(loan_type, 500000, 8.4) - if not frappe.db.get_value("Loan", {"applicant":applicant}): - loan = frappe.new_doc("Loan") - loan.update({ - "applicant": applicant, - "loan_type": loan_type, - "loan_amount": loan_amount, - "repayment_method": repayment_method, - "repayment_periods": repayment_periods, - "disbursement_date": nowdate(), - "repayment_start_date": nowdate(), - "status": "Disbursed", - "mode_of_payment": frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name'), - "payment_account": frappe.db.get_value('Account', {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name"), - "loan_account": frappe.db.get_value('Account', {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name"), - "interest_income_account": frappe.db.get_value('Account', {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name") - }) - loan.insert() - return loan - else: - return frappe.get_doc("Loan", {"applicant":applicant}) \ No newline at end of file diff --git a/erpnext/hr/doctype/loan_application/loan_application.js b/erpnext/hr/doctype/loan_application/loan_application.js deleted file mode 100644 index a73b62a894..0000000000 --- a/erpnext/hr/doctype/loan_application/loan_application.js +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -{% include 'erpnext/hr/loan_common.js' %}; - -frappe.ui.form.on('Loan Application', { - refresh: function(frm) { - frm.trigger("toggle_fields") - frm.trigger("add_toolbar_buttons") - }, - repayment_method: function(frm) { - frm.doc.repayment_amount = frm.doc.repayment_periods = "" - frm.trigger("toggle_fields") - frm.trigger("toggle_required") - }, - toggle_fields: function(frm) { - frm.toggle_enable("repayment_amount", frm.doc.repayment_method=="Repay Fixed Amount per Period") - frm.toggle_enable("repayment_periods", frm.doc.repayment_method=="Repay Over Number of Periods") - }, - toggle_required: function(frm){ - frm.toggle_reqd("repayment_amount", cint(frm.doc.repayment_method=='Repay Fixed Amount per Period')) - frm.toggle_reqd("repayment_periods", cint(frm.doc.repayment_method=='Repay Over Number of Periods')) - }, - add_toolbar_buttons: function(frm) { - if (frm.doc.status == "Approved") { - frm.add_custom_button(__('Create Loan'), function() { - frappe.call({ - method: "erpnext.hr.doctype.loan_application.loan_application.make_loan", - args: { - "source_name": frm.doc.name - }, - callback: function(r) { - if(!r.exc) { - var doc = frappe.model.sync(r.message); - frappe.set_route("Form", r.message.doctype, r.message.name); - } - } - }); - }).addClass("btn-primary"); - } - } -}); diff --git a/erpnext/hr/doctype/loan_application/loan_application.json b/erpnext/hr/doctype/loan_application/loan_application.json deleted file mode 100644 index cc73a86ed0..0000000000 --- a/erpnext/hr/doctype/loan_application/loan_application.json +++ /dev/null @@ -1,840 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "ACC-LOAP-.YYYY.-.#####", - "beta": 0, - "creation": "2016-12-02 12:35:56.046811", - "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": "applicant_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": "Applicant Type", - "length": 0, - "no_copy": 0, - "options": "Employee\nMember", - "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": "applicant", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 1, - "label": "Applicant", - "length": 0, - "no_copy": 0, - "options": "applicant_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": 0, - "depends_on": "applicant", - "fieldname": "applicant_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Applicant 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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 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, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fieldname": "posting_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": "Posting 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": 1, - "bold": 0, - "collapsible": 0, - "columns": 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": 0, - "label": "Status", - "length": 0, - "no_copy": 1, - "options": "Open\nApproved\nRejected", - "permlevel": 1, - "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": "company", - "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": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "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": "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, - "label": "Loan Info", - "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": "loan_type", - "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": "Loan Type", - "length": 0, - "no_copy": 0, - "options": "Loan 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": 0, - "fieldname": "loan_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": "Loan Amount", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_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": 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": "required_by_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": "Required by 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": 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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 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": 0, - "in_standard_filter": 0, - "label": "Reason", - "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": "repayment_info", - "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": "Repayment Info", - "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": "repayment_method", - "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": "Repayment Method", - "length": 0, - "no_copy": 0, - "options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods", - "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, - "fetch_from": "loan_type.rate_of_interest", - "fieldname": "rate_of_interest", - "fieldtype": "Percent", - "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": "Rate of Interest", - "length": 0, - "no_copy": 0, - "options": "", - "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": "total_payable_interest", - "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": "Total Payable Interest", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "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_11", - "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, - "depends_on": "", - "fieldname": "repayment_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": "Monthly Repayment Amount", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "repayment_periods", - "fieldtype": "Int", - "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": "Repayment Period in Months", - "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": "total_payable_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": "Total Payable Amount", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "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": "Loan Application", - "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-08-21 16:15:53.688596", - "modified_by": "Administrator", - "module": "HR", - "name": "Loan Application", - "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": "HR 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": "Employee", - "set_user_permissions": 0, - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Employee", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 0 - } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "applicant_type, applicant, loan_type, loan_amount", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "timeline_field": "applicant", - "title_field": "applicant", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} \ No newline at end of file diff --git a/erpnext/hr/doctype/loan_application/loan_application.py b/erpnext/hr/doctype/loan_application/loan_application.py deleted file mode 100644 index 582bf48bf0..0000000000 --- a/erpnext/hr/doctype/loan_application/loan_application.py +++ /dev/null @@ -1,70 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe, math -from frappe import _ -from frappe.utils import flt, rounded -from frappe.model.mapper import get_mapped_doc -from frappe.model.document import Document - -from erpnext.hr.doctype.loan.loan import get_monthly_repayment_amount, validate_repayment_method - -class LoanApplication(Document): - def validate(self): - validate_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount, self.repayment_periods) - self.validate_loan_amount() - self.get_repayment_details() - - def validate_loan_amount(self): - maximum_loan_limit = frappe.db.get_value('Loan Type', self.loan_type, 'maximum_loan_amount') - if maximum_loan_limit and self.loan_amount > maximum_loan_limit: - frappe.throw(_("Loan Amount cannot exceed Maximum Loan Amount of {0}").format(maximum_loan_limit)) - - def get_repayment_details(self): - if self.repayment_method == "Repay Over Number of Periods": - self.repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods) - - if self.repayment_method == "Repay Fixed Amount per Period": - monthly_interest_rate = flt(self.rate_of_interest) / (12 *100) - if monthly_interest_rate: - min_repayment_amount = self.loan_amount*monthly_interest_rate - if (self.repayment_amount - min_repayment_amount) <= 0: - frappe.throw(_("Repayment Amount must be greater than " \ - + str(flt(min_repayment_amount, 2)))) - self.repayment_periods = math.ceil((math.log(self.repayment_amount) - - math.log(self.repayment_amount - min_repayment_amount)) /(math.log(1 + monthly_interest_rate))) - else: - self.repayment_periods = self.loan_amount / self.repayment_amount - - self.calculate_payable_amount() - - def calculate_payable_amount(self): - balance_amount = self.loan_amount - self.total_payable_amount = 0 - self.total_payable_interest = 0 - - while(balance_amount > 0): - interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100)) - balance_amount = rounded(balance_amount + interest_amount - self.repayment_amount) - - self.total_payable_interest += interest_amount - - self.total_payable_amount = self.loan_amount + self.total_payable_interest - -@frappe.whitelist() -def make_loan(source_name, target_doc = None): - doclist = get_mapped_doc("Loan Application", source_name, { - "Loan Application": { - "doctype": "Loan", - "field_map": { - "repayment_amount": "monthly_repayment_amount" - }, - "validation": { - "docstatus": ["=", 1] - } - } - }, target_doc) - - return doclist diff --git a/erpnext/hr/doctype/loan_application/test_loan_application.js b/erpnext/hr/doctype/loan_application/test_loan_application.js deleted file mode 100644 index b8789c7a87..0000000000 --- a/erpnext/hr/doctype/loan_application/test_loan_application.js +++ /dev/null @@ -1,68 +0,0 @@ -QUnit.module('hr'); - -QUnit.test("Test: Loan Application [HR]", function (assert) { - assert.expect(8); - let done = assert.async(); - let employee_name; - - frappe.run_serially([ - // Creation of Loan Application - () => frappe.db.get_value('Employee', {'employee_name': 'Test Employee 1'}, 'name'), - (r) => { - employee_name = r.message.name; - }, - () => { - return frappe.tests.make('Loan Application', [ - { company: 'For Testing'}, - { applicant: employee_name}, - { applicant_name: 'Test Employee 1'}, - { status: 'Approved'}, - { loan_type: 'Test Loan '}, - { loan_amount: 200000}, - { description: 'This is just a test'}, - { repayment_method: 'Repay Over Number of Periods'}, - { repayment_periods: 24}, - { rate_of_interest: 14} - ]); - }, - () => frappe.timeout(6), - () => frappe.click_button('Submit'), - () => frappe.timeout(1), - () => frappe.click_button('Yes'), - () => frappe.timeout(2), - () => { - // To check if all the amounts are correctly calculated - - assert.ok(cur_frm.get_field('applicant_name').value == 'Test Employee 1', - 'Application created successfully'); - - assert.ok(cur_frm.get_field('status').value=='Approved', - 'Status of application is correctly set'); - - assert.ok(cur_frm.get_field('loan_type').value=='Test Loan', - 'Application is created for correct Loan Type'); - - assert.ok(cur_frm.get_field('status').value=='Approved', - 'Status of application is correctly set'); - - assert.ok(cur_frm.get_field('repayment_amount').value==9603, - 'Repayment amount is correctly calculated'); - - assert.ok(cur_frm.get_field('total_payable_interest').value==30459, - 'Interest amount is correctly calculated'); - - assert.ok(cur_frm.get_field('total_payable_amount').value==230459, - 'Total payable amount is correctly calculated'); - }, - - () => frappe.set_route('List','Loan Application','List'), - () => frappe.timeout(2), - - // Checking the submission of Loan Application - () => { - assert.ok(cur_list.data[0].docstatus==1,'Loan Application submitted successfully'); - }, - () => frappe.timeout(1), - () => done() - ]); -}); \ No newline at end of file diff --git a/erpnext/hr/doctype/loan_type/loan_type.js b/erpnext/hr/doctype/loan_type/loan_type.js deleted file mode 100644 index 72f5775add..0000000000 --- a/erpnext/hr/doctype/loan_type/loan_type.js +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Loan Type', { - refresh: function(frm) { - } -}); diff --git a/erpnext/hr/doctype/loan_type/loan_type.json b/erpnext/hr/doctype/loan_type/loan_type.json deleted file mode 100644 index e595187e18..0000000000 --- a/erpnext/hr/doctype/loan_type/loan_type.json +++ /dev/null @@ -1,259 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:loan_name", - "beta": 0, - "creation": "2016-12-02 10:41:40.732843", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "loan_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": "Loan Name", - "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_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "maximum_loan_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": "Maximum Loan Amount", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_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, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "rate_of_interest", - "fieldtype": "Percent", - "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": "Rate of Interest (%) Yearly", - "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_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 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "disabled", - "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": "Disabled", - "length": 0, - "no_copy": 0, - "options": "", - "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_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": 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": "2017-03-29 21:23:08.665245", - "modified_by": "Administrator", - "module": "HR", - "name": "Loan Type", - "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": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Employee", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/hr/doctype/loan_type/loan_type_dashboard.py b/erpnext/hr/doctype/loan_type/loan_type_dashboard.py deleted file mode 100644 index 07b11fe627..0000000000 --- a/erpnext/hr/doctype/loan_type/loan_type_dashboard.py +++ /dev/null @@ -1,12 +0,0 @@ -from __future__ import unicode_literals -from frappe import _ - -def get_data(): - return { - 'fieldname': 'loan_type', - 'transactions': [ - { - 'items': ['Loan Application'] - }, - ], - } \ No newline at end of file diff --git a/erpnext/hr/doctype/loan_type/test_loan_type.js b/erpnext/hr/doctype/loan_type/test_loan_type.js deleted file mode 100644 index 71354be48d..0000000000 --- a/erpnext/hr/doctype/loan_type/test_loan_type.js +++ /dev/null @@ -1,31 +0,0 @@ -QUnit.module('hr'); - -QUnit.test("Test: Loan Type [HR]", function (assert) { - assert.expect(3); - let done = assert.async(); - - frappe.run_serially([ - // Loan Type creation - () => { - frappe.tests.make('Loan Type', [ - { loan_name: 'Test Loan'}, - { maximum_loan_amount: 400000}, - { rate_of_interest: 14}, - { description: - 'This is just a test.'} - ]); - }, - () => frappe.timeout(7), - () => frappe.set_route('List','Loan Type','List'), - () => frappe.timeout(4), - - // Checking if the fields are correctly set - () => { - assert.ok(cur_list.data.length==1, 'Loan Type created successfully'); - assert.ok(cur_list.data[0].name=='Test Loan', 'Loan title Correctly set'); - assert.ok(cur_list.data[0].disabled==0, 'Loan enabled'); - }, - () => done() - ]); -}); - diff --git a/erpnext/hr/doctype/payroll_entry/payroll_entry.py b/erpnext/hr/doctype/payroll_entry/payroll_entry.py index dfd38ebbe3..9ef3a99930 100644 --- a/erpnext/hr/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/hr/doctype/payroll_entry/payroll_entry.py @@ -157,19 +157,6 @@ class PayrollEntry(Document): for ss in submitted_ss: ss.email_salary_slip() - def get_loan_details(self): - """ - Get loan details from submitted salary slip based on selected criteria - """ - cond = self.get_filter_condition() - return frappe.db.sql(""" select eld.loan_account, eld.loan, - eld.interest_income_account, eld.principal_amount, eld.interest_amount, eld.total_payment,t1.employee - from - `tabSalary Slip` t1, `tabSalary Slip Loan` eld - where - t1.docstatus = 1 and t1.name = eld.parent and start_date >= %s and end_date <= %s %s - """ % ('%s', '%s', cond), (self.start_date, self.end_date), as_dict=True) or [] - def get_salary_component_account(self, salary_component): account = frappe.db.get_value("Salary Component Account", {"parent": salary_component, "company": self.company}, "default_account") @@ -225,7 +212,6 @@ class PayrollEntry(Document): earnings = self.get_salary_component_total(component_type = "earnings") or {} deductions = self.get_salary_component_total(component_type = "deductions") or {} default_payroll_payable_account = self.get_default_payroll_payable_account() - loan_details = self.get_loan_details() jv_name = "" precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") @@ -262,29 +248,6 @@ class PayrollEntry(Document): "project": self.project }) - # Loan - for data in loan_details: - accounts.append({ - "account": data.loan_account, - "credit_in_account_currency": data.principal_amount, - "party_type": "Employee", - "party": data.employee - }) - - if data.interest_amount and not data.interest_income_account: - frappe.throw(_("Select interest income account in loan {0}").format(data.loan)) - - if data.interest_income_account and data.interest_amount: - accounts.append({ - "account": data.interest_income_account, - "credit_in_account_currency": data.interest_amount, - "cost_center": self.cost_center, - "project": self.project, - "party_type": "Employee", - "party": data.employee - }) - payable_amount -= flt(data.total_payment, precision) - # Payable amount accounts.append({ "account": default_payroll_payable_account, diff --git a/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py b/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py index 3cf13226db..35f5a57a1c 100644 --- a/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py @@ -6,20 +6,22 @@ import erpnext import frappe from dateutil.relativedelta import relativedelta from erpnext.accounts.utils import get_fiscal_year, getdate, nowdate +from frappe.utils import add_months from erpnext.hr.doctype.payroll_entry.payroll_entry import get_start_end_dates, get_end_date from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.salary_slip.test_salary_slip import get_salary_component_account, \ make_earning_salary_component, make_deduction_salary_component from erpnext.hr.doctype.salary_structure.test_salary_structure import make_salary_structure -from erpnext.hr.doctype.loan.test_loan import create_loan +from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry +from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import make_accrual_interest_entry_for_term_loans class TestPayrollEntry(unittest.TestCase): def setUp(self): - for dt in ["Salary Slip", "Salary Component", "Salary Component Account", "Payroll Entry", "Loan"]: + for dt in ["Salary Slip", "Salary Component", "Salary Component Account", "Payroll Entry"]: frappe.db.sql("delete from `tab%s`" % dt) - make_earning_salary_component(setup=True) - make_deduction_salary_component(setup=True) + make_earning_salary_component(setup=True, company_list=["_Test Company"]) + make_deduction_salary_component(setup=True, company_list=["_Test Company"]) frappe.db.set_value("HR Settings", None, "email_salary_slip_to_employee", 0) @@ -49,8 +51,8 @@ class TestPayrollEntry(unittest.TestCase): def test_loan(self): branch = "Test Employee Branch" - applicant = make_employee("test_employee@loan.com") - company = erpnext.get_default_company() + applicant = make_employee("test_employee@loan.com", company="_Test Company") + company = "_Test Company" holiday_list = make_holiday("test holiday for loan") company_doc = frappe.get_doc('Company', company) @@ -70,16 +72,21 @@ class TestPayrollEntry(unittest.TestCase): employee_doc.holiday_list = holiday_list employee_doc.save() - loan = create_loan(applicant, - "Personal Loan", 280000, "Repay Over Number of Periods", 20) + salary_structure = "Test Salary Structure for Loan" + make_salary_structure(salary_structure, "Monthly", employee=employee_doc.name, company="_Test Company") + + loan = create_loan(applicant, "Car Loan", 280000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) loan.repay_from_salary = 1 loan.submit() - salary_structure = "Test Salary Structure for Loan" - make_salary_structure(salary_structure, "Monthly", employee_doc.name) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1)) + + make_accrual_interest_entry_for_term_loans(posting_date=nowdate()) + dates = get_start_end_dates('Monthly', nowdate()) - make_payroll_entry(start_date=dates.start_date, - end_date=dates.end_date, branch=branch) + make_payroll_entry(company="_Test Company", start_date=dates.start_date, + end_date=dates.end_date, branch=branch, cost_center="Main - _TC", payment_account="Cash - _TC") name = frappe.db.get_value('Salary Slip', {'posting_date': nowdate(), 'employee': applicant}, 'name') @@ -109,6 +116,13 @@ def make_payroll_entry(**args): payroll_entry.posting_date = nowdate() payroll_entry.payroll_frequency = "Monthly" payroll_entry.branch = args.branch or None + + if args.cost_center: + payroll_entry.cost_center = args.cost_center + + if args.payment_account: + payroll_entry.payment_account = args.payment_account + payroll_entry.save() payroll_entry.create_salary_slips() payroll_entry.submit_salary_slips() diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index ea531cbf9b..eee7974710 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -17,6 +17,7 @@ from erpnext.hr.doctype.additional_salary.additional_salary import get_additiona from erpnext.hr.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period from erpnext.hr.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount from erpnext.hr.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits +from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry class SalarySlip(TransactionBase): def __init__(self, *args, **kwargs): @@ -66,6 +67,7 @@ class SalarySlip(TransactionBase): self.set_status() self.update_status(self.name) self.update_salary_slip_in_additional_salary() + self.make_loan_repayment_entry() if (frappe.db.get_single_value("HR Settings", "email_salary_slip_to_employee")) and not frappe.flags.via_payroll_entry: self.email_salary_slip() @@ -73,6 +75,7 @@ class SalarySlip(TransactionBase): self.set_status() self.update_status() self.update_salary_slip_in_additional_salary() + self.cancel_loan_repayment_entry() def on_trash(self): from frappe.model.naming import revert_series_if_last @@ -754,28 +757,35 @@ class SalarySlip(TransactionBase): self.total_principal_amount = 0 for loan in self.get_loan_details(): - self.append('loans', { - 'loan': loan.name, - 'total_payment': loan.total_payment, - 'interest_amount': loan.interest_amount, - 'principal_amount': loan.principal_amount, - 'loan_account': loan.loan_account, - 'interest_income_account': loan.interest_income_account - }) - self.total_loan_repayment += loan.total_payment - self.total_interest_amount += loan.interest_amount - self.total_principal_amount += loan.principal_amount + amounts = calculate_amounts(loan.name, self.posting_date, "Regular Payment") + + total_payment = amounts['interest_amount'] + amounts['payable_principal_amount'] + + if total_payment: + self.append('loans', { + 'loan': loan.name, + 'total_payment': total_payment, + 'interest_amount': amounts['interest_amount'], + 'principal_amount': amounts['payable_principal_amount'], + 'loan_account': loan.loan_account, + 'interest_income_account': loan.interest_income_account + }) + + self.total_loan_repayment += total_payment + self.total_interest_amount += amounts['interest_amount'] + self.total_principal_amount += amounts['payable_principal_amount'] def get_loan_details(self): - return frappe.db.sql("""select rps.principal_amount, rps.interest_amount, l.name, - rps.total_payment, l.loan_account, l.interest_income_account - from - `tabRepayment Schedule` as rps, `tabLoan` as l - where - l.name = rps.parent and rps.payment_date between %s and %s and - l.repay_from_salary = 1 and l.docstatus = 1 and l.applicant = %s""", - (self.start_date, self.end_date, self.employee), as_dict=True) or [] + + return frappe.get_all("Loan", + fields=["name", "interest_income_account", "loan_account", "loan_type"], + filters = { + "applicant": self.employee, + "docstatus": 1, + "repay_from_salary": 1, + }) + def update_salary_slip_in_additional_salary(self): salary_slip = self.name if self.docstatus==1 else None @@ -784,6 +794,23 @@ class SalarySlip(TransactionBase): where employee=%s and payroll_date between %s and %s and docstatus=1 """, (salary_slip, self.employee, self.start_date, self.end_date)) + def make_loan_repayment_entry(self): + for loan in self.loans: + repayment_entry = create_repayment_entry(loan.loan, self.employee, + self.company, self.posting_date, loan.loan_type, "Regular Payment", loan.interest_amount, + loan.principal_amount, loan.total_payment) + + repayment_entry.save() + repayment_entry.submit() + + loan.loan_repayment_entry = repayment_entry.name + + def cancel_loan_repayment_entry(self): + for loan in self.loans: + if loan.loan_repayment_entry: + repayment_entry = frappe.get_doc("Loan Repayment", loan.loan_repayment_entry) + repayment_entry.cancel() + def email_salary_slip(self): receiver = frappe.db.get_value("Employee", self.employee, "prefered_email") hr_settings = frappe.get_single("HR Settings") diff --git a/erpnext/hr/doctype/salary_slip/test_salary_slip.py b/erpnext/hr/doctype/salary_slip/test_salary_slip.py index 16a75f473f..9acfd1f5c6 100644 --- a/erpnext/hr/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/test_salary_slip.py @@ -18,8 +18,8 @@ from erpnext.hr.doctype.employee_tax_exemption_declaration.test_employee_tax_exe class TestSalarySlip(unittest.TestCase): def setUp(self): - make_earning_salary_component(setup=True) - make_deduction_salary_component(setup=True) + make_earning_salary_component(setup=True, company_list=["_Test Company"]) + make_deduction_salary_component(setup=True, company_list=["_Test Company"]) for dt in ["Leave Application", "Leave Allocation", "Salary Slip"]: frappe.db.sql("delete from `tab%s`" % dt) @@ -50,7 +50,7 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.deductions[0].amount, 5000) self.assertEqual(ss.deductions[1].amount, 5000) self.assertEqual(ss.gross_pay, 78000) - self.assertEqual(ss.net_pay, 67418.0) + self.assertEqual(ss.net_pay, 68000.0) def test_salary_slip_with_holidays_excluded(self): no_of_days = self.get_no_of_days() @@ -70,7 +70,7 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.deductions[0].amount, 5000) self.assertEqual(ss.deductions[1].amount, 5000) self.assertEqual(ss.gross_pay, 78000) - self.assertEqual(ss.net_pay, 67418.0) + self.assertEqual(ss.net_pay, 68000.0) def test_payment_days(self): no_of_days = self.get_no_of_days() @@ -137,21 +137,41 @@ class TestSalarySlip(unittest.TestCase): make_employee("test_employee@salary.com") ss = make_employee_salary_slip("test_employee@salary.com", "Monthly") + ss.company = "_Test Company" + ss.save() ss.submit() email_queue = frappe.db.sql("""select name from `tabEmail Queue`""") self.assertTrue(email_queue) def test_loan_repayment_salary_slip(self): - from erpnext.hr.doctype.loan.test_loan import create_loan_type, create_loan - applicant = make_employee("test_employee@salary.com") - create_loan_type("Car Loan", 500000, 6.4) - loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20) + from erpnext.loan_management.doctype.loan.test_loan import create_loan_type, create_loan, make_loan_disbursement_entry, create_loan_accounts + from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import make_accrual_interest_entry_for_term_loans + + applicant = make_employee("test_loanemployee@salary.com", company="_Test Company") + + create_loan_accounts() + + create_loan_type("Car Loan", 500000, 8.4, + is_term_loan=1, + mode_of_payment='Cash', + payment_account='Payment Account - _TC', + loan_account='Loan Account - _TC', + interest_income_account='Interest Income Account - _TC', + penalty_income_account='Penalty Income Account - _TC') + + loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) loan.repay_from_salary = 1 loan.submit() - ss = make_employee_salary_slip("test_employee@salary.com", "Monthly") + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1)) + + make_accrual_interest_entry_for_term_loans(posting_date=nowdate()) + + ss = make_employee_salary_slip("test_loanemployee@salary.com", "Monthly") ss.submit() - self.assertEqual(ss.total_loan_repayment, 582) + + self.assertEqual(ss.total_loan_repayment, 592) self.assertEqual(ss.net_pay, (flt(ss.gross_pay) - (flt(ss.total_deduction) + flt(ss.total_loan_repayment)))) def test_payroll_frequency(self): @@ -321,7 +341,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): return salary_slip -def make_salary_component(salary_components, test_tax): +def make_salary_component(salary_components, test_tax, company_list=None): for salary_component in salary_components: if not frappe.db.exists('Salary Component', salary_component["salary_component"]): if test_tax: @@ -336,17 +356,22 @@ def make_salary_component(salary_components, test_tax): salary_component["doctype"] = "Salary Component" salary_component["salary_component_abbr"] = salary_component["abbr"] frappe.get_doc(salary_component).insert() - get_salary_component_account(salary_component["salary_component"]) + get_salary_component_account(salary_component["salary_component"], company_list) -def get_salary_component_account(sal_comp): +def get_salary_component_account(sal_comp, company_list=None): company = erpnext.get_default_company() + + if company_list and company not in company_list: + company_list.append(company) + sal_comp = frappe.get_doc("Salary Component", sal_comp) if not sal_comp.get("accounts"): - sal_comp.append("accounts", { - "company": company, - "default_account": create_account(company) - }) - sal_comp.save() + for d in company_list: + sal_comp.append("accounts", { + "company": d, + "default_account": create_account(d) + }) + sal_comp.save() def create_account(company): salary_account = frappe.db.get_value("Account", "Salary - " + frappe.get_cached_value('Company', company, 'abbr')) @@ -359,7 +384,7 @@ def create_account(company): }).insert() return salary_account -def make_earning_salary_component(setup=False, test_tax=False): +def make_earning_salary_component(setup=False, test_tax=False, company_list=None): data = [ { "salary_component": 'Basic Salary', @@ -415,7 +440,7 @@ def make_earning_salary_component(setup=False, test_tax=False): } ]) if setup or test_tax: - make_salary_component(data, test_tax) + make_salary_component(data, test_tax, company_list) data.append({ "salary_component": 'Basic Salary', "abbr":'BS', @@ -426,7 +451,7 @@ def make_earning_salary_component(setup=False, test_tax=False): }) return data -def make_deduction_salary_component(setup=False, test_tax=False): +def make_deduction_salary_component(setup=False, test_tax=False, company_list=None): data = [ { "salary_component": 'Professional Tax', @@ -458,7 +483,7 @@ def make_deduction_salary_component(setup=False, test_tax=False): "round_to_the_nearest_integer": 1 }) if setup or test_tax: - make_salary_component(data, test_tax) + make_salary_component(data, test_tax, company_list) return data diff --git a/erpnext/hr/doctype/salary_slip_loan/salary_slip_loan.json b/erpnext/hr/doctype/salary_slip_loan/salary_slip_loan.json deleted file mode 100644 index 5d1212b461..0000000000 --- a/erpnext/hr/doctype/salary_slip_loan/salary_slip_loan.json +++ /dev/null @@ -1,263 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-11-08 12:51:12.834479", - "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": "loan", - "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": "Loan", - "length": 0, - "no_copy": 0, - "options": "Loan", - "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, - "fieldname": "loan_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": "Loan 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": 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": "interest_income_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": "Interest Income 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": 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": "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_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "principal_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": "Principal Amount", - "length": 0, - "no_copy": 0, - "options": "", - "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": "interest_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": "Interest Amount", - "length": 0, - "no_copy": 0, - "options": "", - "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": "total_payment", - "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": "Total Payment", - "length": 0, - "no_copy": 0, - "options": "", - "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 - } - ], - "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-02-26 05:24:31.369630", - "modified_by": "Administrator", - "module": "HR", - "name": "Salary Slip Loan", - "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/hr/doctype/salary_structure/test_salary_structure.py b/erpnext/hr/doctype/salary_structure/test_salary_structure.py index 848c3df57a..78150946c8 100644 --- a/erpnext/hr/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/hr/doctype/salary_structure/test_salary_structure.py @@ -86,16 +86,17 @@ class TestSalaryStructure(unittest.TestCase): self.assertEqual(salary_structure_assignment.base, 5000) self.assertEqual(salary_structure_assignment.variable, 200) -def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None, test_tax=False): +def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None, + test_tax=False, company=None): if test_tax: frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure)) if not frappe.db.exists('Salary Structure', salary_structure): details = { "doctype": "Salary Structure", "name": salary_structure, - "company": erpnext.get_default_company(), - "earnings": make_earning_salary_component(test_tax=test_tax), - "deductions": make_deduction_salary_component(test_tax=test_tax), + "company": company or erpnext.get_default_company(), + "earnings": make_earning_salary_component(test_tax=test_tax, company_list=["_Test Company"]), + "deductions": make_deduction_salary_component(test_tax=test_tax, company_list=["_Test Company"]), "payroll_frequency": payroll_frequency, "payment_account": get_random("Account") } @@ -109,11 +110,11 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do if employee and not frappe.db.get_value("Salary Structure Assignment", {'employee':employee, 'docstatus': 1}) and salary_structure_doc.docstatus==1: - create_salary_structure_assignment(employee, salary_structure) + create_salary_structure_assignment(employee, salary_structure, company=company) return salary_structure_doc -def create_salary_structure_assignment(employee, salary_structure, from_date=None): +def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None): if frappe.db.exists("Salary Structure Assignment", {"employee": employee}): frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee)) salary_structure_assignment = frappe.new_doc("Salary Structure Assignment") @@ -122,7 +123,7 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non salary_structure_assignment.variable = 5000 salary_structure_assignment.from_date = from_date or add_months(nowdate(), -1) salary_structure_assignment.salary_structure = salary_structure - salary_structure_assignment.company = erpnext.get_default_company() + salary_structure_assignment.company = company or erpnext.get_default_company() salary_structure_assignment.save(ignore_permissions=True) salary_structure_assignment.submit() return salary_structure_assignment diff --git a/erpnext/hr/report/loan_repayment/loan_repayment.js b/erpnext/hr/report/loan_repayment/loan_repayment.js deleted file mode 100644 index 21aa206160..0000000000 --- a/erpnext/hr/report/loan_repayment/loan_repayment.js +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt -/* eslint-disable */ - -frappe.query_reports["Loan Repayment"] = { - "filters": [ - - ] -} diff --git a/erpnext/hr/report/loan_repayment/loan_repayment.json b/erpnext/hr/report/loan_repayment/loan_repayment.json deleted file mode 100644 index b967dfdb38..0000000000 --- a/erpnext/hr/report/loan_repayment/loan_repayment.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "add_total_row": 0, - "creation": "2019-03-29 18:58:00.166032", - "disable_prepared_report": 0, - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "letter_head": "", - "modified": "2019-03-29 18:58:00.166032", - "modified_by": "Administrator", - "module": "HR", - "name": "Loan Repayment", - "owner": "Administrator", - "prepared_report": 0, - "ref_doctype": "Loan", - "report_name": "Loan Repayment", - "report_type": "Script Report", - "roles": [ - { - "role": "HR Manager" - }, - { - "role": "Employee" - } - ] -} diff --git a/erpnext/hr/report/loan_repayment/loan_repayment.py b/erpnext/hr/report/loan_repayment/loan_repayment.py deleted file mode 100644 index beca776964..0000000000 --- a/erpnext/hr/report/loan_repayment/loan_repayment.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe import _ - -def execute(filters=None): - - columns = create_columns() - data = get_record() - return columns, data - -def create_columns(): - return [ - { - "label": _("Employee"), - "fieldtype": "Data", - "fieldname": "employee", - "options": "Employee", - "width": 200 - }, - { - "label": _("Loan"), - "fieldtype": "Link", - "fieldname": "loan_name", - "options": "Loan", - "width": 200 - }, - { - "label": _("Loan Amount"), - "fieldtype": "Currency", - "fieldname": "loan_amount", - "options": "currency", - "width": 100 - }, - { - "label": _("Interest"), - "fieldtype": "Data", - "fieldname": "interest", - "width": 100 - }, - { - "label": _("Payable Amount"), - "fieldtype": "Currency", - "fieldname": "payable_amount", - "options": "currency", - "width": 100 - }, - { - "label": _("EMI"), - "fieldtype": "Currency", - "fieldname": "emi", - "options": "currency", - "width": 100 - }, - { - "label": _("Paid Amount"), - "fieldtype": "Currency", - "fieldname": "paid_amount", - "options": "currency", - "width": 100 - }, - { - "label": _("Outstanding Amount"), - "fieldtype": "Currency", - "fieldname": "out_amt", - "options": "currency", - "width": 100 - }, - ] - -def get_record(): - data = [] - loans = frappe.get_all("Loan", - filters=[("status", "=", "Disbursed")], - fields=["applicant", "applicant_name", "name", "loan_amount", "rate_of_interest", - "total_payment", "monthly_repayment_amount", "total_amount_paid"] - ) - - for loan in loans: - row = { - "employee": loan.applicant + ": " + loan.applicant_name, - "loan_name": loan.name, - "loan_amount": loan.loan_amount, - "interest": str(loan.rate_of_interest) + "%", - "payable_amount": loan.total_payment, - "emi": loan.monthly_repayment_amount, - "paid_amount": loan.total_amount_paid, - "out_amt": loan.total_payment - loan.total_amount_paid - } - - data.append(row) - - return data diff --git a/erpnext/hr/doctype/loan/__init__.py b/erpnext/loan_management/__init__.py similarity index 100% rename from erpnext/hr/doctype/loan/__init__.py rename to erpnext/loan_management/__init__.py diff --git a/erpnext/hr/doctype/loan_application/__init__.py b/erpnext/loan_management/doctype/__init__.py similarity index 100% rename from erpnext/hr/doctype/loan_application/__init__.py rename to erpnext/loan_management/doctype/__init__.py diff --git a/erpnext/hr/doctype/loan_type/__init__.py b/erpnext/loan_management/doctype/loan/__init__.py similarity index 100% rename from erpnext/hr/doctype/loan_type/__init__.py rename to erpnext/loan_management/doctype/loan/__init__.py diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js new file mode 100644 index 0000000000..8b220171e8 --- /dev/null +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -0,0 +1,190 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +{% include 'erpnext/loan_management/loan_common.js' %}; + +frappe.ui.form.on('Loan', { + setup: function(frm) { + frm.make_methods = { + 'Loan Disbursement': function() { frm.trigger('make_loan_disbursement') }, + 'Loan Security Unpledge': function() { frm.trigger('create_loan_security_unpledge') } + } + }, + onload: function (frm) { + frm.set_query("loan_application", function () { + return { + "filters": { + "applicant": frm.doc.applicant, + "docstatus": 1, + "status": "Approved" + } + }; + }); + + $.each(["penalty_income_account", "interest_income_account"], function(i, field) { + frm.set_query(field, function () { + return { + "filters": { + "company": frm.doc.company, + "root_type": "Income", + "is_group": 0 + } + }; + }); + }); + + $.each(["payment_account", "loan_account"], function (i, field) { + frm.set_query(field, function () { + return { + "filters": { + "company": frm.doc.company, + "root_type": "Asset", + "is_group": 0 + } + }; + }); + }) + + frm.set_query('loan_security_pledge', function(doc, cdt, cdn) { + return { + filters: { + applicant: frm.doc.applicant, + docstatus: 1, + loan_application: frm.doc.loan_application || '' + } + }; + }); + }, + + refresh: function (frm) { + if (frm.doc.docstatus == 1) { + if (frm.doc.status == "Sanctioned" || frm.doc.status == 'Partially Disbursed') { + frm.add_custom_button(__('Loan Disbursement'), function() { + frm.trigger("make_loan_disbursement"); + },__('Create')); + } + + if (["Disbursed", "Partially Disbursed"].includes(frm.doc.status) && (!frm.doc.repay_from_salary)) { + frm.add_custom_button(__('Loan Repayment'), function() { + frm.trigger("make_repayment_entry"); + },__('Create')); + + } + + if (frm.doc.status == "Loan Closure Requested") { + frm.add_custom_button(__('Loan Security Unpledge'), function() { + frm.trigger("create_loan_security_unpledge"); + },__('Create')); + } + } + frm.trigger("toggle_fields"); + }, + + loan_type: function(frm) { + frm.toggle_reqd("repayment_method", frm.doc.is_term_loan); + frm.toggle_display("repayment_method", 1 - frm.doc.is_term_loan); + frm.toggle_display("repayment_periods", s1 - frm.doc.is_term_loan); + }, + + is_secured_loan: function(frm) { + frm.toggle_reqd("loan_security_pledge", frm.doc.is_secured_loan); + }, + + make_loan_disbursement: function (frm) { + frappe.call({ + args: { + "loan": frm.doc.name, + "company": frm.doc.company, + "applicant_type": frm.doc.applicant_type, + "applicant": frm.doc.applicant, + "as_dict": 1 + }, + method: "erpnext.loan_management.doctype.loan.loan.make_loan_disbursement", + callback: function (r) { + if (r.message) + var doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + }) + }, + + make_repayment_entry: function(frm) { + frappe.call({ + args: { + "loan": frm.doc.name, + "applicant_type": frm.doc.applicant_type, + "applicant": frm.doc.applicant, + "loan_type": frm.doc.loan_type, + "company": frm.doc.company, + "as_dict": 1 + }, + method: "erpnext.loan_management.doctype.loan.loan.make_repayment_entry", + callback: function (r) { + if (r.message) + var doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + }) + }, + + create_loan_security_unpledge: function(frm) { + frappe.call({ + method: "erpnext.loan_management.doctype.loan.loan.create_loan_security_unpledge", + args : { + "loan": frm.doc.name, + "applicant_type": frm.doc.applicant_type, + "applicant": frm.doc.applicant, + "company": frm.doc.company + }, + callback: function(r) { + if (r.message) + var doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + }) + }, + + loan_application: function (frm) { + if(frm.doc.loan_application){ + return frappe.call({ + method: "erpnext.loan_management.doctype.loan.loan.get_loan_application", + args: { + "loan_application": frm.doc.loan_application + }, + callback: function (r) { + if (!r.exc && r.message) { + + let loan_fields = ["loan_type", "loan_amount", "repayment_method", + "monthly_repayment_amount", "repayment_periods", "rate_of_interest", "is_secured_loan"] + + loan_fields.forEach(field => { + frm.set_value(field, r.message[field]); + }); + + if (frm.doc.is_secured_loan) { + $.each(r.message.proposed_pledges, function(i, d) { + let row = frm.add_child("securities"); + row.loan_security = d.loan_security; + row.qty = d.qty; + row.loan_security_price = d.loan_security_price; + row.amount = d.amount; + row.haircut = d.haircut; + }); + + frm.refresh_fields("securities"); + } + } + } + }); + } + }, + + repayment_method: function (frm) { + frm.trigger("toggle_fields") + }, + + toggle_fields: function (frm) { + frm.toggle_enable("monthly_repayment_amount", frm.doc.repayment_method == "Repay Fixed Amount per Period") + frm.toggle_enable("repayment_periods", frm.doc.repayment_method == "Repay Over Number of Periods") + } +}); diff --git a/erpnext/hr/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json similarity index 69% rename from erpnext/hr/doctype/loan/loan.json rename to erpnext/loan_management/doctype/loan/loan.json index 2b2d481827..2834e5b655 100644 --- a/erpnext/hr/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -2,7 +2,7 @@ "actions": [], "allow_import": 1, "autoname": "ACC-LOAN-.YYYY.-.#####", - "creation": "2016-12-02 10:11:49.673604", + "creation": "2019-08-29 17:29:18.176786", "doctype": "DocType", "document_type": "Document", "editable_grid": 1, @@ -11,32 +11,41 @@ "applicant_type", "applicant", "applicant_name", - "loan_type", "loan_application", "column_break_3", - "posting_date", "company", + "posting_date", "status", "repay_from_salary", "section_break_8", + "loan_type", "loan_amount", + "is_secured_loan", "rate_of_interest", "disbursement_date", - "repayment_start_date", + "disbursed_amount", "column_break_11", + "is_term_loan", "repayment_method", "repayment_periods", "monthly_repayment_amount", + "repayment_start_date", + "loan_security_details_section", + "loan_security_pledge", + "column_break_25", + "maximum_loan_value", "account_info", "mode_of_payment", "payment_account", "column_break_9", "loan_account", "interest_income_account", + "penalty_income_account", "section_break_15", "repayment_schedule", "section_break_17", "total_payment", + "total_principal_paid", "column_break_19", "total_interest_payable", "total_amount_paid", @@ -47,7 +56,7 @@ "fieldname": "applicant_type", "fieldtype": "Select", "label": "Applicant Type", - "options": "Employee\nMember", + "options": "Employee\nMember\nCustomer", "reqd": 1 }, { @@ -75,6 +84,7 @@ "fieldname": "loan_type", "fieldtype": "Link", "in_list_view": 1, + "in_standard_filter": 1, "label": "Loan Type", "options": "Loan Type", "reqd": 1 @@ -95,6 +105,7 @@ { "fieldname": "company", "fieldtype": "Link", + "in_standard_filter": 1, "label": "Company", "options": "Company", "remember_last_selected_value": 1, @@ -104,9 +115,10 @@ "default": "Sanctioned", "fieldname": "status", "fieldtype": "Select", + "in_standard_filter": 1, "label": "Status", "no_copy": 1, - "options": "Sanctioned\nDisbursed\nRepaid/Closed", + "options": "Sanctioned\nPartially Disbursed\nDisbursed\nLoan Closure Requested\nClosed", "read_only": 1 }, { @@ -125,8 +137,7 @@ "fieldname": "loan_amount", "fieldtype": "Currency", "label": "Loan Amount", - "options": "Company:company:default_currency", - "reqd": 1 + "options": "Company:company:default_currency" }, { "fetch_from": "loan_type.rate_of_interest", @@ -143,29 +154,30 @@ "label": "Disbursement Date" }, { + "depends_on": "is_term_loan", "fieldname": "repayment_start_date", "fieldtype": "Date", - "label": "Repayment Start Date", - "reqd": 1 + "label": "Repayment Start Date" }, { "fieldname": "column_break_11", "fieldtype": "Column Break" }, { - "default": "Repay Over Number of Periods", + "depends_on": "is_term_loan", "fieldname": "repayment_method", "fieldtype": "Select", "label": "Repayment Method", - "options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods", - "reqd": 1 + "options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods" }, { + "depends_on": "is_term_loan", "fieldname": "repayment_periods", "fieldtype": "Int", "label": "Repayment Period in Months" }, { + "depends_on": "is_term_loan", "fieldname": "monthly_repayment_amount", "fieldtype": "Currency", "label": "Monthly Repayment Amount", @@ -178,17 +190,21 @@ "label": "Account Info" }, { + "fetch_from": "loan_type.mode_of_payment", "fieldname": "mode_of_payment", "fieldtype": "Link", "label": "Mode of Payment", "options": "Mode of Payment", + "read_only": 1, "reqd": 1 }, { + "fetch_from": "loan_type.payment_account", "fieldname": "payment_account", "fieldtype": "Link", "label": "Payment Account", "options": "Account", + "read_only": 1, "reqd": 1 }, { @@ -196,25 +212,31 @@ "fieldtype": "Column Break" }, { + "fetch_from": "loan_type.loan_account", "fieldname": "loan_account", "fieldtype": "Link", "label": "Loan Account", "options": "Account", + "read_only": 1, "reqd": 1 }, { + "fetch_from": "loan_type.interest_income_account", "fieldname": "interest_income_account", "fieldtype": "Link", "label": "Interest Income Account", "options": "Account", + "read_only": 1, "reqd": 1 }, { + "depends_on": "is_term_loan", "fieldname": "section_break_15", "fieldtype": "Section Break", "label": "Repayment Schedule" }, { + "depends_on": "eval:doc.is_term_loan == 1", "fieldname": "repayment_schedule", "fieldtype": "Table", "label": "Repayment Schedule", @@ -230,7 +252,7 @@ "default": "0", "fieldname": "total_payment", "fieldtype": "Currency", - "label": "Total Payment", + "label": "Total Payable Amount", "options": "Company:company:default_currency", "read_only": 1 }, @@ -240,6 +262,7 @@ }, { "default": "0", + "depends_on": "is_term_loan", "fieldname": "total_interest_payable", "fieldtype": "Currency", "label": "Total Interest Payable", @@ -262,13 +285,74 @@ "options": "Loan", "print_hide": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_secured_loan", + "fieldtype": "Check", + "label": "Is Secured Loan" + }, + { + "depends_on": "is_secured_loan", + "fieldname": "loan_security_details_section", + "fieldtype": "Section Break", + "label": "Loan Security Details" + }, + { + "default": "0", + "fetch_from": "loan_type.is_term_loan", + "fieldname": "is_term_loan", + "fieldtype": "Check", + "label": "Is Term Loan", + "read_only": 1 + }, + { + "fetch_from": "loan_type.penalty_income_account", + "fieldname": "penalty_income_account", + "fieldtype": "Link", + "label": "Penalty Income Account", + "options": "Account", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "total_principal_paid", + "fieldtype": "Currency", + "label": "Total Principal Paid", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "loan_security_pledge", + "fieldtype": "Link", + "label": "Loan Security Pledge", + "options": "Loan Security Pledge" + }, + { + "fieldname": "disbursed_amount", + "fieldtype": "Currency", + "label": "Disbursed Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fetch_from": "loan_security_pledge.maximum_loan_value", + "fieldname": "maximum_loan_value", + "fieldtype": "Currency", + "label": "Maximum Loan Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "column_break_25", + "fieldtype": "Column Break" } ], "is_submittable": 1, "links": [], - "modified": "2019-12-12 14:45:38.823072", + "modified": "2020-02-07 01:31:25.172173", "modified_by": "Administrator", - "module": "HR", + "module": "Loan Management", "name": "Loan", "owner": "Administrator", "permissions": [ @@ -281,7 +365,7 @@ "print": 1, "read": 1, "report": 1, - "role": "HR Manager", + "role": "Loan Manager", "share": 1, "submit": 1, "write": 1 diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py new file mode 100644 index 0000000000..696410b7bb --- /dev/null +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -0,0 +1,254 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, math, json +import erpnext +from frappe import _ +from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime + +from erpnext.controllers.accounts_controller import AccountsController + +class Loan(AccountsController): + def validate(self): + self.set_loan_amount() + + self.set_missing_fields() + self.validate_accounts() + self.validate_loan_security_pledge() + self.validate_loan_amount() + self.check_sanctioned_amount_limit() + + if self.is_term_loan: + validate_repayment_method(self.repayment_method, self.loan_amount, self.monthly_repayment_amount, + self.repayment_periods, self.is_term_loan) + self.make_repayment_schedule() + self.set_repayment_period() + + self.calculate_totals() + + def validate_accounts(self): + for fieldname in ['payment_account', 'loan_account', 'interest_income_account', 'penalty_income_account']: + company = frappe.get_value("Account", self.get(fieldname), 'company') + + if company != self.company: + frappe.throw(_("Account {0} does not belongs to company {1}").format(frappe.bold(self.get(fieldname)), + frappe.bold(self.company))) + + def on_submit(self): + self.link_loan_security_pledge() + + def on_cancel(self): + self.unlink_loan_security_pledge() + + def set_missing_fields(self): + if not self.company: + self.company = erpnext.get_default_company() + + if not self.posting_date: + self.posting_date = nowdate() + + if self.loan_type and not self.rate_of_interest: + self.rate_of_interest = frappe.db.get_value("Loan Type", self.loan_type, "rate_of_interest") + + if self.repayment_method == "Repay Over Number of Periods": + self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods) + + def validate_loan_security_pledge(self): + + if self.is_secured_loan and not self.loan_security_pledge: + frappe.throw(_("Loan Security Pledge is mandatory for secured loan")) + + if self.loan_security_pledge: + loan_security_details = frappe.db.get_value("Loan Security Pledge", self.loan_security_pledge, + ['loan', 'company'], as_dict=1) + + if loan_security_details.loan: + frappe.throw(_("Loan Security Pledge already pledged against loan {0}").format(loan_security_details.loan)) + + if loan_security_details.company != self.company: + frappe.throw(_("Loan Security Pledge Company and Loan Company must be same")) + + def check_sanctioned_amount_limit(self): + total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company) + sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company) + + if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit): + frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant))) + + def make_repayment_schedule(self): + + if not self.repayment_start_date: + frappe.throw(_("Repayment Start Date is mandatory for term loans")) + + self.repayment_schedule = [] + payment_date = self.repayment_start_date + balance_amount = self.loan_amount + while(balance_amount > 0): + interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100)) + principal_amount = self.monthly_repayment_amount - interest_amount + balance_amount = rounded(balance_amount + interest_amount - self.monthly_repayment_amount) + + if balance_amount < 0: + principal_amount += balance_amount + balance_amount = 0.0 + + total_payment = principal_amount + interest_amount + self.append("repayment_schedule", { + "payment_date": payment_date, + "principal_amount": principal_amount, + "interest_amount": interest_amount, + "total_payment": total_payment, + "balance_loan_amount": balance_amount + }) + next_payment_date = add_months(payment_date, 1) + payment_date = next_payment_date + + def set_repayment_period(self): + if self.repayment_method == "Repay Fixed Amount per Period": + repayment_periods = len(self.repayment_schedule) + + self.repayment_periods = repayment_periods + + def calculate_totals(self): + self.total_payment = 0 + self.total_interest_payable = 0 + self.total_amount_paid = 0 + + if self.is_term_loan: + for data in self.repayment_schedule: + self.total_payment += data.total_payment + self.total_interest_payable +=data.interest_amount + else: + self.total_payment = self.loan_amount + + def set_loan_amount(self): + + if not self.loan_amount and self.is_secured_loan and self.loan_security_pledge: + self.loan_amount = self.maximum_loan_value + + def validate_loan_amount(self): + if self.is_secured_loan and self.loan_amount > self.maximum_loan_value: + msg = _("Loan amount cannot be greater than {0}").format(self.maximum_loan_value) + frappe.throw(msg) + + if not self.loan_amount: + frappe.throw(_("Loan amount is mandatory")) + + def link_loan_security_pledge(self): + frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET + loan = %s, status = 'Pledged', pledge_time = %s + where name = %s """, (self.name, now_datetime(), self.loan_security_pledge)) + + def unlink_loan_security_pledge(self): + frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET + loan = '', status = 'Unpledged' + where name = %s """, (self.loan_security_pledge)) + +def update_total_amount_paid(doc): + total_amount_paid = 0 + for data in doc.repayment_schedule: + if data.paid: + total_amount_paid += data.total_payment + frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid) + +def get_total_loan_amount(applicant_type, applicant, company): + return frappe.db.get_value('Loan', + {'applicant_type': applicant_type, 'company': company, 'applicant': applicant, 'docstatus': 1}, + 'sum(loan_amount)') + +def get_sanctioned_amount_limit(applicant_type, applicant, company): + return frappe.db.get_value('Sanctioned Loan Amount', + {'applicant_type': applicant_type, 'company': company, 'applicant': applicant}, + 'sanctioned_amount_limit') + +def validate_repayment_method(repayment_method, loan_amount, monthly_repayment_amount, repayment_periods, is_term_loan): + + if is_term_loan and not repayment_method: + frappe.throw(_("Repayment Method is mandatory for term loans")) + + if repayment_method == "Repay Over Number of Periods" and not repayment_periods: + frappe.throw(_("Please enter Repayment Periods")) + + if repayment_method == "Repay Fixed Amount per Period": + if not monthly_repayment_amount: + frappe.throw(_("Please enter repayment Amount")) + if monthly_repayment_amount > loan_amount: + frappe.throw(_("Monthly Repayment Amount cannot be greater than Loan Amount")) + +def get_monthly_repayment_amount(repayment_method, loan_amount, rate_of_interest, repayment_periods): + if rate_of_interest: + monthly_interest_rate = flt(rate_of_interest) / (12 *100) + monthly_repayment_amount = math.ceil((loan_amount * monthly_interest_rate * + (1 + monthly_interest_rate)**repayment_periods) \ + / ((1 + monthly_interest_rate)**repayment_periods - 1)) + else: + monthly_repayment_amount = math.ceil(flt(loan_amount) / repayment_periods) + return monthly_repayment_amount + +@frappe.whitelist() +def get_loan_application(loan_application): + loan = frappe.get_doc("Loan Application", loan_application) + if loan: + return loan.as_dict() + +def close_loan(loan, total_amount_paid): + frappe.db.set_value("Loan", loan, "total_amount_paid", total_amount_paid) + frappe.db.set_value("Loan", loan, "status", "Closed") + +@frappe.whitelist() +def make_loan_disbursement(loan, company, applicant_type, applicant, disbursed_amount=0, as_dict=0): + disbursement_entry = frappe.new_doc("Loan Disbursement") + disbursement_entry.against_loan = loan + disbursement_entry.applicant_type = applicant_type + disbursement_entry.applicant = applicant + disbursement_entry.company = company + disbursement_entry.disbursement_date = nowdate() + + if disbursed_amount: + disbursement_entry.disbursed_amount = disbursed_amount + if as_dict: + return disbursement_entry.as_dict() + else: + return disbursement_entry + +@frappe.whitelist() +def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as_dict=0): + repayment_entry = frappe.new_doc("Loan Repayment") + repayment_entry.against_loan = loan + repayment_entry.applicant_type = applicant_type + repayment_entry.applicant = applicant + repayment_entry.company = company + repayment_entry.loan_type = loan_type + repayment_entry.posting_date = nowdate() + + if as_dict: + return repayment_entry.as_dict() + else: + return repayment_entry + +@frappe.whitelist() +def create_loan_security_unpledge(loan, applicant_type, applicant, company): + loan_security_pledge_details = frappe.db.sql(""" + SELECT p.parent, p.loan_security, p.qty as qty FROM `tabLoan Security Pledge` lsp , `tabPledge` p + WHERE p.parent = lsp.name AND lsp.loan = %s AND lsp.docstatus = 1 + """,(loan), as_dict=1) + + unpledge_request = frappe.new_doc("Loan Security Unpledge") + unpledge_request.applicant_type = applicant_type + unpledge_request.applicant = applicant + unpledge_request.loan = loan + unpledge_request.company = company + + for loan_security in loan_security_pledge_details: + unpledge_request.append('securities', { + "loan_security": loan_security.loan_security, + "qty": loan_security.qty, + "against_pledge": loan_security.parent + }) + + return unpledge_request.as_dict() + + + diff --git a/erpnext/loan_management/doctype/loan/loan_dashboard.py b/erpnext/loan_management/doctype/loan/loan_dashboard.py new file mode 100644 index 0000000000..90d5ae2650 --- /dev/null +++ b/erpnext/loan_management/doctype/loan/loan_dashboard.py @@ -0,0 +1,19 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'loan', + 'non_standard_fieldnames': { + 'Loan Disbursement': 'against_loan', + 'Loan Repayment': 'against_loan', + }, + 'transactions': [ + { + 'items': ['Loan Security Pledge', 'Loan Security Shortfall', 'Loan Disbursement'] + }, + { + 'items': ['Loan Repayment', 'Loan Interest Accrual', 'Loan Security Unpledge'] + } + ] + } \ 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 new file mode 100644 index 0000000000..08c2f2267f --- /dev/null +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -0,0 +1,559 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import erpnext +import unittest +from frappe.utils import (nowdate, add_days, getdate, now_datetime, add_to_date, get_datetime, + add_months, get_first_day, get_last_day, flt, date_diff) +from erpnext.selling.doctype.customer.test_customer import get_customer_dict +from erpnext.hr.doctype.salary_structure.test_salary_structure import make_employee +from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual +from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import (make_accrual_interest_entry_for_term_loans, days_in_year) + +from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import check_for_ltv_shortfall + +class TestLoan(unittest.TestCase): + def setUp(self): + create_loan_accounts() + create_loan_type("Personal Loan", 500000, 8.4, + is_term_loan=1, + mode_of_payment='Cash', + payment_account='Payment Account - _TC', + loan_account='Loan Account - _TC', + interest_income_account='Interest Income Account - _TC', + penalty_income_account='Penalty Income Account - _TC') + + create_loan_type("Stock Loan", 2000000, 13.5, 25, 1, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', + 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + + create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', + 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + + create_loan_security_type() + create_loan_security() + + create_loan_security_price("Test Security 1", 500, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) + create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) + + self.applicant1 = make_employee("robert_loan@loan.com") + if not frappe.db.exists("Customer", "_Test Loan Customer"): + frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) + + self.applicant2 = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name') + + create_loan(self.applicant1, "Personal Loan", 280000, "Repay Over Number of Periods", 20) + + def test_loan(self): + loan = frappe.get_doc("Loan", {"applicant":self.applicant1}) + self.assertEquals(loan.monthly_repayment_amount, 15052) + self.assertEquals(loan.total_interest_payable, 21034) + self.assertEquals(loan.total_payment, 301034) + + schedule = loan.repayment_schedule + + self.assertEqual(len(schedule), 20) + + for idx, principal_amount, interest_amount, balance_loan_amount in [[3, 13369, 1683, 227079], [19, 14941, 105, 0], [17, 14740, 312, 29785]]: + self.assertEqual(schedule[idx].principal_amount, principal_amount) + self.assertEqual(schedule[idx].interest_amount, interest_amount) + self.assertEqual(schedule[idx].balance_loan_amount, balance_loan_amount) + + loan.repayment_method = "Repay Fixed Amount per Period" + loan.monthly_repayment_amount = 14000 + loan.save() + + self.assertEquals(len(loan.repayment_schedule), 22) + self.assertEquals(loan.total_interest_payable, 22712) + self.assertEquals(loan.total_payment, 302712) + + def test_loan_with_security(self): + pledges = [] + pledges.append({ + "loan_security": "Test Security 1", + "qty": 4000.00, + "haircut": 50, + "loan_security_price": 500.00 + }) + + loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) + + loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_security_pledge.name) + + self.assertEquals(loan.loan_amount, 1000000) + + def test_loan_disbursement(self): + pledges = [] + pledges.append({ + "loan_security": "Test Security 1", + "qty": 4000.00, + "haircut": 50 + }) + + loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) + + loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_security_pledge.name) + self.assertEquals(loan.loan_amount, 1000000) + + loan.submit() + + loan_disbursement_entry1 = make_loan_disbursement_entry(loan.name, 500000) + loan_disbursement_entry2 = make_loan_disbursement_entry(loan.name, 500000) + + loan = frappe.get_doc("Loan", loan.name) + gl_entries1 = frappe.db.get_all("GL Entry", + fields=["name"], + filters = {'voucher_type': 'Loan Disbursement', 'voucher_no': loan_disbursement_entry1.name} + ) + + gl_entries2 = frappe.db.get_all("GL Entry", + fields=["name"], + filters = {'voucher_type': 'Loan Disbursement', 'voucher_no': loan_disbursement_entry2.name} + ) + + self.assertEquals(loan.status, "Disbursed") + self.assertEquals(loan.disbursed_amount, 1000000) + self.assertTrue(gl_entries1) + self.assertTrue(gl_entries2) + + def test_regular_loan_repayment(self): + pledges = [] + pledges.append({ + "loan_security": "Test Security 1", + "qty": 4000.00, + "haircut": 50 + }) + + loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_security_pledge.name, + posting_date=get_first_day(nowdate())) + + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime(first_date).year) * 100) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + + process_loan_interest_accrual(posting_date = last_date) + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), "Regular Payment", 111118.68) + repayment_entry.save() + + penalty_amount = (accrued_interest_amount * 5 * 25) / (100 * days_in_year(get_datetime(first_date).year)) + + self.assertEquals(flt(repayment_entry.interest_payable, 2), flt(accrued_interest_amount, 2)) + self.assertEquals(flt(repayment_entry.penalty_amount, 2), flt(penalty_amount, 2)) + + repayment_entry.submit() + + def test_loan_closure_repayment(self): + pledges = [] + pledges.append({ + "loan_security": "Test Security 1", + "qty": 4000.00, + "haircut": 50 + }) + + loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_security_pledge.name, + posting_date=get_first_day(nowdate())) + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + + # Adding 6 since repayment is made 5 days late after due date + # and since payment type is loan closure so interest should be considered for those + # 6 days as well though in grace period + no_of_days += 6 + + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime(first_date).year) * 100) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + process_loan_interest_accrual(posting_date = last_date) + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), + "Loan Closure", 13315.0681) + repayment_entry.save() + + repayment_entry.amount_paid = repayment_entry.payable_amount + + self.assertEquals(flt(repayment_entry.interest_payable, 3), flt(accrued_interest_amount, 3)) + self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) + + repayment_entry.submit() + loan.load_from_db() + self.assertEquals(loan.status, "Loan Closure Requested") + + def test_loan_repayment_for_term_loan(self): + pledges = [] + pledges.append({ + "loan_security": "Test Security 2", + "qty": 4000.00, + "haircut": 50 + }) + + pledges.append({ + "loan_security": "Test Security 1", + "qty": 2000.00, + "haircut": 50 + }) + + loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) + + loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, + loan_security_pledge.name, posting_date=add_months(nowdate(), -1)) + + loan.submit() + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1)) + + make_accrual_interest_entry_for_term_loans(posting_date=nowdate()) + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(get_last_day(nowdate()), 5), + "Regular Payment", 89768.7534247) + + repayment_entry.save() + repayment_entry.submit() + + repayment_entry.load_from_db() + + self.assertEquals(repayment_entry.interest_payable, 11250.00) + self.assertEquals(repayment_entry.payable_principal_amount, 78303.00) + + def test_partial_loan_repayment(self): + pledges = [] + pledges.append({ + "loan_security": "Test Security 1", + "qty": 4000.00, + "haircut": 50 + }) + + loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_security_pledge.name, + posting_date=get_first_day(nowdate())) + + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime().year) * 100) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + + process_loan_interest_accrual(posting_date = add_days(first_date, 15)) + process_loan_interest_accrual(posting_date = add_days(first_date, 30)) + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 1), "Regular Payment", 6500) + repayment_entry.save() + repayment_entry.submit() + + penalty_amount = (accrued_interest_amount * 4 * 25) / (100 * days_in_year(get_datetime(first_date).year)) + + lia = frappe.get_all("Loan Interest Accrual", fields=["is_paid"], + filters={"loan": loan.name}, order_by="posting_date") + + self.assertTrue(lia[0].get('is_paid')) + self.assertFalse(lia[1].get('is_paid')) + + def test_security_shortfall(self): + pledges = [] + pledges.append({ + "loan_security": "Test Security 2", + "qty": 8000.00, + "haircut": 50, + }) + + loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) + + loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_security_pledge.name) + loan.submit() + + make_loan_disbursement_entry(loan.name, loan.loan_amount) + + frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = %s + where loan_security=%s""", (100, 'Test Security 2')) + + check_for_ltv_shortfall() + loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name}) + + self.assertTrue(loan_security_shortfall) + + self.assertEquals(loan_security_shortfall.loan_amount, 1000000.00) + self.assertEquals(loan_security_shortfall.security_value, 400000.00) + self.assertEquals(loan_security_shortfall.shortfall_amount, 600000.00) + +def create_loan_accounts(): + if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): + frappe.get_doc({ + "doctype": "Account", + "account_name": "Loans and Advances (Assets)", + "company": "_Test Company", + "root_type": "Asset", + "report_type": "Balance Sheet", + "currency": "INR", + "parent_account": "Current Assets - _TC", + "account_type": "Bank", + "is_group": 1 + }).insert(ignore_permissions=True) + + if not frappe.db.exists("Account", "Loan Account - _TC"): + frappe.get_doc({ + "doctype": "Account", + "company": "_Test Company", + "account_name": "Loan Account", + "root_type": "Asset", + "report_type": "Balance Sheet", + "currency": "INR", + "parent_account": "Loans and Advances (Assets) - _TC", + "account_type": "Bank", + }).insert(ignore_permissions=True) + + if not frappe.db.exists("Account", "Payment Account - _TC"): + frappe.get_doc({ + "doctype": "Account", + "company": "_Test Company", + "account_name": "Payment Account", + "root_type": "Asset", + "report_type": "Balance Sheet", + "currency": "INR", + "parent_account": "Bank Accounts - _TC", + "account_type": "Bank", + }).insert(ignore_permissions=True) + + if not frappe.db.exists("Account", "Interest Income Account - _TC"): + frappe.get_doc({ + "doctype": "Account", + "company": "_Test Company", + "root_type": "Income", + "account_name": "Interest Income Account", + "report_type": "Profit and Loss", + "currency": "INR", + "parent_account": "Direct Income - _TC", + "account_type": "Income Account", + }).insert(ignore_permissions=True) + + if not frappe.db.exists("Account", "Penalty Income Account - _TC"): + frappe.get_doc({ + "doctype": "Account", + "company": "_Test Company", + "account_name": "Penalty Income Account", + "root_type": "Income", + "report_type": "Profit and Loss", + "currency": "INR", + "parent_account": "Direct Income - _TC", + "account_type": "Income Account", + }).insert(ignore_permissions=True) + +def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_interest_rate=None, is_term_loan=None, grace_period_in_days=None, + mode_of_payment=None, payment_account=None, loan_account=None, interest_income_account=None, penalty_income_account=None, + repayment_method=None, repayment_periods=None): + + if not frappe.db.exists("Loan Type", loan_name): + loan_type = frappe.get_doc({ + "doctype": "Loan Type", + "company": "_Test Company", + "loan_name": loan_name, + "is_term_loan": is_term_loan, + "maximum_loan_amount": maximum_loan_amount, + "rate_of_interest": rate_of_interest, + "penalty_interest_rate": penalty_interest_rate, + "grace_period_in_days": grace_period_in_days, + "mode_of_payment": mode_of_payment, + "payment_account": payment_account, + "loan_account": loan_account, + "interest_income_account": interest_income_account, + "penalty_income_account": penalty_income_account, + "repayment_method": repayment_method, + "repayment_periods": repayment_periods + }).insert() + + loan_type.submit() + +def create_loan_security_type(): + if not frappe.db.exists("Loan Security Type", "Stock"): + frappe.get_doc({ + "doctype": "Loan Security Type", + "loan_security_type": "Stock", + "unit_of_measure": "Nos", + "haircut": 50.00 + }).insert(ignore_permissions=True) + +def create_loan_security(): + if not frappe.db.exists("Loan Security", "Test Security 1"): + frappe.get_doc({ + "doctype": "Loan Security", + "loan_security_type": "Stock", + "loan_security_code": "532779", + "loan_security_name": "Test Security 1", + "unit_of_measure": "Nos", + "haircut": 50.00, + }).insert(ignore_permissions=True) + + if not frappe.db.exists("Loan Security", "Test Security 2"): + frappe.get_doc({ + "doctype": "Loan Security", + "loan_security_type": "Stock", + "loan_security_code": "531335", + "loan_security_name": "Test Security 2", + "unit_of_measure": "Nos", + "haircut": 50.00, + }).insert(ignore_permissions=True) + +def create_loan_security_pledge(applicant, pledges): + + lsp = frappe.new_doc("Loan Security Pledge") + lsp.applicant_type = 'Customer' + lsp.applicant = applicant + lsp.company = "_Test Company" + + for pledge in pledges: + lsp.append('securities', { + "loan_security": pledge['loan_security'], + "qty": pledge['qty'], + "haircut": pledge['haircut'] + }) + + lsp.save() + lsp.submit() + + return lsp + +def make_loan_disbursement_entry(loan, amount, disbursement_date=None): + + loan_disbursement_entry = frappe.get_doc({ + "doctype": "Loan Disbursement", + "against_loan": loan, + "disbursement_date": disbursement_date, + "company": "_Test Company", + "disbursed_amount": amount, + "cost_center": 'Main - _TC' + }).insert(ignore_permissions=True) + + loan_disbursement_entry.save() + loan_disbursement_entry.submit() + + return loan_disbursement_entry + +def create_loan_security_price(loan_security, loan_security_price, uom, from_date, to_date): + + if not frappe.db.get_value("Loan Security Price",{"loan_security": loan_security, + "valid_from": ("<=", from_date), "valid_upto": (">=", to_date)}, 'name'): + + lsp = frappe.get_doc({ + "doctype": "Loan Security Price", + "loan_security": loan_security, + "loan_security_price": loan_security_price, + "uom": uom, + "valid_from":from_date, + "valid_upto": to_date + }).insert(ignore_permissions=True) + +def create_repayment_entry(loan, applicant, posting_date, payment_type, paid_amount): + + lr = frappe.get_doc({ + "doctype": "Loan Repayment", + "against_loan": loan, + "payment_type": payment_type, + "company": "_Test Company", + "posting_date": posting_date or nowdate(), + "applicant": applicant, + "amount_paid": paid_amount, + "loan_type": "Stock Loan" + }).insert(ignore_permissions=True) + + return lr + + +def create_loan(applicant, loan_type, loan_amount, repayment_method, repayment_periods, + repayment_start_date=None, posting_date=None): + + loan = frappe.get_doc({ + "doctype": "Loan", + "applicant_type": "Employee", + "company": "_Test Company", + "applicant": applicant, + "loan_type": loan_type, + "loan_amount": loan_amount, + "repayment_method": repayment_method, + "repayment_periods": repayment_periods, + "repayment_start_date": nowdate(), + "is_term_loan": 1, + "posting_date": posting_date or nowdate() + }) + + loan.save() + return loan + +def create_loan_with_security(applicant, loan_type, repayment_method, repayment_periods, loan_security_pledge, + posting_date=None, repayment_start_date=None): + + loan = frappe.get_doc({ + "doctype": "Loan", + "company": "_Test Company", + "applicant_type": "Customer", + "posting_date": posting_date or nowdate(), + "applicant": applicant, + "loan_type": loan_type, + "is_term_loan": 1, + "is_secured_loan": 1, + "repayment_method": repayment_method, + "repayment_periods": repayment_periods, + "repayment_start_date": repayment_start_date or nowdate(), + "mode_of_payment": frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name'), + "loan_security_pledge": loan_security_pledge, + "payment_account": 'Payment Account - _TC', + "loan_account": 'Loan Account - _TC', + "interest_income_account": 'Interest Income Account - _TC', + "penalty_income_account": 'Penalty Income Account - _TC', + }) + + loan.save() + + return loan + +def create_demand_loan(applicant, loan_type, loan_security_pledge, posting_date=None): + + loan = frappe.get_doc({ + "doctype": "Loan", + "company": "_Test Company", + "applicant_type": "Customer", + "posting_date": posting_date or nowdate(), + "applicant": applicant, + "loan_type": loan_type, + "is_term_loan": 0, + "is_secured_loan": 1, + "mode_of_payment": frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name'), + "loan_security_pledge": loan_security_pledge, + "payment_account": 'Payment Account - _TC', + "loan_account": 'Loan Account - _TC', + "interest_income_account": 'Interest Income Account - _TC', + "penalty_income_account": 'Penalty Income Account - _TC', + }) + + loan.save() + + return loan \ No newline at end of file diff --git a/erpnext/hr/doctype/repayment_schedule/__init__.py b/erpnext/loan_management/doctype/loan_application/__init__.py similarity index 100% rename from erpnext/hr/doctype/repayment_schedule/__init__.py rename to erpnext/loan_management/doctype/loan_application/__init__.py diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.js b/erpnext/loan_management/doctype/loan_application/loan_application.js new file mode 100644 index 0000000000..57050d86c6 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_application/loan_application.js @@ -0,0 +1,127 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +{% include 'erpnext/loan_management/loan_common.js' %}; + +frappe.ui.form.on('Loan Application', { + + setup: function(frm) { + frm.make_methods = { + 'Loan': function() { frm.trigger('create_loan') }, + 'Loan Security Pledge': function() { frm.trigger('create_loan_security_pledge') }, + } + }, + refresh: function(frm) { + frm.trigger("toggle_fields"); + frm.trigger("add_toolbar_buttons"); + }, + repayment_method: function(frm) { + frm.doc.repayment_amount = frm.doc.repayment_periods = "" + frm.trigger("toggle_fields") + frm.trigger("toggle_required") + }, + toggle_fields: function(frm) { + frm.toggle_enable("repayment_amount", frm.doc.repayment_method=="Repay Fixed Amount per Period") + frm.toggle_enable("repayment_periods", frm.doc.repayment_method=="Repay Over Number of Periods") + }, + toggle_required: function(frm){ + frm.toggle_reqd("repayment_amount", cint(frm.doc.repayment_method=='Repay Fixed Amount per Period')) + frm.toggle_reqd("repayment_periods", cint(frm.doc.repayment_method=='Repay Over Number of Periods')) + }, + add_toolbar_buttons: function(frm) { + if (frm.doc.status == "Approved") { + + frappe.db.get_value("Loan Security Pledge", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { + if (!r) { + frm.add_custom_button(__('Loan Security Pledge'), function() { + frm.trigger('create_loan_security_pledge') + },__('Create')) + } + }); + + frappe.db.get_value("Loan", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { + if (!r) { + frm.add_custom_button(__('Loan'), function() { + frm.trigger('create_loan') + },__('Create')) + } else { + frm.set_df_property('status', 'read_only', 1); + } + }); + } + }, + create_loan: function(frm) { + if (frm.doc.status != "Approved") { + frappe.throw(__("Cannot create loan until application is approved")) + } + + frappe.model.open_mapped_doc({ + method: 'erpnext.loan_management.doctype.loan_application.loan_application.create_loan', + frm: frm + }); + }, + create_loan_security_pledge: function(frm) { + frappe.call({ + method: "erpnext.loan_management.doctype.loan_application.loan_application.create_pledge", + args: { + loan_application: frm.doc.name + }, + callback: function(r) { + frappe.set_route("Form", "Loan Security Pledge", r.message); + } + }) + }, + is_term_loan: function(frm) { + frm.set_df_property('repayment_method', 'hidden', 1 - frm.doc.is_term_loan); + frm.set_df_property('repayment_method', 'reqd', frm.doc.is_term_loan); + }, + is_secured_loan: function(frm) { + frm.set_df_property('proposed_pledges', 'reqd', frm.doc.is_secured_loan); + }, + + calculate_amounts: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.qty) { + frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.loan_security_price); + frappe.model.set_value(cdt, cdn, 'post_haircut_amount', cint(row.amount - (row.amount * row.haircut/100))); + } else if (row.amount) { + frappe.model.set_value(cdt, cdn, 'qty', cint(row.amount / row.loan_security_price)); + frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.loan_security_price); + frappe.model.set_value(cdt, cdn, 'post_haircut_amount', cint(row.amount - (row.amount * row.haircut/100))); + } + + let maximum_amount = 0; + + $.each(frm.doc.proposed_pledges || [], function(i, item){ + maximum_amount += item.post_haircut_amount; + }); + + if (flt(maximum_amount)) { + frm.set_value('maximum_loan_amount', flt(maximum_amount)); + } + } +}); + +frappe.ui.form.on("Proposed Pledge", { + loan_security: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + frappe.call({ + method: "erpnext.loan_management.doctype.loan_security_price.loan_security_price.get_loan_security_price", + args: { + loan_security: row.loan_security + }, + callback: function(r) { + frappe.model.set_value(cdt, cdn, 'loan_security_price', r.message); + frm.events.calculate_amounts(frm, cdt, cdn); + } + }) + }, + + amount: function(frm, cdt, cdn) { + frm.events.calculate_amounts(frm, cdt, cdn); + }, + + qty: function(frm, cdt, cdn) { + frm.events.calculate_amounts(frm, cdt, cdn); + }, +}) diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.json b/erpnext/loan_management/doctype/loan_application/loan_application.json new file mode 100644 index 0000000000..4c433029d7 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_application/loan_application.json @@ -0,0 +1,278 @@ +{ + "autoname": "ACC-LOAP-.YYYY.-.#####", + "creation": "2019-08-29 17:46:49.201740", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "applicant_type", + "applicant", + "applicant_name", + "column_break_2", + "company", + "posting_date", + "status", + "section_break_4", + "loan_type", + "is_term_loan", + "loan_amount", + "is_secured_loan", + "rate_of_interest", + "column_break_7", + "description", + "loan_security_details_section", + "proposed_pledges", + "maximum_loan_amount", + "repayment_info", + "repayment_method", + "total_payable_amount", + "column_break_11", + "repayment_periods", + "repayment_amount", + "total_payable_interest", + "amended_from" + ], + "fields": [ + { + "fieldname": "applicant_type", + "fieldtype": "Select", + "label": "Applicant Type", + "options": "Employee\nMember\nCustomer", + "reqd": 1 + }, + { + "fieldname": "applicant", + "fieldtype": "Dynamic Link", + "in_global_search": 1, + "in_standard_filter": 1, + "label": "Applicant", + "options": "applicant_type", + "reqd": 1 + }, + { + "depends_on": "applicant", + "fieldname": "applicant_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Applicant Name", + "read_only": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date" + }, + { + "allow_on_submit": 1, + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "no_copy": 1, + "options": "Open\nApproved\nRejected", + "permlevel": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "label": "Loan Info" + }, + { + "fieldname": "loan_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Loan Type", + "options": "Loan Type", + "reqd": 1 + }, + { + "bold": 1, + "fieldname": "loan_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Loan Amount", + "options": "Company:company:default_currency" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Reason" + }, + { + "depends_on": "eval: doc.is_term_loan == 1", + "fieldname": "repayment_info", + "fieldtype": "Section Break", + "label": "Repayment Info" + }, + { + "depends_on": "eval: doc.is_term_loan == 1", + "fetch_from": "loan_type.repayment_method", + "fetch_if_empty": 1, + "fieldname": "repayment_method", + "fieldtype": "Select", + "label": "Repayment Method", + "options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods" + }, + { + "fetch_from": "loan_type.rate_of_interest", + "fieldname": "rate_of_interest", + "fieldtype": "Percent", + "label": "Rate of Interest", + "read_only": 1 + }, + { + "depends_on": "is_term_loan", + "fieldname": "total_payable_interest", + "fieldtype": "Currency", + "label": "Total Payable Interest", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "depends_on": "repayment_method", + "fieldname": "repayment_amount", + "fieldtype": "Currency", + "label": "Monthly Repayment Amount", + "options": "Company:company:default_currency" + }, + { + "depends_on": "repayment_method", + "fieldname": "repayment_periods", + "fieldtype": "Int", + "label": "Repayment Period in Months" + }, + { + "fieldname": "total_payable_amount", + "fieldtype": "Currency", + "label": "Total Payable Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Loan Application", + "print_hide": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_secured_loan", + "fieldtype": "Check", + "label": "Is Secured Loan" + }, + { + "depends_on": "eval:doc.is_secured_loan == 1", + "fieldname": "loan_security_details_section", + "fieldtype": "Section Break", + "label": "Loan Security Details" + }, + { + "depends_on": "eval:doc.is_secured_loan == 1", + "fieldname": "proposed_pledges", + "fieldtype": "Table", + "label": "Proposed Pledges", + "options": "Proposed Pledge" + }, + { + "fieldname": "maximum_loan_amount", + "fieldtype": "Currency", + "label": "Maximum Loan Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "loan_type.is_term_loan", + "fieldname": "is_term_loan", + "fieldtype": "Check", + "label": "Is Term Loan", + "read_only": 1 + } + ], + "is_submittable": 1, + "modified": "2019-10-24 10:32:03.740558", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Application", + "owner": "Administrator", + "permissions": [ + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "delete": 1, + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "share": 1 + } + ], + "search_fields": "applicant_type, applicant, loan_type, loan_amount", + "sort_field": "modified", + "sort_order": "DESC", + "timeline_field": "applicant", + "title_field": "applicant", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py new file mode 100644 index 0000000000..691962bf14 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_application/loan_application.py @@ -0,0 +1,206 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, math +from frappe import _ +from frappe.utils import flt, rounded, cint +from frappe.model.mapper import get_mapped_doc +from frappe.model.document import Document +from erpnext.loan_management.doctype.loan.loan import (get_monthly_repayment_amount, validate_repayment_method, + get_total_loan_amount, get_sanctioned_amount_limit) +from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price +import json +from six import string_types + +class LoanApplication(Document): + def validate(self): + + validate_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount, + self.repayment_periods, self.is_term_loan) + + self.validate_loan_type() + self.set_pledge_amount() + self.set_loan_amount() + self.validate_loan_amount() + self.get_repayment_details() + self.check_sanctioned_amount_limit() + + def validate_loan_type(self): + company = frappe.get_value("Loan Type", self.loan_type, "company") + if company != self.company: + frappe.throw(_("Please select Loan Type for company {0}").format(frappe.bold(self.company))) + + def validate_loan_amount(self): + if not self.loan_amount: + frappe.throw(_("Loan Amount is mandatory")) + + maximum_loan_limit = frappe.db.get_value('Loan Type', self.loan_type, 'maximum_loan_amount') + if maximum_loan_limit and self.loan_amount > maximum_loan_limit: + frappe.throw(_("Loan Amount cannot exceed Maximum Loan Amount of {0}").format(maximum_loan_limit)) + + if self.maximum_loan_amount and self.loan_amount > self.maximum_loan_amount: + frappe.throw(_("Loan Amount exceeds maximum loan amount of {0} as per proposed securities").format(self.maximum_loan_amount)) + + def check_sanctioned_amount_limit(self): + total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company) + sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company) + + if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit): + frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant))) + + def set_pledge_amount(self): + for proposed_pledge in self.proposed_pledges: + + if not proposed_pledge.qty and not proposed_pledge.amount: + frappe.throw(_("Qty or Amount is mandatroy for loan security")) + + proposed_pledge.loan_security_price = get_loan_security_price(proposed_pledge.loan_security) + + if not proposed_pledge.qty: + proposed_pledge.qty = cint(proposed_pledge.amount/proposed_pledge.loan_security_price) + + proposed_pledge.amount = proposed_pledge.qty * proposed_pledge.loan_security_price + proposed_pledge.post_haircut_amount = cint(proposed_pledge.amount - (proposed_pledge.amount * proposed_pledge.haircut/100)) + + def get_repayment_details(self): + + if self.is_term_loan: + if self.repayment_method == "Repay Over Number of Periods": + self.repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods) + + if self.repayment_method == "Repay Fixed Amount per Period": + monthly_interest_rate = flt(self.rate_of_interest) / (12 *100) + if monthly_interest_rate: + min_repayment_amount = self.loan_amount*monthly_interest_rate + if self.repayment_amount - min_repayment_amount <= 0: + frappe.throw(_("Repayment Amount must be greater than " \ + + str(flt(min_repayment_amount, 2)))) + self.repayment_periods = math.ceil((math.log(self.repayment_amount) - + math.log(self.repayment_amount - min_repayment_amount)) /(math.log(1 + monthly_interest_rate))) + else: + self.repayment_periods = self.loan_amount / self.repayment_amount + + self.calculate_payable_amount() + else: + self.total_payable_amount = self.loan_amount + + def calculate_payable_amount(self): + balance_amount = self.loan_amount + self.total_payable_amount = 0 + self.total_payable_interest = 0 + + while(balance_amount > 0): + interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100)) + balance_amount = rounded(balance_amount + interest_amount - self.repayment_amount) + + self.total_payable_interest += interest_amount + + self.total_payable_amount = self.loan_amount + self.total_payable_interest + + def set_loan_amount(self): + if self.is_secured_loan and not self.proposed_pledges: + frappe.throw(_("Proposed Pledges are mandatory for secured Loans")) + + if not self.loan_amount and self.is_secured_loan and self.proposed_pledges: + self.loan_amount = 0 + for security in self.proposed_pledges: + self.loan_amount += security.post_haircut_amount + +@frappe.whitelist() +def create_loan(source_name, target_doc=None, submit=0): + def update_accounts(source_doc, target_doc, source_parent): + account_details = frappe.get_all("Loan Type", + fields=["mode_of_payment", "payment_account","loan_account", "interest_income_account", "penalty_income_account"], + filters = {'name': source_doc.loan_type} + )[0] + + loan_security_pledge = frappe.db.get_value("Loan Security Pledge", {"loan_application": source_name}, 'name') + + target_doc.mode_of_payment = account_details.mode_of_payment + target_doc.payment_account = account_details.payment_account + target_doc.loan_account = account_details.loan_account + target_doc.interest_income_account = account_details.interest_income_account + target_doc.penalty_income_account = account_details.penalty_income_account + + if loan_security_pledge: + target_doc.is_secured_loan = 1 + target_doc.loan_security_pledge = loan_security_pledge + + doclist = get_mapped_doc("Loan Application", source_name, { + "Loan Application": { + "doctype": "Loan", + "validation": { + "docstatus": ["=", 1] + }, + "postprocess": update_accounts, + "field_no_map": [ + "is_secured_loan" + ] + } + }, target_doc) + + if submit: + doclist.submit() + + return doclist + +@frappe.whitelist() +def create_pledge(loan_application): + loan_application_doc = frappe.get_doc("Loan Application", loan_application) + + lsp = frappe.new_doc("Loan Security Pledge") + lsp.applicant_type = loan_application_doc.applicant_type + lsp.applicant = loan_application_doc.applicant + lsp.loan_application = loan_application_doc.name + lsp.company = loan_application_doc.company + + for pledge in loan_application_doc.proposed_pledges: + + lsp.append('securities', { + "loan_security": pledge.loan_security, + "qty": pledge.qty, + "loan_security_price": pledge.loan_security_price, + "haircut": pledge.haircut + }) + + lsp.save() + lsp.submit() + + message = _("Loan Security Pledge Created : {0}").format(lsp.name) + frappe.msgprint(message) + + return lsp.name + +#This is a sandbox method to get the proposed pledges +@frappe.whitelist() +def get_proposed_pledge(securities): + if isinstance(securities, string_types): + securities = json.loads(securities) + + proposed_pledges = { + 'securities': [] + } + maximum_loan_amount = 0 + + for security in securities: + security = frappe._dict(security) + if not security.qty and not security.amount: + frappe.throw(_("Qty or Amount is mandatroy for loan security")) + + security.loan_security_price = get_loan_security_price(security.loan_security) + + if not security.qty: + 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) + + maximum_loan_amount += security.post_haircut_amount + + proposed_pledges['securities'].append(security) + + proposed_pledges['maximum_loan_amount'] = maximum_loan_amount + + return proposed_pledges diff --git a/erpnext/hr/doctype/loan_application/loan_application_dashboard.py b/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py similarity index 77% rename from erpnext/hr/doctype/loan_application/loan_application_dashboard.py rename to erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py index 232c6e3c5b..bf3f58b83e 100644 --- a/erpnext/hr/doctype/loan_application/loan_application_dashboard.py +++ b/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py @@ -6,7 +6,7 @@ def get_data(): 'fieldname': 'loan_application', 'transactions': [ { - 'items': ['Loan'] + 'items': ['Loan', 'Loan Security Pledge'] }, ], } \ No newline at end of file diff --git a/erpnext/hr/doctype/loan_application/test_loan_application.py b/erpnext/loan_management/doctype/loan_application/test_loan_application.py similarity index 53% rename from erpnext/hr/doctype/loan_application/test_loan_application.py rename to erpnext/loan_management/doctype/loan_application/test_loan_application.py index b08b522503..99c807b2cd 100644 --- a/erpnext/hr/doctype/loan_application/test_loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/test_loan_application.py @@ -1,39 +1,33 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals import frappe import unittest from erpnext.hr.doctype.salary_structure.test_salary_structure import make_employee +from erpnext.loan_management.doctype.loan.test_loan import create_loan_type, create_loan_accounts class TestLoanApplication(unittest.TestCase): def setUp(self): - self.create_loan_type() - self.applicant = make_employee("kate_loan@loan.com") + create_loan_accounts() + create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', + 'Interest Income Account - _TC', 'Penalty Income Account - _TC', 'Repay Over Number of Periods', 18) + self.applicant = make_employee("kate_loan@loan.com", "_Test Company") self.create_loan_application() - def create_loan_type(self): - if not frappe.db.get_value("Loan Type", "Home Loan"): - frappe.get_doc({ - "doctype": "Loan Type", - "loan_name": "Home Loan", - "maximum_loan_amount": 500000, - "rate_of_interest": 9.2 - }).insert() - def create_loan_application(self): - if not frappe.db.get_value("Loan Application", {"applicant":self.applicant}, "name"): - loan_application = frappe.new_doc("Loan Application") - loan_application.update({ - "applicant": self.applicant, - "loan_type": "Home Loan", - "rate_of_interest": 9.2, - "loan_amount": 250000, - "repayment_method": "Repay Over Number of Periods", - "repayment_periods": 18 - }) - loan_application.insert() + loan_application = frappe.new_doc("Loan Application") + loan_application.update({ + "applicant": self.applicant, + "loan_type": "Home Loan", + "rate_of_interest": 9.2, + "loan_amount": 250000, + "repayment_method": "Repay Over Number of Periods", + "repayment_periods": 18, + "company": "_Test Company" + }) + loan_application.insert() def test_loan_totals(self): diff --git a/erpnext/hr/doctype/salary_slip_loan/__init__.py b/erpnext/loan_management/doctype/loan_disbursement/__init__.py similarity index 100% rename from erpnext/hr/doctype/salary_slip_loan/__init__.py rename to erpnext/loan_management/doctype/loan_disbursement/__init__.py diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.js b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.js new file mode 100644 index 0000000000..487ef23102 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.js @@ -0,0 +1,17 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +{% include 'erpnext/loan_management/loan_common.js' %}; + +frappe.ui.form.on('Loan Disbursement', { + refresh: function(frm) { + frm.set_query('against_loan', function() { + return { + 'filters': { + 'docstatus': 1, + 'status': 'Sanctioned' + } + } + }) + } +}); diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json new file mode 100644 index 0000000000..72a4ddcc8f --- /dev/null +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json @@ -0,0 +1,165 @@ +{ + "autoname": "LM-DIS-.#####", + "creation": "2019-09-07 12:44:49.125452", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "against_loan", + "disbursement_date", + "posting_date", + "column_break_4", + "company", + "applicant_type", + "applicant", + "section_break_7", + "pending_amount_for_disbursal", + "disbursed_amount", + "accounting_dimensions_section", + "cost_center", + "section_break_13", + "customer_details_section", + "bank_account", + "amended_from" + ], + "fields": [ + { + "fieldname": "against_loan", + "fieldtype": "Link", + "label": "Against Loan ", + "options": "Loan" + }, + { + "fieldname": "disbursement_date", + "fieldtype": "Date", + "label": "Disbursement Date" + }, + { + "fieldname": "disbursed_amount", + "fieldtype": "Currency", + "label": "Disbursed Amount", + "options": "Company:company:default_currency" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Loan Disbursement", + "print_hide": 1, + "read_only": 1 + }, + { + "fetch_from": "against_loan.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "read_only": 1 + }, + { + "fetch_from": "against_loan.applicant", + "fieldname": "applicant", + "fieldtype": "Dynamic Link", + "label": "Applicant", + "options": "applicant_type", + "read_only": 1 + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "posting_date", + "fieldtype": "Date", + "hidden": 1, + "label": "Posting Date", + "read_only": 1 + }, + { + "fieldname": "pending_amount_for_disbursal", + "fieldtype": "Currency", + "label": "Pending Amount For Disbursal", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_7", + "fieldtype": "Section Break" + }, + { + "fieldname": "section_break_13", + "fieldtype": "Section Break" + }, + { + "fieldname": "customer_details_section", + "fieldtype": "Section Break", + "label": "Customer Details" + }, + { + "fetch_from": "against_loan.applicant_type", + "fieldname": "applicant_type", + "fieldtype": "Select", + "label": "Applicant Type", + "options": "Employee\nMember\nCustomer", + "read_only": 1 + }, + { + "fieldname": "bank_account", + "fieldtype": "Link", + "label": "Bank Account", + "options": "Bank Account" + } + ], + "is_submittable": 1, + "modified": "2019-10-24 12:32:32.230881", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Disbursement", + "owner": "Administrator", + "permissions": [ + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "submit": 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/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py new file mode 100644 index 0000000000..fa7db2d565 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, erpnext +from frappe.model.document import Document +from frappe.utils import nowdate, getdate, add_days, flt +from erpnext.controllers.accounts_controller import AccountsController +from erpnext.accounts.general_ledger import make_gl_entries +from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual + +class LoanDisbursement(AccountsController): + + def validate(self): + self.set_missing_values() + self.set_pending_amount_for_disbursal() + + def before_submit(self): + self.set_status_and_amounts() + + def on_submit(self): + self.make_gl_entries() + + def on_cancel(self): + self.make_gl_entries(cancel=1) + + def set_missing_values(self): + if not self.disbursement_date: + self.disbursement_date = nowdate() + + if not self.cost_center: + self.cost_center = erpnext.get_default_cost_center(self.company) + + if not self.posting_date: + self.posting_date = self.disbursement_date or nowdate() + + if not self.bank_account and self.applicant_type == "Customer": + self.bank_account = frappe.db.get_value("Customer", self.applicant, "default_bank_account") + + def set_pending_amount_for_disbursal(self): + loan_amount, disbursed_amount = frappe.db.get_value('Loan', + {'name': self.against_loan}, ['loan_amount', 'disbursed_amount']) + + self.pending_amount_for_disbursal = loan_amount - disbursed_amount + + def set_status_and_amounts(self): + + loan_details = frappe.get_all("Loan", + fields = ["loan_amount", "disbursed_amount", "total_principal_paid", "status", "is_term_loan"], + filters= { "name": self.against_loan } + )[0] + + if loan_details.status == "Disbursed" and not loan_details.is_term_loan: + process_loan_interest_accrual(posting_date=add_days(self.disbursement_date, -1), + loan=self.against_loan) + + disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount + + if flt(disbursed_amount) - flt(loan_details.total_principal_paid) > flt(loan_details.loan_amount): + frappe.throw(_("Disbursed Amount cannot be greater than loan amount")) + + if flt(disbursed_amount) > flt(loan_details.loan_amount): + total_principal_paid = loan_details.total_principal_paid - (disbursed_amount - loan_details.loan_amount) + frappe.db.set_value("Loan", self.against_loan, "total_principal_paid", total_principal_paid) + + if flt(loan_details.loan_amount) == flt(disbursed_amount): + frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed") + else: + frappe.db.set_value("Loan", self.against_loan, "status", "Partially Disbursed") + + frappe.db.set_value("Loan", self.against_loan, { + "disbursement_date": self.disbursement_date, + "disbursed_amount": disbursed_amount + }) + + def make_gl_entries(self, cancel=0, adv_adj=0): + gle_map = [] + loan_details = frappe.get_doc("Loan", self.against_loan) + + gle_map.append( + self.get_gl_dict({ + "account": loan_details.loan_account, + "against": loan_details.applicant, + "debit": self.disbursed_amount, + "debit_in_account_currency": self.disbursed_amount, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": "Against Loan:" + self.against_loan, + "cost_center": self.cost_center, + "party_type": self.applicant_type, + "party": self.applicant, + "posting_date": self.disbursement_date + }) + ) + + gle_map.append( + self.get_gl_dict({ + "account": loan_details.payment_account, + "against": loan_details.applicant, + "credit": self.disbursed_amount, + "credit_in_account_currency": self.disbursed_amount, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": "Against Loan:" + self.against_loan, + "cost_center": self.cost_center, + "party_type": self.applicant_type, + "party": self.applicant, + "posting_date": self.disbursement_date + }) + ) + + if gle_map: + make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj) diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py new file mode 100644 index 0000000000..968e377fcc --- /dev/null +++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals +import frappe +import unittest +from frappe.utils import (nowdate, add_days, get_datetime, get_first_day, get_last_day, date_diff, flt, add_to_date) +from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_pledge, create_repayment_entry, + make_loan_disbursement_entry, create_loan_accounts, create_loan_security_type, create_loan_security, create_demand_loan, create_loan_security_price) +from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual +from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import (make_accrual_interest_entry_for_term_loans, days_in_year) +from erpnext.selling.doctype.customer.test_customer import get_customer_dict + +class TestLoanDisbursement(unittest.TestCase): + + def setUp(self): + create_loan_accounts() + + create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', + 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + + create_loan_security_type() + create_loan_security() + + create_loan_security_price("Test Security 1", 500, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) + create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) + + if not frappe.db.exists("Customer", "_Test Loan Customer"): + frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) + + self.applicant = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name') + + def test_loan_topup(self): + pledges = [] + pledges.append({ + "loan_security": "Test Security 1", + "qty": 4000.00, + "haircut": 50, + "loan_security_price": 500.00 + }) + + loan_security_pledge = create_loan_security_pledge(self.applicant, pledges) + + loan = create_demand_loan(self.applicant, "Demand Loan", loan_security_pledge.name, + posting_date=get_first_day(nowdate())) + + loan.submit() + + first_date = get_first_day(nowdate()) + last_date = get_last_day(nowdate()) + + no_of_days = date_diff(last_date, first_date) + 1 + + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime().year) * 100) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + + process_loan_interest_accrual(posting_date=add_days(last_date, 1)) + + # Paid 511095.89 amount includes 5,00,000 principal amount and 11095.89 interest amount + repayment_entry = create_repayment_entry(loan.name, self.applicant, add_days(get_last_day(nowdate()), 5), + "Regular Payment", 611095.89) + repayment_entry.submit() + + loan.reload() + + make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 16)) + + total_principal_paid = loan.total_principal_paid + + loan.reload() + + # Loan Topup will result in decreasing the Total Principal Paid + self.assertEqual(flt(loan.total_principal_paid, 2), flt(total_principal_paid - 500000, 2)) diff --git a/erpnext/hr/report/loan_repayment/__init__.py b/erpnext/loan_management/doctype/loan_interest_accrual/__init__.py similarity index 100% rename from erpnext/hr/report/loan_repayment/__init__.py rename to erpnext/loan_management/doctype/loan_interest_accrual/__init__.py diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.js b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.js new file mode 100644 index 0000000000..177b23593c --- /dev/null +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.js @@ -0,0 +1,10 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +{% include 'erpnext/loan_management/loan_common.js' %}; + +frappe.ui.form.on('Loan Interest Accrual', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json new file mode 100644 index 0000000000..33f496fc3e --- /dev/null +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json @@ -0,0 +1,182 @@ +{ + "actions": [], + "autoname": "LM-LIA-.#####", + "creation": "2019-09-09 22:34:36.346812", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan", + "applicant_type", + "applicant", + "interest_income_account", + "loan_account", + "column_break_4", + "company", + "posting_date", + "is_term_loan", + "is_paid", + "section_break_7", + "pending_principal_amount", + "payable_principal_amount", + "column_break_14", + "interest_amount", + "section_break_15", + "process_loan_interest_accrual", + "amended_from" + ], + "fields": [ + { + "fieldname": "loan", + "fieldtype": "Link", + "label": "Loan", + "options": "Loan" + }, + { + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date" + }, + { + "fieldname": "pending_principal_amount", + "fieldtype": "Currency", + "label": "Pending Principal Amount", + "options": "Company:company:default_currency" + }, + { + "fieldname": "interest_amount", + "fieldtype": "Currency", + "label": "Interest Amount", + "options": "Company:company:default_currency" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Loan Interest Accrual", + "print_hide": 1, + "read_only": 1 + }, + { + "fetch_from": "loan.applicant_type", + "fieldname": "applicant_type", + "fieldtype": "Select", + "label": "Applicant Type", + "options": "Employee\nMember\nCustomer" + }, + { + "fetch_from": "loan.applicant", + "fieldname": "applicant", + "fieldtype": "Dynamic Link", + "label": "Applicant", + "options": "applicant_type" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fetch_from": "loan.interest_income_account", + "fieldname": "interest_income_account", + "fieldtype": "Data", + "label": "Interest Income Account" + }, + { + "fetch_from": "loan.loan_account", + "fieldname": "loan_account", + "fieldtype": "Data", + "label": "Loan Account" + }, + { + "fieldname": "section_break_7", + "fieldtype": "Section Break", + "label": "Amounts" + }, + { + "fetch_from": "loan.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "default": "0", + "fieldname": "is_paid", + "fieldtype": "Check", + "label": "Is Paid", + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "loan.is_term_loan", + "fieldname": "is_term_loan", + "fieldtype": "Check", + "label": "Is Term Loan", + "read_only": 1 + }, + { + "depends_on": "is_term_loan", + "fieldname": "payable_principal_amount", + "fieldtype": "Currency", + "label": "Payable Principal Amount", + "options": "Company:company:default_currency" + }, + { + "fieldname": "section_break_15", + "fieldtype": "Section Break" + }, + { + "fieldname": "process_loan_interest_accrual", + "fieldtype": "Link", + "label": "Process Loan Interest Accrual", + "options": "Process Loan Interest Accrual" + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + } + ], + "in_create": 1, + "is_submittable": 1, + "links": [], + "modified": "2020-02-07 01:22:06.924125", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Interest Accrual", + "owner": "Administrator", + "permissions": [ + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "submit": 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/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py new file mode 100644 index 0000000000..a3442e4439 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, erpnext +from frappe import _ +from frappe.model.document import Document +from frappe.utils import (nowdate, getdate, now_datetime, get_datetime, flt, date_diff, get_last_day, cint, + get_first_day, get_datetime, add_days) +from erpnext.controllers.accounts_controller import AccountsController +from erpnext.accounts.general_ledger import make_gl_entries + +class LoanInterestAccrual(AccountsController): + def validate(self): + if not self.loan: + frappe.throw(_("Loan is mandatory")) + + if not self.posting_date: + self.posting_date = nowdate() + + if not self.interest_amount: + frappe.throw(_("Interest Amount is mandatory")) + + + def on_submit(self): + self.make_gl_entries() + + def on_cancel(self): + self.make_gl_entries(cancel=1) + + def make_gl_entries(self, cancel=0, adv_adj=0): + gle_map = [] + + gle_map.append( + self.get_gl_dict({ + "account": self.loan_account, + "party_type": self.applicant_type, + "party": self.applicant, + "against": self.interest_income_account, + "debit": self.interest_amount, + "debit_in_account_currency": self.interest_amount, + "against_voucher_type": "Loan", + "against_voucher": self.loan, + "remarks": _("Against Loan:") + self.loan, + "cost_center": erpnext.get_default_cost_center(self.company), + "posting_date": self.posting_date + }) + ) + + gle_map.append( + self.get_gl_dict({ + "account": self.interest_income_account, + "party_type": self.applicant_type, + "party": self.applicant, + "against": self.loan_account, + "credit": self.interest_amount, + "credit_in_account_currency": self.interest_amount, + "against_voucher_type": "Loan", + "against_voucher": self.loan, + "remarks": _("Against Loan:") + self.loan, + "cost_center": erpnext.get_default_cost_center(self.company), + "posting_date": self.posting_date + }) + ) + + if gle_map: + make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj) + + +# For Eg: If Loan disbursement date is '01-09-2019' and disbursed amount is 1000000 and +# rate of interest is 13.5 then first loan interest accural will be on '01-10-2019' +# which means interest will be accrued for 30 days which should be equal to 11095.89 +def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest): + no_of_days = get_no_of_days_for_interest_accural(loan, posting_date) + + if no_of_days <= 0: + return + + pending_principal_amount = loan.total_payment - loan.total_interest_payable \ + - loan.total_amount_paid + + interest_per_day = (pending_principal_amount * loan.rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100) + payable_interest = interest_per_day * no_of_days + + make_loan_interest_accrual_entry(loan.name, loan.applicant_type, loan.applicant,loan.interest_income_account, + loan.loan_account, pending_principal_amount, payable_interest, process_loan_interest = process_loan_interest, + posting_date=posting_date) + +def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None): + query_filters = { + "status": "Disbursed", + "docstatus": 1 + } + + if loan_type: + query_filters.update({ + "loan_type": loan_type + }) + + if not open_loans: + open_loans = frappe.get_all("Loan", + fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", "is_term_loan", + "disbursement_date", "applicant_type", "applicant", "rate_of_interest", "total_interest_payable", "repayment_start_date"], + filters=query_filters) + + for loan in open_loans: + calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest) + +def make_accrual_interest_entry_for_term_loans(posting_date=None): + curr_date = posting_date or add_days(nowdate(), 1) + + term_loans = frappe.db.sql("""SELECT l.name, l.total_payment, l.total_amount_paid, l.loan_account, + l.interest_income_account, l.is_term_loan, l.disbursement_date, l.applicant_type, l.applicant, + l.rate_of_interest, l.total_interest_payable, l.repayment_start_date, rs.name as payment_entry, + rs.payment_date, rs.principal_amount, rs.interest_amount, rs.is_accrued , rs.balance_loan_amount + FROM `tabLoan` l, `tabRepayment Schedule` rs + WHERE rs.parent = l.name + AND l.docstatus=1 + AND l.is_term_loan =1 + AND rs.payment_date <= %s + AND rs.is_accrued=0 + AND l.status = 'Disbursed'""", (curr_date), as_dict=1) + + accrued_entries = [] + + for loan in term_loans: + accrued_entries.append(loan.payment_entry) + make_loan_interest_accrual_entry(loan.name, loan.applicant_type, loan.applicant,loan.interest_income_account, + loan.loan_account, loan.principal_amount + loan.balance_loan_amount, loan.interest_amount, + payable_principal = loan.principal_amount , posting_date=posting_date) + + frappe.db.sql("""UPDATE `tabRepayment Schedule` + SET is_accrued = 1 where name in (%s)""" #nosec + % ", ".join(['%s']*len(accrued_entries)), tuple(accrued_entries)) + +def make_loan_interest_accrual_entry(loan, applicant_type, applicant, interest_income_account, loan_account, + pending_principal_amount, interest_amount, payable_principal=None, process_loan_interest=None, posting_date=None): + loan_interest_accrual = frappe.new_doc("Loan Interest Accrual") + loan_interest_accrual.loan = loan + loan_interest_accrual.applicant_type = applicant_type + loan_interest_accrual.applicant = applicant + loan_interest_accrual.interest_income_account = interest_income_account + loan_interest_accrual.loan_account = loan_account + loan_interest_accrual.pending_principal_amount = flt(pending_principal_amount, 2) + loan_interest_accrual.interest_amount = flt(interest_amount, 2) + loan_interest_accrual.posting_date = posting_date or nowdate() + loan_interest_accrual.process_loan_interest_accrual = process_loan_interest + + if payable_principal: + loan_interest_accrual.payable_principal_amount = payable_principal + + loan_interest_accrual.save() + loan_interest_accrual.submit() + + +def get_no_of_days_for_interest_accural(loan, posting_date): + last_interest_accrual_date = get_last_accural_date_in_current_month(loan) + + no_of_days = date_diff(posting_date or nowdate(), last_interest_accrual_date) + 1 + + return no_of_days + +def get_last_accural_date_in_current_month(loan): + last_posting_date = frappe.db.sql(""" SELECT MAX(posting_date) from `tabLoan Interest Accrual` + WHERE loan = %s""", (loan.name)) + + if last_posting_date[0][0]: + return last_posting_date[0][0] + else: + return loan.disbursement_date + +def days_in_year(year): + days = 365 + + if (year % 4 == 0) and (year % 100 != 0) or (year % 400 == 0): + days = 366 + + return days + diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py new file mode 100644 index 0000000000..e681ae42c3 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals +import frappe +import unittest +from frappe.utils import (nowdate, add_days, get_datetime, get_first_day, get_last_day, date_diff, flt, add_to_date) +from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_pledge, create_loan_security_price, + make_loan_disbursement_entry, create_loan_accounts, create_loan_security_type, create_loan_security, create_demand_loan) +from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual +from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import (make_accrual_interest_entry_for_term_loans, days_in_year) +from erpnext.selling.doctype.customer.test_customer import get_customer_dict + +class TestLoanInterestAccrual(unittest.TestCase): + def setUp(self): + create_loan_accounts() + + create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', + 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + + create_loan_security_type() + create_loan_security() + + create_loan_security_price("Test Security 1", 500, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) + + if not frappe.db.exists("Customer", "_Test Loan Customer"): + frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) + + self.applicant = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name') + + def test_loan_interest_accural(self): + pledges = [] + pledges.append({ + "loan_security": "Test Security 1", + "qty": 4000.00, + "haircut": 50, + "loan_security_price": 500.00 + }) + + loan_security_pledge = create_loan_security_pledge(self.applicant, pledges) + + loan = create_demand_loan(self.applicant, "Demand Loan", loan_security_pledge.name, + posting_date=get_first_day(nowdate())) + + loan.submit() + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime(first_date).year) * 100) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + + process_loan_interest_accrual(posting_date=last_date) + + loan_interest_accural = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name}) + + self.assertEquals(flt(loan_interest_accural.interest_amount, 2), flt(accrued_interest_amount, 2)) diff --git a/erpnext/loan_management/doctype/loan_repayment/__init__.py b/erpnext/loan_management/doctype/loan_repayment/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.js b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.js new file mode 100644 index 0000000000..82a2d802b8 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.js @@ -0,0 +1,64 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +{% include 'erpnext/loan_management/loan_common.js' %}; + +frappe.ui.form.on('Loan Repayment', { + // refresh: function(frm) { + + // } + onload: function(frm) { + frm.set_query('against_loan', function() { + return { + 'filters': { + 'docstatus': 1 + } + }; + }); + + if (frm.doc.against_loan && frm.doc.posting_date && frm.doc.docstatus == 0) { + frm.trigger('calculate_repayment_amounts'); + } + }, + + posting_date : function(frm) { + frm.trigger('calculate_repayment_amounts'); + }, + + against_loan: function(frm) { + if (frm.doc.posting_date) { + frm.trigger('calculate_repayment_amounts'); + } + }, + + payment_type: function(frm) { + if (frm.doc.posting_date) { + frm.trigger('calculate_repayment_amounts'); + } + }, + + calculate_repayment_amounts: function(frm) { + frappe.call({ + method: 'erpnext.loan_management.doctype.loan_repayment.loan_repayment.calculate_amounts', + args: { + 'against_loan': frm.doc.against_loan, + 'posting_date': frm.doc.posting_date, + 'payment_type': frm.doc.payment_type + }, + callback: function(r) { + let amounts = r.message; + frm.set_value('amount_paid', 0.0); + frm.set_df_property('amount_paid', 'read_only', frm.doc.payment_type == "Loan Closure" ? 1:0); + + frm.set_value('pending_principal_amount', amounts['pending_principal_amount']); + if (frm.doc.is_term_loan || frm.doc.payment_type == "Loan Closure") { + frm.set_value('payable_principal_amount', amounts['payable_principal_amount']); + frm.set_value('amount_paid', amounts['payable_amount']); + } + frm.set_value('interest_payable', amounts['interest_amount']); + frm.set_value('penalty_amount', amounts['penalty_amount']); + frm.set_value('payable_amount', amounts['payable_amount']); + } + }); + } +}); diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json new file mode 100644 index 0000000000..92e98177ea --- /dev/null +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json @@ -0,0 +1,267 @@ +{ + "actions": [], + "autoname": "LM-REP-.####", + "creation": "2019-09-03 14:44:39.977266", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "against_loan", + "applicant_type", + "applicant", + "loan_type", + "payment_type", + "column_break_3", + "company", + "posting_date", + "is_term_loan", + "payment_details_section", + "due_date", + "pending_principal_amount", + "interest_payable", + "payable_amount", + "column_break_9", + "payable_principal_amount", + "penalty_amount", + "amount_paid", + "accounting_dimensions_section", + "cost_center", + "references_section", + "reference_number", + "column_break_21", + "reference_date", + "paid_accrual_entries", + "partial_paid_entry", + "principal_amount_paid", + "amended_from" + ], + "fields": [ + { + "fieldname": "against_loan", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Against Loan", + "options": "Loan", + "reqd": 1 + }, + { + "fieldname": "posting_date", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Posting Date", + "reqd": 1 + }, + { + "fieldname": "payment_details_section", + "fieldtype": "Section Break", + "label": "Payment Details" + }, + { + "fieldname": "penalty_amount", + "fieldtype": "Currency", + "label": "Penalty Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "interest_payable", + "fieldtype": "Currency", + "label": "Interest Payable", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fetch_from": "against_loan.applicant", + "fieldname": "applicant", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Applicant", + "options": "applicant_type", + "read_only": 1, + "reqd": 1 + }, + { + "fetch_from": "against_loan.loan_type", + "fieldname": "loan_type", + "fieldtype": "Link", + "label": "Loan Type", + "options": "Loan Type", + "read_only": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "default": "Regular Payment", + "fieldname": "payment_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Payment Type", + "options": "\nRegular Payment\nLoan Closure", + "reqd": 1 + }, + { + "fieldname": "payable_amount", + "fieldtype": "Currency", + "label": "Payable Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "bold": 1, + "fieldname": "amount_paid", + "fieldtype": "Currency", + "label": "Amount Paid", + "options": "Company:company:default_currency", + "reqd": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Loan Repayment", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fetch_from": "against_loan.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "read_only": 1 + }, + { + "fieldname": "pending_principal_amount", + "fieldtype": "Currency", + "label": "Pending Principal Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "paid_accrual_entries", + "fieldtype": "Text", + "hidden": 1, + "label": "Paid Accrual Entries", + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "against_loan.is_term_loan", + "fieldname": "is_term_loan", + "fieldtype": "Check", + "label": "Is Term Loan", + "read_only": 1 + }, + { + "depends_on": "eval:doc.payment_type==\"Loan Closure\" || doc.is_term_loan", + "fieldname": "payable_principal_amount", + "fieldtype": "Currency", + "label": "Payable Principal Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "references_section", + "fieldtype": "Section Break", + "label": "References" + }, + { + "fieldname": "reference_number", + "fieldtype": "Data", + "label": "Reference Number" + }, + { + "fieldname": "reference_date", + "fieldtype": "Date", + "label": "Reference Date" + }, + { + "fieldname": "column_break_21", + "fieldtype": "Column Break" + }, + { + "fieldname": "partial_paid_entry", + "fieldtype": "Text", + "hidden": 1, + "label": "Partial Paid Entry", + "read_only": 1 + }, + { + "default": "0.0", + "fieldname": "principal_amount_paid", + "fieldtype": "Currency", + "hidden": 1, + "label": "Principal Amount Paid", + "read_only": 1 + }, + { + "fetch_from": "against_loan.applicant_type", + "fieldname": "applicant_type", + "fieldtype": "Select", + "label": "Applicant Type", + "options": "Employee\nMember\nCustomer", + "read_only": 1 + }, + { + "fieldname": "due_date", + "fieldtype": "Date", + "label": "Due Date", + "read_only": 1 + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-02-24 07:35:47.168123", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Repayment", + "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": "Loan Manager", + "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/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py new file mode 100644 index 0000000000..a70e312880 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -0,0 +1,315 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, erpnext +import json +from frappe import _ +from frappe.utils import flt, getdate +from six import iteritems +from frappe.model.document import Document +from frappe.utils import date_diff, add_days, getdate, add_months, get_first_day, get_datetime +from erpnext.controllers.accounts_controller import AccountsController +from erpnext.accounts.general_ledger import make_gl_entries +from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import update_shortfall_status + +class LoanRepayment(AccountsController): + + def validate(self): + amounts = calculate_amounts(self.against_loan, self.posting_date, self.payment_type) + self.set_missing_values(amounts) + + def before_submit(self): + self.mark_as_paid() + + def on_submit(self): + self.make_gl_entries() + + def on_cancel(self): + self.mark_as_unpaid() + self.make_gl_entries(cancel=1) + + def set_missing_values(self, amounts): + if not self.posting_date: + self.posting_date = get_datetime() + + if not self.cost_center: + self.cost_center = erpnext.get_default_cost_center(self.company) + + if not self.interest_payable: + self.interest_payable = amounts['interest_amount'] + + if not self.penalty_amount: + self.penalty_amount = amounts['penalty_amount'] + + if not self.pending_principal_amount: + self.pending_principal_amount = amounts['pending_principal_amount'] + + if not self.payable_principal_amount and self.is_term_loan: + self.payable_principal_amount = amounts['payable_principal_amount'] + + if not self.payable_amount: + self.payable_amount = amounts['payable_amount'] + + if amounts.get('paid_accrual_entries'): + self.paid_accrual_entries = frappe.as_json(amounts.get('paid_accrual_entries')) + + if amounts.get('due_date'): + self.due_date = amounts.get('due_date') + + def mark_as_paid(self): + paid_entries = [] + paid_amount = self.amount_paid + interest_paid = paid_amount + + if not paid_amount: + frappe.throw(_("Amount paid cannot be zero")) + + if self.amount_paid < self.penalty_amount: + msg = _("Paid amount cannot be less than {0}").format(self.penalty_amount) + frappe.throw(msg) + + if self.payment_type == "Loan Closure" and flt(self.amount_paid, 2) < flt(self.payable_amount, 2): + msg = _("Amount of {0} is required for Loan closure").format(self.payable_amount) + frappe.throw(msg) + + loan = frappe.get_doc("Loan", self.against_loan) + + if self.paid_accrual_entries: + paid_accrual_entries = json.loads(self.paid_accrual_entries) + + if paid_amount - self.penalty_amount > 0 and self.paid_accrual_entries: + + interest_paid = paid_amount - self.penalty_amount + + for lia, interest_amount in iteritems(paid_accrual_entries): + if interest_amount <= interest_paid: + paid_entries.append(lia) + interest_paid -= interest_amount + elif interest_paid: + self.partial_paid_entry = frappe.as_json({"name": lia, "interest_amount": interest_amount}) + frappe.db.set_value("Loan Interest Accrual", lia, "interest_amount", + interest_amount - interest_paid) + interest_paid = 0 + + if paid_entries: + self.paid_accrual_entries = frappe.as_json(paid_entries) + else: + self.paid_accrual_entries = "" + + if interest_paid: + self.principal_amount_paid = interest_paid + + if paid_entries: + frappe.db.sql("""UPDATE `tabLoan Interest Accrual` + SET is_paid = 1 where name in (%s)""" #nosec + % ", ".join(['%s']*len(paid_entries)), tuple(paid_entries)) + + if flt(loan.total_principal_paid + self.principal_amount_paid, 2) >= flt(loan.total_payment, 2): + frappe.db.set_value("Loan", self.against_loan, "status", "Loan Closure Requested") + + frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s + WHERE name = %s """, (loan.total_amount_paid + self.amount_paid, + loan.total_principal_paid + self.principal_amount_paid, self.against_loan)) + + update_shortfall_status(self.against_loan, self.principal_amount_paid) + + def mark_as_unpaid(self): + + loan = frappe.get_doc("Loan", self.against_loan) + + if self.paid_accrual_entries: + paid_accrual_entries = json.loads(self.paid_accrual_entries) + + if self.paid_accrual_entries: + frappe.db.sql("""UPDATE `tabLoan Interest Accrual` + SET is_paid = 0 where name in (%s)""" #nosec + % ", ".join(['%s']*len(paid_accrual_entries)), tuple(paid_accrual_entries)) + + if self.partial_paid_entry: + partial_paid_entry = json.loads(self.partial_paid_entry) + frappe.db.set_value("Loan Interest Accrual", partial_paid_entry["name"], "interest_amount", + partial_paid_entry["interest_amount"]) + + frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s + WHERE name = %s """, (loan.total_amount_paid - self.amount_paid, + loan.total_principal_paid - self.principal_amount_paid, self.against_loan)) + + if loan.status == "Loan Closure Requested": + frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed") + + def make_gl_entries(self, cancel=0, adv_adj=0): + gle_map = [] + loan_details = frappe.get_doc("Loan", self.against_loan) + + if self.penalty_amount: + gle_map.append( + self.get_gl_dict({ + "account": loan_details.loan_account, + "against": loan_details.payment_account, + "debit": self.penalty_amount, + "debit_in_account_currency": self.penalty_amount, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Against Loan:") + self.against_loan, + "cost_center": self.cost_center, + "party_type": self.applicant_type, + "party": self.applicant, + "posting_date": getdate(self.posting_date) + }) + ) + + gle_map.append( + self.get_gl_dict({ + "account": loan_details.penalty_income_account, + "against": loan_details.payment_account, + "credit": self.penalty_amount, + "credit_in_account_currency": self.penalty_amount, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Against Loan:") + self.against_loan, + "cost_center": self.cost_center, + "party_type": self.applicant_type, + "party": self.applicant, + "posting_date": getdate(self.posting_date) + }) + ) + + gle_map.append( + self.get_gl_dict({ + "account": loan_details.payment_account, + "against": loan_details.loan_account + ", " + loan_details.interest_income_account + + ", " + loan_details.penalty_income_account, + "debit": self.amount_paid, + "debit_in_account_currency": self.amount_paid , + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Against Loan:") + self.against_loan, + "cost_center": self.cost_center, + "party_type": self.applicant_type, + "party": self.applicant, + "posting_date": getdate(self.posting_date) + }) + ) + + gle_map.append( + self.get_gl_dict({ + "account": loan_details.loan_account, + "party_type": loan_details.applicant_type, + "party": loan_details.applicant, + "against": loan_details.payment_account, + "credit": self.amount_paid, + "credit_in_account_currency": self.amount_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Against Loan:") + self.against_loan, + "cost_center": self.cost_center, + "posting_date": getdate(self.posting_date) + }) + ) + + if gle_map: + make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj) + +def create_repayment_entry(loan, applicant, company, posting_date, loan_type, + payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None): + + lr = frappe.get_doc({ + "doctype": "Loan Repayment", + "against_loan": loan, + "payment_type": payment_type, + "company": company, + "posting_date": posting_date, + "applicant": applicant, + "penalty_amount": penalty_amount, + "interets_payable": interest_payable, + "payable_principal_amount": payable_principal_amount, + "amount_paid": amount_paid, + "loan_type": loan_type + }).insert() + + return lr + +def get_accrued_interest_entries(against_loan): + accrued_interest_entries = frappe.get_all("Loan Interest Accrual", + fields=["name", "interest_amount", "posting_date", "payable_principal_amount"], + filters = { + "loan": against_loan, + "is_paid": 0 + }, order_by="posting_date") + + return accrued_interest_entries + +# This function returns the amounts that are payable at the time of loan repayment based on posting date +# So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable + +def get_amounts(amounts, against_loan, posting_date, payment_type): + + against_loan_doc = frappe.get_doc("Loan", against_loan) + loan_type_details = frappe.get_doc("Loan Type", against_loan_doc.loan_type) + accrued_interest_entries = get_accrued_interest_entries(against_loan_doc.name) + + pending_accrual_entries = {} + + total_pending_interest = 0 + penalty_amount = 0 + payable_principal_amount = 0 + final_due_date = '' + + for entry in accrued_interest_entries: + # Loan repayment due date is one day after the loan interest is accrued + # no of late days are calculated based on loan repayment posting date + # and if no_of_late days are positive then penalty is levied + + due_date = add_days(entry.posting_date, 1) + no_of_late_days = date_diff(posting_date, + add_days(due_date, loan_type_details.grace_period_in_days)) + 1 + + if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary): + penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)/365 + + total_pending_interest += entry.interest_amount + payable_principal_amount += entry.payable_principal_amount + + pending_accrual_entries.setdefault(entry.name, entry.interest_amount) + final_due_date = due_date + + pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable + + if payment_type == "Loan Closure" and not payable_principal_amount: + pending_days = date_diff(posting_date, entry.posting_date) + 1 + payable_principal_amount = pending_principal_amount + per_day_interest = (payable_principal_amount * (loan_type_details.rate_of_interest / 100))/365 + total_pending_interest += (pending_days * per_day_interest) + + amounts["pending_principal_amount"] = pending_principal_amount + amounts["payable_principal_amount"] = payable_principal_amount + amounts["interest_amount"] = total_pending_interest + amounts["penalty_amount"] = penalty_amount + amounts["payable_amount"] = payable_principal_amount + total_pending_interest + penalty_amount + amounts["paid_accrual_entries"] = pending_accrual_entries + + if final_due_date: + amounts["due_date"] = final_due_date + + return amounts + +@frappe.whitelist() +def calculate_amounts(against_loan, posting_date, payment_type): + + amounts = { + 'penalty_amount': 0.0, + 'interest_amount': 0.0, + 'pending_principal_amount': 0.0, + 'payable_principal_amount': 0.0, + 'payable_amount': 0.0, + 'due_date': '' + } + + amounts = get_amounts(amounts, against_loan, posting_date, payment_type) + + return amounts + + + diff --git a/erpnext/loan_management/doctype/loan_repayment/test_loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/test_loan_repayment.py new file mode 100644 index 0000000000..73585a5159 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_repayment/test_loan_repayment.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLoanRepayment(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/loan_security/__init__.py b/erpnext/loan_management/doctype/loan_security/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/loan_security/loan_security.js b/erpnext/loan_management/doctype/loan_security/loan_security.js new file mode 100644 index 0000000000..0e815af76a --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security/loan_security.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Loan Security', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/loan_management/doctype/loan_security/loan_security.json b/erpnext/loan_management/doctype/loan_security/loan_security.json new file mode 100644 index 0000000000..e6984ee7f1 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security/loan_security.json @@ -0,0 +1,95 @@ +{ + "autoname": "field:loan_security_name", + "creation": "2019-09-02 15:07:08.885593", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan_security_type", + "loan_security_code", + "loan_security_name", + "unit_of_measure", + "column_break_3", + "haircut", + "disabled" + ], + "fields": [ + { + "fieldname": "loan_security_name", + "fieldtype": "Data", + "label": "Loan Security Name", + "unique": 1 + }, + { + "fetch_from": "loan_security_type.haircut", + "fieldname": "haircut", + "fieldtype": "Percent", + "label": "Haircut %" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "loan_security_type", + "fieldtype": "Link", + "label": "Loan Security Type", + "options": "Loan Security Type" + }, + { + "fieldname": "loan_security_code", + "fieldtype": "Data", + "label": "Loan Security Code", + "unique": 1 + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fetch_from": "loan_security_type.unit_of_measure", + "fieldname": "unit_of_measure", + "fieldtype": "Link", + "label": "Unit Of Measure", + "options": "UOM" + } + ], + "modified": "2019-11-16 11:36:37.901656", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Security", + "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": "Loan Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "search_fields": "loan_security_code", + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_security/loan_security.py b/erpnext/loan_management/doctype/loan_security/loan_security.py new file mode 100644 index 0000000000..800ad12957 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security/loan_security.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, 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 LoanSecurity(Document): + pass diff --git a/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py b/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py new file mode 100644 index 0000000000..878b3fd051 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'loan_security', + 'transactions': [ + { + 'items': ['Loan Application', 'Loan Security Price'] + }, + { + 'items': ['Loan Security Pledge', 'Loan Security Unpledge'] + } + ] + } \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_security/test_loan_security.py b/erpnext/loan_management/doctype/loan_security/test_loan_security.py new file mode 100644 index 0000000000..24dbc68046 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security/test_loan_security.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLoanSecurity(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/loan_security_pledge/__init__.py b/erpnext/loan_management/doctype/loan_security_pledge/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js new file mode 100644 index 0000000000..82837b3dac --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js @@ -0,0 +1,40 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Loan Security Pledge', { + calculate_amounts: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.loan_security_price); + frappe.model.set_value(cdt, cdn, 'post_haircut_amount', cint(row.amount - (row.amount * row.haircut/100))); + + let amount = 0; + let maximum_amount = 0; + $.each(frm.doc.securities || [], function(i, item){ + amount += item.amount; + maximum_amount += item.post_haircut_amount; + }); + + frm.set_value('total_security_value', amount); + frm.set_value('maximum_loan_value', maximum_amount); + } +}); + +frappe.ui.form.on("Pledge", { + loan_security: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + frappe.call({ + method: "erpnext.loan_management.doctype.loan_security_price.loan_security_price.get_loan_security_price", + args: { + loan_security: row.loan_security + }, + callback: function(r) { + frappe.model.set_value(cdt, cdn, 'loan_security_price', r.message); + frm.events.calculate_amounts(frm, cdt, cdn); + } + }); + }, + + qty: function(frm, cdt, cdn) { + frm.events.calculate_amounts(frm, cdt, cdn); + }, +}); \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json new file mode 100644 index 0000000000..1553844704 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json @@ -0,0 +1,176 @@ +{ + "autoname": "LS-.{applicant}.-.#####", + "creation": "2019-08-29 18:48:51.371674", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan_details_section", + "loan_application", + "loan", + "applicant_type", + "applicant", + "column_break_3", + "company", + "pledge_time", + "status", + "loan_security_details_section", + "securities", + "section_break_10", + "total_security_value", + "column_break_11", + "maximum_loan_value", + "amended_from" + ], + "fields": [ + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Loan Security Pledge", + "print_hide": 1, + "read_only": 1 + }, + { + "fetch_from": "loan_application.applicant", + "fieldname": "applicant", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Applicant", + "options": "applicant_type", + "reqd": 1 + }, + { + "fieldname": "loan_security_details_section", + "fieldtype": "Section Break", + "label": "Loan Security Details" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "loan", + "fieldtype": "Link", + "label": "Loan", + "options": "Loan", + "read_only": 1 + }, + { + "fieldname": "loan_application", + "fieldtype": "Link", + "label": "Loan Application", + "options": "Loan Application", + "read_only": 1 + }, + { + "fieldname": "total_security_value", + "fieldtype": "Currency", + "label": "Total Security Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "maximum_loan_value", + "fieldtype": "Currency", + "label": "Maximum Loan Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "loan_details_section", + "fieldtype": "Section Break", + "label": "Loan Details" + }, + { + "default": "Requested", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Requested\nUnpledged\nPledged\nPartially Pledged", + "read_only": 1 + }, + { + "fieldname": "pledge_time", + "fieldtype": "Datetime", + "label": "Pledge Time", + "read_only": 1 + }, + { + "fieldname": "securities", + "fieldtype": "Table", + "label": "Securities", + "options": "Pledge", + "reqd": 1 + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break", + "label": "Totals" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fetch_from": "loan.applicant_type", + "fieldname": "applicant_type", + "fieldtype": "Select", + "label": "Applicant Type", + "options": "Employee\nMember\nCustomer", + "reqd": 1 + } + ], + "is_submittable": 1, + "modified": "2019-10-10 13:22:53.297519", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Security Pledge", + "owner": "Administrator", + "permissions": [ + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "quick_entry": 1, + "search_fields": "applicant", + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py new file mode 100644 index 0000000000..b405ccae55 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import now_datetime, cint +from frappe.model.document import Document +from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import update_shortfall_status +from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price + +class LoanSecurityPledge(Document): + def validate(self): + self.set_pledge_amount() + + def on_submit(self): + if self.loan: + self.db_set("status", "Pledged") + self.db_set("pledge_time", now_datetime()) + update_shortfall_status(self.loan, self.total_security_value) + update_loan(self.loan, self.maximum_loan_value) + + def set_pledge_amount(self): + total_security_value = 0 + maximum_loan_value = 0 + + for pledge in self.securities: + + if not pledge.qty and not pledge.amount: + frappe.throw(_("Qty or Amount is mandatroy for loan security")) + + pledge.loan_security_price = get_loan_security_price(pledge.loan_security) + + if not pledge.qty: + pledge.qty = cint(pledge.amount/pledge.loan_security_price) + + pledge.amount = pledge.qty * pledge.loan_security_price + pledge.post_haircut_amount = cint(pledge.amount - (pledge.amount * pledge.haircut/100)) + + total_security_value += pledge.amount + maximum_loan_value += pledge.post_haircut_amount + + self.total_security_value = total_security_value + self.maximum_loan_value = maximum_loan_value + +def update_loan(loan, maximum_value_against_pledge): + maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_value']) + + frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_value=%s, is_secured_loan=1 + WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan)) diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge_list.js b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge_list.js new file mode 100644 index 0000000000..174d1b0d62 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge_list.js @@ -0,0 +1,15 @@ +// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// License: GNU General Public License v3. See license.txt + +// render +frappe.listview_settings['Loan Security Pledge'] = { + add_fields: ["status"], + get_indicator: function(doc) { + var status_color = { + "Unpledged": "orange", + "Pledged": "green", + "Partially Pledged": "green" + }; + return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; + } +}; diff --git a/erpnext/loan_management/doctype/loan_security_pledge/test_loan_security_pledge.py b/erpnext/loan_management/doctype/loan_security_pledge/test_loan_security_pledge.py new file mode 100644 index 0000000000..d2347c0098 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_pledge/test_loan_security_pledge.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLoanSecurityPledge(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/loan_security_price/__init__.py b/erpnext/loan_management/doctype/loan_security_price/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.js b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.js new file mode 100644 index 0000000000..31b4ec7249 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Loan Security Price', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json new file mode 100644 index 0000000000..db260a4a9e --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json @@ -0,0 +1,117 @@ +{ + "autoname": "LM-LSP-.####", + "creation": "2019-09-03 18:20:31.382887", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan_security", + "loan_security_type", + "column_break_2", + "uom", + "section_break_4", + "loan_security_price", + "section_break_6", + "valid_from", + "column_break_8", + "valid_upto" + ], + "fields": [ + { + "fieldname": "loan_security", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Loan Security", + "options": "Loan Security", + "reqd": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fetch_from": "loan_security.unit_of_measure", + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM", + "read_only": 1 + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, + { + "fieldname": "loan_security_price", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Loan Security Price", + "reqd": 1 + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "fieldname": "valid_from", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Valid From", + "reqd": 1 + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "fieldname": "valid_upto", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Valid Upto", + "reqd": 1 + }, + { + "fetch_from": "loan_security.loan_security_type", + "fieldname": "loan_security_type", + "fieldtype": "Link", + "label": "Loan Security Type", + "options": "Loan Security Type", + "read_only": 1 + } + ], + "modified": "2019-10-26 09:46:46.069667", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Security Price", + "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": "Loan Manager", + "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/loan_management/doctype/loan_security_price/loan_security_price.py b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py new file mode 100644 index 0000000000..2855b52610 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. 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 frappe.utils import getdate, now_datetime, add_to_date, get_datetime, get_timestamp, get_datetime_str +from six import iteritems + +class LoanSecurityPrice(Document): + def validate(self): + self.validate_dates() + + def validate_dates(self): + + if self.valid_from > self.valid_upto: + frappe.throw(_("Valid From Time must be lesser than Valid Upto Time.")) + + existing_loan_security = frappe.db.sql(""" SELECT name from `tabLoan Security Price` + WHERE loan_security = %s AND name != %s AND (valid_from BETWEEN %s and %s OR valid_upto BETWEEN %s and %s) """, + (self.loan_security, self.name, self.valid_from, self.valid_upto, self.valid_from, self.valid_upto)) + + if existing_loan_security: + frappe.throw(_("Loan Security Price overlapping with {0}").format(existing_loan_security[0][0])) + +@frappe.whitelist() +def get_loan_security_price(loan_security, valid_time=None): + if not valid_time: + valid_time = get_datetime() + + loan_security_price = frappe.db.get_value("Loan Security Price", { + 'loan_security': loan_security, + 'valid_from': ("<=",valid_time), + 'valid_upto': (">=", valid_time) + }, 'loan_security_price') + + if not loan_security_price: + frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(loan_security))) + else: + return loan_security_price + + + + + + + + + diff --git a/erpnext/loan_management/doctype/loan_security_price/test_loan_security_price.py b/erpnext/loan_management/doctype/loan_security_price/test_loan_security_price.py new file mode 100644 index 0000000000..2fe0bd5a24 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_price/test_loan_security_price.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLoanSecurityPrice(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/__init__.py b/erpnext/loan_management/doctype/loan_security_shortfall/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.js b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.js new file mode 100644 index 0000000000..f26c138371 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.js @@ -0,0 +1,25 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Loan Security Shortfall', { + refresh: function(frm) { + frm.add_custom_button(__("Add Loan Security"), function() { + frm.trigger('shortfall_action'); + }); + }, + + shortfall_action: function(frm) { + frappe.call({ + method: "erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall.add_security", + args: { + 'loan': frm.doc.loan + }, + callback: function(r) { + if (r.message) { + let doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + } + }); + } +}); diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json new file mode 100644 index 0000000000..102bc0d71d --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json @@ -0,0 +1,126 @@ +{ + "autoname": "LM-LSS-.#####", + "creation": "2019-09-06 11:33:34.709540", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan", + "status", + "column_break_3", + "shortfall_time", + "section_break_3", + "loan_amount", + "shortfall_amount", + "column_break_8", + "security_value", + "section_break_8", + "process_loan_security_shortfall" + ], + "fields": [ + { + "fieldname": "loan", + "fieldtype": "Link", + "label": "Loan ", + "options": "Loan", + "read_only": 1 + }, + { + "fieldname": "loan_amount", + "fieldtype": "Currency", + "label": "Loan Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "security_value", + "fieldtype": "Currency", + "label": "Security Value ", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "shortfall_amount", + "fieldtype": "Currency", + "label": "Shortfall Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break" + }, + { + "description": "America/New_York", + "fieldname": "shortfall_time", + "fieldtype": "Datetime", + "label": "Shortfall Time", + "read_only": 1 + }, + { + "default": "Pending", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "\nPending\nCompleted", + "read_only": 1 + }, + { + "fieldname": "section_break_8", + "fieldtype": "Section Break" + }, + { + "fieldname": "process_loan_security_shortfall", + "fieldtype": "Link", + "label": "Process Loan Security Shortfall", + "options": "Process Loan Security Shortfall", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + } + ], + "in_create": 1, + "modified": "2019-10-24 06:24:26.128997", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Security Shortfall", + "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": "Loan Manager", + "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/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py new file mode 100644 index 0000000000..599f6dafaa --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import get_datetime +from frappe.model.document import Document +from six import iteritems + +class LoanSecurityShortfall(Document): + pass + +def update_shortfall_status(loan, security_value): + loan_security_shortfall = frappe.db.get_value("Loan Security Shortfall", + {"loan": loan, "status": "Pending"}, ['name', 'shortfall_amount'], as_dict=1) + + if not loan_security_shortfall: + return + + if security_value >= loan_security_shortfall.shortfall_amount: + frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, "status", "Completed") + else: + frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, + "shortfall_amount", loan_security_shortfall.shortfall_amount - security_value) + + +@frappe.whitelist() +def add_security(loan): + loan_details = frappe.db.get_value("Loan", loan, ['applicant', 'company', 'applicant_type'], as_dict=1) + + loan_security_pledge = frappe.new_doc("Loan Security Pledge") + loan_security_pledge.loan = loan + loan_security_pledge.company = loan_details.company + loan_security_pledge.applicant_type = loan_details.applicant_type + loan_security_pledge.applicant = loan_details.applicant + + return loan_security_pledge.as_dict() + +def check_for_ltv_shortfall(process_loan_security_shortfall=None): + + update_time = get_datetime() + + if not process_loan_security_shortfall: + process = frappe.new_doc("Process Loan Security Shortfall") + process.update_time = update_time + process.submit() + + process_loan_security_shortfall = process.name + + loan_security_price_map = frappe._dict(frappe.get_all("Loan Security Price", + fields=["loan_security", "loan_security_price"], + filters = { + "valid_from": ("<=", update_time), + "valid_upto": (">=", update_time) + }, as_list=1)) + + loans = frappe.db.sql(""" SELECT l.name, l.loan_amount, l.total_principal_paid, lp.loan_security, lp.haircut, lp.qty + FROM `tabLoan` l, `tabPledge` lp , `tabLoan Security Pledge`p WHERE lp.parent = p.name and p.loan = l.name and l.docstatus = 1 + and l.is_secured_loan and l.status = 'Disbursed' and p.status in ('Pledged', 'Partially Unpledged')""", as_dict=1) + + loan_security_map = {} + + for loan in loans: + loan_security_map.setdefault(loan.name, { + "loan_amount": loan.loan_amount - loan.total_principal_paid, + "security_value": 0.0 + }) + + current_loan_security_amount = loan_security_price_map.get(loan.loan_security, 0) * loan.qty + + loan_security_map[loan.name]['security_value'] += current_loan_security_amount - (current_loan_security_amount * loan.haircut/100) + + for loan, value in iteritems(loan_security_map): + if value["security_value"] < value["loan_amount"]: + create_loan_security_shortfall(loan, value, process_loan_security_shortfall) + +def create_loan_security_shortfall(loan, value, process_loan_security_shortfall): + + existing_shortfall = frappe.db.get_value("Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name") + + if existing_shortfall: + ltv_shortfall = frappe.get_doc("Loan Security Shortfall", existing_shortfall) + else: + ltv_shortfall = frappe.new_doc("Loan Security Shortfall") + ltv_shortfall.loan = loan + + ltv_shortfall.shortfall_time = get_datetime() + ltv_shortfall.loan_amount = value["loan_amount"] + ltv_shortfall.security_value = value["security_value"] + ltv_shortfall.shortfall_amount = value["loan_amount"] - value["security_value"] + ltv_shortfall.process_loan_security_shortfall = process_loan_security_shortfall + ltv_shortfall.save() + diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/test_loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/test_loan_security_shortfall.py new file mode 100644 index 0000000000..b82f3d2593 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_shortfall/test_loan_security_shortfall.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLoanSecurityShortfall(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/loan_security_type/__init__.py b/erpnext/loan_management/doctype/loan_security_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/loan_security_type/loan_security_type.js b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.js new file mode 100644 index 0000000000..3a1e0689c1 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Loan Security Type', { + // refresh: function(frm) { + + // }, +}); diff --git a/erpnext/loan_management/doctype/loan_security_type/loan_security_type.json b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.json new file mode 100644 index 0000000000..a5ab057cdc --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.json @@ -0,0 +1,73 @@ +{ + "autoname": "field:loan_security_type", + "creation": "2019-08-29 18:46:07.322056", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan_security_type", + "unit_of_measure", + "haircut", + "disabled" + ], + "fields": [ + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "loan_security_type", + "fieldtype": "Data", + "label": "Loan Security Type", + "unique": 1 + }, + { + "fieldname": "haircut", + "fieldtype": "Percent", + "label": "Haircut %" + }, + { + "fieldname": "unit_of_measure", + "fieldtype": "Link", + "label": "Unit Of Measure", + "options": "UOM" + } + ], + "modified": "2019-10-10 03:05:37.912866", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Security Type", + "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": "Loan Manager", + "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/loan_management/doctype/loan_security_type/loan_security_type.py b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.py new file mode 100644 index 0000000000..cb8a50a27b --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, 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 LoanSecurityType(Document): + pass diff --git a/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py b/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py new file mode 100644 index 0000000000..ac33589b54 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'loan_security_type', + 'transactions': [ + { + 'items': ['Loan Security', 'Loan Security Price'] + }, + { + 'items': ['Loan Security Pledge', 'Loan Security Unpledge'] + } + ] + } \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_security_type/test_loan_security_type.py b/erpnext/loan_management/doctype/loan_security_type/test_loan_security_type.py new file mode 100644 index 0000000000..f7d845a779 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_type/test_loan_security_type.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLoanSecurityType(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/__init__.py b/erpnext/loan_management/doctype/loan_security_unpledge/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js new file mode 100644 index 0000000000..72c5f38cf3 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js @@ -0,0 +1,13 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Loan Security Unpledge', { + refresh: function(frm) { + + frm.set_query("against_pledge", "securities", () => { + return { + filters : [["status", "in", ["Pledged", "Partially Pledged"]]] + }; + }); + } +}); diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json new file mode 100644 index 0000000000..ba94855031 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json @@ -0,0 +1,159 @@ +{ + "autoname": "LSU-.{applicant}.-.#####", + "creation": "2019-09-21 13:23:16.117028", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan_details_section", + "loan", + "applicant_type", + "applicant", + "column_break_3", + "company", + "unpledge_time", + "status", + "loan_security_details_section", + "securities", + "unpledge_type", + "amended_from" + ], + "fields": [ + { + "fieldname": "loan_details_section", + "fieldtype": "Section Break", + "label": "Loan Details" + }, + { + "fetch_from": "loan_application.applicant", + "fieldname": "applicant", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Applicant", + "options": "applicant_type", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "loan", + "fieldtype": "Link", + "label": "Loan", + "options": "Loan", + "reqd": 1 + }, + { + "allow_on_submit": 1, + "default": "Requested", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Requested\nApproved", + "permlevel": 1 + }, + { + "fieldname": "unpledge_time", + "fieldtype": "Datetime", + "label": "Unpledge Time", + "read_only": 1 + }, + { + "fieldname": "loan_security_details_section", + "fieldtype": "Section Break", + "label": "Loan Security Details" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Loan Security Unpledge", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "securities", + "fieldtype": "Table", + "label": "Securities", + "options": "Unpledge", + "reqd": 1 + }, + { + "fieldname": "unpledge_type", + "fieldtype": "Data", + "hidden": 1, + "label": "Unpledge Type", + "read_only": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fetch_from": "loan.applicant_type", + "fieldname": "applicant_type", + "fieldtype": "Select", + "label": "Applicant Type", + "options": "Employee\nMember\nCustomer", + "reqd": 1 + } + ], + "is_submittable": 1, + "modified": "2019-10-28 07:41:47.084882", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Security Unpledge", + "owner": "Administrator", + "permissions": [ + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "delete": 1, + "email": 1, + "export": 1, + "permlevel": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "search_fields": "applicant", + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py new file mode 100644 index 0000000000..02b1ecb4ca --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. 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 frappe.utils import get_datetime, flt +import json +from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price + +class LoanSecurityUnpledge(Document): + def validate(self): + self.validate_pledges() + + def validate_pledges(self): + pledge_details = self.get_pledge_details() + + loan = frappe.get_doc("Loan", self.loan) + + pledge_qty_map = {} + remaining_qty = 0 + unpledge_value = 0 + + for pledge in pledge_details: + pledge_qty_map.setdefault((pledge.parent, pledge.loan_security), pledge.qty) + + for security in self.securities: + pledged_qty = pledge_qty_map.get((security.against_pledge, security.loan_security), 0) + if not pledged_qty: + frappe.throw(_("Zero qty of {0} pledged against loan {0}").format(frappe.bold(security.loan_security), + frappe.bold(self.loan))) + + unpledge_qty = pledged_qty - security.qty + security_price = security.qty * get_loan_security_price(security.loan_security) + + if unpledge_qty < 0: + frappe.throw(_("Cannot unpledge more than {0} qty of {0}").format(frappe.bold(pledged_qty), + frappe.bold(security.loan_security))) + + remaining_qty += unpledge_qty + unpledge_value += security_price - flt(security_price * security.haircut/100) + + if unpledge_value > loan.total_principal_paid: + frappe.throw(_("Cannot Unpledge, loan security value is greater than the repaid amount")) + + if not remaining_qty: + self.db_set('unpledge_type', 'Unpledged') + else: + self.db_set('unpledge_type', 'Partially Pledged') + + + def get_pledge_details(self): + pledge_details = frappe.db.sql(""" + SELECT p.parent, p.loan_security, p.qty as qty FROM + `tabLoan Security Pledge` lsp, + `tabPledge` p + WHERE + p.parent = lsp.name + AND lsp.loan = %s + AND lsp.docstatus = 1 + AND lsp.status = "Pledged" + """,(self.loan), as_dict=1) + + return pledge_details + + def on_update_after_submit(self): + if self.status == "Approved": + frappe.db.sql(""" + UPDATE + `tabPledge` p, `tabUnpledge` u, `tabLoan Security Pledge` lsp, + `tabLoan Security Unpledge` lsu SET p.qty = (p.qty - u.qty) + WHERE + lsp.loan = %s + AND lsu.status = 'Requested' + AND u.parent = %s + AND p.parent = u.against_pledge + AND p.loan_security = u.loan_security""",(self.loan, self.name)) + + frappe.db.sql("""UPDATE `tabLoan Security Pledge` + SET status = %s WHERE loan = %s""", (self.unpledge_type, self.loan)) + + if self.unpledge_type == 'Unpledged': + frappe.db.set_value("Loan", self.loan, 'status', 'Closed') diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge_list.js b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge_list.js new file mode 100644 index 0000000000..196ebbb96a --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge_list.js @@ -0,0 +1,14 @@ +// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// License: GNU General Public License v3. See license.txt + +// render +frappe.listview_settings['Loan Security Unpledge'] = { + add_fields: ["status"], + get_indicator: function(doc) { + var status_color = { + "Requested": "orange", + "Approved": "green", + }; + return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; + } +}; diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/test_loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/test_loan_security_unpledge.py new file mode 100644 index 0000000000..5b5c205bda --- /dev/null +++ b/erpnext/loan_management/doctype/loan_security_unpledge/test_loan_security_unpledge.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLoanSecurityUnpledge(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/loan_type/__init__.py b/erpnext/loan_management/doctype/loan_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.js b/erpnext/loan_management/doctype/loan_type/loan_type.js new file mode 100644 index 0000000000..04c89c4549 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_type/loan_type.js @@ -0,0 +1,30 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Loan Type', { + onload: function(frm) { + $.each(["penalty_income_account", "interest_income_account"], function (i, field) { + frm.set_query(field, function () { + return { + "filters": { + "company": frm.doc.company, + "root_type": "Income", + "is_group": 0 + } + }; + }); + }); + + $.each(["payment_account", "loan_account"], function (i, field) { + frm.set_query(field, function () { + return { + "filters": { + "company": frm.doc.company, + "root_type": "Asset", + "is_group": 0 + } + }; + }); + }); + } +}); diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json new file mode 100644 index 0000000000..a3525db9a5 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_type/loan_type.json @@ -0,0 +1,170 @@ +{ + "actions": [], + "autoname": "field:loan_name", + "creation": "2019-08-29 18:08:38.159726", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan_name", + "maximum_loan_amount", + "rate_of_interest", + "penalty_interest_rate", + "grace_period_in_days", + "column_break_2", + "company", + "is_term_loan", + "disabled", + "description", + "account_details_section", + "mode_of_payment", + "payment_account", + "loan_account", + "column_break_12", + "interest_income_account", + "penalty_income_account", + "amended_from" + ], + "fields": [ + { + "fieldname": "loan_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Loan Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "maximum_loan_amount", + "fieldtype": "Currency", + "label": "Maximum Loan Amount", + "options": "Company:company:default_currency" + }, + { + "fieldname": "rate_of_interest", + "fieldtype": "Percent", + "label": "Rate of Interest (%) Yearly", + "reqd": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "description", + "fieldtype": "Text", + "label": "Description" + }, + { + "fieldname": "account_details_section", + "fieldtype": "Section Break", + "label": "Account Details" + }, + { + "fieldname": "mode_of_payment", + "fieldtype": "Link", + "label": "Mode of Payment", + "options": "Mode of Payment", + "reqd": 1 + }, + { + "fieldname": "payment_account", + "fieldtype": "Link", + "label": "Payment Account", + "options": "Account", + "reqd": 1 + }, + { + "fieldname": "loan_account", + "fieldtype": "Link", + "label": "Loan Account", + "options": "Account", + "reqd": 1 + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, + { + "fieldname": "interest_income_account", + "fieldtype": "Link", + "label": "Interest Income Account", + "options": "Account", + "reqd": 1 + }, + { + "fieldname": "penalty_income_account", + "fieldtype": "Link", + "label": "Penalty Income Account", + "options": "Account", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "is_term_loan", + "fieldtype": "Check", + "label": "Is Term Loan" + }, + { + "description": "Penalty Interest Rate is levied on the pending interest amount on a daily basis in case of delayed repayment ", + "fieldname": "penalty_interest_rate", + "fieldtype": "Percent", + "label": "Penalty Interest Rate (%) Per Day" + }, + { + "fieldname": "grace_period_in_days", + "fieldtype": "Int", + "label": "Grace Period in Days" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Loan Type", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-02-03 05:03:00.334813", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Type", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Loan Manager", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "Employee" + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.py b/erpnext/loan_management/doctype/loan_type/loan_type.py new file mode 100644 index 0000000000..14b18ab57a --- /dev/null +++ b/erpnext/loan_management/doctype/loan_type/loan_type.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. 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 + +class LoanType(Document): + def validate(self): + self.validate_accounts() + + def validate_accounts(self): + for fieldname in ['payment_account', 'loan_account', 'interest_income_account', 'penalty_income_account']: + company = frappe.get_value("Account", self.get(fieldname), 'company') + + if company and company != self.company: + frappe.throw(_("Account {0} does not belong to company {1}").format(frappe.bold(self.get(fieldname)), + frappe.bold(self.company))) + diff --git a/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py b/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py new file mode 100644 index 0000000000..58c668948c --- /dev/null +++ b/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'loan_type', + 'transactions': [ + { + 'items': ['Loan Repayment', 'Loan'] + }, + { + 'items': ['Loan Application'] + } + ] + } \ No newline at end of file diff --git a/erpnext/hr/doctype/loan_type/test_loan_type.py b/erpnext/loan_management/doctype/loan_type/test_loan_type.py similarity index 53% rename from erpnext/hr/doctype/loan_type/test_loan_type.py rename to erpnext/loan_management/doctype/loan_type/test_loan_type.py index 078e11e262..5877ab6f7f 100644 --- a/erpnext/hr/doctype/loan_type/test_loan_type.py +++ b/erpnext/loan_management/doctype/loan_type/test_loan_type.py @@ -1,12 +1,10 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals -import frappe +# import frappe import unittest -# test_records = frappe.get_test_records('Loan Type') - class TestLoanType(unittest.TestCase): pass diff --git a/erpnext/loan_management/doctype/pledge/__init__.py b/erpnext/loan_management/doctype/pledge/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/pledge/pledge.js b/erpnext/loan_management/doctype/pledge/pledge.js new file mode 100644 index 0000000000..fb6ab10778 --- /dev/null +++ b/erpnext/loan_management/doctype/pledge/pledge.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Pledge', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/loan_management/doctype/pledge/pledge.json b/erpnext/loan_management/doctype/pledge/pledge.json new file mode 100644 index 0000000000..f22a21e3be --- /dev/null +++ b/erpnext/loan_management/doctype/pledge/pledge.json @@ -0,0 +1,99 @@ +{ + "creation": "2019-09-09 17:06:16.756573", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan_security", + "loan_security_type", + "loan_security_code", + "uom", + "column_break_5", + "qty", + "haircut", + "loan_security_price", + "amount", + "post_haircut_amount" + ], + "fields": [ + { + "fieldname": "loan_security", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Loan Security", + "options": "Loan Security", + "reqd": 1 + }, + { + "fetch_from": "loan_security.loan_security_type", + "fieldname": "loan_security_type", + "fieldtype": "Link", + "label": "Loan Security Type", + "options": "Loan Security Type", + "read_only": 1 + }, + { + "fetch_from": "loan_security.loan_security_code", + "fieldname": "loan_security_code", + "fieldtype": "Data", + "label": "Loan Security Code" + }, + { + "fetch_from": "loan_security.unit_of_measure", + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM" + }, + { + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Quantity" + }, + { + "fieldname": "loan_security_price", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Loan Security Price", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fetch_from": "loan_security.haircut", + "fieldname": "haircut", + "fieldtype": "Percent", + "label": "Haircut %", + "read_only": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "Company:company:default_currency" + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "post_haircut_amount", + "fieldtype": "Currency", + "label": "Post Haircut Amount", + "options": "Company:company:default_currency", + "read_only": 1 + } + ], + "istable": 1, + "modified": "2019-12-03 10:59:58.001421", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Pledge", + "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/hr/doctype/loan_type/loan_type.py b/erpnext/loan_management/doctype/pledge/pledge.py similarity index 62% rename from erpnext/hr/doctype/loan_type/loan_type.py rename to erpnext/loan_management/doctype/pledge/pledge.py index 2714e206d8..0457ad7abd 100644 --- a/erpnext/hr/doctype/loan_type/loan_type.py +++ b/erpnext/loan_management/doctype/pledge/pledge.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2019, 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 LoanType(Document): +class Pledge(Document): pass diff --git a/erpnext/loan_management/doctype/pledge/test_pledge.py b/erpnext/loan_management/doctype/pledge/test_pledge.py new file mode 100644 index 0000000000..2e01dc114d --- /dev/null +++ b/erpnext/loan_management/doctype/pledge/test_pledge.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestPledge(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/__init__.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.js b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.js new file mode 100644 index 0000000000..c596be2d2a --- /dev/null +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Process Loan Interest Accrual', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json new file mode 100644 index 0000000000..7f79cb1fd9 --- /dev/null +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json @@ -0,0 +1,81 @@ +{ + "actions": [], + "autoname": "LM-PLA-.#####", + "creation": "2019-09-19 06:08:12.363640", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "posting_date", + "loan_type", + "loan", + "amended_from" + ], + "fields": [ + { + "fieldname": "posting_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Posting Date", + "reqd": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Process Loan Interest Accrual", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "loan_type", + "fieldtype": "Link", + "label": "Loan Type", + "options": "Loan Type" + }, + { + "fieldname": "loan", + "fieldtype": "Link", + "label": "Loan ", + "options": "Loan" + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-02-01 08:14:33.978636", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Process Loan Interest Accrual", + "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": "Loan Manager", + "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/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py new file mode 100644 index 0000000000..0f33da918d --- /dev/null +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import nowdate +from frappe.model.document import Document +from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import make_accrual_interest_entry_for_demand_loans + +class ProcessLoanInterestAccrual(Document): + def on_submit(self): + open_loans = [] + + if self.loan: + loan_doc = frappe.get_doc('Loan', self.loan) + open_loans.append(loan_doc) + + make_accrual_interest_entry_for_demand_loans(self.posting_date, self.name, + open_loans = open_loans, loan_type = self.loan_type) + +def process_loan_interest_accrual(posting_date=None, loan_type=None, loan=None): + loan_process = frappe.new_doc('Process Loan Interest Accrual') + loan_process.posting_date = posting_date or nowdate() + loan_process.loan_type = loan_type + loan_process.loan = loan + + loan_process.submit() + diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py new file mode 100644 index 0000000000..243a7a3ba6 --- /dev/null +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'process_loan_interest_accrual', + 'transactions': [ + { + 'items': ['Loan Interest Accrual'] + } + ] + } \ No newline at end of file diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/test_process_loan_interest_accrual.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/test_process_loan_interest_accrual.py new file mode 100644 index 0000000000..6bfd3f4210 --- /dev/null +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/test_process_loan_interest_accrual.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestProcessLoanInterestAccrual(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/__init__.py b/erpnext/loan_management/doctype/process_loan_security_shortfall/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.js b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.js new file mode 100644 index 0000000000..645e3ada9a --- /dev/null +++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Process Loan Security Shortfall', { + onload: function(frm) { + frm.set_value('update_time', frappe.datetime.now_datetime()); + } +}); diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json new file mode 100644 index 0000000000..ffc3671132 --- /dev/null +++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json @@ -0,0 +1,67 @@ +{ + "actions": [], + "autoname": "LM-PLS-.#####", + "creation": "2019-09-19 06:43:26.742336", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "update_time", + "amended_from" + ], + "fields": [ + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Process Loan Security Shortfall", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "update_time", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Update Time", + "read_only": 1, + "reqd": 1 + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-02-01 08:14:05.845161", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Process Loan Security Shortfall", + "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": "Loan Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py new file mode 100644 index 0000000000..417e3678c9 --- /dev/null +++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import get_datetime +from frappe import _ +from frappe.model.document import Document +from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import check_for_ltv_shortfall + +class ProcessLoanSecurityShortfall(Document): + def onload(self): + self.set_onload('update_time', get_datetime()) + + def on_submit(self): + check_for_ltv_shortfall(process_loan_security_shortfall = self.name) diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py new file mode 100644 index 0000000000..dc9bd81a1d --- /dev/null +++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'process_loan_security_shortfall', + 'transactions': [ + { + 'items': ['Loan Security Shortfall'] + } + ] + } \ No newline at end of file diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/test_process_loan_security_shortfall.py b/erpnext/loan_management/doctype/process_loan_security_shortfall/test_process_loan_security_shortfall.py new file mode 100644 index 0000000000..cd379a1bea --- /dev/null +++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/test_process_loan_security_shortfall.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestProcessLoanSecurityShortfall(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/proposed_pledge/__init__.py b/erpnext/loan_management/doctype/proposed_pledge/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json new file mode 100644 index 0000000000..aee7c2ced5 --- /dev/null +++ b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json @@ -0,0 +1,70 @@ +{ + "creation": "2019-08-29 22:29:37.628178", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan_security", + "qty", + "loan_security_price", + "amount", + "haircut", + "post_haircut_amount" + ], + "fields": [ + { + "fieldname": "loan_security_price", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Loan Security Price", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "Company:company:default_currency" + }, + { + "fetch_from": "loan_security.haircut", + "fieldname": "haircut", + "fieldtype": "Percent", + "label": "Haircut %", + "read_only": 1 + }, + { + "fetch_from": "loan_security_pledge.qty", + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Quantity" + }, + { + "fieldname": "loan_security", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Loan Security", + "options": "Loan Security" + }, + { + "fieldname": "post_haircut_amount", + "fieldtype": "Currency", + "label": "Post Haircut Amount", + "options": "Company:company:default_currency", + "read_only": 1 + } + ], + "istable": 1, + "modified": "2019-12-02 10:23:11.498308", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Proposed Pledge", + "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/loan_management/doctype/proposed_pledge/proposed_pledge.py b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.py new file mode 100644 index 0000000000..dfa5c7965a --- /dev/null +++ b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, 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 ProposedPledge(Document): + pass diff --git a/erpnext/loan_management/doctype/repayment_schedule/__init__.py b/erpnext/loan_management/doctype/repayment_schedule/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/doctype/repayment_schedule/repayment_schedule.json b/erpnext/loan_management/doctype/repayment_schedule/repayment_schedule.json similarity index 89% rename from erpnext/hr/doctype/repayment_schedule/repayment_schedule.json rename to erpnext/loan_management/doctype/repayment_schedule/repayment_schedule.json index 5bb2d370fa..7f71bebeba 100644 --- a/erpnext/hr/doctype/repayment_schedule/repayment_schedule.json +++ b/erpnext/loan_management/doctype/repayment_schedule/repayment_schedule.json @@ -1,5 +1,5 @@ { - "creation": "2016-12-20 15:32:25.078334", + "creation": "2019-09-12 12:57:07.940159", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -9,11 +9,10 @@ "interest_amount", "total_payment", "balance_loan_amount", - "paid" + "is_accrued" ], "fields": [ { - "allow_on_submit": 1, "columns": 2, "fieldname": "payment_date", "fieldtype": "Date", @@ -61,17 +60,17 @@ }, { "default": "0", - "fieldname": "paid", + "fieldname": "is_accrued", "fieldtype": "Check", "in_list_view": 1, - "label": "Paid", + "label": "Is Accrued", "read_only": 1 } ], "istable": 1, - "modified": "2019-10-29 11:45:10.694557", + "modified": "2019-09-12 12:57:07.940159", "modified_by": "Administrator", - "module": "HR", + "module": "Loan Management", "name": "Repayment Schedule", "owner": "Administrator", "permissions": [], diff --git a/erpnext/hr/doctype/repayment_schedule/repayment_schedule.py b/erpnext/loan_management/doctype/repayment_schedule/repayment_schedule.py similarity index 71% rename from erpnext/hr/doctype/repayment_schedule/repayment_schedule.py rename to erpnext/loan_management/doctype/repayment_schedule/repayment_schedule.py index 8abee5e089..2aa27b0968 100644 --- a/erpnext/hr/doctype/repayment_schedule/repayment_schedule.py +++ b/erpnext/loan_management/doctype/repayment_schedule/repayment_schedule.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2019, 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 RepaymentSchedule(Document): diff --git a/erpnext/loan_management/doctype/salary_slip_loan/__init__.py b/erpnext/loan_management/doctype/salary_slip_loan/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json new file mode 100644 index 0000000000..ce020fff07 --- /dev/null +++ b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json @@ -0,0 +1,96 @@ +{ + "creation": "2019-08-29 18:11:36.829526", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan", + "loan_type", + "loan_account", + "interest_income_account", + "column_break_4", + "principal_amount", + "interest_amount", + "total_payment", + "loan_repayment_entry" + ], + "fields": [ + { + "fieldname": "loan", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Loan", + "options": "Loan", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "loan_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Loan Account", + "options": "Account", + "read_only": 1 + }, + { + "fieldname": "interest_income_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Interest Income Account", + "options": "Account", + "read_only": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "principal_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Principal Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "interest_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Interest Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "total_payment", + "fieldtype": "Currency", + "label": "Total Payment", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "loan_repayment_entry", + "fieldtype": "Link", + "label": "Loan Repayment Entry", + "options": "Loan Repayment", + "read_only": 1 + }, + { + "fetch_from": "loan.loan_type", + "fieldname": "loan_type", + "fieldtype": "Link", + "label": "Loan Type", + "options": "Loan Type" + } + ], + "istable": 1, + "modified": "2019-10-28 09:15:31.174244", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Salary Slip Loan", + "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/hr/doctype/salary_slip_loan/salary_slip_loan.py b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.py similarity index 71% rename from erpnext/hr/doctype/salary_slip_loan/salary_slip_loan.py rename to erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.py index 83908ce627..9ee0b96dc1 100644 --- a/erpnext/hr/doctype/salary_slip_loan/salary_slip_loan.py +++ b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2019, 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 SalarySlipLoan(Document): diff --git a/erpnext/loan_management/doctype/sanctioned_loan_amount/__init__.py b/erpnext/loan_management/doctype/sanctioned_loan_amount/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.js b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.js new file mode 100644 index 0000000000..5361e7ca2a --- /dev/null +++ b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Sanctioned Loan Amount', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.json b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.json new file mode 100644 index 0000000000..0447cd911c --- /dev/null +++ b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.json @@ -0,0 +1,88 @@ +{ + "actions": [], + "autoname": "LM-SLA-.####", + "creation": "2019-11-23 10:19:06.179736", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "applicant_type", + "applicant", + "column_break_3", + "company", + "sanctioned_amount_limit" + ], + "fields": [ + { + "fieldname": "applicant_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Applicant Type", + "options": "Employee\nMember\nCustomer", + "reqd": 1 + }, + { + "fieldname": "applicant", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Applicant", + "options": "applicant_type", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "sanctioned_amount_limit", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Sanctioned Amount Limit", + "options": "Company:company:default_currency", + "reqd": 1 + } + ], + "links": [], + "modified": "2020-02-25 05:10:52.421193", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Sanctioned Loan Amount", + "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": "Loan Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py new file mode 100644 index 0000000000..74a131015b --- /dev/null +++ b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, 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 SanctionedLoanAmount(Document): + def validate(self): + sanctioned_doc = frappe.db.exists('Sanctioned Loan Amount', {'applicant': self.applicant, 'company': self.company}) + + if sanctioned_doc and sanctioned_doc != self.name: + frappe.throw(_("Sanctioned Loan Amount already exists for {0} against company {1}").format( + frappe.bold(self.applicant), frappe.bold(self.company) + )) diff --git a/erpnext/loan_management/doctype/sanctioned_loan_amount/test_sanctioned_loan_amount.py b/erpnext/loan_management/doctype/sanctioned_loan_amount/test_sanctioned_loan_amount.py new file mode 100644 index 0000000000..ba1372f175 --- /dev/null +++ b/erpnext/loan_management/doctype/sanctioned_loan_amount/test_sanctioned_loan_amount.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestSanctionedLoanAmount(unittest.TestCase): + pass diff --git a/erpnext/loan_management/doctype/unpledge/__init__.py b/erpnext/loan_management/doctype/unpledge/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/unpledge/unpledge.json b/erpnext/loan_management/doctype/unpledge/unpledge.json new file mode 100644 index 0000000000..9e6277d5f8 --- /dev/null +++ b/erpnext/loan_management/doctype/unpledge/unpledge.json @@ -0,0 +1,84 @@ +{ + "creation": "2019-09-21 13:22:19.793797", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan_security", + "against_pledge", + "loan_security_type", + "loan_security_code", + "haircut", + "uom", + "column_break_5", + "qty" + ], + "fields": [ + { + "fieldname": "loan_security", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Loan Security", + "options": "Loan Security", + "reqd": 1 + }, + { + "fetch_from": "loan_security.loan_security_type", + "fieldname": "loan_security_type", + "fieldtype": "Link", + "label": "Loan Security Type", + "options": "Loan Security Type", + "read_only": 1 + }, + { + "fetch_from": "loan_security.loan_security_code", + "fieldname": "loan_security_code", + "fieldtype": "Data", + "label": "Loan Security Code" + }, + { + "fetch_from": "loan_security.unit_of_measure", + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM" + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Quantity", + "reqd": 1 + }, + { + "fieldname": "against_pledge", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Against Pledge", + "options": "Loan Security Pledge", + "reqd": 1 + }, + { + "fetch_from": "loan_security.haircut", + "fieldname": "haircut", + "fieldtype": "Percent", + "label": "Haircut", + "read_only": 1 + } + ], + "istable": 1, + "modified": "2019-10-02 12:48:18.588236", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Unpledge", + "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/loan_management/doctype/unpledge/unpledge.py b/erpnext/loan_management/doctype/unpledge/unpledge.py new file mode 100644 index 0000000000..205230a308 --- /dev/null +++ b/erpnext/loan_management/doctype/unpledge/unpledge.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, 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 Unpledge(Document): + pass diff --git a/erpnext/hr/loan_common.js b/erpnext/loan_management/loan_common.js similarity index 56% rename from erpnext/hr/loan_common.js rename to erpnext/loan_management/loan_common.js index 3e754fafab..3a47a88cbe 100644 --- a/erpnext/hr/loan_common.js +++ b/erpnext/loan_management/loan_common.js @@ -4,15 +4,28 @@ frappe.ui.form.on(cur_frm.doctype, { refresh: function(frm) { if (!frappe.boot.active_domains.includes("Non Profit")) { - frm.set_df_property('applicant_type', 'options', ['Employee']); + frm.set_df_property('applicant_type', 'options', ['Employee', 'Customer']); frm.refresh_field('applicant_type'); } - }, - applicant_type: function(frm) { - frm.set_value("applicant", null); - frm.set_value("applicant_name", null); + + if (['Loan Disbursement', 'Loan Repayment', 'Loan Interest Accrual'].includes(frm.doc.doctype) + && frm.doc.docstatus == 1) { + + frm.add_custom_button(__("Accounting Ledger"), function() { + frappe.route_options = { + voucher_no: frm.doc.name, + company: frm.doc.company + }; + + frappe.set_route("query-report", "General Ledger"); + },__("View")); + } }, applicant: function(frm) { + if (!["Loan Application", "Loan"].includes(frm.doc.doctype)) { + return; + } + if (frm.doc.applicant) { frappe.model.with_doc(frm.doc.applicant_type, frm.doc.applicant, function() { var applicant = frappe.model.get_doc(frm.doc.applicant_type, frm.doc.applicant); diff --git a/erpnext/loan_management/report/__init__.py b/erpnext/loan_management/report/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/report/loan_repayment_and_closure/__init__.py b/erpnext/loan_management/report/loan_repayment_and_closure/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.js b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.js new file mode 100644 index 0000000000..ed5e937c99 --- /dev/null +++ b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.js @@ -0,0 +1,41 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Loan Repayment and Closure"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_user_default("Company") + }, + { + "fieldname":"applicant_type", + "label": __("Applicant Type"), + "fieldtype": "Select", + "options": ["Customer", "Employee"], + "reqd": 1, + "default": "Customer", + on_change: function() { + frappe.query_report.set_filter_value('applicant', ""); + } + }, + { + "fieldname": "applicant", + "label": __("Applicant"), + "fieldtype": "Dynamic Link", + "get_options": function() { + var applicant_type = frappe.query_report.get_filter_value('applicant_type'); + var applicant = frappe.query_report.get_filter_value('applicant'); + if(applicant && !applicant_type) { + frappe.throw(__("Please select Applicant Type first")); + } + return applicant_type; + } + + }, + ] +}; diff --git a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.json b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.json new file mode 100644 index 0000000000..52d5b2c71a --- /dev/null +++ b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.json @@ -0,0 +1,27 @@ +{ + "add_total_row": 0, + "creation": "2019-09-03 16:54:55.616593", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-02-25 07:16:47.696994", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Repayment and Closure", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Loan Repayment", + "report_name": "Loan Repayment and Closure", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Loan Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py new file mode 100644 index 0000000000..b63cc8ed5a --- /dev/null +++ b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py @@ -0,0 +1,129 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +def execute(filters=None): + columns = get_columns() + data = get_data(filters) + return columns, data + +def get_columns(): + return [ + { + "label": _("Posting Date"), + "fieldtype": "Date", + "fieldname": "posting_date", + "width": 100 + }, + { + "label": _("Loan Repayment"), + "fieldtype": "Link", + "fieldname": "loan_repayment", + "options": "Loan Repayment", + "width": 100 + }, + { + "label": _("Against Loan"), + "fieldtype": "Link", + "fieldname": "against_loan", + "options": "Loan", + "width": 200 + }, + { + "label": _("Applicant"), + "fieldtype": "Data", + "fieldname": "applicant", + "width": 150 + }, + { + "label": _("Payment Type"), + "fieldtype": "Data", + "fieldname": "payment_type", + "width": 150 + }, + { + "label": _("Principal Amount"), + "fieldtype": "Currency", + "fieldname": "principal_amount", + "options": "currency", + "width": 100 + }, + { + "label": _("Interest Amount"), + "fieldtype": "Currency", + "fieldname": "interest", + "options": "currency", + "width": 100 + }, + { + "label": _("Penalty Amount"), + "fieldtype": "Currency", + "fieldname": "penalty", + "options": "currency", + "width": 100 + }, + { + "label": _("Payable Amount"), + "fieldtype": "Currency", + "fieldname": "payable_amount", + "options": "currency", + "width": 100 + }, + { + "label": _("Paid Amount"), + "fieldtype": "Currency", + "fieldname": "paid_amount", + "options": "currency", + "width": 100 + }, + { + "label": _("Currency"), + "fieldtype": "Link", + "fieldname": "currency", + "options": "Currency", + "width": 100 + } + ] + +def get_data(filters): + data = [] + + query_filters = { + "docstatus": 1, + "company": filters.get('company'), + } + + if filters.get('applicant'): + query_filters.update({ + "applicant": filters.get('applicant') + }) + + loan_repayments = frappe.get_all("Loan Repayment", + filters = query_filters, + fields=["posting_date", "applicant", "name", "against_loan", "payment_type", "payable_amount", + "pending_principal_amount", "interest_payable", "penalty_amount", "amount_paid"] + ) + + default_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") + + for repayment in loan_repayments: + row = { + "posting_date": repayment.posting_date, + "loan_repayment": repayment.name, + "applicant": repayment.applicant, + "payment_type": repayment.payment_type, + "against_loan": repayment.against_loan, + "principal_amount": repayment.pending_principal_amount, + "interest": repayment.interest_payable, + "penalty": repayment.penalty_amount, + "payable_amount": repayment.payable_amount, + "paid_amount": repayment.amount_paid, + "currency": default_currency + } + + data.append(row) + + return data \ No newline at end of file diff --git a/erpnext/loan_management/report/loan_security_status/__init__.py b/erpnext/loan_management/report/loan_security_status/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/report/loan_security_status/loan_security_status.js b/erpnext/loan_management/report/loan_security_status/loan_security_status.js new file mode 100644 index 0000000000..6e6191c7e4 --- /dev/null +++ b/erpnext/loan_management/report/loan_security_status/loan_security_status.js @@ -0,0 +1,46 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Loan Security Status"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_user_default("Company") + }, + { + "fieldname":"applicant_type", + "label": __("Applicant Type"), + "fieldtype": "Select", + "options": ["Customer", "Employee"], + "reqd": 1, + "default": "Customer", + on_change: function() { + frappe.query_report.set_filter_value('applicant', ""); + } + }, + { + "fieldname": "applicant", + "label": __("Applicant"), + "fieldtype": "Dynamic Link", + "get_options": function() { + var applicant_type = frappe.query_report.get_filter_value('applicant_type'); + var applicant = frappe.query_report.get_filter_value('applicant'); + if(applicant && !applicant_type) { + frappe.throw(__("Please select Applicant Type first")); + } + return applicant_type; + } + }, + { + "fieldname":"pledge_status", + "label": __("Pledge Status"), + "fieldtype": "Select", + "options": ["", "Requested", "Pledged", "Partially Pledged", "Unpledged"], + }, + ] +}; diff --git a/erpnext/loan_management/report/loan_security_status/loan_security_status.json b/erpnext/loan_management/report/loan_security_status/loan_security_status.json new file mode 100644 index 0000000000..9eb948dec8 --- /dev/null +++ b/erpnext/loan_management/report/loan_security_status/loan_security_status.json @@ -0,0 +1,27 @@ +{ + "add_total_row": 1, + "creation": "2019-10-07 05:57:33.435705", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2019-10-07 13:45:46.793949", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Security Status", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Loan Security", + "report_name": "Loan Security Status", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Loan Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/loan_management/report/loan_security_status/loan_security_status.py b/erpnext/loan_management/report/loan_security_status/loan_security_status.py new file mode 100644 index 0000000000..ea6a2ee645 --- /dev/null +++ b/erpnext/loan_management/report/loan_security_status/loan_security_status.py @@ -0,0 +1,135 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +def execute(filters=None): + columns = get_columns(filters) + data = get_data(filters) + return columns, data + +def get_columns(filters): + columns= [ + { + "label": _("Loan Security Pledge"), + "fieldtype": "Link", + "fieldname": "loan_security_pledge", + "options": "Loan Security Pledge", + "width": 200 + }, + { + "label": _("Loan"), + "fieldtype": "Link", + "fieldname": "loan", + "options": "Loan", + "width": 200 + }, + { + "label": _("Applicant"), + "fieldtype": "Data", + "fieldname": "applicant", + "width": 200 + }, + { + "label": _("Status"), + "fieldtype": "Data", + "fieldname": "status", + "width": 100 + }, + { + "label": _("Pledge Time"), + "fieldtype": "Data", + "fieldname": "pledge_time", + "width": 150 + }, + { + "label": _("Loan Security"), + "fieldtype": "Link", + "fieldname": "loan_security", + "options": "Loan Security", + "width": 150 + }, + { + "label": _("Quantity"), + "fieldtype": "Float", + "fieldname": "qty", + "width": 100 + }, + { + "label": _("Loan Security Price"), + "fieldtype": "Currency", + "fieldname": "loan_security_price", + "options": "currency", + "width": 200 + }, + { + "label": _("Loan Security Value"), + "fieldtype": "Currency", + "fieldname": "loan_security_value", + "options": "currency", + "width": 200 + }, + { + "label": _("Currency"), + "fieldtype": "Link", + "fieldname": "currency", + "options": "Currency", + "width": 50 + } + ] + + return columns + +def get_data(filters): + + loan_security_price_map = frappe._dict(frappe.get_all("Loan Security", + fields=["name", "loan_security_price"], as_list=1 + )) + + data = [] + conditions = get_conditions(filters) + + loan_security_pledges = frappe.db.sql(""" + SELECT + p.name, p.applicant, p.loan, p.status, p.pledge_time, + c.loan_security, c.qty + FROM + `tabLoan Security Pledge` p, `tabPledge` c + WHERE + p.docstatus = 1 + AND c.parent = p.name + AND p.company = %(company)s + {conditions} + """.format(conditions = conditions), (filters), as_dict=1) #nosec + + default_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") + + for pledge in loan_security_pledges: + row = {} + row["loan_security_pledge"] = pledge.name + row["loan"] = pledge.loan + row["applicant"] = pledge.applicant + row["status"] = pledge.status + row["pledge_time"] = pledge.pledge_time + row["loan_security"] = pledge.loan_security + row["qty"] = pledge.qty + row["loan_security_price"] = loan_security_price_map.get(pledge.loan_security) + row["loan_security_value"] = row["loan_security_price"] * pledge.qty + row["currency"] = default_currency + + data.append(row) + + return data + +def get_conditions(filters): + conditions = [] + + if filters.get("applicant"): + conditions.append("p.applicant = %(applicant)s") + + if filters.get("pledge_status"): + conditions.append(" p.status = %(pledge_status)s") + + return "AND {}".format(" AND ".join(conditions)) if conditions else "" diff --git a/erpnext/modules.txt b/erpnext/modules.txt index 316d6de20e..3b347582c3 100644 --- a/erpnext/modules.txt +++ b/erpnext/modules.txt @@ -23,4 +23,5 @@ Non Profit Hotels Hub Node Quality Management -Communication \ No newline at end of file +Communication +Loan Management \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 42d537a73c..b0fc7ea59c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -510,7 +510,6 @@ erpnext.patches.v10_0.repost_requested_qty_for_non_stock_uom_items erpnext.patches.v11_0.merge_land_unit_with_location erpnext.patches.v11_0.add_index_on_nestedset_doctypes erpnext.patches.v11_0.remove_modules_setup_page -erpnext.patches.v11_0.rename_employee_loan_to_loan erpnext.patches.v11_0.move_leave_approvers_from_employee #13-06-2018 erpnext.patches.v11_0.update_department_lft_rgt erpnext.patches.v11_0.add_default_email_template_for_leave @@ -634,6 +633,7 @@ execute:frappe.reload_doc('desk', 'doctype','dashboard_chart') erpnext.patches.v12_0.add_default_dashboards erpnext.patches.v12_0.remove_bank_remittance_custom_fields erpnext.patches.v12_0.generate_leave_ledger_entries +execute:frappe.delete_doc_if_exists("Report", "Loan Repayment") erpnext.patches.v12_0.move_credit_limit_to_customer_credit_limit erpnext.patches.v12_0.add_variant_of_in_item_attribute_table erpnext.patches.v12_0.rename_bank_account_field_in_journal_entry_account diff --git a/erpnext/patches/v11_0/rename_employee_loan_to_loan.py b/erpnext/patches/v11_0/rename_employee_loan_to_loan.py deleted file mode 100644 index b2ff6b8c5d..0000000000 --- a/erpnext/patches/v11_0/rename_employee_loan_to_loan.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import unicode_literals -import frappe -from frappe.model.utils.rename_field import rename_field - -def execute(): - if frappe.db.table_exists("Employee Loan Application") and not frappe.db.table_exists("Loan Application"): - frappe.rename_doc("DocType", "Employee Loan Application", "Loan Application", force=True) - - if frappe.db.table_exists("Employee Loan") and not frappe.db.table_exists("Loan"): - frappe.rename_doc("DocType", "Employee Loan", "Loan", force=True) - - frappe.reload_doc("hr", "doctype", "loan_application") - frappe.reload_doc("hr", "doctype", "loan") - frappe.reload_doc("hr", "doctype", "salary_slip_loan") - - for doctype in ['Loan', 'Salary Slip Loan']: - if frappe.db.has_column(doctype, 'employee_loan_account'): - rename_field(doctype, "employee_loan_account", "loan_account") - - columns = {'employee': 'applicant', 'employee_name': 'applicant_name'} - for doctype in ['Loan Application', 'Loan']: - frappe.db.sql(""" update `tab{doctype}` set applicant_type = 'Employee' """ - .format(doctype=doctype)) - for column, new_column in columns.items(): - if frappe.db.has_column(doctype, column): - rename_field(doctype, column, new_column) - - frappe.delete_doc('DocType', doctype) \ No newline at end of file From 1e6788b028a5c318ea759f29154ce69727e1a437 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 26 Feb 2020 11:25:34 +0530 Subject: [PATCH 093/102] fix: reorder material request not created if doctype has custom mandatory field (#20719) --- erpnext/stock/reorder_item.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index 39fb024023..97776739a8 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -116,6 +116,8 @@ def create_material_request(material_requests): else: exceptions_list.append(frappe.get_traceback()) + frappe.log_error(frappe.get_traceback()) + for request_type in material_requests: for company in material_requests[request_type]: try: @@ -158,6 +160,7 @@ def create_material_request(material_requests): schedule_dates = [d.schedule_date for d in mr.items] mr.schedule_date = max(schedule_dates or [nowdate()]) + mr.flags.ignore_mandatory = True mr.insert() mr.submit() mr_list.append(mr) From 99d89d6cb8960407b6341215ad61d5892af3b829 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Wed, 26 Feb 2020 11:26:07 +0530 Subject: [PATCH 094/102] fix(HR): show correct closing leave balance (#20723) --- .../hr/report/employee_leave_balance/employee_leave_balance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e967db87c4..35c8630e8e 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -76,7 +76,7 @@ def get_data(filters, leave_types): opening = get_leave_balance_on(employee.name, leave_type, filters.from_date) # closing balance - closing = get_leave_balance_on(employee.name, leave_type, filters.to_date) + closing = max(opening - leaves_taken, 0) row += [opening, leaves_taken, closing] From 7229f694ea80bc7692d47a9be7147160cb908bda Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 26 Feb 2020 11:28:08 +0530 Subject: [PATCH 095/102] fix: rate and amount in material request copying from sales order (#20717) --- .../material_request_item/material_request_item.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json index 795971b5e3..9d1dafb136 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.json +++ b/erpnext/stock/doctype/material_request_item/material_request_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-02-22 01:28:02", "doctype": "DocType", @@ -185,12 +186,14 @@ { "fieldname": "rate", "fieldtype": "Currency", - "label": "Rate" + "label": "Rate", + "no_copy": 1 }, { "fieldname": "amount", "fieldtype": "Currency", "label": "Amount", + "no_copy": 1, "read_only": 1 }, { @@ -407,7 +410,8 @@ ], "idx": 1, "istable": 1, - "modified": "2019-06-02 06:49:36.493957", + "links": [], + "modified": "2020-02-25 03:09:10.698967", "modified_by": "Administrator", "module": "Stock", "name": "Material Request Item", From 3957ac1408b723fd7ff4877ad20ee60ee6a7dc47 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 26 Feb 2020 11:36:55 +0530 Subject: [PATCH 096/102] perf: search for customer's return invoices then filter out gl_entries (#20709) Co-authored-by: Nabin Hait --- .../payment_reconciliation.py | 18 ++++++++++++------ .../doctype/sales_invoice/sales_invoice.py | 3 +++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 2c04a27b0c..3080496186 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -92,6 +92,7 @@ class PaymentReconciliation(Document): FROM `tab{doc}`, `tabGL Entry` WHERE (`tab{doc}`.name = `tabGL Entry`.against_voucher or `tab{doc}`.name = `tabGL Entry`.voucher_no) + and `tab{doc}`.{party_type_field} = %(party)s and `tab{doc}`.is_return = 1 and `tab{doc}`.return_against IS NULL and `tabGL Entry`.against_voucher_type = %(voucher_type)s and `tab{doc}`.docstatus = 1 and `tabGL Entry`.party = %(party)s @@ -99,12 +100,17 @@ class PaymentReconciliation(Document): GROUP BY `tab{doc}`.name Having amount > 0 - """.format(doc=voucher_type, dr_or_cr=dr_or_cr, reconciled_dr_or_cr=reconciled_dr_or_cr), { - 'party': self.party, - 'party_type': self.party_type, - 'voucher_type': voucher_type, - 'account': self.receivable_payable_account - }, as_dict=1) + """.format( + doc=voucher_type, + dr_or_cr=dr_or_cr, + reconciled_dr_or_cr=reconciled_dr_or_cr, + party_type_field=frappe.scrub(self.party_type)), + { + 'party': self.party, + 'party_type': self.party_type, + 'voucher_type': voucher_type, + 'account': self.receivable_payable_account + }, as_dict=1) def add_payment_entries(self, entries): self.set('payments', []) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index f5dd6e78d5..658e703b4e 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1548,6 +1548,9 @@ def get_loyalty_programs(customer): else: return lp_details +def on_doctype_update(): + frappe.db.add_index("Sales Invoice", ["customer", "is_return", "return_against"]) + @frappe.whitelist() def create_invoice_discounting(source_name, target_doc=None): invoice = frappe.get_doc("Sales Invoice", source_name) From 10aab6533809b4ca3b89abe130f8f3aa3181fb2c Mon Sep 17 00:00:00 2001 From: RJPvT <48353029+RJPvT@users.noreply.github.com> Date: Wed, 26 Feb 2020 07:29:10 +0100 Subject: [PATCH 097/102] fix: smaller then instead of bigger then :-( dumb mistake vdevelop #20693 (#20702) --- erpnext/projects/doctype/task/task.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index d56bb23816..1cb2c50cbf 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -212,10 +212,10 @@ def set_multiple_status(names, status): task.save() def set_tasks_as_overdue(): - tasks = frappe.get_all("Task", filters={'status':['not in',['Cancelled', 'Closed']]}) + tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"]) for task in tasks: - if frappe.db.get_value("Task", task.name, "status") in 'Pending Review': - if getdate(frappe.db.get_value("Task", task.name, "review_date")) < getdate(today()): + if task.status == "Pending Review": + if getdate(task.review_date) > getdate(today()): continue frappe.get_doc("Task", task.name).update_status() From 08fa7590ef6a543f13c283353486e2693790e0f5 Mon Sep 17 00:00:00 2001 From: sharmatripti <61048272+sharmatripti@users.noreply.github.com> Date: Wed, 26 Feb 2020 12:02:37 +0530 Subject: [PATCH 098/102] fix: Employee field should be filtered based on department on doctype Instructor (#20705) * Fix: Filtered Employee based on Department on Instructor doctype (#20598) * Fix: Syntax error (#20598) * Fix: Syntax error (#20598) * Fix: Syntax error (#20598) * Fix: Syntax error (#20598) * fix : Filtered Employee based on Department on Instructor doctype (#20704) --- erpnext/education/doctype/instructor/instructor.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/education/doctype/instructor/instructor.js b/erpnext/education/doctype/instructor/instructor.js index 71e044bb70..69bd2cf008 100644 --- a/erpnext/education/doctype/instructor/instructor.js +++ b/erpnext/education/doctype/instructor/instructor.js @@ -12,7 +12,6 @@ frappe.ui.form.on("Instructor", { } }; }); - frm.set_query("department", "instructor_log", function() { return { "filters": { @@ -49,5 +48,12 @@ frappe.ui.form.on("Instructor", { frappe.set_route("List", "Assessment Plan"); }, __("Assessment Plan")); } + frm.set_query("employee", function(doc) { + return { + "filters": { + "department": doc.department, + } + }; + }); } -}); +}); \ No newline at end of file From 12bc43e49569f42fe1e0d502df5df86d5f662e4b Mon Sep 17 00:00:00 2001 From: abhijitkumbharInd <40858925+abhijitkumbharInd@users.noreply.github.com> Date: Wed, 26 Feb 2020 12:04:41 +0530 Subject: [PATCH 099/102] fix: Education | Quiz accepting duplicate question #20622 (#20708) --- erpnext/education/doctype/quiz/quiz.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/erpnext/education/doctype/quiz/quiz.js b/erpnext/education/doctype/quiz/quiz.js index 122cf37818..7b870886ec 100644 --- a/erpnext/education/doctype/quiz/quiz.js +++ b/erpnext/education/doctype/quiz/quiz.js @@ -4,5 +4,18 @@ frappe.ui.form.on('Quiz', { refresh: function(frm) { + }, + validate: function(frm){ + frm.events.check_duplicate_question(frm.doc.question); + }, + check_duplicate_question: function(questions_data){ + var questions = []; + questions_data.forEach(function(q){ + questions.push(q.question_link); + }); + var questions_set = new Set(questions); + if (questions.length != questions_set.size) { + frappe.throw(__("The question cannot be duplicate")); + } } -}); +}); \ No newline at end of file From ba8840727a8f7ab1481e1ce7bdc829a1a70fbcb9 Mon Sep 17 00:00:00 2001 From: abhijitkumbharInd <40858925+abhijitkumbharInd@users.noreply.github.com> Date: Wed, 26 Feb 2020 12:48:36 +0530 Subject: [PATCH 100/102] Create a new item on Material Request showing error Item None not found. #20608 (#20731) --- erpnext/stock/doctype/material_request/material_request.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 7ef2d6460f..6b26d38377 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -145,6 +145,7 @@ frappe.ui.form.on('Material Request', { }, get_item_data: function(frm, item) { + if (!item.item_code) return; frm.call({ method: "erpnext.stock.get_item_details.get_item_details", child: item, From aa3ba41e831852fbbbf8eb7622fa37cf26c3694c Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 26 Feb 2020 12:51:46 +0530 Subject: [PATCH 101/102] fix: same free item not working for pricing rule (#20712) --- .../doctype/pricing_rule/pricing_rule.py | 2 +- .../doctype/pricing_rule/test_pricing_rule.py | 60 +++++++++++++++++++ .../accounts/doctype/pricing_rule/utils.py | 9 +-- 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 924e108130..e13fcb96df 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -248,7 +248,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa if pricing_rule.price_or_product_discount == "Price": apply_price_discount_rule(pricing_rule, item_details, args) else: - get_product_discount_rule(pricing_rule, item_details, doc) + get_product_discount_rule(pricing_rule, item_details, args, doc) item_details.has_pricing_rule = 1 diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 9c1fef69fa..2da71dfd0e 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -326,6 +326,66 @@ class TestPricingRule(unittest.TestCase): self.assertEquals(item.discount_amount, 110) self.assertEquals(item.rate, 990) + def test_pricing_rule_for_product_discount_on_same_item(self): + frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule') + test_record = { + "doctype": "Pricing Rule", + "title": "_Test Pricing Rule", + "apply_on": "Item Code", + "currency": "USD", + "items": [{ + "item_code": "_Test Item", + }], + "selling": 1, + "rate_or_discount": "Discount Percentage", + "rate": 0, + "min_qty": 0, + "max_qty": 7, + "discount_percentage": 17.5, + "price_or_product_discount": "Product", + "same_item": 1, + "free_qty": 1, + "company": "_Test Company" + } + frappe.get_doc(test_record.copy()).insert() + + # With pricing rule + so = make_sales_order(item_code="_Test Item", qty=1) + so.load_from_db() + self.assertEqual(so.items[1].is_free_item, 1) + self.assertEqual(so.items[1].item_code, "_Test Item") + + + def test_pricing_rule_for_product_discount_on_different_item(self): + frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule') + test_record = { + "doctype": "Pricing Rule", + "title": "_Test Pricing Rule", + "apply_on": "Item Code", + "currency": "USD", + "items": [{ + "item_code": "_Test Item", + }], + "selling": 1, + "rate_or_discount": "Discount Percentage", + "rate": 0, + "min_qty": 0, + "max_qty": 7, + "discount_percentage": 17.5, + "price_or_product_discount": "Product", + "same_item": 0, + "free_item": "_Test Item 2", + "free_qty": 1, + "company": "_Test Company" + } + frappe.get_doc(test_record.copy()).insert() + + # With pricing rule + so = make_sales_order(item_code="_Test Item", qty=1) + so.load_from_db() + self.assertEqual(so.items[1].is_free_item, 1) + self.assertEqual(so.items[1].item_code, "_Test Item 2") + def make_pricing_rule(**args): args = frappe._dict(args) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index e475563c77..a2bb2ee927 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -435,7 +435,7 @@ def apply_pricing_rule_on_transaction(doc): doc.calculate_taxes_and_totals() elif d.price_or_product_discount == 'Product': item_details = frappe._dict({'parenttype': doc.doctype}) - get_product_discount_rule(d, item_details, doc) + get_product_discount_rule(d, item_details, doc=doc) apply_pricing_rule_for_free_items(doc, item_details.free_item_data) doc.set_missing_values() @@ -443,9 +443,10 @@ def get_applied_pricing_rules(item_row): return (item_row.get("pricing_rules").split(',') if item_row.get("pricing_rules") else []) -def get_product_discount_rule(pricing_rule, item_details, doc=None): - free_item = (pricing_rule.free_item - if not pricing_rule.same_item or pricing_rule.apply_on == 'Transaction' else item_details.item_code) +def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): + free_item = pricing_rule.free_item + if pricing_rule.same_item: + free_item = item_details.item_code or args.item_code if not free_item: frappe.throw(_("Free item not set in the pricing rule {0}") From b5c5bdd6718c84e04b92f1d8a80968848c00df6b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 26 Feb 2020 12:57:05 +0530 Subject: [PATCH 102/102] fix: API fix for loan security pledge creation --- .../doctype/loan_application/loan_application.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py index 691962bf14..d3b816464f 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/loan_application.py @@ -147,7 +147,7 @@ def create_loan(source_name, target_doc=None, submit=0): return doclist @frappe.whitelist() -def create_pledge(loan_application): +def create_pledge(loan_application, loan=None): loan_application_doc = frappe.get_doc("Loan Application", loan_application) lsp = frappe.new_doc("Loan Security Pledge") @@ -156,6 +156,9 @@ def create_pledge(loan_application): lsp.loan_application = loan_application_doc.name lsp.company = loan_application_doc.company + if loan: + lsp.loan = loan + for pledge in loan_application_doc.proposed_pledges: lsp.append('securities', {