Merge branch 'develop' of https://github.com/frappe/erpnext into accounting_dimension_filters

This commit is contained in:
Deepesh Garg
2020-11-25 14:51:11 +05:30
244 changed files with 4883 additions and 2442 deletions

View File

@@ -5,7 +5,7 @@
"es6": true "es6": true
}, },
"parserOptions": { "parserOptions": {
"ecmaVersion": 6, "ecmaVersion": 9,
"sourceType": "module" "sourceType": "module"
}, },
"extends": "eslint:recommended", "extends": "eslint:recommended",
@@ -15,6 +15,14 @@
"tab", "tab",
{ "SwitchCase": 1 } { "SwitchCase": 1 }
], ],
"brace-style": [
"error",
"1tbs"
],
"space-unary-ops": [
"error",
{ "words": true }
],
"linebreak-style": [ "linebreak-style": [
"error", "error",
"unix" "unix"
@@ -44,12 +52,10 @@
"no-control-regex": [ "no-control-regex": [
"off" "off"
], ],
"spaced-comment": [ "space-before-blocks": "warn",
"warn" "keyword-spacing": "warn",
], "comma-spacing": "warn",
"no-trailing-spaces": [ "key-spacing": "warn"
"warn"
]
}, },
"root": true, "root": true,
"globals": { "globals": {

View File

@@ -5,7 +5,7 @@
<p>ERP made simple</p> <p>ERP made simple</p>
</p> </p>
[![Build Status](https://travis-ci.com/frappe/erpnext.svg)](https://travis-ci.com/frappe/erpnext) [![Build Status](https://api.travis-ci.com/frappe/erpnext.svg?branch=develop)](https://travis-ci.com/frappe/erpnext)
[![Open Source Helpers](https://www.codetriage.com/frappe/erpnext/badges/users.svg)](https://www.codetriage.com/frappe/erpnext) [![Open Source Helpers](https://www.codetriage.com/frappe/erpnext/badges/users.svg)](https://www.codetriage.com/frappe/erpnext)
[![Coverage Status](https://coveralls.io/repos/github/frappe/erpnext/badge.svg?branch=develop)](https://coveralls.io/github/frappe/erpnext?branch=develop) [![Coverage Status](https://coveralls.io/repos/github/frappe/erpnext/badge.svg?branch=develop)](https://coveralls.io/github/frappe/erpnext?branch=develop)

View File

@@ -23,7 +23,7 @@
{ {
"hidden": 0, "hidden": 0,
"label": "Reports", "label": "Reports",
"links": "[\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Trial Balance for Party\",\n \"name\": \"Trial Balance for Party\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Journal Entry\"\n ],\n \"doctype\": \"Journal Entry\",\n \"is_query_report\": true,\n \"label\": \"Payment Period Based On Invoice Date\",\n \"name\": \"Payment Period Based On Invoice Date\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Partners Commission\",\n \"name\": \"Sales Partners Commission\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"is_query_report\": true,\n \"label\": \"Customer Credit Balance\",\n \"name\": \"Customer Credit Balance\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Payment Summary\",\n \"name\": \"Sales Payment Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Address\"\n ],\n \"doctype\": \"Address\",\n \"is_query_report\": true,\n \"label\": \"Address And Contacts\",\n \"name\": \"Address And Contacts\",\n \"type\": \"report\"\n }\n]" "links": "[\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Trial Balance for Party\",\n \"name\": \"Trial Balance for Party\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Journal Entry\"\n ],\n \"doctype\": \"Journal Entry\",\n \"is_query_report\": true,\n \"label\": \"Payment Period Based On Invoice Date\",\n \"name\": \"Payment Period Based On Invoice Date\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Partners Commission\",\n \"name\": \"Sales Partners Commission\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"is_query_report\": true,\n \"label\": \"Customer Credit Balance\",\n \"name\": \"Customer Credit Balance\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Payment Summary\",\n \"name\": \"Sales Payment Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Address\"\n ],\n \"doctype\": \"Address\",\n \"is_query_report\": true,\n \"label\": \"Address And Contacts\",\n \"name\": \"Address And Contacts\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"DATEV Export\",\n \"name\": \"DATEV\",\n \"type\": \"report\"\n }\n]"
}, },
{ {
"hidden": 0, "hidden": 0,
@@ -43,7 +43,7 @@
{ {
"hidden": 0, "hidden": 0,
"label": "Bank Statement", "label": "Bank Statement",
"links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]" "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Clearance\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Bank Reconciliation Statement\",\n \"name\": \"Bank Reconciliation Statement\",\n \"type\": \"report\"\n }\n]"
}, },
{ {
"hidden": 0, "hidden": 0,
@@ -79,6 +79,11 @@
"hidden": 0, "hidden": 0,
"label": "Profitability", "label": "Profitability",
"links": "[\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Gross Profit\",\n \"name\": \"Gross Profit\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Profitability Analysis\",\n \"name\": \"Profitability Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Invoice Trends\",\n \"name\": \"Sales Invoice Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Purchase Invoice Trends\",\n \"name\": \"Purchase Invoice Trends\",\n \"type\": \"report\"\n }\n]" "links": "[\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Gross Profit\",\n \"name\": \"Gross Profit\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Profitability Analysis\",\n \"name\": \"Profitability Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Invoice Trends\",\n \"name\": \"Sales Invoice Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Purchase Invoice Trends\",\n \"name\": \"Purchase Invoice Trends\",\n \"type\": \"report\"\n }\n]"
},
{
"hidden": 0,
"label": "Value-Added Tax (VAT UAE)",
"links": "[\n {\n \"country\": \"United Arab Emirates\",\n \"label\": \"UAE VAT Settings\",\n \"name\": \"UAE VAT Settings\",\n \"type\": \"doctype\"\n },\n {\n \"country\": \"United Arab Emirates\",\n \"is_query_report\": true,\n \"label\": \"UAE VAT 201\",\n \"name\": \"UAE VAT 201\",\n \"type\": \"report\"\n }\n\n]"
} }
], ],
"category": "Modules", "category": "Modules",
@@ -98,7 +103,7 @@
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"label": "Accounting", "label": "Accounting",
"modified": "2020-10-08 20:31:46.022470", "modified": "2020-11-11 18:35:11.542909",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting", "name": "Accounting",
@@ -108,7 +113,7 @@
"pin_to_top": 0, "pin_to_top": 0,
"shortcuts": [ "shortcuts": [
{ {
"label": "Chart of Accounts", "label": "Chart Of Accounts",
"link_to": "Account", "link_to": "Account",
"type": "DocType" "type": "DocType"
}, },

View File

@@ -94,8 +94,7 @@ frappe.ui.form.on('Chart of Accounts Importer', {
callback: function(r) { callback: function(r) {
if(r.message===false) { if(r.message===false) {
frm.set_value("company", ""); frm.set_value("company", "");
frappe.throw(__(`Transactions against the company already exist! frappe.throw(__("Transactions against the Company already exist! Chart of Accounts can only be imported for a Company with no transactions."));
Chart Of accounts can be imported for company with no transactions`));
} else { } else {
frm.trigger("refresh"); frm.trigger("refresh");
} }

View File

@@ -9,11 +9,7 @@ frappe.ui.form.on('Fiscal Year', {
} }
}, },
refresh: function (frm) { refresh: function (frm) {
let doc = frm.doc; if (!frm.doc.__islocal && (frm.doc.name != frappe.sys_defaults.fiscal_year)) {
frm.toggle_enable('year_start_date', doc.__islocal);
frm.toggle_enable('year_end_date', doc.__islocal);
if (!doc.__islocal && (doc.name != frappe.sys_defaults.fiscal_year)) {
frm.add_custom_button(__("Set as Default"), () => frm.events.set_as_default(frm)); frm.add_custom_button(__("Set as Default"), () => frm.events.set_as_default(frm));
frm.set_intro(__("To set this Fiscal Year as Default, click on 'Set as Default'")); frm.set_intro(__("To set this Fiscal Year as Default, click on 'Set as Default'"));
} else { } else {
@@ -24,8 +20,10 @@ frappe.ui.form.on('Fiscal Year', {
return frm.call('set_as_default'); return frm.call('set_as_default');
}, },
year_start_date: function(frm) { year_start_date: function(frm) {
let year_end_date = if (!frm.doc.is_short_year) {
frappe.datetime.add_days(frappe.datetime.add_months(frm.doc.year_start_date, 12), -1); let year_end_date =
frm.set_value("year_end_date", year_end_date); frappe.datetime.add_days(frappe.datetime.add_months(frm.doc.year_start_date, 12), -1);
frm.set_value("year_end_date", year_end_date);
}
}, },
}); });

View File

@@ -1,347 +1,126 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0,
"allow_import": 1, "allow_import": 1,
"allow_rename": 0,
"autoname": "field:year", "autoname": "field:year",
"beta": 0,
"creation": "2013-01-22 16:50:25", "creation": "2013-01-22 16:50:25",
"custom": 0,
"description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.", "description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.",
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"editable_grid": 0, "engine": "InnoDB",
"field_order": [
"year",
"disabled",
"is_short_year",
"year_start_date",
"year_end_date",
"companies",
"auto_created"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "For e.g. 2012, 2012-13", "description": "For e.g. 2012, 2012-13",
"fieldname": "year", "fieldname": "year",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Year Name", "label": "Year Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "year", "oldfieldname": "year",
"oldfieldtype": "Data", "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, "reqd": 1,
"search_index": 0, "unique": 1
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "disabled", "fieldname": "disabled",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Disabled"
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "year_start_date", "fieldname": "year_start_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Year Start Date", "label": "Year Start Date",
"length": 0,
"no_copy": 1, "no_copy": 1,
"oldfieldname": "year_start_date", "oldfieldname": "year_start_date",
"oldfieldtype": "Date", "oldfieldtype": "Date",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0, "set_only_once": 1
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "year_end_date", "fieldname": "year_end_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Year End Date", "label": "Year End Date",
"length": 0,
"no_copy": 1, "no_copy": 1,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0, "set_only_once": 1
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "companies", "fieldname": "companies",
"fieldtype": "Table", "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": "Companies", "label": "Companies",
"length": 0, "options": "Fiscal Year Company"
"no_copy": 0,
"options": "Fiscal Year 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": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0", "default": "0",
"fieldname": "auto_created", "fieldname": "auto_created",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1, "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": "Auto Created", "label": "Auto Created",
"length": 0,
"no_copy": 1, "no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0, "read_only": 1
"read_only": 1, },
"remember_last_selected_value": 0, {
"report_hide": 0, "default": "0",
"reqd": 0, "description": "Less than 12 months.",
"search_index": 0, "fieldname": "is_short_year",
"set_only_once": 0, "fieldtype": "Check",
"translatable": 0, "label": "Is Short Year",
"unique": 0 "set_only_once": 1
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-calendar", "icon": "fa fa-calendar",
"idx": 1, "idx": 1,
"image_view": 0, "links": [],
"in_create": 0, "modified": "2020-11-05 12:16:53.081573",
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-04-25 14:21:41.273354",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Fiscal Year", "name": "Fiscal Year",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "System Manager", "role": "System Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 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, "read": 1,
"report": 0, "role": "Sales User"
"role": "Sales User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1, "read": 1,
"report": 0, "role": "Purchase User"
"role": "Purchase User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1, "read": 1,
"report": 0, "role": "Accounts User"
"role": "Accounts User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1, "read": 1,
"report": 0, "role": "Stock User"
"role": "Stock User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1, "read": 1,
"report": 0, "role": "Employee"
"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": 1, "show_name_in_global_search": 1,
"sort_field": "name", "sort_field": "name",
"sort_order": "DESC", "sort_order": "DESC"
"track_changes": 0,
"track_seen": 0
} }

View File

@@ -36,6 +36,11 @@ class FiscalYear(Document):
frappe.throw(_("Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved.")) frappe.throw(_("Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."))
def validate_dates(self): def validate_dates(self):
if self.is_short_year:
# Fiscal Year can be shorter than one year, in some jurisdictions
# under certain circumstances. For example, in the USA and Germany.
return
if getdate(self.year_start_date) > getdate(self.year_end_date): if getdate(self.year_start_date) > getdate(self.year_end_date):
frappe.throw(_("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"), frappe.throw(_("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"),
FiscalYearIncorrectDate) FiscalYearIncorrectDate)
@@ -116,12 +121,8 @@ def auto_create_fiscal_year():
pass pass
def get_from_and_to_date(fiscal_year): def get_from_and_to_date(fiscal_year):
from_and_to_date_tuple = frappe.db.sql("""select year_start_date, year_end_date fields = [
from `tabFiscal Year` where name=%s""", (fiscal_year))[0] "year_start_date as from_date",
"year_end_date as to_date"
from_and_to_date = { ]
"from_date": from_and_to_date_tuple[0], return frappe.db.get_value("Fiscal Year", fiscal_year, fields, as_dict=1)
"to_date": from_and_to_date_tuple[1]
}
return from_and_to_date

View File

@@ -11,6 +11,7 @@ test_records = frappe.get_test_records('Fiscal Year')
test_ignore = ["Company"] test_ignore = ["Company"]
class TestFiscalYear(unittest.TestCase): class TestFiscalYear(unittest.TestCase):
def test_extra_year(self): def test_extra_year(self):
if frappe.db.exists("Fiscal Year", "_Test Fiscal Year 2000"): if frappe.db.exists("Fiscal Year", "_Test Fiscal Year 2000"):
frappe.delete_doc("Fiscal Year", "_Test Fiscal Year 2000") frappe.delete_doc("Fiscal Year", "_Test Fiscal Year 2000")

View File

@@ -1,4 +1,11 @@
[ [
{
"doctype": "Fiscal Year",
"year": "_Test Short Fiscal Year 2011",
"is_short_year": 1,
"year_end_date": "2011-04-01",
"year_start_date": "2011-12-31"
},
{ {
"doctype": "Fiscal Year", "doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2012", "year": "_Test Fiscal Year 2012",

View File

@@ -1,5 +1,6 @@
{ {
"actions": [], "actions": [],
"allow_auto_repeat": 1,
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2013-03-25 10:53:52", "creation": "2013-03-25 10:53:52",
@@ -503,7 +504,7 @@
"idx": 176, "idx": 176,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-06-02 18:15:46.955697", "modified": "2020-10-30 13:56:01.121995",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry", "name": "Journal Entry",

View File

@@ -1,5 +1,6 @@
{ {
"actions": [], "actions": [],
"allow_auto_repeat": 1,
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2016-06-01 14:38:51.012597", "creation": "2016-06-01 14:38:51.012597",
@@ -587,7 +588,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-09-02 13:39:43.383705", "modified": "2020-10-30 13:56:20.007336",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",

View File

@@ -1,5 +1,6 @@
{ {
"actions": [], "actions": [],
"allow_auto_repeat": 1,
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2020-01-24 15:29:29.933693", "creation": "2020-01-24 15:29:29.933693",
@@ -1580,7 +1581,7 @@
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-09-28 16:51:24.641755", "modified": "2020-10-30 13:56:51.056083",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice", "name": "POS Invoice",

View File

@@ -39,6 +39,7 @@ class POSInvoice(SalesInvoice):
self.validate_serialised_or_batched_item() self.validate_serialised_or_batched_item()
self.validate_stock_availablility() self.validate_stock_availablility()
self.validate_return_items_qty() self.validate_return_items_qty()
self.validate_non_stock_items()
self.set_status() self.set_status()
self.set_account_for_mode_of_payment() self.set_account_for_mode_of_payment()
self.validate_pos() self.validate_pos()
@@ -175,6 +176,14 @@ class POSInvoice(SalesInvoice):
.format(d.idx, bold_serial_no, bold_return_against) .format(d.idx, bold_serial_no, bold_return_against)
) )
def validate_non_stock_items(self):
for d in self.get("items"):
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
if not is_stock_item:
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice. ").format(
d.idx, frappe.bold(d.item_code)
), title=_("Invalid Item"))
def validate_mode_of_payment(self): def validate_mode_of_payment(self):
if len(self.payments) == 0: if len(self.payments) == 0:
frappe.throw(_("At least one mode of payment is required for POS invoice.")) frappe.throw(_("At least one mode of payment is required for POS invoice."))

View File

@@ -42,56 +42,56 @@ frappe.ui.form.on('Pricing Rule', {
<tr><td> <tr><td>
<h4> <h4>
<i class="fa fa-hand-right"></i> <i class="fa fa-hand-right"></i>
${__('Notes')} {{__('Notes')}}
</h4> </h4>
<ul> <ul>
<li> <li>
${__("Pricing Rule is made to overwrite Price List / define discount percentage, based on some criteria.")} {{__("Pricing Rule is made to overwrite Price List / define discount percentage, based on some criteria.")}}
</li> </li>
<li> <li>
${__("If selected Pricing Rule is made for 'Rate', it will overwrite Price List. Pricing Rule rate is the final rate, so no further discount should be applied. Hence, in transactions like Sales Order, Purchase Order etc, it will be fetched in 'Rate' field, rather than 'Price List Rate' field.")} {{__("If selected Pricing Rule is made for 'Rate', it will overwrite Price List. Pricing Rule rate is the final rate, so no further discount should be applied. Hence, in transactions like Sales Order, Purchase Order etc, it will be fetched in 'Rate' field, rather than 'Price List Rate' field.")}}
</li> </li>
<li> <li>
${__('Discount Percentage can be applied either against a Price List or for all Price List.')} {{__('Discount Percentage can be applied either against a Price List or for all Price List.')}}
</li> </li>
<li> <li>
${__('To not apply Pricing Rule in a particular transaction, all applicable Pricing Rules should be disabled.')} {{__('To not apply Pricing Rule in a particular transaction, all applicable Pricing Rules should be disabled.')}}
</li> </li>
</ul> </ul>
</td></tr> </td></tr>
<tr><td> <tr><td>
<h4><i class="fa fa-question-sign"></i> <h4><i class="fa fa-question-sign"></i>
${__('How Pricing Rule is applied?')} {{__('How Pricing Rule is applied?')}}
</h4> </h4>
<ol> <ol>
<li> <li>
${__("Pricing Rule is first selected based on 'Apply On' field, which can be Item, Item Group or Brand.")} {{__("Pricing Rule is first selected based on 'Apply On' field, which can be Item, Item Group or Brand.")}}
</li> </li>
<li> <li>
${__("Then Pricing Rules are filtered out based on Customer, Customer Group, Territory, Supplier, Supplier Type, Campaign, Sales Partner etc.")} {{__("Then Pricing Rules are filtered out based on Customer, Customer Group, Territory, Supplier, Supplier Type, Campaign, Sales Partner etc.")}}
</li> </li>
<li> <li>
${__('Pricing Rules are further filtered based on quantity.')} {{__('Pricing Rules are further filtered based on quantity.')}}
</li> </li>
<li> <li>
${__('If two or more Pricing Rules are found based on the above conditions, Priority is applied. Priority is a number between 0 to 20 while default value is zero (blank). Higher number means it will take precedence if there are multiple Pricing Rules with same conditions.')} {{__('If two or more Pricing Rules are found based on the above conditions, Priority is applied. Priority is a number between 0 to 20 while default value is zero (blank). Higher number means it will take precedence if there are multiple Pricing Rules with same conditions.')}}
</li> </li>
<li> <li>
${__('Even if there are multiple Pricing Rules with highest priority, then following internal priorities are applied:')} {{__('Even if there are multiple Pricing Rules with highest priority, then following internal priorities are applied:')}}
<ul> <ul>
<li> <li>
${__('Item Code > Item Group > Brand')} {{__('Item Code > Item Group > Brand')}}
</li> </li>
<li> <li>
${__('Customer > Customer Group > Territory')} {{__('Customer > Customer Group > Territory')}}
</li> </li>
<li> <li>
${__('Supplier > Supplier Type')} {{__('Supplier > Supplier Type')}}
</li> </li>
</ul> </ul>
</li> </li>
<li> <li>
${__('If multiple Pricing Rules continue to prevail, users are asked to set Priority manually to resolve conflict.')} {{__('If multiple Pricing Rules continue to prevail, users are asked to set Priority manually to resolve conflict.')}}
</li> </li>
</ol> </ol>
</td></tr> </td></tr>

View File

@@ -352,8 +352,14 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
pricing_rule_rate = 0.0 pricing_rule_rate = 0.0
if pricing_rule.currency == args.currency: if pricing_rule.currency == args.currency:
pricing_rule_rate = pricing_rule.rate pricing_rule_rate = pricing_rule.rate
if pricing_rule_rate:
# Override already set price list rate (from item price)
# if pricing_rule_rate > 0
item_details.update({
"price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1),
})
item_details.update({ item_details.update({
"price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1),
"discount_percentage": 0.0 "discount_percentage": 0.0
}) })

View File

@@ -484,6 +484,43 @@ class TestPricingRule(unittest.TestCase):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1") frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2") frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
def test_item_price_with_pricing_rule(self):
item = make_item("Water Flask")
make_item_price("Water Flask", "_Test Price List", 100)
pricing_rule_record = {
"doctype": "Pricing Rule",
"title": "_Test Water Flask Rule",
"apply_on": "Item Code",
"items": [{
"item_code": "Water Flask",
}],
"selling": 1,
"currency": "INR",
"rate_or_discount": "Rate",
"rate": 0,
"margin_type": "Percentage",
"margin_rate_or_amount": 2,
"company": "_Test Company"
}
rule = frappe.get_doc(pricing_rule_record)
rule.insert()
si = create_sales_invoice(do_not_save=True, item_code="Water Flask")
si.selling_price_list = "_Test Price List"
si.save()
# If rate in Rule is 0, give preference to Item Price if it exists
self.assertEqual(si.items[0].price_list_rate, 100)
self.assertEqual(si.items[0].margin_rate_or_amount, 2)
self.assertEqual(si.items[0].rate_with_margin, 102)
self.assertEqual(si.items[0].rate, 102)
si.delete()
rule.delete()
frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
item.delete()
def make_pricing_rule(**args): def make_pricing_rule(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@@ -1,5 +1,6 @@
{ {
"actions": [], "actions": [],
"allow_auto_repeat": 1,
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2013-05-21 16:16:39", "creation": "2013-05-21 16:16:39",
@@ -1334,7 +1335,8 @@
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"modified": "2020-09-21 12:22:09.164068", "links": [],
"modified": "2020-10-30 13:57:18.266978",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@@ -998,7 +998,7 @@ def make_purchase_invoice(**args):
'expense_account': args.expense_account or '_Test Account Cost for Goods Sold - _TC', 'expense_account': args.expense_account or '_Test Account Cost for Goods Sold - _TC',
"conversion_factor": 1.0, "conversion_factor": 1.0,
"serial_no": args.serial_no, "serial_no": args.serial_no,
"stock_uom": "_Test UOM", "stock_uom": args.uom or "_Test UOM",
"cost_center": args.cost_center or "_Test Cost Center - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC",
"project": args.project, "project": args.project,
"rejected_warehouse": args.rejected_warehouse or "", "rejected_warehouse": args.rejected_warehouse or "",
@@ -1040,7 +1040,8 @@ def make_purchase_invoice_against_cost_center(**args):
pi.is_return = args.is_return pi.is_return = args.is_return
pi.credit_to = args.return_against or "Creditors - _TC" pi.credit_to = args.return_against or "Creditors - _TC"
pi.is_subcontracted = args.is_subcontracted or "No" pi.is_subcontracted = args.is_subcontracted or "No"
pi.supplier_warehouse = "_Test Warehouse 1 - _TC" if args.supplier_warehouse:
pi.supplier_warehouse = "_Test Warehouse 1 - _TC"
pi.append("items", { pi.append("items", {
"item_code": args.item or args.item_code or "_Test Item", "item_code": args.item or args.item_code or "_Test Item",

View File

@@ -1,5 +1,6 @@
{ {
"actions": [], "actions": [],
"allow_auto_repeat": 1,
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2013-05-24 19:29:05", "creation": "2013-05-24 19:29:05",
@@ -1955,7 +1956,7 @@
"idx": 181, "idx": 181,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-10-09 15:59:57.544736", "modified": "2020-10-30 13:57:45.086303",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@@ -1401,6 +1401,7 @@ def make_delivery_note(source_name, target_doc=None):
def set_missing_values(source, target): def set_missing_values(source, target):
target.ignore_pricing_rule = 1 target.ignore_pricing_rule = 1
target.run_method("set_missing_values") target.run_method("set_missing_values")
target.run_method("set_po_nos")
target.run_method("calculate_taxes_and_totals") target.run_method("calculate_taxes_and_totals")
def update_item(source_doc, target_doc, source_parent): def update_item(source_doc, target_doc, source_parent):

View File

@@ -140,9 +140,9 @@ def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_detai
else: else:
tds_amount = _get_tds(net_total, tax_details.rate) tds_amount = _get_tds(net_total, tax_details.rate)
else: else:
supplier_credit_amount = frappe.get_all('Purchase Invoice Item', supplier_credit_amount = frappe.get_all('Purchase Invoice',
fields = ['sum(net_amount)'], fields = ['sum(net_total)'],
filters = {'parent': ('in', vouchers), 'docstatus': 1}, as_list=1) filters = {'name': ('in', vouchers), 'docstatus': 1, "apply_tds": 1}, as_list=1)
supplier_credit_amount = (supplier_credit_amount[0][0] supplier_credit_amount = (supplier_credit_amount[0][0]
if supplier_credit_amount and supplier_credit_amount[0][0] else 0) if supplier_credit_amount and supplier_credit_amount[0][0] else 0)

View File

@@ -7,6 +7,7 @@ import frappe
import unittest import unittest
from frappe.utils import today from frappe.utils import today
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
test_dependencies = ["Supplier Group"] test_dependencies = ["Supplier Group"]
@@ -101,6 +102,32 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices: for d in invoices:
d.cancel() d.cancel()
def test_single_threshold_tds_with_previous_vouchers_and_no_tds(self):
invoices = []
doc = create_supplier(supplier_name = "Test TDS Supplier ABC",
tax_withholding_category="Single Threshold TDS")
supplier = doc.name
pi = create_purchase_invoice(supplier=supplier)
pi.submit()
invoices.append(pi)
# TDS not applied
pi = create_purchase_invoice(supplier=supplier, do_not_apply_tds=True)
pi.submit()
invoices.append(pi)
pi = create_purchase_invoice(supplier=supplier)
pi.submit()
invoices.append(pi)
self.assertEqual(pi.taxes_and_charges_deducted, 2000)
self.assertEqual(pi.grand_total, 8000)
# delete invoices to avoid clashing
for d in invoices:
d.cancel()
def create_purchase_invoice(**args): def create_purchase_invoice(**args):
# return sales invoice doc object # return sales invoice doc object
item = frappe.get_doc('Item', {'item_name': 'TDS Item'}) item = frappe.get_doc('Item', {'item_name': 'TDS Item'})
@@ -109,7 +136,7 @@ def create_purchase_invoice(**args):
pi = frappe.get_doc({ pi = frappe.get_doc({
"doctype": "Purchase Invoice", "doctype": "Purchase Invoice",
"posting_date": today(), "posting_date": today(),
"apply_tds": 1, "apply_tds": 0 if args.do_not_apply_tds else 1,
"supplier": args.supplier, "supplier": args.supplier,
"company": '_Test Company', "company": '_Test Company',
"taxes_and_charges": "", "taxes_and_charges": "",

View File

@@ -156,7 +156,7 @@ erpnext.accounts.bankTransactionUpload = class bankTransactionUpload {
setup_transactions_dom() { setup_transactions_dom() {
const me = this; const me = this;
me.parent.$main_section.append(`<div class="transactions-table"></div>`) me.parent.$main_section.append('<div class="transactions-table"></div>');
} }
create_datatable() { create_datatable() {
@@ -167,9 +167,7 @@ erpnext.accounts.bankTransactionUpload = class bankTransactionUpload {
}) })
} }
catch(err) { catch(err) {
let msg = __(`Your file could not be processed by ERPNext. let msg = __("Your file could not be processed. It should be a standard CSV or XLSX file with headers in the first row.");
<br>It should be a standard CSV or XLSX file.
<br>The headers should be in the first row.`)
frappe.throw(msg) frappe.throw(msg)
} }

View File

@@ -59,7 +59,7 @@ def _get_party_details(party=None, account=None, party_type="Customer", company=
billing_address=party_address, shipping_address=shipping_address) billing_address=party_address, shipping_address=shipping_address)
if fetch_payment_terms_template: if fetch_payment_terms_template:
party_details["payment_terms_template"] = get_pyt_term_template(party.name, party_type, company) party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company)
if not party_details.get("currency"): if not party_details.get("currency"):
party_details["currency"] = currency party_details["currency"] = currency
@@ -315,7 +315,7 @@ def get_due_date(posting_date, party_type, party, company=None, bill_date=None):
due_date = None due_date = None
if (bill_date or posting_date) and party: if (bill_date or posting_date) and party:
due_date = bill_date or posting_date due_date = bill_date or posting_date
template_name = get_pyt_term_template(party, party_type, company) template_name = get_payment_terms_template(party, party_type, company)
if template_name: if template_name:
due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d") due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d")
@@ -422,7 +422,7 @@ def set_taxes(party, party_type, posting_date, company, customer_group=None, sup
@frappe.whitelist() @frappe.whitelist()
def get_pyt_term_template(party_name, party_type, company=None): def get_payment_terms_template(party_name, party_type, company=None):
if party_type not in ("Customer", "Supplier"): if party_type not in ("Customer", "Supplier"):
return return
template = None template = None

View File

@@ -160,6 +160,8 @@ class ReceivablePayableReport(object):
else: else:
# advance / unlinked payment or other adjustment # advance / unlinked payment or other adjustment
row.paid -= gle_balance row.paid -= gle_balance
if gle.cost_center:
row.cost_center = str(gle.cost_center)
def update_sub_total_row(self, row, party): def update_sub_total_row(self, row, party):
total_row = self.total_row_map.get(party) total_row = self.total_row_map.get(party)
@@ -210,7 +212,6 @@ class ReceivablePayableReport(object):
for key, row in self.voucher_balance.items(): for key, row in self.voucher_balance.items():
row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision) row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision)
row.invoice_grand_total = row.invoiced row.invoice_grand_total = row.invoiced
if abs(row.outstanding) > 1.0/10 ** self.currency_precision: if abs(row.outstanding) > 1.0/10 ** self.currency_precision:
# non-zero oustanding, we must consider this row # non-zero oustanding, we must consider this row
@@ -577,7 +578,7 @@ class ReceivablePayableReport(object):
self.gl_entries = frappe.db.sql(""" self.gl_entries = frappe.db.sql("""
select select
name, posting_date, account, party_type, party, voucher_type, voucher_no, name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
against_voucher_type, against_voucher, account_currency, remarks, {0} against_voucher_type, against_voucher, account_currency, remarks, {0}
from from
`tabGL Entry` `tabGL Entry`
@@ -741,6 +742,7 @@ class ReceivablePayableReport(object):
self.add_column(_("Customer Contact"), fieldname='customer_primary_contact', self.add_column(_("Customer Contact"), fieldname='customer_primary_contact',
fieldtype='Link', options='Contact') fieldtype='Link', options='Contact')
self.add_column(label=_('Cost Center'), fieldname='cost_center', fieldtype='Data')
self.add_column(label=_('Voucher Type'), fieldname='voucher_type', fieldtype='Data') self.add_column(label=_('Voucher Type'), fieldname='voucher_type', fieldtype='Data')
self.add_column(label=_('Voucher No'), fieldname='voucher_no', fieldtype='Dynamic Link', self.add_column(label=_('Voucher No'), fieldname='voucher_no', fieldtype='Dynamic Link',
options='voucher_type', width=180) options='voucher_type', width=180)

View File

@@ -372,8 +372,8 @@ frappe.ui.form.on('Asset', {
doctype_field = frappe.scrub(doctype) doctype_field = frappe.scrub(doctype)
frm.set_value(doctype_field, ''); frm.set_value(doctype_field, '');
frappe.msgprint({ frappe.msgprint({
title: __(`Invalid ${doctype}`), title: __('Invalid {0}', [__(doctype)]),
message: __(`The selected ${doctype} doesn't contains selected Asset Item.`), message: __('The selected {0} does not contain the selected Asset Item.', [__(doctype)]),
indicator: 'red' indicator: 'red'
}); });
} }
@@ -435,7 +435,7 @@ frappe.ui.form.on('Asset Finance Book', {
depreciation_start_date: function(frm, cdt, cdn) { depreciation_start_date: function(frm, cdt, cdn) {
const book = locals[cdt][cdn]; const book = locals[cdt][cdn];
if (frm.doc.available_for_use_date && book.depreciation_start_date == frm.doc.available_for_use_date) { if (frm.doc.available_for_use_date && book.depreciation_start_date == frm.doc.available_for_use_date) {
frappe.msgprint(__(`Depreciation Posting Date should not be equal to Available for Use Date.`)); frappe.msgprint(__("Depreciation Posting Date should not be equal to Available for Use Date."));
book.depreciation_start_date = ""; book.depreciation_start_date = "";
frm.refresh_field("finance_books"); frm.refresh_field("finance_books");
} }

View File

@@ -96,6 +96,11 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
this.frm.set_df_property("drop_ship", "hidden", !is_drop_ship); this.frm.set_df_property("drop_ship", "hidden", !is_drop_ship);
if(doc.docstatus == 1) { if(doc.docstatus == 1) {
this.frm.fields_dict.items_section.wrapper.addClass("hide-border");
if(!this.frm.doc.set_warehouse) {
this.frm.fields_dict.items_section.wrapper.removeClass("hide-border");
}
if(!in_list(["Closed", "Delivered"], doc.status)) { if(!in_list(["Closed", "Delivered"], doc.status)) {
if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received) < 100 && flt(this.frm.doc.per_billed) < 100) { if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received) < 100 && flt(this.frm.doc.per_billed) < 100) {
this.frm.add_custom_button(__('Update Items'), () => { this.frm.add_custom_button(__('Update Items'), () => {
@@ -132,16 +137,25 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
if(doc.status != "Closed") { if(doc.status != "Closed") {
if (doc.status != "On Hold") { if (doc.status != "On Hold") {
if(flt(doc.per_received) < 100 && allow_receipt) { if(flt(doc.per_received) < 100 && allow_receipt) {
cur_frm.add_custom_button(__('Receipt'), this.make_purchase_receipt, __('Create')); cur_frm.add_custom_button(__('Purchase Receipt'), this.make_purchase_receipt, __('Create'));
if(doc.is_subcontracted==="Yes" && me.has_unsupplied_items()) { if(doc.is_subcontracted==="Yes" && me.has_unsupplied_items()) {
cur_frm.add_custom_button(__('Material to Supplier'), cur_frm.add_custom_button(__('Material to Supplier'),
function() { me.make_stock_entry(); }, __("Transfer")); function() { me.make_stock_entry(); }, __("Transfer"));
} }
} }
if(flt(doc.per_billed) < 100) if(flt(doc.per_billed) < 100)
cur_frm.add_custom_button(__('Invoice'), cur_frm.add_custom_button(__('Purchase Invoice'),
this.make_purchase_invoice, __('Create')); this.make_purchase_invoice, __('Create'));
if(flt(doc.per_billed)==0 && doc.status != "Delivered") {
cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create'));
}
if(flt(doc.per_billed)==0) {
this.frm.add_custom_button(__('Payment Request'),
function() { me.make_payment_request() }, __('Create'));
}
if(!doc.auto_repeat) { if(!doc.auto_repeat) {
cur_frm.add_custom_button(__('Subscription'), function() { cur_frm.add_custom_button(__('Subscription'), function() {
erpnext.utils.make_subscription(doc.doctype, doc.name) erpnext.utils.make_subscription(doc.doctype, doc.name)
@@ -162,13 +176,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
}); });
} }
} }
if(flt(doc.per_billed)==0) {
this.frm.add_custom_button(__('Payment Request'),
function() { me.make_payment_request() }, __('Create'));
}
if(flt(doc.per_billed)==0 && doc.status != "Delivered") {
cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create'));
}
cur_frm.page.set_inner_btn_group_as_primary(__('Create')); cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
} }
} else if(doc.docstatus===0) { } else if(doc.docstatus===0) {
@@ -364,12 +372,16 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
method: "erpnext.stock.doctype.material_request.material_request.make_purchase_order", method: "erpnext.stock.doctype.material_request.material_request.make_purchase_order",
source_doctype: "Material Request", source_doctype: "Material Request",
target: me.frm, target: me.frm,
setters: {}, setters: {
schedule_date: undefined,
status: undefined
},
get_query_filters: { get_query_filters: {
material_request_type: "Purchase", material_request_type: "Purchase",
docstatus: 1, docstatus: 1,
status: ["!=", "Stopped"], status: ["!=", "Stopped"],
per_ordered: ["<", 99.99], per_ordered: ["<", 99.99],
company: me.frm.doc.company
} }
}) })
}, __("Get Items From")); }, __("Get Items From"));
@@ -381,16 +393,17 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
source_doctype: "Supplier Quotation", source_doctype: "Supplier Quotation",
target: me.frm, target: me.frm,
setters: { setters: {
supplier: me.frm.doc.supplier supplier: me.frm.doc.supplier,
valid_till: undefined
}, },
get_query_filters: { get_query_filters: {
docstatus: 1, docstatus: 1,
status: ["!=", "Stopped"], status: ["not in", ["Stopped", "Expired"]],
} }
}) })
}, __("Get Items From")); }, __("Get Items From"));
this.frm.add_custom_button(__('Update rate as per last purchase'), this.frm.add_custom_button(__('Update Rate as per Last Purchase'),
function() { function() {
frappe.call({ frappe.call({
"method": "get_last_purchase_rate", "method": "get_last_purchase_rate",

View File

@@ -1,5 +1,6 @@
{ {
"actions": [], "actions": [],
"allow_auto_repeat": 1,
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2013-05-21 16:16:39", "creation": "2013-05-21 16:16:39",
@@ -30,8 +31,8 @@
"customer_contact_email", "customer_contact_email",
"section_addresses", "section_addresses",
"supplier_address", "supplier_address",
"contact_person",
"address_display", "address_display",
"contact_person",
"contact_display", "contact_display",
"contact_mobile", "contact_mobile",
"contact_email", "contact_email",
@@ -49,12 +50,14 @@
"plc_conversion_rate", "plc_conversion_rate",
"ignore_pricing_rule", "ignore_pricing_rule",
"sec_warehouse", "sec_warehouse",
"set_warehouse",
"col_break_warehouse",
"is_subcontracted", "is_subcontracted",
"col_break_warehouse",
"supplier_warehouse", "supplier_warehouse",
"items_section", "before_items_section",
"scan_barcode", "scan_barcode",
"items_col_break",
"set_warehouse",
"items_section",
"items", "items",
"sb_last_purchase", "sb_last_purchase",
"total_qty", "total_qty",
@@ -108,18 +111,13 @@
"payment_terms_template", "payment_terms_template",
"payment_schedule", "payment_schedule",
"tracking_section", "tracking_section",
"per_billed", "status",
"column_break_75", "column_break_75",
"per_billed",
"per_received", "per_received",
"terms_section_break", "terms_section_break",
"tc_name", "tc_name",
"terms", "terms",
"more_info",
"status",
"ref_sq",
"column_break_74",
"party_account_currency",
"inter_company_order_reference",
"column_break5", "column_break5",
"letter_head", "letter_head",
"select_print_heading", "select_print_heading",
@@ -131,7 +129,12 @@
"to_date", "to_date",
"column_break_97", "column_break_97",
"auto_repeat", "auto_repeat",
"update_auto_repeat_reference" "update_auto_repeat_reference",
"more_info",
"ref_sq",
"column_break_74",
"party_account_currency",
"inter_company_order_reference"
], ],
"fields": [ "fields": [
{ {
@@ -313,34 +316,34 @@
{ {
"fieldname": "supplier_address", "fieldname": "supplier_address",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Select Supplier Address", "label": "Supplier Address",
"options": "Address", "options": "Address",
"print_hide": 1 "print_hide": 1
}, },
{ {
"fieldname": "contact_person", "fieldname": "contact_person",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Contact Person", "label": "Supplier Contact",
"options": "Contact", "options": "Contact",
"print_hide": 1 "print_hide": 1
}, },
{ {
"fieldname": "address_display", "fieldname": "address_display",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Address", "label": "Supplier Address Details",
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "contact_display", "fieldname": "contact_display",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"in_global_search": 1, "in_global_search": 1,
"label": "Contact", "label": "Contact Name",
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "contact_mobile", "fieldname": "contact_mobile",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Mobile No", "label": "Contact Mobile No",
"read_only": 1 "read_only": 1
}, },
{ {
@@ -358,14 +361,14 @@
{ {
"fieldname": "shipping_address", "fieldname": "shipping_address",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Select Shipping Address", "label": "Company Shipping Address",
"options": "Address", "options": "Address",
"print_hide": 1 "print_hide": 1
}, },
{ {
"fieldname": "shipping_address_display", "fieldname": "shipping_address_display",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Shipping Address", "label": "Shipping Address Details",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@@ -433,7 +436,8 @@
}, },
{ {
"fieldname": "sec_warehouse", "fieldname": "sec_warehouse",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"label": "Subcontracting"
}, },
{ {
"description": "Sets 'Warehouse' in each row of the Items table.", "description": "Sets 'Warehouse' in each row of the Items table.",
@@ -466,6 +470,7 @@
{ {
"fieldname": "items_section", "fieldname": "items_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_border": 1,
"oldfieldtype": "Section Break", "oldfieldtype": "Section Break",
"options": "fa fa-shopping-cart" "options": "fa fa-shopping-cart"
}, },
@@ -598,7 +603,8 @@
}, },
{ {
"fieldname": "section_break_52", "fieldname": "section_break_52",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"hide_border": 1
}, },
{ {
"fieldname": "taxes", "fieldname": "taxes",
@@ -626,10 +632,12 @@
{ {
"fieldname": "totals", "fieldname": "totals",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Taxes and Charges",
"oldfieldtype": "Section Break", "oldfieldtype": "Section Break",
"options": "fa fa-money" "options": "fa fa-money"
}, },
{ {
"depends_on": "base_taxes_and_charges_added",
"fieldname": "base_taxes_and_charges_added", "fieldname": "base_taxes_and_charges_added",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Taxes and Charges Added (Company Currency)", "label": "Taxes and Charges Added (Company Currency)",
@@ -640,6 +648,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "base_taxes_and_charges_deducted",
"fieldname": "base_taxes_and_charges_deducted", "fieldname": "base_taxes_and_charges_deducted",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Taxes and Charges Deducted (Company Currency)", "label": "Taxes and Charges Deducted (Company Currency)",
@@ -650,6 +659,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "base_total_taxes_and_charges",
"fieldname": "base_total_taxes_and_charges", "fieldname": "base_total_taxes_and_charges",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Total Taxes and Charges (Company Currency)", "label": "Total Taxes and Charges (Company Currency)",
@@ -665,6 +675,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"depends_on": "taxes_and_charges_added",
"fieldname": "taxes_and_charges_added", "fieldname": "taxes_and_charges_added",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Taxes and Charges Added", "label": "Taxes and Charges Added",
@@ -675,6 +686,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "taxes_and_charges_deducted",
"fieldname": "taxes_and_charges_deducted", "fieldname": "taxes_and_charges_deducted",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Taxes and Charges Deducted", "label": "Taxes and Charges Deducted",
@@ -685,6 +697,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "total_taxes_and_charges",
"fieldname": "total_taxes_and_charges", "fieldname": "total_taxes_and_charges",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Total Taxes and Charges", "label": "Total Taxes and Charges",
@@ -694,7 +707,7 @@
}, },
{ {
"collapsible": 1, "collapsible": 1,
"collapsible_depends_on": "discount_amount", "collapsible_depends_on": "apply_discount_on",
"fieldname": "discount_section", "fieldname": "discount_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Additional Discount" "label": "Additional Discount"
@@ -734,7 +747,8 @@
}, },
{ {
"fieldname": "totals_section", "fieldname": "totals_section",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"label": "Totals"
}, },
{ {
"fieldname": "base_grand_total", "fieldname": "base_grand_total",
@@ -902,12 +916,12 @@
}, },
{ {
"fieldname": "ref_sq", "fieldname": "ref_sq",
"fieldtype": "Data", "fieldtype": "Link",
"hidden": 1, "label": "Supplier Quotation",
"label": "Ref SQ",
"no_copy": 1, "no_copy": 1,
"oldfieldname": "ref_sq", "oldfieldname": "ref_sq",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"options": "Supplier Quotation",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@@ -1061,7 +1075,7 @@
"collapsible": 1, "collapsible": 1,
"fieldname": "tracking_section", "fieldname": "tracking_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Tracking" "label": "Order Status"
}, },
{ {
"fieldname": "column_break_75", "fieldname": "column_break_75",
@@ -1070,21 +1084,29 @@
{ {
"fieldname": "billing_address", "fieldname": "billing_address",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Select Billing Address", "label": "Company Billing Address",
"options": "Address" "options": "Address"
}, },
{ {
"fieldname": "billing_address_display", "fieldname": "billing_address_display",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Billing Address", "label": "Billing Address Details",
"read_only": 1 "read_only": 1
},
{
"fieldname": "before_items_section",
"fieldtype": "Section Break"
},
{
"fieldname": "items_col_break",
"fieldtype": "Column Break"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-10-07 14:31:57.661221", "modified": "2020-10-30 13:58:14.697921",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@@ -24,6 +24,7 @@
"col_break2", "col_break2",
"uom", "uom",
"conversion_factor", "conversion_factor",
"stock_qty",
"sec_break1", "sec_break1",
"price_list_rate", "price_list_rate",
"discount_percentage", "discount_percentage",
@@ -46,11 +47,8 @@
"column_break_32", "column_break_32",
"base_net_rate", "base_net_rate",
"base_net_amount", "base_net_amount",
"billed_amt",
"warehouse_and_reference", "warehouse_and_reference",
"warehouse", "warehouse",
"delivered_by_supplier",
"project",
"material_request", "material_request",
"material_request_item", "material_request_item",
"sales_order", "sales_order",
@@ -58,36 +56,37 @@
"supplier_quotation", "supplier_quotation",
"supplier_quotation_item", "supplier_quotation_item",
"col_break5", "col_break5",
"delivered_by_supplier",
"against_blanket_order", "against_blanket_order",
"blanket_order", "blanket_order",
"blanket_order_rate", "blanket_order_rate",
"item_group", "item_group",
"brand", "brand",
"bom",
"include_exploded_items",
"section_break_56", "section_break_56",
"stock_qty",
"column_break_60",
"received_qty", "received_qty",
"returned_qty", "returned_qty",
"manufacture_details", "column_break_60",
"manufacturer", "billed_amt",
"column_break_14",
"manufacturer_part_no",
"more_info_section_break",
"is_fixed_asset",
"item_tax_rate",
"accounting_details", "accounting_details",
"expense_account", "expense_account",
"column_break_68", "manufacture_details",
"manufacturer",
"manufacturer_part_no",
"column_break_14",
"bom",
"include_exploded_items",
"item_weight_details", "item_weight_details",
"weight_per_unit", "weight_per_unit",
"total_weight", "total_weight",
"column_break_40", "column_break_40",
"weight_uom", "weight_uom",
"accounting_dimensions_section", "accounting_dimensions_section",
"cost_center", "project",
"dimension_col_break", "dimension_col_break",
"cost_center",
"more_info_section_break",
"is_fixed_asset",
"item_tax_rate",
"section_break_72", "section_break_72",
"page_break" "page_break"
], ],
@@ -346,6 +345,7 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "is_free_item",
"fieldname": "is_free_item", "fieldname": "is_free_item",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Free Item", "label": "Is Free Item",
@@ -508,9 +508,10 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "delivered_by_supplier",
"fieldname": "delivered_by_supplier", "fieldname": "delivered_by_supplier",
"fieldtype": "Check", "fieldtype": "Check",
"label": "To be delivered to customer", "label": "To be Delivered to Customer",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@@ -558,6 +559,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:parent.is_subcontracted == 'Yes'",
"fieldname": "bom", "fieldname": "bom",
"fieldtype": "Link", "fieldtype": "Link",
"label": "BOM", "label": "BOM",
@@ -574,21 +576,21 @@
}, },
{ {
"fieldname": "section_break_56", "fieldname": "section_break_56",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"label": "Billed, Received & Returned"
}, },
{ {
"fieldname": "stock_qty", "fieldname": "stock_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Qty as per Stock UOM", "label": "Qty in Stock UOM",
"no_copy": 1, "no_copy": 1,
"oldfieldname": "stock_qty",
"oldfieldtype": "Currency",
"print_hide": 1, "print_hide": 1,
"print_width": "100px", "print_width": "100px",
"read_only": 1, "read_only": 1,
"width": "100px" "width": "100px"
}, },
{ {
"depends_on": "received_qty",
"fieldname": "received_qty", "fieldname": "received_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Received Qty", "label": "Received Qty",
@@ -612,9 +614,10 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"depends_on": "billed_amt",
"fieldname": "billed_amt", "fieldname": "billed_amt",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Billed Amt", "label": "Billed Amount",
"no_copy": 1, "no_copy": 1,
"options": "currency", "options": "currency",
"print_hide": 1, "print_hide": 1,
@@ -633,6 +636,7 @@
"report_hide": 1 "report_hide": 1
}, },
{ {
"collapsible": 1,
"fieldname": "accounting_details", "fieldname": "accounting_details",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Accounting Details" "label": "Accounting Details"
@@ -644,10 +648,6 @@
"options": "Account", "options": "Account",
"print_hide": 1 "print_hide": 1
}, },
{
"fieldname": "column_break_68",
"fieldtype": "Column Break"
},
{ {
"fieldname": "cost_center", "fieldname": "cost_center",
"fieldtype": "Link", "fieldtype": "Link",
@@ -715,6 +715,7 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "is_fixed_asset",
"fetch_from": "item_code.is_fixed_asset", "fetch_from": "item_code.is_fixed_asset",
"fieldname": "is_fixed_asset", "fieldname": "is_fixed_asset",
"fieldtype": "Check", "fieldtype": "Check",
@@ -728,9 +729,10 @@
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-04-21 11:55:58.643393", "modified": "2020-10-30 11:59:47.670951",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item", "name": "Purchase Order Item",

View File

@@ -120,3 +120,20 @@ class TestSupplier(unittest.TestCase):
# Rollback # Rollback
address.delete() address.delete()
def create_supplier(**args):
args = frappe._dict(args)
try:
doc = frappe.get_doc({
"doctype": "Supplier",
"supplier_name": args.supplier_name,
"supplier_group": args.supplier_group or "Services",
"supplier_type": args.supplier_type or "Company",
"tax_withholding_category": args.tax_withholding_category
}).insert()
return doc
except frappe.DuplicateEntryError:
return frappe.get_doc("Supplier", args.supplier_name)

View File

@@ -1,5 +1,6 @@
{ {
"actions": [], "actions": [],
"allow_auto_repeat": 1,
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2013-05-21 16:16:45", "creation": "2013-05-21 16:16:45",
@@ -807,7 +808,7 @@
"idx": 29, "idx": 29,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-10-01 20:56:17.932007", "modified": "2020-10-30 13:58:33.043971",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier Quotation", "name": "Supplier Quotation",

View File

@@ -1,12 +1,14 @@
{ {
"actions": [],
"autoname": "Prompt", "autoname": "Prompt",
"creation": "2019-06-05 11:48:30.572795", "creation": "2019-06-05 11:48:30.572795",
"doctype": "DocType", "doctype": "DocType",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"communication_channel",
"communication_medium_type", "communication_medium_type",
"catch_all",
"column_break_3", "column_break_3",
"catch_all",
"provider", "provider",
"disabled", "disabled",
"timeslots_section", "timeslots_section",
@@ -54,9 +56,16 @@
"fieldtype": "Table", "fieldtype": "Table",
"label": "Timeslots", "label": "Timeslots",
"options": "Communication Medium Timeslot" "options": "Communication Medium Timeslot"
},
{
"fieldname": "communication_channel",
"fieldtype": "Select",
"label": "Communication Channel",
"options": "\nExotel"
} }
], ],
"modified": "2019-06-05 11:49:30.769006", "links": [],
"modified": "2020-10-27 16:22:08.068542",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Communication", "module": "Communication",
"name": "Communication Medium", "name": "Communication Medium",

View File

@@ -371,13 +371,27 @@ class SellingController(StockController):
self.make_sl_entries(sl_entries) self.make_sl_entries(sl_entries)
def set_po_nos(self): def set_po_nos(self):
if self.doctype in ("Delivery Note", "Sales Invoice") and hasattr(self, "items"): if self.doctype == 'Sales Invoice' and hasattr(self, "items"):
ref_fieldname = "against_sales_order" if self.doctype == "Delivery Note" else "sales_order" self.set_pos_for_sales_invoice()
sales_orders = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)])) if self.doctype == 'Delivery Note' and hasattr(self, "items"):
if sales_orders: self.set_pos_for_delivery_note()
po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)})
if po_nos and po_nos[0].get('po_no'): def set_pos_for_sales_invoice(self):
self.po_no = ', '.join(list(set([d.po_no for d in po_nos if d.po_no]))) po_nos = []
self.get_po_nos('Sales Order', 'sales_order', po_nos)
self.get_po_nos('Delivery Note', 'delivery_note', po_nos)
self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(','))))
def set_pos_for_delivery_note(self):
po_nos = []
self.get_po_nos('Sales Order', 'against_sales_order', po_nos)
self.get_po_nos('Sales Invoice', 'against_sales_invoice', po_nos)
self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(','))))
def get_po_nos(self, ref_doctype, ref_fieldname, po_nos):
doc_list = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)]))
if doc_list:
po_nos += [d.po_no for d in frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) if d.get('po_no')]
def set_gross_profit(self): def set_gross_profit(self):
if self.doctype in ["Sales Order", "Quotation"]: if self.doctype in ["Sales Order", "Quotation"]:
@@ -402,26 +416,26 @@ class SellingController(StockController):
return return
for d in self.get('items'): for d in self.get('items'):
if self.doctype == "Sales Invoice": if self.doctype in ["POS Invoice","Sales Invoice"]:
e = [d.item_code, d.description, d.warehouse, d.sales_order or d.delivery_note, d.batch_no or ''] stock_items = [d.item_code, d.description, d.warehouse, d.sales_order or d.delivery_note, d.batch_no or '']
f = [d.item_code, d.description, d.sales_order or d.delivery_note] non_stock_items = [d.item_code, d.description, d.sales_order or d.delivery_note]
elif self.doctype == "Delivery Note": elif self.doctype == "Delivery Note":
e = [d.item_code, d.description, d.warehouse, d.against_sales_order or d.against_sales_invoice, d.batch_no or ''] stock_items = [d.item_code, d.description, d.warehouse, d.against_sales_order or d.against_sales_invoice, d.batch_no or '']
f = [d.item_code, d.description, d.against_sales_order or d.against_sales_invoice] non_stock_items = [d.item_code, d.description, d.against_sales_order or d.against_sales_invoice]
elif self.doctype in ["Sales Order", "Quotation"]: elif self.doctype in ["Sales Order", "Quotation"]:
e = [d.item_code, d.description, d.warehouse, ''] stock_items = [d.item_code, d.description, d.warehouse, '']
f = [d.item_code, d.description] non_stock_items = [d.item_code, d.description]
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1: if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1:
if e in check_list: if stock_items in check_list:
frappe.throw(_("Note: Item {0} entered multiple times").format(d.item_code)) frappe.throw(_("Note: Item {0} entered multiple times").format(d.item_code))
else: else:
check_list.append(e) check_list.append(stock_items)
else: else:
if f in chk_dupl_itm: if non_stock_items in chk_dupl_itm:
frappe.throw(_("Note: Item {0} entered multiple times").format(d.item_code)) frappe.throw(_("Note: Item {0} entered multiple times").format(d.item_code))
else: else:
chk_dupl_itm.append(f) chk_dupl_itm.append(non_stock_items)
def validate_target_warehouse(self): def validate_target_warehouse(self):
items = self.get("items") + (self.get("packed_items") or []) items = self.get("items") + (self.get("packed_items") or [])

View File

@@ -229,9 +229,9 @@ class StockController(AccountsController):
def check_expense_account(self, item): def check_expense_account(self, item):
if not item.get("expense_account"): if not item.get("expense_account"):
frappe.throw(_("Row #{0}: Expense Account not set for Item {1}. Please set an Expense \ msg = _("Please set an Expense Account in the Items table")
Account in the Items table").format(item.idx, frappe.bold(item.item_code)), frappe.throw(_("Row #{0}: Expense Account not set for the Item {1}. {2}")
title=_("Expense Account Missing")) .format(item.idx, frappe.bold(item.item_code), msg), title=_("Expense Account Missing"))
else: else:
is_expense_account = frappe.db.get_value("Account", is_expense_account = frappe.db.get_value("Account",
@@ -247,7 +247,9 @@ class StockController(AccountsController):
for d in self.items: for d in self.items:
if not d.batch_no: continue if not d.batch_no: continue
serial_nos = [sr.name for sr in frappe.get_all("Serial No", {'batch_no': d.batch_no})] serial_nos = [sr.name for sr in frappe.get_all("Serial No",
{'batch_no': d.batch_no, 'status': 'Inactive'})]
if serial_nos: if serial_nos:
frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None) frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None)

View File

@@ -4,7 +4,7 @@ function check_times(frm) {
let from_time = Date.parse('01/01/2019 ' + d.from_time); let from_time = Date.parse('01/01/2019 ' + d.from_time);
let to_time = Date.parse('01/01/2019 ' + d.to_time); let to_time = Date.parse('01/01/2019 ' + d.to_time);
if (from_time > to_time) { if (from_time > to_time) {
frappe.throw(__(`In row ${i + 1} of Appointment Booking Slots : "To Time" must be later than "From Time"`)); frappe.throw(__('In row {0} of Appointment Booking Slots: "To Time" must be later than "From Time".', [i + 1]));
} }
}); });
} }

View File

@@ -23,8 +23,7 @@
{ {
"fieldname": "contract_terms", "fieldname": "contract_terms",
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"label": "Contract Terms and Conditions", "label": "Contract Terms and Conditions"
"read_only": 1
}, },
{ {
"fieldname": "sb_fulfilment", "fieldname": "sb_fulfilment",
@@ -45,7 +44,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2020-06-03 00:24:58.179816", "modified": "2020-11-11 17:49:44.879363",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Contract Template", "name": "Contract Template",

View File

@@ -4,48 +4,55 @@
"item_code": "Computer", "item_code": "Computer",
"gross_purchase_amount": 100000, "gross_purchase_amount": 100000,
"asset_owner": "Company", "asset_owner": "Company",
"available_for_use_date": "2017-01-02" "available_for_use_date": "2017-01-02",
"location": "Main Location"
}, },
{ {
"asset_name": "Macbook Air - 1", "asset_name": "Macbook Air - 1",
"item_code": "Computer", "item_code": "Computer",
"gross_purchase_amount": 60000, "gross_purchase_amount": 60000,
"asset_owner": "Company", "asset_owner": "Company",
"available_for_use_date": "2017-10-02" "available_for_use_date": "2017-10-02",
"location": "Avg Location"
}, },
{ {
"asset_name": "Conferrence Table", "asset_name": "Conferrence Table",
"item_code": "Table", "item_code": "Table",
"gross_purchase_amount": 30000, "gross_purchase_amount": 30000,
"asset_owner": "Company", "asset_owner": "Company",
"available_for_use_date": "2018-10-02" "available_for_use_date": "2018-10-02",
"location": "Zany Location"
}, },
{ {
"asset_name": "Lunch Table", "asset_name": "Lunch Table",
"item_code": "Table", "item_code": "Table",
"gross_purchase_amount": 20000, "gross_purchase_amount": 20000,
"asset_owner": "Company", "asset_owner": "Company",
"available_for_use_date": "2018-06-02" "available_for_use_date": "2018-06-02",
"location": "Fletcher Location"
}, },
{ {
"asset_name": "ERPNext", "asset_name": "ERPNext",
"item_code": "ERP", "item_code": "ERP",
"gross_purchase_amount": 100000, "gross_purchase_amount": 100000,
"asset_owner": "Company", "asset_owner": "Company",
"available_for_use_date": "2018-09-02" "available_for_use_date": "2018-09-02",
"location":"Main Location"
}, },
{ {
"asset_name": "Chair 1", "asset_name": "Chair 1",
"item_code": "Chair", "item_code": "Chair",
"gross_purchase_amount": 10000, "gross_purchase_amount": 10000,
"asset_owner": "Company", "asset_owner": "Company",
"available_for_use_date": "2018-07-02" "available_for_use_date": "2018-07-02",
"location": "Zany Location"
}, },
{ {
"asset_name": "Chair 2", "asset_name": "Chair 2",
"item_code": "Chair", "item_code": "Chair",
"gross_purchase_amount": 10000, "gross_purchase_amount": 10000,
"asset_owner": "Company", "asset_owner": "Company",
"available_for_use_date": "2018-07-02" "available_for_use_date": "2018-07-02",
"location": "Avg Location"
} }
] ]

View File

@@ -0,0 +1,22 @@
[
{
"location_name": "Main Location",
"latitude": 40.0,
"longitude": 20.0
},
{
"location_name": "Avg Location",
"latitude": 63.0,
"longitude": 99.3
},
{
"location_name": "Zany Location",
"latitude": 47.5,
"longitude": 10.0
},
{
"location_name": "Fletcher Location",
"latitude": 100.90,
"longitude": 80
}
]

View File

@@ -9,6 +9,7 @@ from erpnext.demo.domains import data
from six import iteritems from six import iteritems
def setup_data(): def setup_data():
import_json("Location")
import_json("Asset Category") import_json("Asset Category")
setup_item() setup_item()
setup_workstation() setup_workstation()

View File

@@ -79,7 +79,7 @@ def make_stock_reconciliation():
if item.qty: if item.qty:
item.qty = item.qty - round(random.randint(1, item.qty)) item.qty = item.qty - round(random.randint(1, item.qty))
try: try:
stock_reco.insert(ignore_permissions=True) stock_reco.insert(ignore_permissions=True, ignore_mandatory=True)
stock_reco.submit() stock_reco.submit()
frappe.db.commit() frappe.db.commit()
except OpeningEntryAccountError: except OpeningEntryAccountError:

View File

@@ -6,8 +6,10 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe import _ from frappe import _
from frappe.utils import get_link_to_form, getdate from frappe.utils import get_link_to_form, getdate, formatdate
from erpnext import get_default_company
from erpnext.education.api import get_student_group_students from erpnext.education.api import get_student_group_students
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
class StudentAttendance(Document): class StudentAttendance(Document):
def validate(self): def validate(self):
@@ -17,6 +19,7 @@ class StudentAttendance(Document):
self.set_student_group() self.set_student_group()
self.validate_student() self.validate_student()
self.validate_duplication() self.validate_duplication()
self.validate_is_holiday()
def set_date(self): def set_date(self):
if self.course_schedule: if self.course_schedule:
@@ -78,3 +81,18 @@ class StudentAttendance(Document):
record = get_link_to_form('Student Attendance', attendance_record) record = get_link_to_form('Student Attendance', attendance_record)
frappe.throw(_('Student Attendance record {0} already exists against the Student {1}') frappe.throw(_('Student Attendance record {0} already exists against the Student {1}')
.format(record, frappe.bold(self.student)), title=_('Duplicate Entry')) .format(record, frappe.bold(self.student)), title=_('Duplicate Entry'))
def validate_is_holiday(self):
holiday_list = get_holiday_list()
if is_holiday(holiday_list, self.date):
frappe.throw(_('Attendance cannot be marked for {0} as it is a holiday.').format(
frappe.bold(formatdate(self.date))))
def get_holiday_list(company=None):
if not company:
company = get_default_company() or frappe.get_all('Company')[0].name
holiday_list = frappe.get_cached_value('Company', company, 'default_holiday_list')
if not holiday_list:
frappe.throw(_('Please set a default Holiday List for Company {0}').format(frappe.bold(get_default_company())))
return holiday_list

View File

@@ -11,6 +11,7 @@
"column_break_3", "column_break_3",
"from_date", "from_date",
"to_date", "to_date",
"total_leave_days",
"section_break_5", "section_break_5",
"attendance_based_on", "attendance_based_on",
"student_group", "student_group",
@@ -110,11 +111,17 @@
{ {
"fieldname": "column_break_11", "fieldname": "column_break_11",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "total_leave_days",
"fieldtype": "Float",
"label": "Total Leave Days",
"read_only": 1
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-07-08 13:22:38.329002", "modified": "2020-09-21 18:10:24.440669",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Education", "module": "Education",
"name": "Student Leave Application", "name": "Student Leave Application",

View File

@@ -6,11 +6,14 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from datetime import timedelta from datetime import timedelta
from frappe.utils import get_link_to_form, getdate from frappe.utils import get_link_to_form, getdate, date_diff, flt
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list
from frappe.model.document import Document from frappe.model.document import Document
class StudentLeaveApplication(Document): class StudentLeaveApplication(Document):
def validate(self): def validate(self):
self.validate_holiday_list()
self.validate_duplicate() self.validate_duplicate()
self.validate_from_to_dates('from_date', 'to_date') self.validate_from_to_dates('from_date', 'to_date')
@@ -39,10 +42,19 @@ class StudentLeaveApplication(Document):
frappe.throw(_('Leave application {0} already exists against the student {1}') frappe.throw(_('Leave application {0} already exists against the student {1}')
.format(link, frappe.bold(self.student)), title=_('Duplicate Entry')) .format(link, frappe.bold(self.student)), title=_('Duplicate Entry'))
def validate_holiday_list(self):
holiday_list = get_holiday_list()
self.total_leave_days = get_number_of_leave_days(self.from_date, self.to_date, holiday_list)
def update_attendance(self): def update_attendance(self):
holiday_list = get_holiday_list()
for dt in daterange(getdate(self.from_date), getdate(self.to_date)): for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
date = dt.strftime('%Y-%m-%d') date = dt.strftime('%Y-%m-%d')
if is_holiday(holiday_list, date):
continue
attendance = frappe.db.exists('Student Attendance', { attendance = frappe.db.exists('Student Attendance', {
'student': self.student, 'student': self.student,
'date': date, 'date': date,
@@ -89,3 +101,19 @@ class StudentLeaveApplication(Document):
def daterange(start_date, end_date): def daterange(start_date, end_date):
for n in range(int ((end_date - start_date).days)+1): for n in range(int ((end_date - start_date).days)+1):
yield start_date + timedelta(n) yield start_date + timedelta(n)
def get_number_of_leave_days(from_date, to_date, holiday_list):
number_of_days = date_diff(to_date, from_date) + 1
holidays = frappe.db.sql("""
SELECT
COUNT(DISTINCT holiday_date)
FROM `tabHoliday` h1,`tabHoliday List` h2
WHERE
h1.parent = h2.name and
h1.holiday_date between %s and %s and
h2.name = %s""", (from_date, to_date, holiday_list))[0][0]
number_of_days = flt(number_of_days) - flt(holidays)
return number_of_days

View File

@@ -5,13 +5,15 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
from frappe.utils import getdate, add_days from frappe.utils import getdate, add_days, add_months
from erpnext import get_default_company
from erpnext.education.doctype.student_group.test_student_group import get_random_group from erpnext.education.doctype.student_group.test_student_group import get_random_group
from erpnext.education.doctype.student.test_student import create_student from erpnext.education.doctype.student.test_student import create_student
class TestStudentLeaveApplication(unittest.TestCase): class TestStudentLeaveApplication(unittest.TestCase):
def setUp(self): def setUp(self):
frappe.db.sql("""delete from `tabStudent Leave Application`""") frappe.db.sql("""delete from `tabStudent Leave Application`""")
create_holiday_list()
def test_attendance_record_creation(self): def test_attendance_record_creation(self):
leave_application = create_leave_application() leave_application = create_leave_application()
@@ -35,20 +37,45 @@ class TestStudentLeaveApplication(unittest.TestCase):
attendance_status = frappe.db.get_value('Student Attendance', {'leave_application': leave_application.name}, 'docstatus') attendance_status = frappe.db.get_value('Student Attendance', {'leave_application': leave_application.name}, 'docstatus')
self.assertTrue(attendance_status, 2) self.assertTrue(attendance_status, 2)
def test_holiday(self):
today = getdate()
leave_application = create_leave_application(from_date=today, to_date= add_days(today, 1), submit=0)
def create_leave_application(from_date=None, to_date=None, mark_as_present=0): # holiday list validation
company = get_default_company() or frappe.get_all('Company')[0].name
frappe.db.set_value('Company', company, 'default_holiday_list', '')
self.assertRaises(frappe.ValidationError, leave_application.save)
frappe.db.set_value('Company', company, 'default_holiday_list', 'Test Holiday List for Student')
leave_application.save()
leave_application.reload()
self.assertEqual(leave_application.total_leave_days, 1)
# check no attendance record created for a holiday
leave_application.submit()
self.assertIsNone(frappe.db.exists('Student Attendance', {'leave_application': leave_application.name, 'date': add_days(today, 1)}))
def tearDown(self):
company = get_default_company() or frappe.get_all('Company')[0].name
frappe.db.set_value('Company', company, 'default_holiday_list', '_Test Holiday List')
def create_leave_application(from_date=None, to_date=None, mark_as_present=0, submit=1):
student = get_student() student = get_student()
leave_application = frappe.get_doc({ leave_application = frappe.new_doc('Student Leave Application')
'doctype': 'Student Leave Application', leave_application.student = student.name
'student': student.name, leave_application.attendance_based_on = 'Student Group'
'attendance_based_on': 'Student Group', leave_application.student_group = get_random_group().name
'student_group': get_random_group().name, leave_application.from_date = from_date if from_date else getdate()
'from_date': from_date if from_date else getdate(), leave_application.to_date = from_date if from_date else getdate()
'to_date': from_date if from_date else getdate(), leave_application.mark_as_present = mark_as_present
'mark_as_present': mark_as_present
}).insert() if submit:
leave_application.submit() leave_application.insert()
leave_application.submit()
return leave_application return leave_application
def create_student_attendance(date=None, status=None): def create_student_attendance(date=None, status=None):
@@ -68,3 +95,21 @@ def get_student():
first_name='Test', first_name='Test',
last_name='Student' last_name='Student'
)) ))
def create_holiday_list():
holiday_list = 'Test Holiday List for Student'
today = getdate()
if not frappe.db.exists('Holiday List', holiday_list):
frappe.get_doc(dict(
doctype = 'Holiday List',
holiday_list_name = holiday_list,
from_date = add_months(today, -6),
to_date = add_months(today, 6),
holidays = [
dict(holiday_date=add_days(today, 1), description = 'Test')
]
)).insert()
company = get_default_company() or frappe.get_all('Company')[0].name
frappe.db.set_value('Company', company, 'default_holiday_list', holiday_list)
return holiday_list

View File

@@ -3,8 +3,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import cstr, cint, getdate from frappe.utils import formatdate
from frappe import msgprint, _ from frappe import msgprint, _
from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
def execute(filters=None): def execute(filters=None):
if not filters: filters = {} if not filters: filters = {}
@@ -15,6 +17,11 @@ def execute(filters=None):
columns = get_columns(filters) columns = get_columns(filters)
date = filters.get("date") date = filters.get("date")
holiday_list = get_holiday_list()
if is_holiday(holiday_list, filters.get("date")):
msgprint(_("No attendance has been marked for {0} as it is a Holiday").format(frappe.bold(formatdate(filters.get("date")))))
absent_students = get_absent_students(date) absent_students = get_absent_students(date)
leave_applicants = get_leave_applications(date) leave_applicants = get_leave_applications(date)
if absent_students: if absent_students:

View File

@@ -3,8 +3,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import cstr, cint, getdate from frappe.utils import formatdate
from frappe import msgprint, _ from frappe import msgprint, _
from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
def execute(filters=None): def execute(filters=None):
if not filters: filters = {} if not filters: filters = {}
@@ -12,6 +14,10 @@ def execute(filters=None):
if not filters.get("date"): if not filters.get("date"):
msgprint(_("Please select date"), raise_exception=1) msgprint(_("Please select date"), raise_exception=1)
holiday_list = get_holiday_list()
if is_holiday(holiday_list, filters.get("date")):
msgprint(_("No attendance has been marked for {0} as it is a Holiday").format(frappe.bold(formatdate(filters.get("date")))))
columns = get_columns(filters) columns = get_columns(filters)
active_student_group = get_active_student_group() active_student_group = get_active_student_group()

View File

@@ -7,6 +7,8 @@ from frappe.utils import cstr, cint, getdate, get_first_day, get_last_day, date_
from frappe import msgprint, _ from frappe import msgprint, _
from calendar import monthrange from calendar import monthrange
from erpnext.education.api import get_student_group_students from erpnext.education.api import get_student_group_students
from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list
from erpnext.support.doctype.issue.issue import get_holidays
def execute(filters=None): def execute(filters=None):
if not filters: filters = {} if not filters: filters = {}
@@ -19,26 +21,32 @@ def execute(filters=None):
students_list = get_students_list(students) students_list = get_students_list(students)
att_map = get_attendance_list(from_date, to_date, filters.get("student_group"), students_list) att_map = get_attendance_list(from_date, to_date, filters.get("student_group"), students_list)
data = [] data = []
for stud in students: for stud in students:
row = [stud.student, stud.student_name] row = [stud.student, stud.student_name]
student_status = frappe.db.get_value("Student", stud.student, "enabled") student_status = frappe.db.get_value("Student", stud.student, "enabled")
date = from_date date = from_date
total_p = total_a = 0.0 total_p = total_a = 0.0
for day in range(total_days_in_month): for day in range(total_days_in_month):
status="None" status="None"
if att_map.get(stud.student): if att_map.get(stud.student):
status = att_map.get(stud.student).get(date, "None") status = att_map.get(stud.student).get(date, "None")
elif not student_status: elif not student_status:
status = "Inactive" status = "Inactive"
else: else:
status = "None" status = "None"
status_map = {"Present": "P", "Absent": "A", "None": "", "Inactive":"-"}
status_map = {"Present": "P", "Absent": "A", "None": "", "Inactive":"-", "Holiday":"H"}
row.append(status_map[status]) row.append(status_map[status])
if status == "Present": if status == "Present":
total_p += 1 total_p += 1
elif status == "Absent": elif status == "Absent":
total_a += 1 total_a += 1
date = add_days(date, 1) date = add_days(date, 1)
row += [total_p, total_a] row += [total_p, total_a]
data.append(row) data.append(row)
return columns, data return columns, data
@@ -63,14 +71,19 @@ def get_attendance_list(from_date, to_date, student_group, students_list):
and date between %s and %s and date between %s and %s
order by student, date''', order by student, date''',
(student_group, from_date, to_date), as_dict=1) (student_group, from_date, to_date), as_dict=1)
att_map = {} att_map = {}
students_with_leave_application = get_students_with_leave_application(from_date, to_date, students_list) students_with_leave_application = get_students_with_leave_application(from_date, to_date, students_list)
for d in attendance_list: for d in attendance_list:
att_map.setdefault(d.student, frappe._dict()).setdefault(d.date, "") att_map.setdefault(d.student, frappe._dict()).setdefault(d.date, "")
if students_with_leave_application.get(d.date) and d.student in students_with_leave_application.get(d.date): if students_with_leave_application.get(d.date) and d.student in students_with_leave_application.get(d.date):
att_map[d.student][d.date] = "Present" att_map[d.student][d.date] = "Present"
else: else:
att_map[d.student][d.date] = d.status att_map[d.student][d.date] = d.status
att_map = mark_holidays(att_map, from_date, to_date, students_list)
return att_map return att_map
def get_students_with_leave_application(from_date, to_date, students_list): def get_students_with_leave_application(from_date, to_date, students_list):
@@ -108,3 +121,14 @@ def get_attendance_years():
if not year_list: if not year_list:
year_list = [getdate().year] year_list = [getdate().year]
return "\n".join(str(year) for year in year_list) return "\n".join(str(year) for year in year_list)
def mark_holidays(att_map, from_date, to_date, students_list):
holiday_list = get_holiday_list()
holidays = get_holidays(holiday_list)
for dt in daterange(getdate(from_date), getdate(to_date)):
if dt in holidays:
for student in students_list:
att_map.setdefault(student, frappe._dict()).setdefault(dt, "Holiday")
return att_map

View File

@@ -2,12 +2,13 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
import json import json
from frappe.utils import cstr, cint, nowdate, flt from frappe.utils import cstr, cint, nowdate, getdate, flt, get_request_session, get_datetime
from erpnext.erpnext_integrations.utils import validate_webhooks_request from erpnext.erpnext_integrations.utils import validate_webhooks_request
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note, make_sales_invoice from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note, make_sales_invoice
from erpnext.erpnext_integrations.doctype.shopify_settings.sync_product import sync_item_from_shopify from erpnext.erpnext_integrations.doctype.shopify_settings.sync_product import sync_item_from_shopify
from erpnext.erpnext_integrations.doctype.shopify_settings.sync_customer import create_customer from erpnext.erpnext_integrations.doctype.shopify_settings.sync_customer import create_customer
from erpnext.erpnext_integrations.doctype.shopify_log.shopify_log import make_shopify_log, dump_request_data from erpnext.erpnext_integrations.doctype.shopify_log.shopify_log import make_shopify_log, dump_request_data
from erpnext.erpnext_integrations.doctype.shopify_settings.shopify_settings import get_shopify_url, get_header
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
@validate_webhooks_request("Shopify Settings", 'X-Shopify-Hmac-Sha256', secret_key='shared_secret') @validate_webhooks_request("Shopify Settings", 'X-Shopify-Hmac-Sha256', secret_key='shared_secret')
@@ -18,7 +19,7 @@ def store_request_data(order=None, event=None):
dump_request_data(order, event) dump_request_data(order, event)
def sync_sales_order(order, request_id=None): def sync_sales_order(order, request_id=None, old_order_sync=False):
frappe.set_user('Administrator') frappe.set_user('Administrator')
shopify_settings = frappe.get_doc("Shopify Settings") shopify_settings = frappe.get_doc("Shopify Settings")
frappe.flags.request_id = request_id frappe.flags.request_id = request_id
@@ -27,7 +28,7 @@ def sync_sales_order(order, request_id=None):
try: try:
validate_customer(order, shopify_settings) validate_customer(order, shopify_settings)
validate_item(order, shopify_settings) validate_item(order, shopify_settings)
create_order(order, shopify_settings) create_order(order, shopify_settings, old_order_sync=old_order_sync)
except Exception as e: except Exception as e:
make_shopify_log(status="Error", exception=e) make_shopify_log(status="Error", exception=e)
@@ -77,13 +78,13 @@ def validate_item(order, shopify_settings):
if item.get("product_id") and not frappe.db.get_value("Item", {"shopify_product_id": item.get("product_id")}, "name"): if item.get("product_id") and not frappe.db.get_value("Item", {"shopify_product_id": item.get("product_id")}, "name"):
sync_item_from_shopify(shopify_settings, item) sync_item_from_shopify(shopify_settings, item)
def create_order(order, shopify_settings, company=None): def create_order(order, shopify_settings, old_order_sync=False, company=None):
so = create_sales_order(order, shopify_settings, company) so = create_sales_order(order, shopify_settings, company)
if so: if so:
if order.get("financial_status") == "paid": if order.get("financial_status") == "paid":
create_sales_invoice(order, shopify_settings, so) create_sales_invoice(order, shopify_settings, so, old_order_sync=old_order_sync)
if order.get("fulfillments"): if order.get("fulfillments") and not old_order_sync:
create_delivery_note(order, shopify_settings, so) create_delivery_note(order, shopify_settings, so)
def create_sales_order(shopify_order, shopify_settings, company=None): def create_sales_order(shopify_order, shopify_settings, company=None):
@@ -92,7 +93,7 @@ def create_sales_order(shopify_order, shopify_settings, company=None):
so = frappe.db.get_value("Sales Order", {"shopify_order_id": shopify_order.get("id")}, "name") so = frappe.db.get_value("Sales Order", {"shopify_order_id": shopify_order.get("id")}, "name")
if not so: if not so:
items = get_order_items(shopify_order.get("line_items"), shopify_settings) items = get_order_items(shopify_order.get("line_items"), shopify_settings, getdate(shopify_order.get('created_at')))
if not items: if not items:
message = 'Following items exists in the shopify order but relevant records were not found in the shopify Product master' message = 'Following items exists in the shopify order but relevant records were not found in the shopify Product master'
@@ -106,8 +107,10 @@ def create_sales_order(shopify_order, shopify_settings, company=None):
"doctype": "Sales Order", "doctype": "Sales Order",
"naming_series": shopify_settings.sales_order_series or "SO-Shopify-", "naming_series": shopify_settings.sales_order_series or "SO-Shopify-",
"shopify_order_id": shopify_order.get("id"), "shopify_order_id": shopify_order.get("id"),
"shopify_order_number": shopify_order.get("name"),
"customer": customer or shopify_settings.default_customer, "customer": customer or shopify_settings.default_customer,
"delivery_date": nowdate(), "transaction_date": getdate(shopify_order.get("created_at")) or nowdate(),
"delivery_date": getdate(shopify_order.get("created_at")) or nowdate(),
"company": shopify_settings.company, "company": shopify_settings.company,
"selling_price_list": shopify_settings.price_list, "selling_price_list": shopify_settings.price_list,
"ignore_pricing_rule": 1, "ignore_pricing_rule": 1,
@@ -132,32 +135,42 @@ def create_sales_order(shopify_order, shopify_settings, company=None):
frappe.db.commit() frappe.db.commit()
return so return so
def create_sales_invoice(shopify_order, shopify_settings, so): def create_sales_invoice(shopify_order, shopify_settings, so, old_order_sync=False):
if not frappe.db.get_value("Sales Invoice", {"shopify_order_id": shopify_order.get("id")}, "name")\ if not frappe.db.get_value("Sales Invoice", {"shopify_order_id": shopify_order.get("id")}, "name")\
and so.docstatus==1 and not so.per_billed and cint(shopify_settings.sync_sales_invoice): and so.docstatus==1 and not so.per_billed and cint(shopify_settings.sync_sales_invoice):
if old_order_sync:
posting_date = getdate(shopify_order.get('created_at'))
else:
posting_date = nowdate()
si = make_sales_invoice(so.name, ignore_permissions=True) si = make_sales_invoice(so.name, ignore_permissions=True)
si.shopify_order_id = shopify_order.get("id") si.shopify_order_id = shopify_order.get("id")
si.shopify_order_number = shopify_order.get("name")
si.set_posting_time = 1
si.posting_date = posting_date
si.due_date = posting_date
si.naming_series = shopify_settings.sales_invoice_series or "SI-Shopify-" si.naming_series = shopify_settings.sales_invoice_series or "SI-Shopify-"
si.flags.ignore_mandatory = True si.flags.ignore_mandatory = True
set_cost_center(si.items, shopify_settings.cost_center) set_cost_center(si.items, shopify_settings.cost_center)
si.insert(ignore_mandatory=True) si.insert(ignore_mandatory=True)
si.submit() si.submit()
make_payament_entry_against_sales_invoice(si, shopify_settings) make_payament_entry_against_sales_invoice(si, shopify_settings, posting_date)
frappe.db.commit() frappe.db.commit()
def set_cost_center(items, cost_center): def set_cost_center(items, cost_center):
for item in items: for item in items:
item.cost_center = cost_center item.cost_center = cost_center
def make_payament_entry_against_sales_invoice(doc, shopify_settings): def make_payament_entry_against_sales_invoice(doc, shopify_settings, posting_date=None):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
payemnt_entry = get_payment_entry(doc.doctype, doc.name, bank_account=shopify_settings.cash_bank_account) payment_entry = get_payment_entry(doc.doctype, doc.name, bank_account=shopify_settings.cash_bank_account)
payemnt_entry.flags.ignore_mandatory = True payment_entry.flags.ignore_mandatory = True
payemnt_entry.reference_no = doc.name payment_entry.reference_no = doc.name
payemnt_entry.reference_date = nowdate() payment_entry.posting_date = posting_date or nowdate()
payemnt_entry.insert(ignore_permissions=True) payment_entry.reference_date = posting_date or nowdate()
payemnt_entry.submit() payment_entry.insert(ignore_permissions=True)
payment_entry.submit()
def create_delivery_note(shopify_order, shopify_settings, so): def create_delivery_note(shopify_order, shopify_settings, so):
if not cint(shopify_settings.sync_delivery_note): if not cint(shopify_settings.sync_delivery_note):
@@ -169,6 +182,9 @@ def create_delivery_note(shopify_order, shopify_settings, so):
dn = make_delivery_note(so.name) dn = make_delivery_note(so.name)
dn.shopify_order_id = fulfillment.get("order_id") dn.shopify_order_id = fulfillment.get("order_id")
dn.shopify_order_number = shopify_order.get("name")
dn.set_posting_time = 1
dn.posting_date = getdate(fulfillment.get("created_at"))
dn.shopify_fulfillment_id = fulfillment.get("id") dn.shopify_fulfillment_id = fulfillment.get("id")
dn.naming_series = shopify_settings.delivery_note_series or "DN-Shopify-" dn.naming_series = shopify_settings.delivery_note_series or "DN-Shopify-"
dn.items = get_fulfillment_items(dn.items, fulfillment.get("line_items"), shopify_settings) dn.items = get_fulfillment_items(dn.items, fulfillment.get("line_items"), shopify_settings)
@@ -187,7 +203,7 @@ def get_discounted_amount(order):
discounted_amount += flt(discount.get("amount")) discounted_amount += flt(discount.get("amount"))
return discounted_amount return discounted_amount
def get_order_items(order_items, shopify_settings): def get_order_items(order_items, shopify_settings, delivery_date):
items = [] items = []
all_product_exists = True all_product_exists = True
product_not_exists = [] product_not_exists = []
@@ -205,7 +221,7 @@ def get_order_items(order_items, shopify_settings):
"item_code": item_code, "item_code": item_code,
"item_name": shopify_item.get("name"), "item_name": shopify_item.get("name"),
"rate": shopify_item.get("price"), "rate": shopify_item.get("price"),
"delivery_date": nowdate(), "delivery_date": delivery_date,
"qty": shopify_item.get("quantity"), "qty": shopify_item.get("quantity"),
"stock_uom": shopify_item.get("uom") or _("Nos"), "stock_uom": shopify_item.get("uom") or _("Nos"),
"warehouse": shopify_settings.warehouse "warehouse": shopify_settings.warehouse
@@ -265,3 +281,64 @@ def get_tax_account_head(tax):
frappe.throw(_("Tax Account not specified for Shopify Tax {0}").format(tax.get("title"))) frappe.throw(_("Tax Account not specified for Shopify Tax {0}").format(tax.get("title")))
return tax_account return tax_account
@frappe.whitelist(allow_guest=True)
def sync_old_orders():
frappe.set_user('Administrator')
shopify_settings = frappe.get_doc('Shopify Settings')
if not shopify_settings.sync_missing_orders:
return
url = get_url(shopify_settings)
session = get_request_session()
try:
res = session.get(url, headers=get_header(shopify_settings))
res.raise_for_status()
orders = res.json()["orders"]
for order in orders:
if is_sync_complete(shopify_settings, order):
stop_sync(shopify_settings)
return
sync_sales_order(order=order, old_order_sync=True)
last_order_id = order.get('id')
if last_order_id:
shopify_settings.load_from_db()
shopify_settings.last_order_id = last_order_id
shopify_settings.save()
frappe.db.commit()
except Exception as e:
raise e
def stop_sync(shopify_settings):
shopify_settings.sync_missing_orders = 0
shopify_settings.last_order_id = ''
shopify_settings.save()
frappe.db.commit()
def get_url(shopify_settings):
last_order_id = shopify_settings.last_order_id
if not last_order_id:
if shopify_settings.sync_based_on == 'Date':
url = get_shopify_url("admin/api/2020-10/orders.json?limit=250&created_at_min={0}&since_id=0".format(
get_datetime(shopify_settings.from_date)), shopify_settings)
else:
url = get_shopify_url("admin/api/2020-10/orders.json?limit=250&since_id={0}".format(
shopify_settings.from_order_id), shopify_settings)
else:
url = get_shopify_url("admin/api/2020-10/orders.json?limit=250&since_id={0}".format(last_order_id), shopify_settings)
return url
def is_sync_complete(shopify_settings, order):
if shopify_settings.sync_based_on == 'Date':
return getdate(shopify_settings.to_date) < getdate(order.get('created_at'))
else:
return cstr(order.get('id')) == cstr(shopify_settings.to_order_id)

View File

@@ -1,7 +1,9 @@
{ {
"actions": [],
"creation": "2015-05-18 05:21:07.270859", "creation": "2015-05-18 05:21:07.270859",
"doctype": "DocType", "doctype": "DocType",
"document_type": "System", "document_type": "System",
"engine": "InnoDB",
"field_order": [ "field_order": [
"status_html", "status_html",
"enable_shopify", "enable_shopify",
@@ -40,7 +42,16 @@
"sales_invoice_series", "sales_invoice_series",
"section_break_22", "section_break_22",
"html_16", "html_16",
"taxes" "taxes",
"syncing_details_section",
"sync_missing_orders",
"sync_based_on",
"column_break_41",
"from_date",
"to_date",
"from_order_id",
"to_order_id",
"last_order_id"
], ],
"fields": [ "fields": [
{ {
@@ -255,10 +266,71 @@
"fieldtype": "Table", "fieldtype": "Table",
"label": "Shopify Tax Account", "label": "Shopify Tax Account",
"options": "Shopify Tax Account" "options": "Shopify Tax Account"
},
{
"collapsible": 1,
"fieldname": "syncing_details_section",
"fieldtype": "Section Break",
"label": "Syncing Missing Orders"
},
{
"depends_on": "eval:doc.sync_missing_orders",
"fieldname": "last_order_id",
"fieldtype": "Data",
"label": "Last Order Id",
"read_only": 1
},
{
"fieldname": "column_break_41",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "On checking this Order from the ",
"fieldname": "sync_missing_orders",
"fieldtype": "Check",
"label": "Sync Missing Old Shopify Orders"
},
{
"depends_on": "eval:doc.sync_missing_orders",
"fieldname": "sync_based_on",
"fieldtype": "Select",
"label": "Sync Based On",
"mandatory_depends_on": "eval:doc.sync_missing_orders",
"options": "\nDate\nShopify Order Id"
},
{
"depends_on": "eval:doc.sync_based_on == 'Date' && doc.sync_missing_orders",
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From Date",
"mandatory_depends_on": "eval:doc.sync_based_on == 'Date' && doc.sync_missing_orders"
},
{
"depends_on": "eval:doc.sync_based_on == 'Date' && doc.sync_missing_orders",
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To Date",
"mandatory_depends_on": "eval:doc.sync_based_on == 'Date' && doc.sync_missing_orders"
},
{
"depends_on": "eval:doc.sync_based_on == 'Shopify Order Id' && doc.sync_missing_orders",
"fieldname": "from_order_id",
"fieldtype": "Data",
"label": "From Order Id",
"mandatory_depends_on": "eval:doc.sync_based_on == 'Shopify Order Id' && doc.sync_missing_orders"
},
{
"depends_on": "eval:doc.sync_based_on == 'Shopify Order Id' && doc.sync_missing_orders",
"fieldname": "to_order_id",
"fieldtype": "Data",
"label": "To Order Id",
"mandatory_depends_on": "eval:doc.sync_based_on == 'Shopify Order Id' && doc.sync_missing_orders"
} }
], ],
"issingle": 1, "issingle": 1,
"modified": "2020-09-18 17:26:09.703215", "links": [],
"modified": "2020-11-05 20:44:03.664891",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "ERPNext Integrations", "module": "ERPNext Integrations",
"name": "Shopify Settings", "name": "Shopify Settings",

View File

@@ -87,7 +87,7 @@ def get_shopify_url(path, settings):
def get_header(settings): def get_header(settings):
header = {'Content-Type': 'application/json'} header = {'Content-Type': 'application/json'}
return header; return header
@frappe.whitelist() @frappe.whitelist()
def get_series(): def get_series():
@@ -121,17 +121,23 @@ def setup_custom_fields():
], ],
"Sales Order": [ "Sales Order": [
dict(fieldname='shopify_order_id', label='Shopify Order Id', dict(fieldname='shopify_order_id', label='Shopify Order Id',
fieldtype='Data', insert_after='title', read_only=1, print_hide=1) fieldtype='Data', insert_after='title', read_only=1, print_hide=1),
dict(fieldname='shopify_order_number', label='Shopify Order Number',
fieldtype='Data', insert_after='shopify_order_id', read_only=1, print_hide=1)
], ],
"Delivery Note":[ "Delivery Note":[
dict(fieldname='shopify_order_id', label='Shopify Order Id', dict(fieldname='shopify_order_id', label='Shopify Order Id',
fieldtype='Data', insert_after='title', read_only=1, print_hide=1), fieldtype='Data', insert_after='title', read_only=1, print_hide=1),
dict(fieldname='shopify_order_number', label='Shopify Order Number',
fieldtype='Data', insert_after='shopify_order_id', read_only=1, print_hide=1),
dict(fieldname='shopify_fulfillment_id', label='Shopify Fulfillment Id', dict(fieldname='shopify_fulfillment_id', label='Shopify Fulfillment Id',
fieldtype='Data', insert_after='title', read_only=1, print_hide=1) fieldtype='Data', insert_after='title', read_only=1, print_hide=1)
], ],
"Sales Invoice": [ "Sales Invoice": [
dict(fieldname='shopify_order_id', label='Shopify Order Id', dict(fieldname='shopify_order_id', label='Shopify Order Id',
fieldtype='Data', insert_after='title', read_only=1, print_hide=1) fieldtype='Data', insert_after='title', read_only=1, print_hide=1),
dict(fieldname='shopify_order_number', label='Shopify Order Number',
fieldtype='Data', insert_after='shopify_order_id', read_only=1, print_hide=1)
] ]
} }

View File

@@ -75,7 +75,7 @@ class ShopifySettings(unittest.TestCase):
with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_order.json")) as shopify_order: with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_order.json")) as shopify_order:
shopify_order = json.load(shopify_order) shopify_order = json.load(shopify_order)
create_order(shopify_order.get("order"), self.shopify_settings, "_Test Company") create_order(shopify_order.get("order"), self.shopify_settings, False, company="_Test Company")
sales_order = frappe.get_doc("Sales Order", {"shopify_order_id": cstr(shopify_order.get("order").get("id"))}) sales_order = frappe.get_doc("Sales Order", {"shopify_order_id": cstr(shopify_order.get("order").get("id"))})

View File

@@ -43,7 +43,7 @@
{ {
"hidden": 0, "hidden": 0,
"label": "Reports", "label": "Reports",
"links": "[\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Patient Appointment Analytics\",\n\t\t\"doctype\": \"Patient Appointment\"\n\t},\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Lab Test Report\",\n\t\t\"doctype\": \"Lab Test\",\n\t\t\"label\": \"Lab Test Report\"\n\t}\n]" "links": "[\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Patient Appointment Analytics\",\n\t\t\"doctype\": \"Patient Appointment\"\n\t},\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Lab Test Report\",\n\t\t\"doctype\": \"Lab Test\",\n\t\t\"label\": \"Lab Test Report\"\n\t},\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Inpatient Medication Orders\",\n\t\t\"doctype\": \"Inpatient Medication Order\",\n\t\t\"label\": \"Inpatient Medication Orders\"\n\t}\n]"
} }
], ],
"category": "Domains", "category": "Domains",
@@ -64,7 +64,7 @@
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"label": "Healthcare", "label": "Healthcare",
"modified": "2020-06-25 23:50:56.951698", "modified": "2020-11-23 23:00:48.764377",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Healthcare", "name": "Healthcare",

View File

@@ -274,4 +274,6 @@ def get_filters(entry):
def get_current_healthcare_service_unit(inpatient_record): def get_current_healthcare_service_unit(inpatient_record):
ip_record = frappe.get_doc('Inpatient Record', inpatient_record) ip_record = frappe.get_doc('Inpatient Record', inpatient_record)
return ip_record.inpatient_occupancies[-1].service_unit if ip_record.inpatient_occupancies:
return ip_record.inpatient_occupancies[-1].service_unit
return

View File

@@ -0,0 +1,57 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Inpatient Medication Orders"] = {
"filters": [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1
},
{
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
reqd: 1
},
{
fieldname: "to_date",
label: __("To Date"),
fieldtype: "Date",
default: frappe.datetime.now_date(),
reqd: 1
},
{
fieldname: "patient",
label: __("Patient"),
fieldtype: "Link",
options: "Patient"
},
{
fieldname: "service_unit",
label: __("Healthcare Service Unit"),
fieldtype: "Link",
options: "Healthcare Service Unit",
get_query: () => {
var company = frappe.query_report.get_filter_value('company');
return {
filters: {
'company': company,
'is_group': 0
}
}
}
},
{
fieldname: "show_completed_orders",
label: __("Show Completed Orders"),
fieldtype: "Check",
default: 1
}
]
};

View File

@@ -0,0 +1,36 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2020-11-23 17:25:58.802949",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"json": "{}",
"modified": "2020-11-23 19:40:20.227591",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Inpatient Medication Orders",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Inpatient Medication Order",
"report_name": "Inpatient Medication Orders",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
},
{
"role": "Healthcare Administrator"
},
{
"role": "Nursing User"
},
{
"role": "Physician"
}
]
}

View File

@@ -0,0 +1,198 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry import get_current_healthcare_service_unit
def execute(filters=None):
columns = get_columns()
data = get_data(filters)
chart = get_chart_data(data)
return columns, data, None, chart
def get_columns():
return [
{
"fieldname": "patient",
"fieldtype": "Link",
"label": "Patient",
"options": "Patient",
"width": 200
},
{
"fieldname": "healthcare_service_unit",
"fieldtype": "Link",
"label": "Healthcare Service Unit",
"options": "Healthcare Service Unit",
"width": 150
},
{
"fieldname": "drug",
"fieldtype": "Link",
"label": "Drug Code",
"options": "Item",
"width": 150
},
{
"fieldname": "drug_name",
"fieldtype": "Data",
"label": "Drug Name",
"width": 150
},
{
"fieldname": "dosage",
"fieldtype": "Link",
"label": "Dosage",
"options": "Prescription Dosage",
"width": 80
},
{
"fieldname": "dosage_form",
"fieldtype": "Link",
"label": "Dosage Form",
"options": "Dosage Form",
"width": 100
},
{
"fieldname": "date",
"fieldtype": "Date",
"label": "Date",
"width": 100
},
{
"fieldname": "time",
"fieldtype": "Time",
"label": "Time",
"width": 100
},
{
"fieldname": "is_completed",
"fieldtype": "Check",
"label": "Is Order Completed",
"width": 100
},
{
"fieldname": "healthcare_practitioner",
"fieldtype": "Link",
"label": "Healthcare Practitioner",
"options": "Healthcare Practitioner",
"width": 200
},
{
"fieldname": "inpatient_medication_entry",
"fieldtype": "Link",
"label": "Inpatient Medication Entry",
"options": "Inpatient Medication Entry",
"width": 200
},
{
"fieldname": "inpatient_record",
"fieldtype": "Link",
"label": "Inpatient Record",
"options": "Inpatient Record",
"width": 200
}
]
def get_data(filters):
conditions, values = get_conditions(filters)
data = frappe.db.sql("""
SELECT
parent.patient, parent.inpatient_record, parent.practitioner,
child.drug, child.drug_name, child.dosage, child.dosage_form,
child.date, child.time, child.is_completed, child.name
FROM `tabInpatient Medication Order` parent
INNER JOIN `tabInpatient Medication Order Entry` child
ON child.parent = parent.name
WHERE
parent.docstatus = 1
{conditions}
ORDER BY date, time
""".format(conditions=conditions), values, as_dict=1)
data = get_inpatient_details(data, filters.get("service_unit"))
return data
def get_conditions(filters):
conditions = ""
values = dict()
if filters.get("company"):
conditions += " AND parent.company = %(company)s"
values["company"] = filters.get("company")
if filters.get("from_date") and filters.get("to_date"):
conditions += " AND child.date BETWEEN %(from_date)s and %(to_date)s"
values["from_date"] = filters.get("from_date")
values["to_date"] = filters.get("to_date")
if filters.get("patient"):
conditions += " AND parent.patient = %(patient)s"
values["patient"] = filters.get("patient")
if not filters.get("show_completed_orders"):
conditions += " AND child.is_completed = 0"
return conditions, values
def get_inpatient_details(data, service_unit):
service_unit_filtered_data = []
for entry in data:
entry["healthcare_service_unit"] = get_current_healthcare_service_unit(entry.inpatient_record)
if entry.is_completed:
entry["inpatient_medication_entry"] = get_inpatient_medication_entry(entry.name)
if service_unit and entry.healthcare_service_unit and service_unit != entry.healthcare_service_unit:
service_unit_filtered_data.append(entry)
entry.pop("name", None)
for entry in service_unit_filtered_data:
data.remove(entry)
return data
def get_inpatient_medication_entry(order_entry):
return frappe.db.get_value("Inpatient Medication Entry Detail", {"against_imoe": order_entry}, "parent")
def get_chart_data(data):
if not data:
return None
labels = ["Pending", "Completed"]
datasets = []
status_wise_data = {
"Pending": 0,
"Completed": 0
}
for d in data:
if d.is_completed:
status_wise_data["Completed"] += 1
else:
status_wise_data["Pending"] += 1
datasets.append({
"name": "Inpatient Medication Order Status",
"values": [status_wise_data.get("Pending"), status_wise_data.get("Completed")]
})
chart = {
"data": {
"labels": labels,
"datasets": datasets
},
"type": "donut",
"height": 300
}
chart["fieldtype"] = "Data"
return chart

View File

@@ -0,0 +1,128 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import unittest
import frappe
import datetime
from frappe.utils import getdate, now_datetime
from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import create_patient, create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
from erpnext.healthcare.doctype.inpatient_medication_order.test_inpatient_medication_order import create_ipmo, create_ipme
from erpnext.healthcare.report.inpatient_medication_orders.inpatient_medication_orders import execute
class TestInpatientMedicationOrders(unittest.TestCase):
@classmethod
def setUpClass(self):
frappe.db.sql("delete from `tabInpatient Medication Order` where company='_Test Company'")
frappe.db.sql("delete from `tabInpatient Medication Entry` where company='_Test Company'")
self.patient = create_patient()
self.ip_record = create_records(self.patient)
def test_inpatient_medication_orders_report(self):
filters = {
'company': '_Test Company',
'from_date': getdate(),
'to_date': getdate(),
'patient': '_Test IPD Patient',
'service_unit': 'Test Service Unit Ip Occupancy - _TC'
}
report = execute(filters)
expected_data = [
{
'patient': '_Test IPD Patient',
'inpatient_record': self.ip_record.name,
'practitioner': None,
'drug': 'Dextromethorphan',
'drug_name': 'Dextromethorphan',
'dosage': 1.0,
'dosage_form': 'Tablet',
'date': getdate(),
'time': datetime.timedelta(seconds=32400),
'is_completed': 0,
'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC'
},
{
'patient': '_Test IPD Patient',
'inpatient_record': self.ip_record.name,
'practitioner': None,
'drug': 'Dextromethorphan',
'drug_name': 'Dextromethorphan',
'dosage': 1.0,
'dosage_form': 'Tablet',
'date': getdate(),
'time': datetime.timedelta(seconds=50400),
'is_completed': 0,
'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC'
},
{
'patient': '_Test IPD Patient',
'inpatient_record': self.ip_record.name,
'practitioner': None,
'drug': 'Dextromethorphan',
'drug_name': 'Dextromethorphan',
'dosage': 1.0,
'dosage_form': 'Tablet',
'date': getdate(),
'time': datetime.timedelta(seconds=75600),
'is_completed': 0,
'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC'
}
]
self.assertEqual(expected_data, report[1])
filters = frappe._dict(from_date=getdate(), to_date=getdate(), from_time='', to_time='')
ipme = create_ipme(filters)
ipme.submit()
filters = {
'company': '_Test Company',
'from_date': getdate(),
'to_date': getdate(),
'patient': '_Test IPD Patient',
'service_unit': 'Test Service Unit Ip Occupancy - _TC',
'show_completed_orders': 0
}
report = execute(filters)
self.assertEqual(len(report[1]), 0)
def tearDown(self):
if frappe.db.get_value('Patient', self.patient, 'inpatient_record'):
# cleanup - Discharge
schedule_discharge(frappe.as_json({'patient': self.patient}))
self.ip_record.reload()
mark_invoiced_inpatient_occupancy(self.ip_record)
self.ip_record.reload()
discharge_patient(self.ip_record)
for entry in frappe.get_all('Inpatient Medication Entry'):
doc = frappe.get_doc('Inpatient Medication Entry', entry.name)
doc.cancel()
doc.delete()
for entry in frappe.get_all('Inpatient Medication Order'):
doc = frappe.get_doc('Inpatient Medication Order', entry.name)
doc.cancel()
doc.delete()
def create_records(patient):
frappe.db.sql("""delete from `tabInpatient Record`""")
# Admit
ip_record = create_inpatient(patient)
ip_record.expected_length_of_stay = 0
ip_record.save()
ip_record.reload()
service_unit = get_healthcare_service_unit()
admit_patient(ip_record, service_unit, now_datetime())
ipmo = create_ipmo(patient)
ipmo.submit()
return ip_record

View File

@@ -15,10 +15,10 @@ app_logo_url = '/assets/erpnext/images/erp-icon.svg'
develop_version = '13.x.x-develop' develop_version = '13.x.x-develop'
app_include_js = "assets/js/erpnext.min.js" app_include_js = "/assets/js/erpnext.min.js"
app_include_css = "assets/css/erpnext.css" app_include_css = "/assets/css/erpnext.css"
web_include_js = "assets/js/erpnext-web.min.js" web_include_js = "/assets/js/erpnext-web.min.js"
web_include_css = "assets/css/erpnext-web.css" web_include_css = "/assets/css/erpnext-web.css"
doctype_js = { doctype_js = {
"Address": "public/js/address.js", "Address": "public/js/address.js",
@@ -237,6 +237,9 @@ doc_events = {
"Website Settings": { "Website Settings": {
"validate": "erpnext.portal.doctype.products_settings.products_settings.home_page_is_products" "validate": "erpnext.portal.doctype.products_settings.products_settings.home_page_is_products"
}, },
"Tax Category": {
"validate": "erpnext.regional.india.utils.validate_tax_category"
},
"Sales Invoice": { "Sales Invoice": {
"on_submit": [ "on_submit": [
"erpnext.regional.create_transaction_log", "erpnext.regional.create_transaction_log",
@@ -250,7 +253,11 @@ doc_events = {
"on_trash": "erpnext.regional.check_deletion_permission" "on_trash": "erpnext.regional.check_deletion_permission"
}, },
"Purchase Invoice": { "Purchase Invoice": {
"validate": "erpnext.regional.india.utils.update_grand_total_for_rcm" "validate": [
"erpnext.regional.india.utils.update_grand_total_for_rcm",
"erpnext.regional.united_arab_emirates.utils.update_grand_total_for_rcm",
"erpnext.regional.united_arab_emirates.utils.validate_returns"
]
}, },
"Payment Entry": { "Payment Entry": {
"on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning"], "on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
@@ -307,6 +314,7 @@ scheduler_events = {
"erpnext.projects.doctype.project.project.collect_project_status", "erpnext.projects.doctype.project.project.collect_project_status",
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts", "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
"erpnext.support.doctype.issue.issue.set_service_level_agreement_variance", "erpnext.support.doctype.issue.issue.set_service_level_agreement_variance",
"erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders",
], ],
"daily": [ "daily": [
"erpnext.stock.reorder_item.reorder_item", "erpnext.stock.reorder_item.reorder_item",
@@ -390,7 +398,8 @@ regional_overrides = {
'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries' 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries'
}, },
'United Arab Emirates': { 'United Arab Emirates': {
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data' 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data',
'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.united_arab_emirates.utils.make_regional_gl_entries',
}, },
'Saudi Arabia': { 'Saudi Arabia': {
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data' 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data'

View File

@@ -20,7 +20,7 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters):
approvers = [] approvers = []
department_details = {} department_details = {}
department_list = [] department_list = []
employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver", "expense_approver", "shift_request_approver"], as_dict=True) employee = frappe.get_value("Employee", filters.get("employee"), ["employee_name","department", "leave_approver", "expense_approver", "shift_request_approver"], as_dict=True)
employee_department = filters.get("department") or employee.department employee_department = filters.get("department") or employee.department
if employee_department: if employee_department:
@@ -59,11 +59,9 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters):
and approver.approver=user.name""",(d, "%" + txt + "%", parentfield), as_list=True) and approver.approver=user.name""",(d, "%" + txt + "%", parentfield), as_list=True)
if len(approvers) == 0: if len(approvers) == 0:
frappe.throw(_("Please set {0} for the Employee or for Department: {1}"). error_msg = _("Please set {0} for the Employee: {1}").format(field_name, frappe.bold(employee.employee_name))
format( if department_list:
field_name, frappe.bold(employee_department), error_msg += _(" or for Department: {0}").format(frappe.bold(employee_department))
frappe.bold(employee.name) frappe.throw(error_msg, title=_(field_name + " Missing"))
),
title=_(field_name + " Missing"))
return set(tuple(approver) for approver in approvers) return set(tuple(approver) for approver in approvers)

View File

@@ -32,7 +32,7 @@ class LeaveEncashment(Document):
additional_salary.employee = self.employee additional_salary.employee = self.employee
earning_component = frappe.get_value("Leave Type", self.leave_type, "earning_component") earning_component = frappe.get_value("Leave Type", self.leave_type, "earning_component")
if not earning_component: if not earning_component:
frappe.throw(_("Please set Earning Component for Leave type: {0}.".format(self.leave_type))) frappe.throw(_("Please set Earning Component for Leave type: {0}.").format(self.leave_type))
additional_salary.salary_component = earning_component additional_salary.salary_component = earning_component
additional_salary.payroll_date = self.encashment_date additional_salary.payroll_date = self.encashment_date
additional_salary.amount = self.encashment_amount additional_salary.amount = self.encashment_amount
@@ -98,7 +98,11 @@ class LeaveEncashment(Document):
create_leave_ledger_entry(self, args, submit) create_leave_ledger_entry(self, args, submit)
# create reverse entry for expired leaves # create reverse entry for expired leaves
to_date = self.get_leave_allocation().get('to_date') leave_allocation = self.get_leave_allocation()
if not leave_allocation:
return
to_date = leave_allocation.get('to_date')
if to_date < getdate(nowdate()): if to_date < getdate(nowdate()):
args = frappe._dict( args = frappe._dict(
leaves=self.encashable_days, leaves=self.encashable_days,

View File

@@ -24,10 +24,10 @@ erpnext.hr.AttendanceControlPanel = frappe.ui.form.Controller.extend({
} }
window.location.href = repl(frappe.request.url + window.location.href = repl(frappe.request.url +
'?cmd=%(cmd)s&from_date=%(from_date)s&to_date=%(to_date)s', { '?cmd=%(cmd)s&from_date=%(from_date)s&to_date=%(to_date)s', {
cmd: "erpnext.hr.doctype.upload_attendance.upload_attendance.get_template", cmd: "erpnext.hr.doctype.upload_attendance.upload_attendance.get_template",
from_date: this.frm.doc.att_fr_date, from_date: this.frm.doc.att_fr_date,
to_date: this.frm.doc.att_to_date, to_date: this.frm.doc.att_to_date,
}); });
}, },
show_upload() { show_upload() {

View File

@@ -28,7 +28,12 @@ def get_template():
w = UnicodeWriter() w = UnicodeWriter()
w = add_header(w) w = add_header(w)
w = add_data(w, args) try:
w = add_data(w, args)
except Exception as e:
frappe.clear_messages()
frappe.respond_as_web_page("Holiday List Missing", html=e)
return
# write out response as a type csv # write out response as a type csv
frappe.response['result'] = cstr(w.getvalue()) frappe.response['result'] = cstr(w.getvalue())

View File

@@ -3,7 +3,7 @@
{ {
"hidden": 0, "hidden": 0,
"label": "Loan", "label": "Loan",
"links": "[\n {\n \"description\": \"Loan Type for interest and penalty rates\",\n \"label\": \"Loan Type\",\n \"name\": \"Loan Type\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Loan Applications from customers and employees.\",\n \"label\": \"Loan Application\",\n \"name\": \"Loan Application\",\n \"type\": \"doctype\"\n },\n { \"dependencies\": [\n \"Loan Type\"\n ],\n \"description\": \"Loans provided to customers and employees.\",\n \"label\": \"Loan\",\n \"name\": \"Loan\",\n \"type\": \"doctype\"\n }\n]" "links": "[\n {\n \"description\": \"Loan Type for interest and penalty rates\",\n \"label\": \"Loan Type\",\n \"name\": \"Loan Type\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Loan Applications from customers and employees.\",\n \"label\": \"Loan Application\",\n \"name\": \"Loan Application\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Loans provided to customers and employees.\",\n \"label\": \"Loan\",\n \"name\": \"Loan\",\n \"type\": \"doctype\"\n }\n]"
}, },
{ {
"hidden": 0, "hidden": 0,
@@ -13,7 +13,7 @@
{ {
"hidden": 0, "hidden": 0,
"label": "Disbursement and Repayment", "label": "Disbursement and Repayment",
"links": "[\n {\n \"label\": \"Loan Disbursement\",\n \"name\": \"Loan Disbursement\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Repayment\",\n \"name\": \"Loan Repayment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Interest Accrual\",\n \"name\": \"Loan Interest Accrual\",\n \"type\": \"doctype\"\n }\n]" "links": "[\n {\n \"label\": \"Loan Disbursement\",\n \"name\": \"Loan Disbursement\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Repayment\",\n \"name\": \"Loan Repayment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Write Off\",\n \"name\": \"Loan Write Off\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Interest Accrual\",\n \"name\": \"Loan Interest Accrual\",\n \"type\": \"doctype\"\n }\n]"
}, },
{ {
"hidden": 0, "hidden": 0,
@@ -34,10 +34,11 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Desk Page", "doctype": "Desk Page",
"extends_another_page": 0, "extends_another_page": 0,
"hide_custom": 0,
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"label": "Loan", "label": "Loan",
"modified": "2020-06-07 19:42:14.947902", "modified": "2020-10-17 12:59:50.336085",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan", "name": "Loan",

View File

@@ -7,10 +7,14 @@ frappe.ui.form.on('Loan', {
setup: function(frm) { setup: function(frm) {
frm.make_methods = { frm.make_methods = {
'Loan Disbursement': function() { frm.trigger('make_loan_disbursement') }, 'Loan Disbursement': function() { frm.trigger('make_loan_disbursement') },
'Loan Security Unpledge': function() { frm.trigger('create_loan_security_unpledge') } 'Loan Security Unpledge': function() { frm.trigger('create_loan_security_unpledge') },
'Loan Write Off': function() { frm.trigger('make_loan_write_off_entry') }
} }
}, },
onload: function (frm) { onload: function (frm) {
// Ignore loan security pledge on cancel of loan
frm.ignore_doctypes_on_cancel_all = ["Loan Security Pledge"];
frm.set_query("loan_application", function () { frm.set_query("loan_application", function () {
return { return {
"filters": { "filters": {
@@ -21,6 +25,14 @@ frappe.ui.form.on('Loan', {
}; };
}); });
frm.set_query("loan_type", function () {
return {
"filters": {
"docstatus": 1
}
};
});
$.each(["penalty_income_account", "interest_income_account"], function(i, field) { $.each(["penalty_income_account", "interest_income_account"], function(i, field) {
frm.set_query(field, function () { frm.set_query(field, function () {
return { return {
@@ -49,24 +61,33 @@ frappe.ui.form.on('Loan', {
refresh: function (frm) { refresh: function (frm) {
if (frm.doc.docstatus == 1) { if (frm.doc.docstatus == 1) {
if (frm.doc.status == "Sanctioned" || frm.doc.status == 'Partially Disbursed') { if (["Disbursed", "Partially Disbursed"].includes(frm.doc.status) && (!frm.doc.repay_from_salary)) {
frm.add_custom_button(__('Request Loan Closure'), function() {
frm.trigger("request_loan_closure");
},__('Status'));
frm.add_custom_button(__('Loan Repayment'), function() {
frm.trigger("make_repayment_entry");
},__('Create'));
}
if (["Sanctioned", "Partially Disbursed"].includes(frm.doc.status)) {
frm.add_custom_button(__('Loan Disbursement'), function() { frm.add_custom_button(__('Loan Disbursement'), function() {
frm.trigger("make_loan_disbursement"); frm.trigger("make_loan_disbursement");
},__('Create')); },__('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") { if (frm.doc.status == "Loan Closure Requested") {
frm.add_custom_button(__('Loan Security Unpledge'), function() { frm.add_custom_button(__('Loan Security Unpledge'), function() {
frm.trigger("create_loan_security_unpledge"); frm.trigger("create_loan_security_unpledge");
},__('Create')); },__('Create'));
} }
if (["Loan Closure Requested", "Disbursed", "Partially Disbursed"].includes(frm.doc.status)) {
frm.add_custom_button(__('Loan Write Off'), function() {
frm.trigger("make_loan_write_off_entry");
},__('Create'));
}
} }
frm.trigger("toggle_fields"); frm.trigger("toggle_fields");
}, },
@@ -117,6 +138,38 @@ frappe.ui.form.on('Loan', {
}) })
}, },
make_loan_write_off_entry: function(frm) {
frappe.call({
args: {
"loan": frm.doc.name,
"company": frm.doc.company,
"as_dict": 1
},
method: "erpnext.loan_management.doctype.loan.loan.make_loan_write_off",
callback: function (r) {
if (r.message)
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
})
},
request_loan_closure: function(frm) {
frappe.confirm(__("Do you really want to close this loan"),
function() {
frappe.call({
args: {
'loan': frm.doc.name
},
method: "erpnext.loan_management.doctype.loan.loan.request_loan_closure",
callback: function() {
frm.reload_doc();
}
});
}
);
},
create_loan_security_unpledge: function(frm) { create_loan_security_unpledge: function(frm) {
frappe.call({ frappe.call({
method: "erpnext.loan_management.doctype.loan.loan.unpledge_security", method: "erpnext.loan_management.doctype.loan.loan.unpledge_security",

View File

@@ -43,6 +43,7 @@
"section_break_17", "section_break_17",
"total_payment", "total_payment",
"total_principal_paid", "total_principal_paid",
"written_off_amount",
"column_break_19", "column_break_19",
"total_interest_payable", "total_interest_payable",
"total_amount_paid", "total_amount_paid",
@@ -75,6 +76,7 @@
"fieldname": "loan_application", "fieldname": "loan_application",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Loan Application", "label": "Loan Application",
"no_copy": 1,
"options": "Loan Application" "options": "Loan Application"
}, },
{ {
@@ -134,6 +136,7 @@
"fieldname": "loan_amount", "fieldname": "loan_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Loan Amount", "label": "Loan Amount",
"non_negative": 1,
"options": "Company:company:default_currency" "options": "Company:company:default_currency"
}, },
{ {
@@ -148,7 +151,8 @@
"depends_on": "eval:doc.status==\"Disbursed\"", "depends_on": "eval:doc.status==\"Disbursed\"",
"fieldname": "disbursement_date", "fieldname": "disbursement_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Disbursement Date" "label": "Disbursement Date",
"no_copy": 1
}, },
{ {
"depends_on": "is_term_loan", "depends_on": "is_term_loan",
@@ -252,6 +256,7 @@
"fieldname": "total_payment", "fieldname": "total_payment",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Total Payable Amount", "label": "Total Payable Amount",
"no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
}, },
@@ -265,6 +270,7 @@
"fieldname": "total_interest_payable", "fieldname": "total_interest_payable",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Total Interest Payable", "label": "Total Interest Payable",
"no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
}, },
@@ -273,6 +279,7 @@
"fieldname": "total_amount_paid", "fieldname": "total_amount_paid",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Total Amount Paid", "label": "Total Amount Paid",
"no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
}, },
@@ -289,8 +296,7 @@
"default": "0", "default": "0",
"fieldname": "is_secured_loan", "fieldname": "is_secured_loan",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Secured Loan", "label": "Is Secured Loan"
"read_only": 1
}, },
{ {
"default": "0", "default": "0",
@@ -313,6 +319,7 @@
"fieldname": "total_principal_paid", "fieldname": "total_principal_paid",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Total Principal Paid", "label": "Total Principal Paid",
"no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
}, },
@@ -320,21 +327,33 @@
"fieldname": "disbursed_amount", "fieldname": "disbursed_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Disbursed Amount", "label": "Disbursed Amount",
"no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:doc.is_secured_loan",
"fetch_from": "loan_application.maximum_loan_amount", "fetch_from": "loan_application.maximum_loan_amount",
"fieldname": "maximum_loan_amount", "fieldname": "maximum_loan_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Maximum Loan Amount", "label": "Maximum Loan Amount",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "written_off_amount",
"fieldtype": "Currency",
"label": "Written Off Amount",
"no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
} }
], ],
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-08-01 12:36:11.255233", "modified": "2020-11-24 12:27:23.208240",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan", "name": "Loan",

View File

@@ -9,6 +9,7 @@ from frappe import _
from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts
class Loan(AccountsController): class Loan(AccountsController):
def validate(self): def validate(self):
@@ -137,9 +138,12 @@ class Loan(AccountsController):
}) })
def unlink_loan_security_pledge(self): def unlink_loan_security_pledge(self):
frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET pledges = frappe.get_all('Loan Security Pledge', fields=['name'], filters={'loan': self.name})
loan = '', status = 'Unpledged' pledge_list = [d.name for d in pledges]
where name = %s """, (self.loan_security_pledge)) if pledge_list:
frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET
loan = '', status = 'Unpledged'
where name in (%s) """ % (', '.join(['%s']*len(pledge_list))), tuple(pledge_list)) #nosec
def update_total_amount_paid(doc): def update_total_amount_paid(doc):
total_amount_paid = 0 total_amount_paid = 0
@@ -182,6 +186,24 @@ def get_monthly_repayment_amount(repayment_method, loan_amount, rate_of_interest
monthly_repayment_amount = math.ceil(flt(loan_amount) / repayment_periods) monthly_repayment_amount = math.ceil(flt(loan_amount) / repayment_periods)
return monthly_repayment_amount return monthly_repayment_amount
@frappe.whitelist()
def request_loan_closure(loan, posting_date=None):
if not posting_date:
posting_date = getdate()
amounts = calculate_amounts(loan, posting_date)
pending_amount = amounts['payable_amount'] + amounts['unaccrued_interest']
loan_type = frappe.get_value('Loan', loan, 'loan_type')
write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount')
# checking greater than 0 as there may be some minor precision error
if pending_amount < write_off_limit:
# update status as loan closure requested
frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
else:
frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount))
@frappe.whitelist() @frappe.whitelist()
def get_loan_application(loan_application): def get_loan_application(loan_application):
loan = frappe.get_doc("Loan Application", loan_application) loan = frappe.get_doc("Loan Application", loan_application)
@@ -200,6 +222,7 @@ def make_loan_disbursement(loan, company, applicant_type, applicant, pending_amo
disbursement_entry.applicant = applicant disbursement_entry.applicant = applicant
disbursement_entry.company = company disbursement_entry.company = company
disbursement_entry.disbursement_date = nowdate() disbursement_entry.disbursement_date = nowdate()
disbursement_entry.posting_date = nowdate()
disbursement_entry.disbursed_amount = pending_amount disbursement_entry.disbursed_amount = pending_amount
if as_dict: if as_dict:
@@ -222,6 +245,38 @@ def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as
else: else:
return repayment_entry return repayment_entry
@frappe.whitelist()
def make_loan_write_off(loan, company=None, posting_date=None, amount=0, as_dict=0):
if not company:
company = frappe.get_value('Loan', loan, 'company')
if not posting_date:
posting_date = getdate()
amounts = calculate_amounts(loan, posting_date)
pending_amount = amounts['pending_principal_amount']
if amount and (amount > pending_amount):
frappe.throw('Write Off amount cannot be greater than pending loan amount')
if not amount:
amount = pending_amount
# get default write off account from company master
write_off_account = frappe.get_value('Company', company, 'write_off_account')
write_off = frappe.new_doc('Loan Write Off')
write_off.loan = loan
write_off.posting_date = posting_date
write_off.write_off_account = write_off_account
write_off.write_off_amount = amount
write_off.save()
if as_dict:
return write_off.as_dict()
else:
return write_off
@frappe.whitelist() @frappe.whitelist()
def unpledge_security(loan=None, loan_security_pledge=None, as_dict=0, save=0, submit=0, approve=0): def unpledge_security(loan=None, loan_security_pledge=None, as_dict=0, save=0, submit=0, approve=0):
# if loan is passed it will be considered as full unpledge # if loan is passed it will be considered as full unpledge

View File

@@ -13,7 +13,7 @@ def get_data():
'items': ['Loan Security Pledge', 'Loan Security Shortfall', 'Loan Disbursement'] 'items': ['Loan Security Pledge', 'Loan Security Shortfall', 'Loan Disbursement']
}, },
{ {
'items': ['Loan Repayment', 'Loan Interest Accrual', 'Loan Security Unpledge'] 'items': ['Loan Repayment', 'Loan Interest Accrual', 'Loan Write Off', 'Loan Security Unpledge']
} }
] ]
} }

View File

@@ -0,0 +1,16 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.listview_settings['Loan'] = {
get_indicator: function(doc) {
var status_color = {
"Draft": "red",
"Sanctioned": "blue",
"Disbursed": "orange",
"Partially Disbursed": "yellow",
"Loan Closure Requested": "green",
"Closed": "green"
};
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
},
};

View File

@@ -14,7 +14,7 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_
process_loan_interest_accrual_for_term_loans) process_loan_interest_accrual_for_term_loans)
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year
from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall import create_process_loan_security_shortfall from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall import create_process_loan_security_shortfall
from erpnext.loan_management.doctype.loan.loan import unpledge_security from erpnext.loan_management.doctype.loan.loan import unpledge_security, request_loan_closure, make_loan_write_off
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge
from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount
@@ -132,7 +132,7 @@ class TestLoan(unittest.TestCase):
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application) create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit() loan.submit()
self.assertEquals(loan.loan_amount, 1000000) self.assertEquals(loan.loan_amount, 1000000)
@@ -142,30 +142,30 @@ class TestLoan(unittest.TestCase):
no_of_days = date_diff(last_date, first_date) + 1 no_of_days = date_diff(last_date, first_date) + 1
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ accrued_interest_amount = flt((loan.loan_amount * loan.rate_of_interest * no_of_days)
/ (days_in_year(get_datetime(first_date).year) * 100) / (days_in_year(get_datetime(first_date).year) * 100), 2)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date) process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), "Regular Payment", 111118.68) repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), 111119)
repayment_entry.save() repayment_entry.save()
repayment_entry.submit() repayment_entry.submit()
penalty_amount = (accrued_interest_amount * 4 * 25) / (100 * days_in_year(get_datetime(first_date).year)) penalty_amount = (accrued_interest_amount * 5 * 25) / 100
self.assertEquals(flt(repayment_entry.penalty_amount, 2), flt(penalty_amount, 2)) self.assertEquals(flt(repayment_entry.penalty_amount,0), flt(penalty_amount, 0))
amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', amounts = frappe.db.get_all('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount'])
'paid_principal_amount'])
loan.load_from_db() loan.load_from_db()
self.assertEquals(amounts[0], repayment_entry.interest_payable) total_interest_paid = amounts[0]['paid_interest_amount'] + amounts[1]['paid_interest_amount']
self.assertEquals(flt(loan.total_principal_paid, 2), flt(repayment_entry.amount_paid - self.assertEquals(amounts[1]['paid_interest_amount'], repayment_entry.interest_payable)
penalty_amount - amounts[0], 2)) self.assertEquals(flt(loan.total_principal_paid, 0), flt(repayment_entry.amount_paid -
penalty_amount - total_interest_paid, 0))
def test_loan_closure_repayment(self): def test_loan_closure(self):
pledge = [{ pledge = [{
"loan_security": "Test Security 1", "loan_security": "Test Security 1",
"qty": 4000.00 "qty": 4000.00
@@ -174,7 +174,7 @@ class TestLoan(unittest.TestCase):
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application) create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit() loan.submit()
self.assertEquals(loan.loan_amount, 1000000) self.assertEquals(loan.loan_amount, 1000000)
@@ -184,10 +184,10 @@ class TestLoan(unittest.TestCase):
no_of_days = date_diff(last_date, first_date) + 1 no_of_days = date_diff(last_date, first_date) + 1
# Adding 6 since repayment is made 5 days late after due date # Adding 5 since repayment is made 5 days late after due date
# and since payment type is loan closure so interest should be considered for those # and since payment type is loan closure so interest should be considered for those
# 6 days as well though in grace period # 5 days as well though in grace period
no_of_days += 6 no_of_days += 5
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100) / (days_in_year(get_datetime(first_date).year) * 100)
@@ -195,15 +195,17 @@ class TestLoan(unittest.TestCase):
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date) process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
"Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) flt(loan.loan_amount + accrued_interest_amount))
repayment_entry.submit() repayment_entry.submit()
amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)']) amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)'])
self.assertEquals(flt(amount, 2),flt(accrued_interest_amount, 2)) self.assertEquals(flt(amount, 0),flt(accrued_interest_amount, 0))
self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0)
request_loan_closure(loan.name)
loan.load_from_db() loan.load_from_db()
self.assertEquals(loan.status, "Loan Closure Requested") self.assertEquals(loan.status, "Loan Closure Requested")
@@ -230,8 +232,7 @@ class TestLoan(unittest.TestCase):
process_loan_interest_accrual_for_term_loans(posting_date=nowdate()) process_loan_interest_accrual_for_term_loans(posting_date=nowdate())
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(nowdate(), 5), repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(nowdate(), 5), 89768.75)
"Regular Payment", 89768.75)
repayment_entry.submit() repayment_entry.submit()
@@ -281,7 +282,7 @@ class TestLoan(unittest.TestCase):
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application) create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit() loan.submit()
self.assertEquals(loan.loan_amount, 1000000) self.assertEquals(loan.loan_amount, 1000000)
@@ -291,7 +292,7 @@ class TestLoan(unittest.TestCase):
no_of_days = date_diff(last_date, first_date) + 1 no_of_days = date_diff(last_date, first_date) + 1
no_of_days += 6 no_of_days += 5
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100) / (days_in_year(get_datetime(first_date).year) * 100)
@@ -299,10 +300,10 @@ class TestLoan(unittest.TestCase):
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date) process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount))
"Loan Closure", flt(loan.loan_amount + accrued_interest_amount))
repayment_entry.submit() repayment_entry.submit()
request_loan_closure(loan.name)
loan.load_from_db() loan.load_from_db()
self.assertEquals(loan.status, "Loan Closure Requested") self.assertEquals(loan.status, "Loan Closure Requested")
@@ -317,9 +318,9 @@ class TestLoan(unittest.TestCase):
self.assertEqual(loan.status, 'Closed') self.assertEqual(loan.status, 'Closed')
self.assertEquals(sum(pledged_qty.values()), 0) self.assertEquals(sum(pledged_qty.values()), 0)
amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 5))
self.assertEqual(amounts['pending_principal_amount'], 0) self.assertTrue(amounts['pending_principal_amount'] < 0)
self.assertEqual(amounts['payable_principal_amount'], 0) self.assertEquals(amounts['payable_principal_amount'], 0.0)
self.assertEqual(amounts['interest_amount'], 0) self.assertEqual(amounts['interest_amount'], 0)
def test_disbursal_check_with_shortfall(self): def test_disbursal_check_with_shortfall(self):
@@ -381,7 +382,7 @@ class TestLoan(unittest.TestCase):
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application) create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit() loan.submit()
self.assertEquals(loan.loan_amount, 1000000) self.assertEquals(loan.loan_amount, 1000000)
@@ -391,7 +392,7 @@ class TestLoan(unittest.TestCase):
no_of_days = date_diff(last_date, first_date) + 1 no_of_days = date_diff(last_date, first_date) + 1
no_of_days += 6 no_of_days += 5
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100) / (days_in_year(get_datetime(first_date).year) * 100)
@@ -399,20 +400,192 @@ class TestLoan(unittest.TestCase):
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date) process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") amounts = calculate_amounts(loan.name, add_days(last_date, 5))
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount))
"Loan Closure", flt(loan.loan_amount + accrued_interest_amount))
repayment_entry.submit() repayment_entry.submit()
amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
'paid_principal_amount']) 'paid_principal_amount'])
request_loan_closure(loan.name)
loan.load_from_db() loan.load_from_db()
self.assertEquals(loan.status, "Loan Closure Requested") self.assertEquals(loan.status, "Loan Closure Requested")
amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") amounts = calculate_amounts(loan.name, add_days(last_date, 5))
self.assertEquals(amounts['pending_principal_amount'], 0.0) self.assertTrue(amounts['pending_principal_amount'] < 0.0)
def test_partial_unaccrued_interest_payment(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
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
no_of_days += 5.5
# get partial unaccrued interest amount
paid_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_for_demand_loans(posting_date = last_date)
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
paid_amount)
repayment_entry.submit()
repayment_entry.load_from_db()
partial_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * 5) \
/ (days_in_year(get_datetime(first_date).year) * 100)
interest_amount = flt(amounts['interest_amount'] + partial_accrued_interest_amount, 2)
self.assertEqual(flt(repayment_entry.total_interest_paid, 0), flt(interest_amount, 0))
def test_penalty(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
first_date = '2019-10-01'
last_date = '2019-10-30'
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
amounts = calculate_amounts(loan.name, add_days(last_date, 1))
paid_amount = amounts['interest_amount']/2
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
paid_amount)
repayment_entry.submit()
# 30 days - grace period
penalty_days = 30 - 4
penalty_applicable_amount = flt(amounts['interest_amount']/2, 2)
penalty_amount = flt((((penalty_applicable_amount * 25) / 100) * penalty_days), 2)
process = process_loan_interest_accrual_for_demand_loans(posting_date = '2019-11-30')
calculated_penalty_amount = frappe.db.get_value('Loan Interest Accrual',
{'process_loan_interest_accrual': process, 'loan': loan.name}, 'penalty_amount')
self.assertEquals(calculated_penalty_amount, penalty_amount)
def test_loan_write_off_limit(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
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
no_of_days += 5
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_for_demand_loans(posting_date = last_date)
# repay 50 less so that it can be automatically written off
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
flt(loan.loan_amount + accrued_interest_amount - 50))
repayment_entry.submit()
amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)'])
self.assertEquals(flt(amount, 0),flt(accrued_interest_amount, 0))
self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0)
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
self.assertEquals(flt(amounts['pending_principal_amount'], 0), 50)
request_loan_closure(loan.name)
loan.load_from_db()
self.assertEquals(loan.status, "Loan Closure Requested")
def test_loan_amount_write_off(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
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
no_of_days += 5
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_for_demand_loans(posting_date = last_date)
# repay 100 less so that it can be automatically written off
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
flt(loan.loan_amount + accrued_interest_amount - 100))
repayment_entry.submit()
amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)'])
self.assertEquals(flt(amount, 0),flt(accrued_interest_amount, 0))
self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0)
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
self.assertEquals(flt(amounts['pending_principal_amount'], 0), 100)
we = make_loan_write_off(loan.name, amount=amounts['pending_principal_amount'])
we.submit()
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
self.assertEquals(flt(amounts['pending_principal_amount'], 0), 0)
def create_loan_accounts(): def create_loan_accounts():
if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"):
@@ -496,7 +669,8 @@ def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_i
"interest_income_account": interest_income_account, "interest_income_account": interest_income_account,
"penalty_income_account": penalty_income_account, "penalty_income_account": penalty_income_account,
"repayment_method": repayment_method, "repayment_method": repayment_method,
"repayment_periods": repayment_periods "repayment_periods": repayment_periods,
"write_off_amount": 100
}).insert() }).insert()
loan_type.submit() loan_type.submit()
@@ -532,7 +706,7 @@ def create_loan_security():
"haircut": 50.00, "haircut": 50.00,
}).insert(ignore_permissions=True) }).insert(ignore_permissions=True)
def create_loan_security_pledge(applicant, pledges, loan_application): def create_loan_security_pledge(applicant, pledges, loan_application=None, loan=None):
lsp = frappe.new_doc("Loan Security Pledge") lsp = frappe.new_doc("Loan Security Pledge")
lsp.applicant_type = 'Customer' lsp.applicant_type = 'Customer'
@@ -540,11 +714,13 @@ def create_loan_security_pledge(applicant, pledges, loan_application):
lsp.company = "_Test Company" lsp.company = "_Test Company"
lsp.loan_application = loan_application lsp.loan_application = loan_application
if loan:
lsp.loan = loan
for pledge in pledges: for pledge in pledges:
lsp.append('securities', { lsp.append('securities', {
"loan_security": pledge['loan_security'], "loan_security": pledge['loan_security'],
"qty": pledge['qty'], "qty": pledge['qty']
"haircut": pledge['haircut']
}) })
lsp.save() lsp.save()
@@ -582,12 +758,11 @@ def create_loan_security_price(loan_security, loan_security_price, uom, from_dat
"valid_upto": to_date "valid_upto": to_date
}).insert(ignore_permissions=True) }).insert(ignore_permissions=True)
def create_repayment_entry(loan, applicant, posting_date, payment_type, paid_amount): def create_repayment_entry(loan, applicant, posting_date, paid_amount):
lr = frappe.get_doc({ lr = frappe.get_doc({
"doctype": "Loan Repayment", "doctype": "Loan Repayment",
"against_loan": loan, "against_loan": loan,
"payment_type": payment_type,
"company": "_Test Company", "company": "_Test Company",
"posting_date": posting_date or nowdate(), "posting_date": posting_date or nowdate(),
"applicant": applicant, "applicant": applicant,

View File

@@ -127,6 +127,7 @@ def create_loan(source_name, target_doc=None, submit=0):
target_doc.loan_account = account_details.loan_account target_doc.loan_account = account_details.loan_account
target_doc.interest_income_account = account_details.interest_income_account target_doc.interest_income_account = account_details.interest_income_account
target_doc.penalty_income_account = account_details.penalty_income_account target_doc.penalty_income_account = account_details.penalty_income_account
target_doc.loan_application = source_name
doclist = get_mapped_doc("Loan Application", source_name, { doclist = get_mapped_doc("Loan Application", source_name, {

View File

@@ -26,19 +26,24 @@
{ {
"fieldname": "against_loan", "fieldname": "against_loan",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1,
"label": "Against Loan ", "label": "Against Loan ",
"options": "Loan" "options": "Loan",
"reqd": 1
}, },
{ {
"fieldname": "disbursement_date", "fieldname": "disbursement_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Disbursement Date" "label": "Disbursement Date",
"reqd": 1
}, },
{ {
"fieldname": "disbursed_amount", "fieldname": "disbursed_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Disbursed Amount", "label": "Disbursed Amount",
"options": "Company:company:default_currency" "non_negative": 1,
"options": "Company:company:default_currency",
"reqd": 1
}, },
{ {
"fieldname": "amended_from", "fieldname": "amended_from",
@@ -53,17 +58,21 @@
"fetch_from": "against_loan.company", "fetch_from": "against_loan.company",
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1,
"label": "Company", "label": "Company",
"options": "Company", "options": "Company",
"read_only": 1 "read_only": 1,
"reqd": 1
}, },
{ {
"fetch_from": "against_loan.applicant", "fetch_from": "against_loan.applicant",
"fieldname": "applicant", "fieldname": "applicant",
"fieldtype": "Dynamic Link", "fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Applicant", "label": "Applicant",
"options": "applicant_type", "options": "applicant_type",
"read_only": 1 "read_only": 1,
"reqd": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
@@ -102,9 +111,11 @@
"fetch_from": "against_loan.applicant_type", "fetch_from": "against_loan.applicant_type",
"fieldname": "applicant_type", "fieldname": "applicant_type",
"fieldtype": "Select", "fieldtype": "Select",
"in_list_view": 1,
"label": "Applicant Type", "label": "Applicant Type",
"options": "Employee\nMember\nCustomer", "options": "Employee\nMember\nCustomer",
"read_only": 1 "read_only": 1,
"reqd": 1
}, },
{ {
"fieldname": "bank_account", "fieldname": "bank_account",
@@ -117,9 +128,10 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
} }
], ],
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-04-29 05:20:41.629911", "modified": "2020-11-06 10:04:30.882322",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Disbursement", "name": "Loan Disbursement",

View File

@@ -17,6 +17,7 @@ class LoanDisbursement(AccountsController):
def validate(self): def validate(self):
self.set_missing_values() self.set_missing_values()
self.validate_disbursal_amount()
def on_submit(self): def on_submit(self):
self.set_status_and_amounts() self.set_status_and_amounts()
@@ -40,57 +41,21 @@ class LoanDisbursement(AccountsController):
if not self.bank_account and self.applicant_type == "Customer": if not self.bank_account and self.applicant_type == "Customer":
self.bank_account = frappe.db.get_value("Customer", self.applicant, "default_bank_account") self.bank_account = frappe.db.get_value("Customer", self.applicant, "default_bank_account")
def set_status_and_amounts(self, cancel=0): def validate_disbursal_amount(self):
possible_disbursal_amount = get_disbursal_amount(self.against_loan)
if self.disbursed_amount > possible_disbursal_amount:
frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount))
def set_status_and_amounts(self, cancel=0):
loan_details = frappe.get_all("Loan", loan_details = frappe.get_all("Loan",
fields = ["loan_amount", "disbursed_amount", "total_payment", "total_principal_paid", "total_interest_payable", fields = ["loan_amount", "disbursed_amount", "total_payment", "total_principal_paid", "total_interest_payable",
"status", "is_term_loan", "is_secured_loan"], filters= { "name": self.against_loan })[0] "status", "is_term_loan", "is_secured_loan"], filters= { "name": self.against_loan })[0]
if cancel: if cancel:
disbursed_amount = loan_details.disbursed_amount - self.disbursed_amount disbursed_amount, status, total_payment = self.get_values_on_cancel(loan_details)
total_payment = loan_details.total_payment
if loan_details.disbursed_amount > loan_details.loan_amount:
topup_amount = loan_details.disbursed_amount - loan_details.loan_amount
if topup_amount > self.disbursed_amount:
topup_amount = self.disbursed_amount
total_payment = total_payment - topup_amount
if disbursed_amount == 0:
status = "Sanctioned"
elif disbursed_amount >= loan_details.loan_amount:
status = "Disbursed"
else:
status = "Partially Disbursed"
else: else:
disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount disbursed_amount, status, total_payment = self.get_values_on_submit(loan_details)
total_payment = loan_details.total_payment
possible_disbursal_amount = get_disbursal_amount(self.against_loan)
if self.disbursed_amount > possible_disbursal_amount:
frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount))
if loan_details.status == "Disbursed" and not loan_details.is_term_loan:
process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1),
loan=self.against_loan)
if disbursed_amount > loan_details.loan_amount:
topup_amount = disbursed_amount - loan_details.loan_amount
if topup_amount < 0:
topup_amount = 0
if topup_amount > self.disbursed_amount:
topup_amount = self.disbursed_amount
total_payment = total_payment + topup_amount
if flt(disbursed_amount) >= loan_details.loan_amount:
status = "Disbursed"
else:
status = "Partially Disbursed"
frappe.db.set_value("Loan", self.against_loan, { frappe.db.set_value("Loan", self.against_loan, {
"disbursement_date": self.disbursement_date, "disbursement_date": self.disbursement_date,
@@ -99,6 +64,53 @@ class LoanDisbursement(AccountsController):
"total_payment": total_payment "total_payment": total_payment
}) })
def get_values_on_cancel(self, loan_details):
disbursed_amount = loan_details.disbursed_amount - self.disbursed_amount
total_payment = loan_details.total_payment
if loan_details.disbursed_amount > loan_details.loan_amount:
topup_amount = loan_details.disbursed_amount - loan_details.loan_amount
if topup_amount > self.disbursed_amount:
topup_amount = self.disbursed_amount
total_payment = total_payment - topup_amount
if disbursed_amount == 0:
status = "Sanctioned"
elif disbursed_amount >= loan_details.loan_amount:
status = "Disbursed"
else:
status = "Partially Disbursed"
return disbursed_amount, status, total_payment
def get_values_on_submit(self, loan_details):
disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount
total_payment = loan_details.total_payment
if loan_details.status in ("Disbursed", "Partially Disbursed") and not loan_details.is_term_loan:
process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1),
loan=self.against_loan, accrual_type="Disbursement")
if disbursed_amount > loan_details.loan_amount:
topup_amount = disbursed_amount - loan_details.loan_amount
if topup_amount < 0:
topup_amount = 0
if topup_amount > self.disbursed_amount:
topup_amount = self.disbursed_amount
total_payment = total_payment + topup_amount
if flt(disbursed_amount) >= loan_details.loan_amount:
status = "Disbursed"
else:
status = "Partially Disbursed"
return disbursed_amount, status, total_payment
def make_gl_entries(self, cancel=0, adv_adj=0): def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = [] gle_map = []
loan_details = frappe.get_doc("Loan", self.against_loan) loan_details = frappe.get_doc("Loan", self.against_loan)
@@ -111,7 +123,7 @@ class LoanDisbursement(AccountsController):
"debit_in_account_currency": self.disbursed_amount, "debit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
"against_voucher": self.against_loan, "against_voucher": self.against_loan,
"remarks": "Against Loan:" + self.against_loan, "remarks": _("Disbursement against loan:") + self.against_loan,
"cost_center": self.cost_center, "cost_center": self.cost_center,
"party_type": self.applicant_type, "party_type": self.applicant_type,
"party": self.applicant, "party": self.applicant,
@@ -127,10 +139,8 @@ class LoanDisbursement(AccountsController):
"credit_in_account_currency": self.disbursed_amount, "credit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
"against_voucher": self.against_loan, "against_voucher": self.against_loan,
"remarks": "Against Loan:" + self.against_loan, "remarks": _("Disbursement against loan:") + self.against_loan,
"cost_center": self.cost_center, "cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": self.disbursement_date "posting_date": self.disbursement_date
}) })
) )
@@ -155,15 +165,16 @@ def get_total_pledged_security_value(loan):
pledged_securities = get_pledged_security_qty(loan) pledged_securities = get_pledged_security_qty(loan)
for security, qty in pledged_securities.items(): for security, qty in pledged_securities.items():
security_value += (loan_security_price_map.get(security) * qty * hair_cut_map.get(security))/100 after_haircut_percentage = 100 - hair_cut_map.get(security)
security_value += (loan_security_price_map.get(security) * qty * after_haircut_percentage)/100
return security_value return security_value
@frappe.whitelist() @frappe.whitelist()
def get_disbursal_amount(loan): def get_disbursal_amount(loan, on_current_security_price=0):
loan_details = frappe.get_all("Loan", fields = ["loan_amount", "disbursed_amount", "total_payment", loan_details = frappe.get_value("Loan", loan, ["loan_amount", "disbursed_amount", "total_payment",
"total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan"], "total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan",
filters= { "name": loan })[0] "maximum_loan_amount"], as_dict=1)
if loan_details.is_secured_loan and frappe.get_all('Loan Security Shortfall', filters={'loan': loan, if loan_details.is_secured_loan and frappe.get_all('Loan Security Shortfall', filters={'loan': loan,
'status': 'Pending'}): 'status': 'Pending'}):
@@ -173,17 +184,24 @@ def get_disbursal_amount(loan):
pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \ pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \
- flt(loan_details.total_principal_paid) - flt(loan_details.total_principal_paid)
else: else:
pending_principal_amount = flt(loan_details.disbursed_amount) pending_principal_amount = flt(loan_details.disbursed_amount) - flt(loan_details.total_interest_payable) \
- flt(loan_details.total_principal_paid)
security_value = 0.0 security_value = 0.0
if loan_details.is_secured_loan: if loan_details.is_secured_loan and on_current_security_price:
security_value = get_total_pledged_security_value(loan) security_value = get_total_pledged_security_value(loan)
if loan_details.is_secured_loan and not on_current_security_price:
security_value = flt(loan_details.maximum_loan_amount)
if not security_value and not loan_details.is_secured_loan: if not security_value and not loan_details.is_secured_loan:
security_value = flt(loan_details.loan_amount) security_value = flt(loan_details.loan_amount)
disbursal_amount = flt(security_value) - flt(pending_principal_amount) disbursal_amount = flt(security_value) - flt(pending_principal_amount)
if loan_details.is_term_loan and (disbursal_amount + loan_details.loan_amount) > loan_details.loan_amount:
disbursal_amount = loan_details.loan_amount - loan_details.disbursed_amount
return disbursal_amount return disbursal_amount

View File

@@ -8,9 +8,10 @@ from frappe.utils import (nowdate, add_days, get_datetime, get_first_day, get_la
from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_pledge, create_repayment_entry, create_loan_application, from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_pledge, create_repayment_entry, create_loan_application,
make_loan_disbursement_entry, create_loan_accounts, create_loan_security_type, create_loan_security, create_demand_loan, create_loan_security_price) 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_for_demand_loans from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year, get_per_day_interest
from erpnext.selling.doctype.customer.test_customer import get_customer_dict from erpnext.selling.doctype.customer.test_customer import get_customer_dict
from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts
class TestLoanDisbursement(unittest.TestCase): class TestLoanDisbursement(unittest.TestCase):
@@ -60,8 +61,7 @@ class TestLoanDisbursement(unittest.TestCase):
self.assertRaises(frappe.ValidationError, make_loan_disbursement_entry, loan.name, self.assertRaises(frappe.ValidationError, make_loan_disbursement_entry, loan.name,
500000, first_date) 500000, first_date)
repayment_entry = create_repayment_entry(loan.name, self.applicant, add_days(get_last_day(nowdate()), 5), repayment_entry = create_repayment_entry(loan.name, self.applicant, add_days(get_last_day(nowdate()), 5), 611095.89)
"Regular Payment", 611095.89)
repayment_entry.submit() repayment_entry.submit()
loan.reload() loan.reload()
@@ -69,3 +69,50 @@ class TestLoanDisbursement(unittest.TestCase):
# After repayment loan disbursement entry should go through # After repayment loan disbursement entry should go through
make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 16)) make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 16))
# check for disbursement accrual
loan_interest_accrual = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name,
'accrual_type': 'Disbursement'})
self.assertTrue(loan_interest_accrual)
def test_loan_topup_with_additional_pledge(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
first_date = '2019-10-01'
last_date = '2019-10-30'
# Disbursed 10,00,000 amount
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
amounts = calculate_amounts(loan.name, add_days(last_date, 1))
previous_interest = amounts['interest_amount']
pledge1 = [{
"loan_security": "Test Security 1",
"qty": 2000.00
}]
create_loan_security_pledge(self.applicant, pledge1, loan=loan.name)
# Topup 500000
make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 1))
process_loan_interest_accrual_for_demand_loans(posting_date = add_days(last_date, 15))
amounts = calculate_amounts(loan.name, add_days(last_date, 15))
per_day_interest = get_per_day_interest(1500000, 13.5, '2019-10-30')
interest = per_day_interest * 15
self.assertEquals(amounts['pending_principal_amount'], 1500000)
self.assertEquals(amounts['interest_amount'], flt(interest + previous_interest, 2))

View File

@@ -14,6 +14,7 @@
"column_break_4", "column_break_4",
"company", "company",
"posting_date", "posting_date",
"accrual_type",
"is_term_loan", "is_term_loan",
"section_break_7", "section_break_7",
"pending_principal_amount", "pending_principal_amount",
@@ -22,9 +23,11 @@
"column_break_14", "column_break_14",
"interest_amount", "interest_amount",
"paid_interest_amount", "paid_interest_amount",
"penalty_amount",
"section_break_15", "section_break_15",
"process_loan_interest_accrual", "process_loan_interest_accrual",
"repayment_schedule_name", "repayment_schedule_name",
"last_accrual_date",
"amended_from" "amended_from"
], ],
"fields": [ "fields": [
@@ -139,6 +142,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:doc.is_term_loan",
"fieldname": "paid_principal_amount", "fieldname": "paid_principal_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Paid Principal Amount", "label": "Paid Principal Amount",
@@ -149,12 +153,32 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Paid Interest Amount", "label": "Paid Interest Amount",
"options": "Company:company:default_currency" "options": "Company:company:default_currency"
},
{
"fieldname": "accrual_type",
"fieldtype": "Select",
"label": "Accrual Type",
"options": "Regular\nRepayment\nDisbursement"
},
{
"fieldname": "penalty_amount",
"fieldtype": "Currency",
"label": "Penalty Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "last_accrual_date",
"fieldtype": "Date",
"hidden": 1,
"label": "Last Accrual Date",
"read_only": 1
} }
], ],
"in_create": 1, "in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-04-16 11:24:23.258404", "modified": "2020-11-07 05:49:25.448875",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Interest Accrual", "name": "Loan Interest Accrual",

View File

@@ -22,6 +22,8 @@ class LoanInterestAccrual(AccountsController):
if not self.interest_amount and not self.payable_principal_amount: if not self.interest_amount and not self.payable_principal_amount:
frappe.throw(_("Interest Amount or Principal Amount is mandatory")) frappe.throw(_("Interest Amount or Principal Amount is mandatory"))
if not self.last_accrual_date:
self.last_accrual_date = get_last_accrual_date(self.loan)
def on_submit(self): def on_submit(self):
self.make_gl_entries() self.make_gl_entries()
@@ -50,7 +52,8 @@ class LoanInterestAccrual(AccountsController):
"debit_in_account_currency": self.interest_amount, "debit_in_account_currency": self.interest_amount,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
"against_voucher": self.loan, "against_voucher": self.loan,
"remarks": _("Against Loan:") + self.loan, "remarks": _("Interest accrued from {0} to {1} against loan: {2}").format(
self.last_accrual_date, self.posting_date, self.loan),
"cost_center": erpnext.get_default_cost_center(self.company), "cost_center": erpnext.get_default_cost_center(self.company),
"posting_date": self.posting_date "posting_date": self.posting_date
}) })
@@ -59,14 +62,13 @@ class LoanInterestAccrual(AccountsController):
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({
"account": self.interest_income_account, "account": self.interest_income_account,
"party_type": self.applicant_type,
"party": self.applicant,
"against": self.loan_account, "against": self.loan_account,
"credit": self.interest_amount, "credit": self.interest_amount,
"credit_in_account_currency": self.interest_amount, "credit_in_account_currency": self.interest_amount,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
"against_voucher": self.loan, "against_voucher": self.loan,
"remarks": _("Against Loan:") + self.loan, "remarks": ("Interest accrued from {0} to {1} against loan: {2}").format(
self.last_accrual_date, self.posting_date, self.loan),
"cost_center": erpnext.get_default_cost_center(self.company), "cost_center": erpnext.get_default_cost_center(self.company),
"posting_date": self.posting_date "posting_date": self.posting_date
}) })
@@ -79,19 +81,23 @@ class LoanInterestAccrual(AccountsController):
# For Eg: If Loan disbursement date is '01-09-2019' and disbursed amount is 1000000 and # 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' # 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 # 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): def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest, accrual_type):
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts
no_of_days = get_no_of_days_for_interest_accural(loan, posting_date) no_of_days = get_no_of_days_for_interest_accural(loan, posting_date)
precision = cint(frappe.db.get_default("currency_precision")) or 2
if no_of_days <= 0: if no_of_days <= 0:
return return
if loan.status == 'Disbursed': if loan.status == 'Disbursed':
pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid) - flt(loan.total_principal_paid) - flt(loan.written_off_amount)
else: else:
pending_principal_amount = loan.disbursed_amount pending_principal_amount = flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid) - flt(loan.written_off_amount)
interest_per_day = (pending_principal_amount * loan.rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100) interest_per_day = get_per_day_interest(pending_principal_amount, loan.rate_of_interest, posting_date)
payable_interest = interest_per_day * no_of_days payable_interest = interest_per_day * no_of_days
args = frappe._dict({ args = frappe._dict({
@@ -102,13 +108,16 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i
'loan_account': loan.loan_account, 'loan_account': loan.loan_account,
'pending_principal_amount': pending_principal_amount, 'pending_principal_amount': pending_principal_amount,
'interest_amount': payable_interest, 'interest_amount': payable_interest,
'penalty_amount': calculate_amounts(loan.name, posting_date)['penalty_amount'],
'process_loan_interest': process_loan_interest, 'process_loan_interest': process_loan_interest,
'posting_date': posting_date 'posting_date': posting_date,
'accrual_type': accrual_type
}) })
make_loan_interest_accrual_entry(args) if flt(payable_interest, precision) > 0.0:
make_loan_interest_accrual_entry(args)
def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None): def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None, accrual_type="Regular"):
query_filters = { query_filters = {
"status": ('in', ['Disbursed', 'Partially Disbursed']), "status": ('in', ['Disbursed', 'Partially Disbursed']),
"docstatus": 1 "docstatus": 1
@@ -123,13 +132,13 @@ def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_inte
open_loans = frappe.get_all("Loan", open_loans = frappe.get_all("Loan",
fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account",
"is_term_loan", "status", "disbursement_date", "disbursed_amount", "applicant_type", "applicant", "is_term_loan", "status", "disbursement_date", "disbursed_amount", "applicant_type", "applicant",
"rate_of_interest", "total_interest_payable", "total_principal_paid", "repayment_start_date"], "rate_of_interest", "total_interest_payable", "written_off_amount", "total_principal_paid", "repayment_start_date"],
filters=query_filters) filters=query_filters)
for loan in open_loans: for loan in open_loans:
calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest) calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest, accrual_type)
def make_accrual_interest_entry_for_term_loans(posting_date, process_loan_interest, term_loan=None, loan_type=None): def make_accrual_interest_entry_for_term_loans(posting_date, process_loan_interest, term_loan=None, loan_type=None, accrual_type="Regular"):
curr_date = posting_date or add_days(nowdate(), 1) curr_date = posting_date or add_days(nowdate(), 1)
term_loans = get_term_loans(curr_date, term_loan, loan_type) term_loans = get_term_loans(curr_date, term_loan, loan_type)
@@ -148,7 +157,8 @@ def make_accrual_interest_entry_for_term_loans(posting_date, process_loan_intere
'payable_principal': loan.principal_amount, 'payable_principal': loan.principal_amount,
'process_loan_interest': process_loan_interest, 'process_loan_interest': process_loan_interest,
'repayment_schedule_name': loan.payment_entry, 'repayment_schedule_name': loan.payment_entry,
'posting_date': posting_date 'posting_date': posting_date,
'accrual_type': accrual_type
}) })
make_loan_interest_accrual_entry(args) make_loan_interest_accrual_entry(args)
@@ -192,31 +202,33 @@ def make_loan_interest_accrual_entry(args):
loan_interest_accrual.loan_account = args.loan_account loan_interest_accrual.loan_account = args.loan_account
loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, precision) loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, precision)
loan_interest_accrual.interest_amount = flt(args.interest_amount, precision) loan_interest_accrual.interest_amount = flt(args.interest_amount, precision)
loan_interest_accrual.penalty_amount = flt(args.penalty_amount, precision)
loan_interest_accrual.posting_date = args.posting_date or nowdate() loan_interest_accrual.posting_date = args.posting_date or nowdate()
loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest
loan_interest_accrual.repayment_schedule_name = args.repayment_schedule_name loan_interest_accrual.repayment_schedule_name = args.repayment_schedule_name
loan_interest_accrual.payable_principal_amount = args.payable_principal loan_interest_accrual.payable_principal_amount = args.payable_principal
loan_interest_accrual.accrual_type = args.accrual_type
loan_interest_accrual.save() loan_interest_accrual.save()
loan_interest_accrual.submit() loan_interest_accrual.submit()
def get_no_of_days_for_interest_accural(loan, posting_date): def get_no_of_days_for_interest_accural(loan, posting_date):
last_interest_accrual_date = get_last_accural_date_in_current_month(loan) last_interest_accrual_date = get_last_accrual_date(loan.name)
no_of_days = date_diff(posting_date or nowdate(), last_interest_accrual_date) + 1 no_of_days = date_diff(posting_date or nowdate(), last_interest_accrual_date) + 1
return no_of_days return no_of_days
def get_last_accural_date_in_current_month(loan): def get_last_accrual_date(loan):
last_posting_date = frappe.db.sql(""" SELECT MAX(posting_date) from `tabLoan Interest Accrual` last_posting_date = frappe.db.sql(""" SELECT MAX(posting_date) from `tabLoan Interest Accrual`
WHERE loan = %s""", (loan.name)) WHERE loan = %s and docstatus = 1""", (loan))
if last_posting_date[0][0]: if last_posting_date[0][0]:
# interest for last interest accrual date is already booked, so add 1 day # interest for last interest accrual date is already booked, so add 1 day
return add_days(last_posting_date[0][0], 1) return add_days(last_posting_date[0][0], 1)
else: else:
return loan.disbursement_date return frappe.db.get_value('Loan', loan, 'disbursement_date')
def days_in_year(year): def days_in_year(year):
days = 365 days = 365
@@ -226,3 +238,11 @@ def days_in_year(year):
return days return days
def get_per_day_interest(principal_amount, rate_of_interest, posting_date=None):
if not posting_date:
posting_date = getdate()
precision = cint(frappe.db.get_default("currency_precision")) or 2
return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100), precision)

View File

@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
from frappe.utils import (nowdate, add_days, get_datetime, get_first_day, get_last_day, date_diff, flt, add_to_date) 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, from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_price,
make_loan_disbursement_entry, create_loan_accounts, create_loan_security_type, create_loan_security, create_demand_loan, create_loan_application) make_loan_disbursement_entry, create_loan_accounts, create_loan_security_type, create_loan_security, create_demand_loan, create_loan_application)
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year
@@ -57,4 +57,4 @@ class TestLoanInterestAccrual(unittest.TestCase):
loan_interest_accural = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name}) 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)) self.assertEquals(flt(loan_interest_accural.interest_amount, 0), flt(accrued_interest_amount, 0))

View File

@@ -10,11 +10,11 @@
"applicant_type", "applicant_type",
"applicant", "applicant",
"loan_type", "loan_type",
"payment_type",
"column_break_3", "column_break_3",
"company", "company",
"posting_date", "posting_date",
"is_term_loan", "is_term_loan",
"rate_of_interest",
"payment_details_section", "payment_details_section",
"due_date", "due_date",
"pending_principal_amount", "pending_principal_amount",
@@ -31,6 +31,7 @@
"column_break_21", "column_break_21",
"reference_date", "reference_date",
"principal_amount_paid", "principal_amount_paid",
"total_interest_paid",
"repayment_details", "repayment_details",
"amended_from" "amended_from"
], ],
@@ -95,15 +96,6 @@
"fieldname": "column_break_9", "fieldname": "column_break_9",
"fieldtype": "Column Break" "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", "fieldname": "payable_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
@@ -116,6 +108,7 @@
"fieldname": "amount_paid", "fieldname": "amount_paid",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Amount Paid", "label": "Amount Paid",
"non_negative": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"reqd": 1 "reqd": 1
}, },
@@ -195,6 +188,7 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1, "hidden": 1,
"label": "Principal Amount Paid", "label": "Principal Amount Paid",
"options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
}, },
{ {
@@ -217,11 +211,27 @@
"hidden": 1, "hidden": 1,
"label": "Repayment Details", "label": "Repayment Details",
"options": "Loan Repayment Detail" "options": "Loan Repayment Detail"
},
{
"fieldname": "total_interest_paid",
"fieldtype": "Currency",
"hidden": 1,
"label": "Total Interest Paid",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fetch_from": "loan_type.rate_of_interest",
"fieldname": "rate_of_interest",
"fieldtype": "Percent",
"label": "Rate Of Interest",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-05-16 09:40:15.581165", "modified": "2020-11-05 10:06:58.792841",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Repayment", "name": "Loan Repayment",

View File

@@ -14,14 +14,15 @@ from erpnext.controllers.accounts_controller import AccountsController
from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import update_shortfall_status from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import update_shortfall_status
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import get_per_day_interest, get_last_accrual_date
class LoanRepayment(AccountsController): class LoanRepayment(AccountsController):
def validate(self): def validate(self):
amounts = calculate_amounts(self.against_loan, self.posting_date, self.payment_type) amounts = calculate_amounts(self.against_loan, self.posting_date)
self.set_missing_values(amounts) self.set_missing_values(amounts)
self.validate_amount() self.validate_amount()
self.allocate_amounts(amounts['pending_accrual_entries']) self.allocate_amounts(amounts)
def before_submit(self): def before_submit(self):
self.book_unaccrued_interest() self.book_unaccrued_interest()
@@ -32,8 +33,8 @@ class LoanRepayment(AccountsController):
def on_cancel(self): def on_cancel(self):
self.mark_as_unpaid() self.mark_as_unpaid()
self.make_gl_entries(cancel=1)
self.ignore_linked_doctypes = ['GL Entry'] self.ignore_linked_doctypes = ['GL Entry']
self.make_gl_entries(cancel=1)
def set_missing_values(self, amounts): def set_missing_values(self, amounts):
precision = cint(frappe.db.get_default("currency_precision")) or 2 precision = cint(frappe.db.get_default("currency_precision")) or 2
@@ -72,29 +73,36 @@ class LoanRepayment(AccountsController):
msg = _("Paid amount cannot be less than {0}").format(self.penalty_amount) msg = _("Paid amount cannot be less than {0}").format(self.penalty_amount)
frappe.throw(msg) frappe.throw(msg)
if self.payment_type == "Loan Closure" and flt(self.amount_paid, precision) < flt(self.payable_amount, precision):
msg = _("Amount of {0} is required for Loan closure").format(self.payable_amount)
frappe.throw(msg)
def book_unaccrued_interest(self): def book_unaccrued_interest(self):
if self.payment_type == 'Loan Closure': precision = cint(frappe.db.get_default("currency_precision")) or 2
total_interest_paid = 0 if self.total_interest_paid > self.interest_payable:
for payment in self.repayment_details: if not self.is_term_loan:
total_interest_paid += payment.paid_interest_amount # get last loan interest accrual date
last_accrual_date = get_last_accrual_date(self.against_loan)
if total_interest_paid < self.interest_payable: # get posting date upto which interest has to be accrued
if not self.is_term_loan: per_day_interest = flt(get_per_day_interest(self.pending_principal_amount,
process = process_loan_interest_accrual_for_demand_loans(posting_date=self.posting_date, self.rate_of_interest, self.posting_date), 2)
loan=self.against_loan)
lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual': no_of_days = flt(flt(self.total_interest_paid - self.interest_payable,
process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1) precision)/per_day_interest, 0) - 1
self.append('repayment_details', { posting_date = add_days(last_accrual_date, no_of_days)
'loan_interest_accrual': lia.name,
'paid_interest_amount': lia.interest_amount, # book excess interest paid
'paid_principal_amount': lia.payable_principal_amount process = process_loan_interest_accrual_for_demand_loans(posting_date=posting_date,
}) loan=self.against_loan, accrual_type="Repayment")
# get loan interest accrual to update paid amount
lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual':
process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1)
self.append('repayment_details', {
'loan_interest_accrual': lia.name,
'paid_interest_amount': flt(self.total_interest_paid - self.interest_payable, precision),
'paid_principal_amount': 0.0,
'accrual_type': 'Repayment'
})
def update_paid_amount(self): def update_paid_amount(self):
precision = cint(frappe.db.get_default("currency_precision")) or 2 precision = cint(frappe.db.get_default("currency_precision")) or 2
@@ -108,12 +116,6 @@ class LoanRepayment(AccountsController):
WHERE name = %s""", WHERE name = %s""",
(flt(payment.paid_principal_amount, precision), flt(payment.paid_interest_amount, precision), payment.loan_interest_accrual)) (flt(payment.paid_principal_amount, precision), flt(payment.paid_interest_amount, precision), payment.loan_interest_accrual))
if flt(loan.total_principal_paid + self.principal_amount_paid, precision) >= flt(loan.total_payment, precision):
if loan.is_secured_loan:
frappe.db.set_value("Loan", self.against_loan, "status", "Loan Closure Requested")
else:
frappe.db.set_value("Loan", self.against_loan, "status", "Closed")
frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s
WHERE name = %s """, (loan.total_amount_paid + self.amount_paid, WHERE name = %s """, (loan.total_amount_paid + self.amount_paid,
loan.total_principal_paid + self.principal_amount_paid, self.against_loan)) loan.total_principal_paid + self.principal_amount_paid, self.against_loan))
@@ -123,6 +125,8 @@ class LoanRepayment(AccountsController):
def mark_as_unpaid(self): def mark_as_unpaid(self):
loan = frappe.get_doc("Loan", self.against_loan) loan = frappe.get_doc("Loan", self.against_loan)
no_of_repayments = len(self.repayment_details)
for payment in self.repayment_details: for payment in self.repayment_details:
frappe.db.sql(""" UPDATE `tabLoan Interest Accrual` frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
SET paid_principal_amount = `paid_principal_amount` - %s, SET paid_principal_amount = `paid_principal_amount` - %s,
@@ -130,6 +134,12 @@ class LoanRepayment(AccountsController):
WHERE name = %s""", WHERE name = %s""",
(payment.paid_principal_amount, payment.paid_interest_amount, payment.loan_interest_accrual)) (payment.paid_principal_amount, payment.paid_interest_amount, payment.loan_interest_accrual))
# Cancel repayment interest accrual
# checking idx as a preventive measure, repayment accrual will always be the last entry
if payment.accrual_type == 'Repayment' and payment.idx == no_of_repayments:
lia_doc = frappe.get_doc('Loan Interest Accrual', payment.loan_interest_accrual)
lia_doc.cancel()
frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s
WHERE name = %s """, (loan.total_amount_paid - self.amount_paid, WHERE name = %s """, (loan.total_amount_paid - self.amount_paid,
loan.total_principal_paid - self.principal_amount_paid, self.against_loan)) loan.total_principal_paid - self.principal_amount_paid, self.against_loan))
@@ -137,15 +147,17 @@ class LoanRepayment(AccountsController):
if loan.status == "Loan Closure Requested": if loan.status == "Loan Closure Requested":
frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed") frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed")
def allocate_amounts(self, paid_entries): def allocate_amounts(self, repayment_details):
precision = cint(frappe.db.get_default("currency_precision")) or 2
self.set('repayment_details', []) self.set('repayment_details', [])
self.principal_amount_paid = 0 self.principal_amount_paid = 0
total_interest_paid = 0 total_interest_paid = 0
interest_paid = self.amount_paid - self.penalty_amount interest_paid = self.amount_paid - self.penalty_amount
if self.amount_paid - self.penalty_amount > 0 and paid_entries: if self.amount_paid - self.penalty_amount > 0:
interest_paid = self.amount_paid - self.penalty_amount interest_paid = self.amount_paid - self.penalty_amount
for lia, amounts in iteritems(paid_entries): for lia, amounts in iteritems(repayment_details.get('pending_accrual_entries', [])):
if amounts['interest_amount'] + amounts['payable_principal_amount'] <= interest_paid: if amounts['interest_amount'] + amounts['payable_principal_amount'] <= interest_paid:
interest_amount = amounts['interest_amount'] interest_amount = amounts['interest_amount']
paid_principal = amounts['payable_principal_amount'] paid_principal = amounts['payable_principal_amount']
@@ -169,9 +181,24 @@ class LoanRepayment(AccountsController):
'paid_principal_amount': paid_principal 'paid_principal_amount': paid_principal
}) })
if self.payment_type == 'Loan Closure' and total_interest_paid < self.interest_payable: if repayment_details['unaccrued_interest'] and interest_paid:
unaccrued_interest = self.interest_payable - total_interest_paid # no of days for which to accrue interest
interest_paid -= unaccrued_interest # Interest can only be accrued for an entire day and not partial
if interest_paid > repayment_details['unaccrued_interest']:
per_day_interest = flt(get_per_day_interest(self.pending_principal_amount,
self.rate_of_interest, self.posting_date), precision)
interest_paid -= repayment_details['unaccrued_interest']
total_interest_paid += repayment_details['unaccrued_interest']
else:
# get no of days for which interest can be paid
per_day_interest = flt(get_per_day_interest(self.pending_principal_amount,
self.rate_of_interest, self.posting_date), precision)
no_of_days = cint(interest_paid/per_day_interest)
total_interest_paid += no_of_days * per_day_interest
interest_paid -= no_of_days * per_day_interest
self.total_interest_paid = total_interest_paid
if interest_paid: if interest_paid:
self.principal_amount_paid += interest_paid self.principal_amount_paid += interest_paid
@@ -189,7 +216,7 @@ class LoanRepayment(AccountsController):
"debit_in_account_currency": self.penalty_amount, "debit_in_account_currency": self.penalty_amount,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
"against_voucher": self.against_loan, "against_voucher": self.against_loan,
"remarks": _("Against Loan:") + self.against_loan, "remarks": _("Penalty against loan:") + self.against_loan,
"cost_center": self.cost_center, "cost_center": self.cost_center,
"party_type": self.applicant_type, "party_type": self.applicant_type,
"party": self.applicant, "party": self.applicant,
@@ -205,10 +232,8 @@ class LoanRepayment(AccountsController):
"credit_in_account_currency": self.penalty_amount, "credit_in_account_currency": self.penalty_amount,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
"against_voucher": self.against_loan, "against_voucher": self.against_loan,
"remarks": _("Against Loan:") + self.against_loan, "remarks": _("Penalty against loan:") + self.against_loan,
"cost_center": self.cost_center, "cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": getdate(self.posting_date) "posting_date": getdate(self.posting_date)
}) })
) )
@@ -219,13 +244,11 @@ class LoanRepayment(AccountsController):
"against": loan_details.loan_account + ", " + loan_details.interest_income_account "against": loan_details.loan_account + ", " + loan_details.interest_income_account
+ ", " + loan_details.penalty_income_account, + ", " + loan_details.penalty_income_account,
"debit": self.amount_paid, "debit": self.amount_paid,
"debit_in_account_currency": self.amount_paid , "debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
"against_voucher": self.against_loan, "against_voucher": self.against_loan,
"remarks": _("Against Loan:") + self.against_loan, "remarks": _("Repayment against Loan: ") + self.against_loan,
"cost_center": self.cost_center, "cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": getdate(self.posting_date) "posting_date": getdate(self.posting_date)
}) })
) )
@@ -240,7 +263,7 @@ class LoanRepayment(AccountsController):
"credit_in_account_currency": self.amount_paid, "credit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
"against_voucher": self.against_loan, "against_voucher": self.against_loan,
"remarks": _("Against Loan:") + self.against_loan, "remarks": _("Repayment against Loan: ") + self.against_loan,
"cost_center": self.cost_center, "cost_center": self.cost_center,
"posting_date": getdate(self.posting_date) "posting_date": getdate(self.posting_date)
}) })
@@ -273,7 +296,8 @@ def get_accrued_interest_entries(against_loan):
unpaid_accrued_entries = frappe.db.sql( unpaid_accrued_entries = frappe.db.sql(
""" """
SELECT name, posting_date, interest_amount - paid_interest_amount as interest_amount, SELECT name, posting_date, interest_amount - paid_interest_amount as interest_amount,
payable_principal_amount - paid_principal_amount as payable_principal_amount payable_principal_amount - paid_principal_amount as payable_principal_amount,
accrual_type
FROM FROM
`tabLoan Interest Accrual` `tabLoan Interest Accrual`
WHERE WHERE
@@ -282,6 +306,7 @@ def get_accrued_interest_entries(against_loan):
payable_principal_amount - paid_principal_amount > 0) payable_principal_amount - paid_principal_amount > 0)
AND AND
docstatus = 1 docstatus = 1
ORDER BY posting_date
""", (against_loan), as_dict=1) """, (against_loan), as_dict=1)
return unpaid_accrued_entries return unpaid_accrued_entries
@@ -289,7 +314,7 @@ def get_accrued_interest_entries(against_loan):
# This function returns the amounts that are payable at the time of loan repayment based on posting date # 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 # 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): def get_amounts(amounts, against_loan, posting_date):
precision = cint(frappe.db.get_default("currency_precision")) or 2 precision = cint(frappe.db.get_default("currency_precision")) or 2
against_loan_doc = frappe.get_doc("Loan", against_loan) against_loan_doc = frappe.get_doc("Loan", against_loan)
@@ -311,10 +336,10 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
due_date = add_days(entry.posting_date, 1) due_date = add_days(entry.posting_date, 1)
no_of_late_days = date_diff(posting_date, no_of_late_days = date_diff(posting_date,
add_days(due_date, loan_type_details.grace_period_in_days)) 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): if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary) and entry.accrual_type == 'Regular':
penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)/365 penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)
total_pending_interest += entry.interest_amount total_pending_interest += entry.interest_amount
payable_principal_amount += entry.payable_principal_amount payable_principal_amount += entry.payable_principal_amount
@@ -324,23 +349,27 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
'payable_principal_amount': flt(entry.payable_principal_amount, precision) 'payable_principal_amount': flt(entry.payable_principal_amount, precision)
}) })
if not final_due_date: if due_date and not final_due_date:
final_due_date = add_days(due_date, loan_type_details.grace_period_in_days) final_due_date = add_days(due_date, loan_type_details.grace_period_in_days)
if against_loan_doc.status in ('Disbursed', 'Loan Closure Requested', 'Closed'): if against_loan_doc.status in ('Disbursed', 'Loan Closure Requested', 'Closed'):
pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid \
- against_loan_doc.total_interest_payable - against_loan_doc.written_off_amount
else: else:
pending_principal_amount = against_loan_doc.disbursed_amount pending_principal_amount = against_loan_doc.disbursed_amount - against_loan_doc.total_principal_paid \
- against_loan_doc.total_interest_payable - against_loan_doc.written_off_amount
if payment_type == "Loan Closure": unaccrued_interest = 0
if due_date: if due_date:
pending_days = date_diff(posting_date, due_date) + 1 pending_days = date_diff(posting_date, due_date) + 1
else: else:
pending_days = date_diff(posting_date, against_loan_doc.disbursement_date) + 1 last_accrual_date = get_last_accrual_date(against_loan_doc.name)
pending_days = date_diff(posting_date, last_accrual_date) + 1
payable_principal_amount = pending_principal_amount if pending_days > 0:
per_day_interest = (payable_principal_amount * (loan_type_details.rate_of_interest / 100))/365 principal_amount = flt(pending_principal_amount, precision)
total_pending_interest += (pending_days * per_day_interest) per_day_interest = get_per_day_interest(principal_amount, loan_type_details.rate_of_interest, posting_date)
unaccrued_interest += (pending_days * flt(per_day_interest, precision))
amounts["pending_principal_amount"] = flt(pending_principal_amount, precision) amounts["pending_principal_amount"] = flt(pending_principal_amount, precision)
amounts["payable_principal_amount"] = flt(payable_principal_amount, precision) amounts["payable_principal_amount"] = flt(payable_principal_amount, precision)
@@ -348,6 +377,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
amounts["penalty_amount"] = flt(penalty_amount, precision) amounts["penalty_amount"] = flt(penalty_amount, precision)
amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision) amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision)
amounts["pending_accrual_entries"] = pending_accrual_entries amounts["pending_accrual_entries"] = pending_accrual_entries
amounts["unaccrued_interest"] = unaccrued_interest
if final_due_date: if final_due_date:
amounts["due_date"] = final_due_date amounts["due_date"] = final_due_date
@@ -355,7 +385,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
return amounts return amounts
@frappe.whitelist() @frappe.whitelist()
def calculate_amounts(against_loan, posting_date, payment_type): def calculate_amounts(against_loan, posting_date, payment_type=''):
amounts = { amounts = {
'penalty_amount': 0.0, 'penalty_amount': 0.0,
@@ -363,10 +393,17 @@ def calculate_amounts(against_loan, posting_date, payment_type):
'pending_principal_amount': 0.0, 'pending_principal_amount': 0.0,
'payable_principal_amount': 0.0, 'payable_principal_amount': 0.0,
'payable_amount': 0.0, 'payable_amount': 0.0,
'unaccrued_interest': 0.0,
'due_date': '' 'due_date': ''
} }
amounts = get_amounts(amounts, against_loan, posting_date, payment_type) amounts = get_amounts(amounts, against_loan, posting_date)
# update values for closure
if payment_type == 'Loan Closure':
amounts['payable_principal_amount'] = amounts['pending_principal_amount']
amounts['interest_amount'] += amounts['unaccrued_interest']
amounts['payable_amount'] = amounts['payable_principal_amount'] + amounts['interest_amount']
return amounts return amounts

View File

@@ -7,7 +7,8 @@
"field_order": [ "field_order": [
"loan_interest_accrual", "loan_interest_accrual",
"paid_principal_amount", "paid_principal_amount",
"paid_interest_amount" "paid_interest_amount",
"accrual_type"
], ],
"fields": [ "fields": [
{ {
@@ -27,11 +28,20 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Paid Interest Amount", "label": "Paid Interest Amount",
"options": "Company:company:default_currency" "options": "Company:company:default_currency"
},
{
"fetch_from": "loan_interest_accrual.accrual_type",
"fetch_if_empty": 1,
"fieldname": "accrual_type",
"fieldtype": "Select",
"label": "Accrual Type",
"options": "Regular\nRepayment\nDisbursement"
} }
], ],
"index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-04-15 21:50:03.837019", "modified": "2020-10-23 08:09:18.267030",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Repayment Detail", "name": "Loan Repayment Detail",

View File

@@ -25,6 +25,7 @@
}, },
{ {
"fetch_from": "loan_security_type.haircut", "fetch_from": "loan_security_type.haircut",
"fetch_if_empty": 1,
"fieldname": "haircut", "fieldname": "haircut",
"fieldtype": "Percent", "fieldtype": "Percent",
"label": "Haircut %" "label": "Haircut %"
@@ -64,8 +65,9 @@
"reqd": 1 "reqd": 1
} }
], ],
"index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-04-29 13:21:26.043492", "modified": "2020-10-26 07:34:48.601766",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Security", "name": "Loan Security",

View File

@@ -78,7 +78,7 @@ class LoanSecurityPledge(Document):
self.maximum_loan_value = maximum_loan_value self.maximum_loan_value = maximum_loan_value
def update_loan(loan, maximum_value_against_pledge): def update_loan(loan, maximum_value_against_pledge):
maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_value']) maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_amount'])
frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_value=%s, is_secured_loan=1 frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1
WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan)) WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan))

View File

@@ -22,7 +22,7 @@ def update_shortfall_status(loan, security_value):
if security_value >= loan_security_shortfall.shortfall_amount: if security_value >= loan_security_shortfall.shortfall_amount:
frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, { frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, {
"status": "Completed", "status": "Completed",
"shortfall_value": loan_security_shortfall.shortfall_amount}) "shortfall_amount": loan_security_shortfall.shortfall_amount})
else: else:
frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name,
"shortfall_amount", loan_security_shortfall.shortfall_amount - security_value) "shortfall_amount", loan_security_shortfall.shortfall_amount - security_value)

View File

@@ -42,18 +42,20 @@ class LoanSecurityUnpledge(Document):
"valid_upto": (">=", get_datetime()) "valid_upto": (">=", get_datetime())
}, as_list=1)) }, as_list=1))
total_payment, principal_paid, interest_payable = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid', total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid',
'total_interest_payable']) 'total_interest_payable', 'written_off_amount'])
pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount)
security_value = 0 security_value = 0
for security in self.securities: for security in self.securities:
pledged_qty = pledge_qty_map.get(security.loan_security, 0) pledged_qty = pledge_qty_map.get(security.loan_security, 0)
if security.qty > pledged_qty: if security.qty > pledged_qty:
frappe.throw(_("""Row {0}: {1} {2} of {3} is pledged against Loan {4}. msg = _("Row {0}: {1} {2} of {3} is pledged against Loan {4}.").format(security.idx, pledged_qty, security.uom,
You are trying to unpledge more""").format(security.idx, pledged_qty, security.uom, frappe.bold(security.loan_security), frappe.bold(self.loan))
frappe.bold(security.loan_security), frappe.bold(self.loan))) msg += "<br>"
msg += _("You are trying to unpledge more.")
frappe.throw(msg, title=_("Loan Security Unpledge Error"))
qty_after_unpledge = pledged_qty - security.qty qty_after_unpledge = pledged_qty - security.qty
ltv_ratio = ltv_ratio_map.get(security.loan_security_type) ltv_ratio = ltv_ratio_map.get(security.loan_security_type)
@@ -65,10 +67,18 @@ class LoanSecurityUnpledge(Document):
security_value += qty_after_unpledge * current_price security_value += qty_after_unpledge * current_price
if not security_value and flt(pending_principal_amount, 2) > 0: if not security_value and flt(pending_principal_amount, 2) > 0:
frappe.throw("Cannot Unpledge, loan to value ratio is breaching") self._throw(security_value, pending_principal_amount, ltv_ratio)
if security_value and flt(pending_principal_amount/security_value) * 100 > ltv_ratio: if security_value and flt(pending_principal_amount/security_value) * 100 > ltv_ratio:
frappe.throw("Cannot Unpledge, loan to value ratio is breaching") self._throw(security_value, pending_principal_amount, ltv_ratio)
def _throw(self, security_value, pending_principal_amount, ltv_ratio):
msg = _("Loan Security Value after unpledge is {0}").format(frappe.bold(security_value))
msg += '<br>'
msg += _("Pending principal amount is {0}").format(frappe.bold(flt(pending_principal_amount, 2)))
msg += '<br>'
msg += _("Loan To Security Value ratio must always be {0}").format(frappe.bold(ltv_ratio))
frappe.throw(msg, title=_("Loan To Value ratio breach"))
def on_update_after_submit(self): def on_update_after_submit(self):
self.approve() self.approve()

View File

@@ -11,6 +11,7 @@
"rate_of_interest", "rate_of_interest",
"penalty_interest_rate", "penalty_interest_rate",
"grace_period_in_days", "grace_period_in_days",
"write_off_amount",
"column_break_2", "column_break_2",
"company", "company",
"is_term_loan", "is_term_loan",
@@ -76,7 +77,6 @@
"reqd": 1 "reqd": 1
}, },
{ {
"description": "This account is used for booking loan repayments from the borrower and also disbursing loans to the borrower",
"fieldname": "payment_account", "fieldname": "payment_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Payment Account", "label": "Payment Account",
@@ -84,7 +84,6 @@
"reqd": 1 "reqd": 1
}, },
{ {
"description": "This account is capital account which is used to allocate capital for loan disbursal account ",
"fieldname": "loan_account", "fieldname": "loan_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Loan Account", "label": "Loan Account",
@@ -96,7 +95,6 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"description": "This account will be used for booking loan interest accruals",
"fieldname": "interest_income_account", "fieldname": "interest_income_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Interest Income Account", "label": "Interest Income Account",
@@ -104,7 +102,6 @@
"reqd": 1 "reqd": 1
}, },
{ {
"description": "This account will be used for booking penalties levied due to delayed repayments",
"fieldname": "penalty_income_account", "fieldname": "penalty_income_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Penalty Income Account", "label": "Penalty Income Account",
@@ -113,7 +110,6 @@
}, },
{ {
"default": "0", "default": "0",
"description": "If this is not checked the loan by default will be considered as a Demand Loan",
"fieldname": "is_term_loan", "fieldname": "is_term_loan",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Term Loan" "label": "Is Term Loan"
@@ -145,17 +141,27 @@
"label": "Company", "label": "Company",
"options": "Company", "options": "Company",
"reqd": 1 "reqd": 1
},
{
"allow_on_submit": 1,
"description": "Pending amount that will be automatically ignored on loan closure request ",
"fieldname": "write_off_amount",
"fieldtype": "Currency",
"label": "Write Off Amount ",
"options": "Company:company:default_currency"
} }
], ],
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-06-07 18:55:59.346292", "modified": "2020-10-26 07:13:55.029811",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Type", "name": "Loan Type",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"cancel": 1,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
@@ -165,6 +171,7 @@
"report": 1, "report": 1,
"role": "Loan Manager", "role": "Loan Manager",
"share": 1, "share": 1,
"submit": 1,
"write": 1 "write": 1
}, },
{ {

View File

@@ -0,0 +1,36 @@
// Copyright (c) 2020, 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 Write Off', {
loan: function(frm) {
frm.trigger('show_pending_principal_amount');
},
onload: function(frm) {
frm.trigger('show_pending_principal_amount');
},
refresh: function(frm) {
frm.set_query('write_off_account', function(){
return {
filters: {
'company': frm.doc.company,
'root_type': 'Expense',
'is_group': 0
}
}
});
},
show_pending_principal_amount: function(frm) {
if (frm.doc.loan && frm.doc.docstatus === 0) {
frappe.db.get_value('Loan', frm.doc.loan, ['total_payment', 'total_interest_payable',
'total_principal_paid', 'written_off_amount'], function(values) {
frm.set_df_property('write_off_amount', 'description',
"Pending principal amount is " + cstr(flt(values.total_payment - values.total_interest_payable
- values.total_principal_paid - values.written_off_amount, 2)));
frm.refresh_field('write_off_amount');
});
}
}
});

View File

@@ -0,0 +1,157 @@
{
"actions": [],
"autoname": "LM-WO-.#####",
"creation": "2020-10-16 11:09:14.495066",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan",
"applicant_type",
"applicant",
"column_break_3",
"company",
"posting_date",
"accounting_dimensions_section",
"cost_center",
"section_break_9",
"write_off_account",
"column_break_11",
"write_off_amount",
"amended_from"
],
"fields": [
{
"fieldname": "loan",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Loan",
"options": "Loan",
"reqd": 1
},
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Posting Date",
"reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fetch_from": "loan.company",
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"read_only": 1,
"reqd": 1
},
{
"fetch_from": "loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"read_only": 1
},
{
"fetch_from": "loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"label": "Applicant ",
"options": "applicant_type",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"label": "Write Off Details"
},
{
"fieldname": "write_off_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Write Off Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "write_off_amount",
"fieldtype": "Currency",
"label": "Write Off Amount",
"options": "Company:company:default_currency",
"reqd": 1
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Write Off",
"print_hide": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-10-26 07:13:43.663924",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Write Off",
"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
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, erpnext
from frappe import _
from frappe.utils import getdate, flt, cint
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.accounts.general_ledger import make_gl_entries
class LoanWriteOff(AccountsController):
def validate(self):
self.set_missing_values()
self.validate_write_off_amount()
def set_missing_values(self):
if not self.cost_center:
self.cost_center = erpnext.get_default_cost_center(self.company)
def validate_write_off_amount(self):
precision = cint(frappe.db.get_default("currency_precision")) or 2
total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value("Loan", self.loan,
['total_payment', 'total_principal_paid','total_interest_payable', 'written_off_amount'])
pending_principal_amount = flt(flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount),
precision)
if self.write_off_amount > pending_principal_amount:
frappe.throw(_("Write off amount cannot be greater than pending principal amount"))
def on_submit(self):
self.update_outstanding_amount()
self.make_gl_entries()
def on_cancel(self):
self.update_outstanding_amount(cancel=1)
self.ignore_linked_doctypes = ['GL Entry']
self.make_gl_entries(cancel=1)
def update_outstanding_amount(self, cancel=0):
written_off_amount = frappe.db.get_value('Loan', self.loan, 'written_off_amount')
if cancel:
written_off_amount -= self.write_off_amount
else:
written_off_amount += self.write_off_amount
frappe.db.set_value('Loan', self.loan, 'written_off_amount', written_off_amount)
def make_gl_entries(self, cancel=0):
gl_entries = []
loan_details = frappe.get_doc("Loan", self.loan)
gl_entries.append(
self.get_gl_dict({
"account": self.write_off_account,
"against": loan_details.loan_account,
"debit": self.write_off_amount,
"debit_in_account_currency": self.write_off_amount,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _("Against Loan:") + self.loan,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
)
gl_entries.append(
self.get_gl_dict({
"account": loan_details.loan_account,
"party_type": loan_details.applicant_type,
"party": loan_details.applicant,
"against": self.write_off_account,
"credit": self.write_off_amount,
"credit_in_account_currency": self.write_off_amount,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _("Against Loan:") + self.loan,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
)
make_gl_entries(gl_entries, cancel=cancel, merge_entries=False)

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestLoanWriteOff(unittest.TestCase):
pass

View File

@@ -1,4 +1,5 @@
{ {
"actions": [],
"creation": "2019-09-09 17:06:16.756573", "creation": "2019-09-09 17:06:16.756573",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@@ -49,7 +50,8 @@
"fieldname": "qty", "fieldname": "qty",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Quantity" "label": "Quantity",
"non_negative": 1
}, },
{ {
"fieldname": "loan_security_price", "fieldname": "loan_security_price",
@@ -86,7 +88,8 @@
} }
], ],
"istable": 1, "istable": 1,
"modified": "2019-12-03 10:59:58.001421", "links": [],
"modified": "2020-11-05 10:07:15.424937",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Pledge", "name": "Pledge",

View File

@@ -10,6 +10,7 @@
"loan_type", "loan_type",
"loan", "loan",
"process_type", "process_type",
"accrual_type",
"amended_from" "amended_from"
], ],
"fields": [ "fields": [
@@ -47,17 +48,27 @@
"hidden": 1, "hidden": 1,
"label": "Process Type", "label": "Process Type",
"read_only": 1 "read_only": 1
},
{
"fieldname": "accrual_type",
"fieldtype": "Select",
"hidden": 1,
"label": "Accrual Type",
"options": "Regular\nRepayment\nDisbursement",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-04-09 22:52:53.911416", "modified": "2020-11-06 13:28:51.478909",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Process Loan Interest Accrual", "name": "Process Loan Interest Accrual",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"cancel": 1,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
@@ -67,9 +78,11 @@
"report": 1, "report": 1,
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"submit": 1,
"write": 1 "write": 1
}, },
{ {
"cancel": 1,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
@@ -79,6 +92,7 @@
"report": 1, "report": 1,
"role": "Loan Manager", "role": "Loan Manager",
"share": 1, "share": 1,
"submit": 1,
"write": 1 "write": 1
} }
], ],

View File

@@ -20,19 +20,20 @@ class ProcessLoanInterestAccrual(Document):
if (not self.loan or not loan_doc.is_term_loan) and self.process_type != 'Term Loans': if (not self.loan or not loan_doc.is_term_loan) and self.process_type != 'Term Loans':
make_accrual_interest_entry_for_demand_loans(self.posting_date, self.name, make_accrual_interest_entry_for_demand_loans(self.posting_date, self.name,
open_loans = open_loans, loan_type = self.loan_type) open_loans = open_loans, loan_type = self.loan_type, accrual_type=self.accrual_type)
if (not self.loan or loan_doc.is_term_loan) and self.process_type != 'Demand Loans': if (not self.loan or loan_doc.is_term_loan) and self.process_type != 'Demand Loans':
make_accrual_interest_entry_for_term_loans(self.posting_date, self.name, term_loan=self.loan, make_accrual_interest_entry_for_term_loans(self.posting_date, self.name, term_loan=self.loan,
loan_type=self.loan_type) loan_type=self.loan_type, accrual_type=self.accrual_type)
def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type=None, loan=None): def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type=None, loan=None, accrual_type="Regular"):
loan_process = frappe.new_doc('Process Loan Interest Accrual') loan_process = frappe.new_doc('Process Loan Interest Accrual')
loan_process.posting_date = posting_date or nowdate() loan_process.posting_date = posting_date or nowdate()
loan_process.loan_type = loan_type loan_process.loan_type = loan_type
loan_process.process_type = 'Demand Loans' loan_process.process_type = 'Demand Loans'
loan_process.loan = loan loan_process.loan = loan
loan_process.accrual_type = accrual_type
loan_process.submit() loan_process.submit()

View File

@@ -1,4 +1,5 @@
{ {
"actions": [],
"creation": "2019-08-29 22:29:37.628178", "creation": "2019-08-29 22:29:37.628178",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@@ -39,7 +40,8 @@
"fieldname": "qty", "fieldname": "qty",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Quantity" "label": "Quantity",
"non_negative": 1
}, },
{ {
"fieldname": "loan_security", "fieldname": "loan_security",
@@ -56,8 +58,10 @@
"read_only": 1 "read_only": 1
} }
], ],
"index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"modified": "2019-12-02 10:23:11.498308", "links": [],
"modified": "2020-11-05 10:07:37.542344",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Proposed Pledge", "name": "Proposed Pledge",

View File

@@ -52,6 +52,7 @@
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Quantity", "label": "Quantity",
"non_negative": 1,
"reqd": 1 "reqd": 1
}, },
{ {
@@ -62,9 +63,10 @@
"read_only": 1 "read_only": 1
} }
], ],
"index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-05-06 10:50:18.448552", "modified": "2020-11-05 10:07:28.106961",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Unpledge", "name": "Unpledge",

View File

@@ -8,14 +8,14 @@ frappe.ui.form.on(cur_frm.doctype, {
frm.refresh_field('applicant_type'); frm.refresh_field('applicant_type');
} }
if (['Loan Disbursement', 'Loan Repayment', 'Loan Interest Accrual'].includes(frm.doc.doctype) if (['Loan Disbursement', 'Loan Repayment', 'Loan Interest Accrual', 'Loan Write Off'].includes(frm.doc.doctype)
&& frm.doc.docstatus > 0) { && frm.doc.docstatus > 0) {
frm.add_custom_button(__("Accounting Ledger"), function() { frm.add_custom_button(__("Accounting Ledger"), function() {
frappe.route_options = { frappe.route_options = {
voucher_no: frm.doc.name, voucher_no: frm.doc.name,
company: frm.doc.company, company: frm.doc.company,
from_date: frm.doc.posting_date, from_date: moment(frm.doc.posting_date).format('YYYY-MM-DD'),
to_date: moment(frm.doc.modified).format('YYYY-MM-DD'), to_date: moment(frm.doc.modified).format('YYYY-MM-DD'),
show_cancelled_entries: frm.doc.docstatus === 2 show_cancelled_entries: frm.doc.docstatus === 2
}; };

View File

@@ -76,6 +76,7 @@ class BOM(WebsiteGenerator):
self.set_routing_operations() self.set_routing_operations()
self.validate_operations() self.validate_operations()
self.calculate_cost() self.calculate_cost()
self.update_stock_qty()
self.update_cost(update_parent=False, from_child_bom=True, save=False) self.update_cost(update_parent=False, from_child_bom=True, save=False)
def get_context(self, context): def get_context(self, context):
@@ -84,8 +85,6 @@ class BOM(WebsiteGenerator):
def on_update(self): def on_update(self):
frappe.cache().hdel('bom_children', self.name) frappe.cache().hdel('bom_children', self.name)
self.check_recursion() self.check_recursion()
self.update_stock_qty()
self.update_exploded_items()
def on_submit(self): def on_submit(self):
self.manage_default_bom() self.manage_default_bom()
@@ -237,7 +236,8 @@ class BOM(WebsiteGenerator):
self.calculate_cost() self.calculate_cost()
if save: if save:
self.db_update() self.db_update()
self.update_exploded_items()
self.update_exploded_items(save=save)
# update parent BOMs # update parent BOMs
if self.total_cost != existing_bom_cost and update_parent: if self.total_cost != existing_bom_cost and update_parent:
@@ -318,8 +318,6 @@ class BOM(WebsiteGenerator):
m.uom = m.stock_uom m.uom = m.stock_uom
m.qty = m.stock_qty m.qty = m.stock_qty
m.db_update()
def validate_uom_is_interger(self): def validate_uom_is_interger(self):
from erpnext.utilities.transaction_base import validate_uom_is_integer from erpnext.utilities.transaction_base import validate_uom_is_integer
validate_uom_is_integer(self, "uom", "qty", "BOM Item") validate_uom_is_integer(self, "uom", "qty", "BOM Item")
@@ -372,15 +370,6 @@ class BOM(WebsiteGenerator):
if raise_exception: if raise_exception:
frappe.throw(_("BOM recursion: {0} cannot be parent or child of {1}").format(self.name, self.name)) frappe.throw(_("BOM recursion: {0} cannot be parent or child of {1}").format(self.name, self.name))
def update_cost_and_exploded_items(self, bom_list=[]):
bom_list = self.traverse_tree(bom_list)
for bom in bom_list:
bom_obj = frappe.get_doc("BOM", bom)
bom_obj.check_recursion(bom_list=bom_list)
bom_obj.update_exploded_items()
return bom_list
def traverse_tree(self, bom_list=None): def traverse_tree(self, bom_list=None):
def _get_children(bom_no): def _get_children(bom_no):
children = frappe.cache().hget('bom_children', bom_no) children = frappe.cache().hget('bom_children', bom_no)
@@ -472,10 +461,10 @@ class BOM(WebsiteGenerator):
d.rate = rate d.rate = rate
d.amount = (d.stock_qty or d.qty) * rate d.amount = (d.stock_qty or d.qty) * rate
def update_exploded_items(self): def update_exploded_items(self, save=True):
""" Update Flat BOM, following will be correct data""" """ Update Flat BOM, following will be correct data"""
self.get_exploded_items() self.get_exploded_items()
self.add_exploded_items() self.add_exploded_items(save=save)
def get_exploded_items(self): def get_exploded_items(self):
""" Get all raw materials including items from child bom""" """ Get all raw materials including items from child bom"""
@@ -544,11 +533,13 @@ class BOM(WebsiteGenerator):
'sourced_by_supplier': d.get('sourced_by_supplier', 0) 'sourced_by_supplier': d.get('sourced_by_supplier', 0)
})) }))
def add_exploded_items(self): def add_exploded_items(self, save=True):
"Add items to Flat BOM table" "Add items to Flat BOM table"
frappe.db.sql("""delete from `tabBOM Explosion Item` where parent=%s""", self.name)
self.set('exploded_items', []) self.set('exploded_items', [])
if save:
frappe.db.sql("""delete from `tabBOM Explosion Item` where parent=%s""", self.name)
for d in sorted(self.cur_exploded_items, key=itemgetter(0)): for d in sorted(self.cur_exploded_items, key=itemgetter(0)):
ch = self.append('exploded_items', {}) ch = self.append('exploded_items', {})
for i in self.cur_exploded_items[d].keys(): for i in self.cur_exploded_items[d].keys():
@@ -556,7 +547,9 @@ class BOM(WebsiteGenerator):
ch.amount = flt(ch.stock_qty) * flt(ch.rate) ch.amount = flt(ch.stock_qty) * flt(ch.rate)
ch.qty_consumed_per_unit = flt(ch.stock_qty) / flt(self.quantity) ch.qty_consumed_per_unit = flt(ch.stock_qty) / flt(self.quantity)
ch.docstatus = self.docstatus ch.docstatus = self.docstatus
ch.db_insert()
if save:
ch.db_insert()
def validate_bom_links(self): def validate_bom_links(self):
if not self.is_active: if not self.is_active:

View File

@@ -31,6 +31,16 @@ frappe.ui.form.on('Job Card', {
} }
} }
frm.set_query("quality_inspection", function() {
return {
query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query",
filters: {
"item_code": frm.doc.production_item,
"reference_name": frm.doc.name
}
};
});
frm.trigger("toggle_operation_number"); frm.trigger("toggle_operation_number");
if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)

View File

@@ -20,6 +20,7 @@
"production_item", "production_item",
"item_name", "item_name",
"for_quantity", "for_quantity",
"quality_inspection",
"wip_warehouse", "wip_warehouse",
"column_break_12", "column_break_12",
"employee", "employee",
@@ -305,11 +306,19 @@
"label": "Sequence Id", "label": "Sequence Id",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"depends_on": "eval:!doc.__islocal;",
"fieldname": "quality_inspection",
"fieldtype": "Link",
"label": "Quality Inspection",
"no_copy": 1,
"options": "Quality Inspection"
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-10-14 12:58:25.327897", "modified": "2020-11-19 18:26:50.531664",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Job Card", "name": "Job Card",

View File

@@ -353,17 +353,19 @@ def get_operation_details(work_order, operation):
@frappe.whitelist() @frappe.whitelist()
def get_operations(doctype, txt, searchfield, start, page_len, filters): def get_operations(doctype, txt, searchfield, start, page_len, filters):
if filters.get("work_order"): if not filters.get("work_order"):
args = {"parent": filters.get("work_order")} frappe.msgprint(_("Please select a Work Order first."))
if txt: return []
args["operation"] = ("like", "%{0}%".format(txt)) args = {"parent": filters.get("work_order")}
if txt:
args["operation"] = ("like", "%{0}%".format(txt))
return frappe.get_all("Work Order Operation", return frappe.get_all("Work Order Operation",
filters = args, filters = args,
fields = ["distinct operation as operation"], fields = ["distinct operation as operation"],
limit_start = start, limit_start = start,
limit_page_length = page_len, limit_page_length = page_len,
order_by="idx asc", as_list=1) order_by="idx asc", as_list=1)
@frappe.whitelist() @frappe.whitelist()
def make_material_request(source_name, target_doc=None): def make_material_request(source_name, target_doc=None):

Some files were not shown because too many files have changed in this diff Show More