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

This commit is contained in:
Deepesh Garg 2020-11-29 23:07:14 +05:30
commit 681c0e297c
292 changed files with 6016 additions and 2993 deletions

View File

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

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Community Forum
url: https://discuss.erpnext.com/
about: For general QnA, discussions and community help.

View File

@ -5,7 +5,7 @@
<p>ERP made simple</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)
[![Coverage Status](https://coveralls.io/repos/github/frappe/erpnext/badge.svg?branch=develop)](https://coveralls.io/github/frappe/erpnext?branch=develop)

View File

@ -6,9 +6,8 @@ import frappe, json
from frappe import _
from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day, formatdate, get_link_to_form
from erpnext.accounts.report.general_ledger.general_ledger import execute
from frappe.utils.dashboard import cache_source, get_from_date_from_timespan
from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_period_ending
from frappe.utils.dashboard import cache_source
from frappe.utils.dateutils import get_from_date_from_timespan, get_period_ending
from frappe.utils.nestedset import get_descendants_of
@frappe.whitelist()

View File

@ -23,7 +23,7 @@
{
"hidden": 0,
"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,
@ -43,7 +43,7 @@
{
"hidden": 0,
"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,
@ -79,6 +79,11 @@
"hidden": 0,
"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]"
},
{
"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",
@ -98,7 +103,7 @@
"idx": 0,
"is_standard": 1,
"label": "Accounting",
"modified": "2020-10-08 20:31:46.022470",
"modified": "2020-11-11 18:35:11.542909",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
@ -108,7 +113,7 @@
"pin_to_top": 0,
"shortcuts": [
{
"label": "Chart of Accounts",
"label": "Chart Of Accounts",
"link_to": "Account",
"type": "DocType"
},

View File

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

View File

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

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."))
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):
frappe.throw(_("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"),
FiscalYearIncorrectDate)
@ -116,12 +121,8 @@ def auto_create_fiscal_year():
pass
def get_from_and_to_date(fiscal_year):
from_and_to_date_tuple = frappe.db.sql("""select year_start_date, year_end_date
from `tabFiscal Year` where name=%s""", (fiscal_year))[0]
from_and_to_date = {
"from_date": from_and_to_date_tuple[0],
"to_date": from_and_to_date_tuple[1]
}
return from_and_to_date
fields = [
"year_start_date as from_date",
"year_end_date as to_date"
]
return frappe.db.get_value("Fiscal Year", fiscal_year, fields, as_dict=1)

View File

@ -11,6 +11,7 @@ test_records = frappe.get_test_records('Fiscal Year')
test_ignore = ["Company"]
class TestFiscalYear(unittest.TestCase):
def test_extra_year(self):
if frappe.db.exists("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",
"year": "_Test Fiscal Year 2012",

View File

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

View File

@ -339,8 +339,7 @@ class JournalEntry(AccountsController):
currency=account_currency)
if flt(voucher_total) < (flt(order.advance_paid) + total):
frappe.throw(_("Advance paid against {0} {1} cannot be greater \
than Grand Total {2}").format(reference_type, reference_name, formatted_voucher_total))
frappe.throw(_("Advance paid against {0} {1} cannot be greater than Grand Total {2}").format(reference_type, reference_name, formatted_voucher_total))
def validate_invoices(self):
"""Validate totals and docstatus for invoices"""

View File

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

View File

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

View File

@ -39,6 +39,7 @@ class POSInvoice(SalesInvoice):
self.validate_serialised_or_batched_item()
self.validate_stock_availablility()
self.validate_return_items_qty()
self.validate_non_stock_items()
self.set_status()
self.set_account_for_mode_of_payment()
self.validate_pos()
@ -132,15 +133,19 @@ class POSInvoice(SalesInvoice):
msg = ""
item_code = frappe.bold(d.item_code)
serial_nos = get_serial_nos(d.serial_no)
if serialized and batched and (no_batch_selected or no_serial_selected):
msg = (_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.')
.format(d.idx, item_code))
if serialized and no_serial_selected:
elif serialized and no_serial_selected:
msg = (_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.')
.format(d.idx, item_code))
if batched and no_batch_selected:
elif batched and no_batch_selected:
msg = (_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.')
.format(d.idx, item_code))
elif serialized and not no_serial_selected and len(serial_nos) != d.qty:
msg = (_("Row #{}: You must select {} serial numbers for item {}.").format(d.idx, frappe.bold(cint(d.qty)), item_code))
if msg:
error_msg.append(msg)
@ -170,6 +175,14 @@ class POSInvoice(SalesInvoice):
_("Row #{}: Serial No {} cannot be returned since it was not transacted in original invoice {}")
.format(d.idx, bold_serial_no, bold_return_against)
)
def validate_non_stock_items(self):
for d in self.get("items"):
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
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):
if len(self.payments) == 0:

View File

@ -35,6 +35,15 @@ frappe.ui.form.on('POS Profile', {
};
});
frm.set_query("taxes_and_charges", function() {
return {
filters: [
['Sales Taxes and Charges Template', 'company', '=', frm.doc.company],
['Sales Taxes and Charges Template', 'docstatus', '!=', 2]
]
};
});
frm.set_query('company_address', function(doc) {
if(!doc.company) {
frappe.throw(__('Please set Company'));

View File

@ -42,56 +42,56 @@ frappe.ui.form.on('Pricing Rule', {
<tr><td>
<h4>
<i class="fa fa-hand-right"></i>
${__('Notes')}
{{__('Notes')}}
</h4>
<ul>
<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>
${__("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>
${__('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>
${__('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>
</ul>
</td></tr>
<tr><td>
<h4><i class="fa fa-question-sign"></i>
${__('How Pricing Rule is applied?')}
{{__('How Pricing Rule is applied?')}}
</h4>
<ol>
<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>
${__("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>
${__('Pricing Rules are further filtered based on quantity.')}
{{__('Pricing Rules are further filtered based on quantity.')}}
</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>
${__('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>
<li>
${__('Item Code > Item Group > Brand')}
{{__('Item Code > Item Group > Brand')}}
</li>
<li>
${__('Customer > Customer Group > Territory')}
{{__('Customer > Customer Group > Territory')}}
</li>
<li>
${__('Supplier > Supplier Type')}
{{__('Supplier > Supplier Type')}}
</li>
</ul>
</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>
</ol>
</td></tr>

View File

@ -352,8 +352,14 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
pricing_rule_rate = 0.0
if pricing_rule.currency == args.currency:
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({
"price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1),
"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 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):
args = frappe._dict(args)

View File

@ -99,6 +99,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
target: me.frm,
setters: {
supplier: me.frm.doc.supplier || undefined,
schedule_date: undefined
},
get_query_filters: {
docstatus: 1,
@ -107,16 +108,16 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
company: me.frm.doc.company
}
})
}, __("Get items from"));
}, __("Get Items From"));
this.frm.add_custom_button(__('Purchase Receipt'), function() {
erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_invoice",
source_doctype: "Purchase Receipt",
target: me.frm,
date_field: "posting_date",
setters: {
supplier: me.frm.doc.supplier || undefined,
posting_date: undefined
},
get_query_filters: {
docstatus: 1,
@ -125,7 +126,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
is_return: 0
}
})
}, __("Get items from"));
}, __("Get Items From"));
}
this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted==="Yes");

View File

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

View File

@ -998,7 +998,7 @@ def make_purchase_invoice(**args):
'expense_account': args.expense_account or '_Test Account Cost for Goods Sold - _TC',
"conversion_factor": 1.0,
"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",
"project": args.project,
"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.credit_to = args.return_against or "Creditors - _TC"
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", {
"item_code": args.item or args.item_code or "_Test Item",

View File

@ -199,7 +199,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
company: me.frm.doc.company
}
})
}, __("Get items from"));
}, __("Get Items From"));
},
quotation_btn: function() {
@ -223,7 +223,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
company: me.frm.doc.company
}
})
}, __("Get items from"));
}, __("Get Items From"));
},
delivery_note_btn: function() {
@ -251,7 +251,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
};
}
});
}, __("Get items from"));
}, __("Get Items From"));
},
tc_name: function() {
@ -812,10 +812,10 @@ frappe.ui.form.on('Sales Invoice', {
if (cint(frm.doc.docstatus==0) && cur_frm.page.current_view_name!=="pos" && !frm.doc.is_return) {
frm.add_custom_button(__('Healthcare Services'), function() {
get_healthcare_services_to_invoice(frm);
},"Get items from");
},"Get Items From");
frm.add_custom_button(__('Prescriptions'), function() {
get_drugs_to_invoice(frm);
},"Get items from");
},"Get Items From");
}
}
else {
@ -1080,7 +1080,7 @@ var get_drugs_to_invoice = function(frm) {
description:'Quantity will be calculated only for items which has "Nos" as UoM. You may change as required for each invoice item.',
get_query: function(doc) {
return {
filters: {
filters: {
patient: dialog.get_value("patient"),
company: frm.doc.company,
docstatus: 1

View File

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

View File

@ -1401,6 +1401,7 @@ def make_delivery_note(source_name, target_doc=None):
def set_missing_values(source, target):
target.ignore_pricing_rule = 1
target.run_method("set_missing_values")
target.run_method("set_po_nos")
target.run_method("calculate_taxes_and_totals")
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:
tds_amount = _get_tds(net_total, tax_details.rate)
else:
supplier_credit_amount = frappe.get_all('Purchase Invoice Item',
fields = ['sum(net_amount)'],
filters = {'parent': ('in', vouchers), 'docstatus': 1}, as_list=1)
supplier_credit_amount = frappe.get_all('Purchase Invoice',
fields = ['sum(net_total)'],
filters = {'name': ('in', vouchers), 'docstatus': 1, "apply_tds": 1}, as_list=1)
supplier_credit_amount = (supplier_credit_amount[0][0]
if supplier_credit_amount and supplier_credit_amount[0][0] else 0)

View File

@ -7,6 +7,7 @@ import frappe
import unittest
from frappe.utils import today
from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
test_dependencies = ["Supplier Group"]
@ -101,6 +102,32 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices:
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):
# return sales invoice doc object
item = frappe.get_doc('Item', {'item_name': 'TDS Item'})
@ -109,7 +136,7 @@ def create_purchase_invoice(**args):
pi = frappe.get_doc({
"doctype": "Purchase Invoice",
"posting_date": today(),
"apply_tds": 1,
"apply_tds": 0 if args.do_not_apply_tds else 1,
"supplier": args.supplier,
"company": '_Test Company',
"taxes_and_charges": "",

View File

@ -156,7 +156,7 @@ erpnext.accounts.bankTransactionUpload = class bankTransactionUpload {
setup_transactions_dom() {
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() {
@ -167,9 +167,7 @@ erpnext.accounts.bankTransactionUpload = class bankTransactionUpload {
})
}
catch(err) {
let msg = __(`Your file could not be processed by ERPNext.
<br>It should be a standard CSV or XLSX file.
<br>The headers should be in the first row.`)
let msg = __("Your file could not be processed. It should be a standard CSV or XLSX file with headers in the first row.");
frappe.throw(msg)
}

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)
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"):
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
if (bill_date or posting_date) and party:
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:
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()
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"):
return
template = None

View File

@ -19,7 +19,7 @@
</div>
<div class="col-xs-6">
<table>
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.formatdate(doc.creation) }}</td></tr>
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.format_date(doc.creation) }}</td></tr>
</table>
</div>
</div>

View File

@ -160,6 +160,8 @@ class ReceivablePayableReport(object):
else:
# advance / unlinked payment or other adjustment
row.paid -= gle_balance
if gle.cost_center:
row.cost_center = str(gle.cost_center)
def update_sub_total_row(self, row, party):
total_row = self.total_row_map.get(party)
@ -210,7 +212,6 @@ class ReceivablePayableReport(object):
for key, row in self.voucher_balance.items():
row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision)
row.invoice_grand_total = row.invoiced
if abs(row.outstanding) > 1.0/10 ** self.currency_precision:
# non-zero oustanding, we must consider this row
@ -577,7 +578,7 @@ class ReceivablePayableReport(object):
self.gl_entries = frappe.db.sql("""
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}
from
`tabGL Entry`
@ -741,6 +742,7 @@ class ReceivablePayableReport(object):
self.add_column(_("Customer Contact"), fieldname='customer_primary_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 No'), fieldname='voucher_no', fieldtype='Dynamic Link',
options='voucher_type', width=180)

View File

@ -373,8 +373,8 @@ frappe.ui.form.on('Asset', {
doctype_field = frappe.scrub(doctype)
frm.set_value(doctype_field, '');
frappe.msgprint({
title: __(`Invalid ${doctype}`),
message: __(`The selected ${doctype} doesn't contains selected Asset Item.`),
title: __('Invalid {0}', [__(doctype)]),
message: __('The selected {0} does not contain the selected Asset Item.', [__(doctype)]),
indicator: 'red'
});
}
@ -436,7 +436,7 @@ frappe.ui.form.on('Asset Finance Book', {
depreciation_start_date: function(frm, cdt, cdn) {
const book = locals[cdt][cdn];
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 = "";
frm.refresh_field("finance_books");
}

View File

@ -90,6 +90,11 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
this.frm.set_df_property("drop_ship", "hidden", !is_drop_ship);
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(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'), () => {
@ -126,16 +131,25 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
if(doc.status != "Closed") {
if (doc.status != "On Hold") {
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()) {
cur_frm.add_custom_button(__('Material to Supplier'),
function() { me.make_stock_entry(); }, __("Transfer"));
}
}
if(flt(doc.per_billed) < 100)
cur_frm.add_custom_button(__('Invoice'),
cur_frm.add_custom_button(__('Purchase Invoice'),
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) {
cur_frm.add_custom_button(__('Subscription'), function() {
erpnext.utils.make_subscription(doc.doctype, doc.name)
@ -156,13 +170,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'));
}
} else if(doc.docstatus===0) {
@ -299,7 +307,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
if(me.values) {
me.values.sub_con_rm_items.map((row,i) => {
if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) {
frappe.throw(__("Item Code, warehouse, quantity are required on row" + (i+1)));
frappe.throw(__("Item Code, warehouse, quantity are required on row {0}", [i+1]));
}
})
me._make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children())
@ -358,15 +366,19 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
method: "erpnext.stock.doctype.material_request.material_request.make_purchase_order",
source_doctype: "Material Request",
target: me.frm,
setters: {},
setters: {
schedule_date: undefined,
status: undefined
},
get_query_filters: {
material_request_type: "Purchase",
docstatus: 1,
status: ["!=", "Stopped"],
per_ordered: ["<", 99.99],
company: me.frm.doc.company
}
})
}, __("Get items from"));
}, __("Get Items From"));
this.frm.add_custom_button(__('Supplier Quotation'),
function() {
@ -375,16 +387,17 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
source_doctype: "Supplier Quotation",
target: me.frm,
setters: {
supplier: me.frm.doc.supplier
supplier: me.frm.doc.supplier,
valid_till: undefined
},
get_query_filters: {
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() {
frappe.call({
"method": "get_last_purchase_rate",

View File

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

View File

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

View File

@ -217,13 +217,15 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
source_doctype: "Material Request",
target: me.frm,
setters: {
company: me.frm.doc.company
schedule_date: undefined,
status: undefined
},
get_query_filters: {
material_request_type: "Purchase",
docstatus: 1,
status: ["!=", "Stopped"],
per_ordered: ["<", 99.99]
per_ordered: ["<", 99.99],
company: me.frm.doc.company
}
})
}, __("Get Items From"));
@ -236,32 +238,40 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
source_doctype: "Opportunity",
target: me.frm,
setters: {
company: me.frm.doc.company
party_name: undefined,
opportunity_from: undefined,
status: undefined
},
get_query_filters: {
status: ["not in", ["Closed", "Lost"]],
company: me.frm.doc.company
}
})
}, __("Get Items From"));
// Get items from open Material Requests based on supplier
this.frm.add_custom_button(__('Possible Supplier'), function() {
// Create a dialog window for the user to pick their supplier
var d = new frappe.ui.Dialog({
var dialog = new frappe.ui.Dialog({
title: __('Select Possible Supplier'),
fields: [
{fieldname: 'supplier', fieldtype:'Link', options:'Supplier', label:'Supplier', reqd:1},
{fieldname: 'ok_button', fieldtype:'Button', label:'Get Items from Material Requests'},
]
});
// On the user clicking the ok button
d.fields_dict.ok_button.input.onclick = function() {
var btn = d.fields_dict.ok_button.input;
var v = d.get_values();
if(v) {
$(btn).set_working();
{
fieldname: 'supplier',
fieldtype:'Link',
options:'Supplier',
label:'Supplier',
reqd:1,
description: __("Get Items from Material Requests against this Supplier")
}
],
primary_action_label: __("Get Items"),
primary_action: (args) => {
if(!args) return;
dialog.hide();
erpnext.utils.map_current_doc({
method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_item_from_material_requests_based_on_supplier",
source_name: v.supplier,
source_name: args.supplier,
target: me.frm,
setters: {
company: me.frm.doc.company
@ -273,11 +283,11 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
per_ordered: ["<", 99.99]
}
});
$(btn).done_working();
d.hide();
dialog.hide();
}
}
d.show();
});
dialog.show();
}, __("Get Items From"));
// Get Suppliers

View File

@ -120,3 +120,20 @@ class TestSupplier(unittest.TestCase):
# Rollback
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

@ -37,16 +37,18 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext
source_doctype: "Material Request",
target: me.frm,
setters: {
company: me.frm.doc.company
schedule_date: undefined,
status: undefined
},
get_query_filters: {
material_request_type: "Purchase",
docstatus: 1,
status: ["!=", "Stopped"],
per_ordered: ["<", 99.99]
per_ordered: ["<", 99.99],
company: me.frm.doc.company
}
})
}, __("Get items from"));
}, __("Get Items From"));
this.frm.add_custom_button(__("Request for Quotation"),
function() {
@ -58,16 +60,16 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext
source_doctype: "Request for Quotation",
target: me.frm,
setters: {
company: me.frm.doc.company,
transaction_date: null
},
get_query_filters: {
supplier: me.frm.doc.supplier
supplier: me.frm.doc.supplier,
company: me.frm.doc.company
},
get_query_method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_rfq_containing_supplier"
})
}, __("Get items from"));
}, __("Get Items From"));
}
},

View File

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

View File

@ -1,12 +1,14 @@
{
"actions": [],
"autoname": "Prompt",
"creation": "2019-06-05 11:48:30.572795",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"communication_channel",
"communication_medium_type",
"catch_all",
"column_break_3",
"catch_all",
"provider",
"disabled",
"timeslots_section",
@ -54,9 +56,16 @@
"fieldtype": "Table",
"label": "Timeslots",
"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",
"module": "Communication",
"name": "Communication Medium",

View File

@ -371,13 +371,27 @@ class SellingController(StockController):
self.make_sl_entries(sl_entries)
def set_po_nos(self):
if self.doctype in ("Delivery Note", "Sales Invoice") and hasattr(self, "items"):
ref_fieldname = "against_sales_order" if self.doctype == "Delivery Note" else "sales_order"
sales_orders = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)]))
if sales_orders:
po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)})
if po_nos and po_nos[0].get('po_no'):
self.po_no = ', '.join(list(set([d.po_no for d in po_nos if d.po_no])))
if self.doctype == 'Sales Invoice' and hasattr(self, "items"):
self.set_pos_for_sales_invoice()
if self.doctype == 'Delivery Note' and hasattr(self, "items"):
self.set_pos_for_delivery_note()
def set_pos_for_sales_invoice(self):
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):
if self.doctype in ["Sales Order", "Quotation"]:
@ -402,26 +416,26 @@ class SellingController(StockController):
return
for d in self.get('items'):
if self.doctype == "Sales Invoice":
e = [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]
if self.doctype in ["POS Invoice","Sales Invoice"]:
stock_items = [d.item_code, d.description, d.warehouse, d.sales_order or d.delivery_note, d.batch_no or '']
non_stock_items = [d.item_code, d.description, d.sales_order or d.delivery_note]
elif self.doctype == "Delivery Note":
e = [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]
stock_items = [d.item_code, d.description, d.warehouse, d.against_sales_order or d.against_sales_invoice, d.batch_no or '']
non_stock_items = [d.item_code, d.description, d.against_sales_order or d.against_sales_invoice]
elif self.doctype in ["Sales Order", "Quotation"]:
e = [d.item_code, d.description, d.warehouse, '']
f = [d.item_code, d.description]
stock_items = [d.item_code, d.description, d.warehouse, '']
non_stock_items = [d.item_code, d.description]
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))
else:
check_list.append(e)
check_list.append(stock_items)
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))
else:
chk_dupl_itm.append(f)
chk_dupl_itm.append(non_stock_items)
def validate_target_warehouse(self):
items = self.get("items") + (self.get("packed_items") or [])

View File

@ -229,9 +229,9 @@ class StockController(AccountsController):
def check_expense_account(self, item):
if not item.get("expense_account"):
frappe.throw(_("Row #{0}: Expense Account not set for Item {1}. Please set an Expense \
Account in the Items table").format(item.idx, frappe.bold(item.item_code)),
title=_("Expense Account Missing"))
msg = _("Please set an Expense Account in the Items table")
frappe.throw(_("Row #{0}: Expense Account not set for the Item {1}. {2}")
.format(item.idx, frappe.bold(item.item_code), msg), title=_("Expense Account Missing"))
else:
is_expense_account = frappe.db.get_value("Account",
@ -247,7 +247,9 @@ class StockController(AccountsController):
for d in self.items:
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:
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 to_time = Date.parse('01/01/2019 ' + d.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",
"fieldtype": "Text Editor",
"label": "Contract Terms and Conditions",
"read_only": 1
"label": "Contract Terms and Conditions"
},
{
"fieldname": "sb_fulfilment",
@ -45,7 +44,7 @@
}
],
"links": [],
"modified": "2020-06-03 00:24:58.179816",
"modified": "2020-11-11 17:49:44.879363",
"modified_by": "Administrator",
"module": "CRM",
"name": "Contract Template",

View File

@ -4,48 +4,55 @@
"item_code": "Computer",
"gross_purchase_amount": 100000,
"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",
"item_code": "Computer",
"gross_purchase_amount": 60000,
"asset_owner": "Company",
"available_for_use_date": "2017-10-02"
"available_for_use_date": "2017-10-02",
"location": "Avg Location"
},
{
"asset_name": "Conferrence Table",
"item_code": "Table",
"gross_purchase_amount": 30000,
"asset_owner": "Company",
"available_for_use_date": "2018-10-02"
"available_for_use_date": "2018-10-02",
"location": "Zany Location"
},
{
"asset_name": "Lunch Table",
"item_code": "Table",
"gross_purchase_amount": 20000,
"asset_owner": "Company",
"available_for_use_date": "2018-06-02"
"available_for_use_date": "2018-06-02",
"location": "Fletcher Location"
},
{
"asset_name": "ERPNext",
"item_code": "ERP",
"gross_purchase_amount": 100000,
"asset_owner": "Company",
"available_for_use_date": "2018-09-02"
"available_for_use_date": "2018-09-02",
"location":"Main Location"
},
{
"asset_name": "Chair 1",
"item_code": "Chair",
"gross_purchase_amount": 10000,
"asset_owner": "Company",
"available_for_use_date": "2018-07-02"
"available_for_use_date": "2018-07-02",
"location": "Zany Location"
},
{
"asset_name": "Chair 2",
"item_code": "Chair",
"gross_purchase_amount": 10000,
"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
def setup_data():
import_json("Location")
import_json("Asset Category")
setup_item()
setup_workstation()

View File

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

View File

@ -6,8 +6,10 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
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.hr.doctype.holiday_list.holiday_list import is_holiday
class StudentAttendance(Document):
def validate(self):
@ -17,6 +19,7 @@ class StudentAttendance(Document):
self.set_student_group()
self.validate_student()
self.validate_duplication()
self.validate_is_holiday()
def set_date(self):
if self.course_schedule:
@ -78,3 +81,18 @@ class StudentAttendance(Document):
record = get_link_to_form('Student Attendance', attendance_record)
frappe.throw(_('Student Attendance record {0} already exists against the Student {1}')
.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

@ -20,10 +20,10 @@ def get_student_attendance_records(based_on, date=None, student_group=None, cour
student_list = frappe.get_list("Student Group Student", fields=["student", "student_name", "group_roll_number"] , \
filters={"parent": student_group, "active": 1}, order_by= "group_roll_number")
if not student_list:
student_list = frappe.get_list("Student Group Student", fields=["student", "student_name", "group_roll_number"] ,
if not student_list:
student_list = frappe.get_list("Student Group Student", fields=["student", "student_name", "group_roll_number"] ,
filters={"parent": student_group, "active": 1}, order_by= "group_roll_number")
if course_schedule:
student_attendance_list= frappe.db.sql('''select student, status from `tabStudent Attendance` where \
course_schedule= %s''', (course_schedule), as_dict=1)
@ -32,7 +32,7 @@ def get_student_attendance_records(based_on, date=None, student_group=None, cour
student_group= %s and date= %s and \
(course_schedule is Null or course_schedule='')''',
(student_group, date), as_dict=1)
for attendance in student_attendance_list:
for student in student_list:
if student.student == attendance.student:

View File

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

View File

@ -6,11 +6,14 @@ from __future__ import unicode_literals
import frappe
from frappe import _
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
class StudentLeaveApplication(Document):
def validate(self):
self.validate_holiday_list()
self.validate_duplicate()
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}')
.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):
holiday_list = get_holiday_list()
for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
date = dt.strftime('%Y-%m-%d')
if is_holiday(holiday_list, date):
continue
attendance = frappe.db.exists('Student Attendance', {
'student': self.student,
'date': date,
@ -89,3 +101,19 @@ class StudentLeaveApplication(Document):
def daterange(start_date, end_date):
for n in range(int ((end_date - start_date).days)+1):
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 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.test_student import create_student
class TestStudentLeaveApplication(unittest.TestCase):
def setUp(self):
frappe.db.sql("""delete from `tabStudent Leave Application`""")
create_holiday_list()
def test_attendance_record_creation(self):
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')
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()
leave_application = frappe.get_doc({
'doctype': 'Student Leave Application',
'student': student.name,
'attendance_based_on': 'Student Group',
'student_group': get_random_group().name,
'from_date': from_date if from_date else getdate(),
'to_date': from_date if from_date else getdate(),
'mark_as_present': mark_as_present
}).insert()
leave_application.submit()
leave_application = frappe.new_doc('Student Leave Application')
leave_application.student = student.name
leave_application.attendance_based_on = 'Student Group'
leave_application.student_group = get_random_group().name
leave_application.from_date = from_date if from_date else getdate()
leave_application.to_date = from_date if from_date else getdate()
leave_application.mark_as_present = mark_as_present
if submit:
leave_application.insert()
leave_application.submit()
return leave_application
def create_student_attendance(date=None, status=None):
@ -67,4 +94,22 @@ def get_student():
email='test_student@gmail.com',
first_name='Test',
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
import frappe
from frappe.utils import cstr, cint, getdate
from frappe.utils import formatdate
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):
if not filters: filters = {}
@ -15,6 +17,11 @@ def execute(filters=None):
columns = get_columns(filters)
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)
leave_applicants = get_leave_applications(date)
if absent_students:

View File

@ -3,8 +3,10 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import cstr, cint, getdate
from frappe.utils import formatdate
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):
if not filters: filters = {}
@ -12,6 +14,10 @@ def execute(filters=None):
if not filters.get("date"):
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)
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 calendar import monthrange
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):
if not filters: filters = {}
@ -19,26 +21,32 @@ def execute(filters=None):
students_list = get_students_list(students)
att_map = get_attendance_list(from_date, to_date, filters.get("student_group"), students_list)
data = []
for stud in students:
row = [stud.student, stud.student_name]
student_status = frappe.db.get_value("Student", stud.student, "enabled")
date = from_date
total_p = total_a = 0.0
for day in range(total_days_in_month):
status="None"
if att_map.get(stud.student):
status = att_map.get(stud.student).get(date, "None")
elif not student_status:
status = "Inactive"
else:
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])
if status == "Present":
total_p += 1
elif status == "Absent":
total_a += 1
date = add_days(date, 1)
row += [total_p, total_a]
data.append(row)
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
order by student, date''',
(student_group, from_date, to_date), as_dict=1)
att_map = {}
students_with_leave_application = get_students_with_leave_application(from_date, to_date, students_list)
for d in attendance_list:
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):
att_map[d.student][d.date] = "Present"
else:
att_map[d.student][d.date] = d.status
att_map = mark_holidays(att_map, from_date, to_date, students_list)
return att_map
def get_students_with_leave_application(from_date, to_date, students_list):
@ -108,3 +121,14 @@ def get_attendance_years():
if not year_list:
year_list = [getdate().year]
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
from frappe import _
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.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_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_settings.shopify_settings import get_shopify_url, get_header
@frappe.whitelist(allow_guest=True)
@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)
def sync_sales_order(order, request_id=None):
def sync_sales_order(order, request_id=None, old_order_sync=False):
frappe.set_user('Administrator')
shopify_settings = frappe.get_doc("Shopify Settings")
frappe.flags.request_id = request_id
@ -27,7 +28,7 @@ def sync_sales_order(order, request_id=None):
try:
validate_customer(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:
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"):
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)
if so:
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)
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")
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:
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",
"naming_series": shopify_settings.sales_order_series or "SO-Shopify-",
"shopify_order_id": shopify_order.get("id"),
"shopify_order_number": shopify_order.get("name"),
"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,
"selling_price_list": shopify_settings.price_list,
"ignore_pricing_rule": 1,
@ -132,32 +135,42 @@ def create_sales_order(shopify_order, shopify_settings, company=None):
frappe.db.commit()
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")\
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.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.flags.ignore_mandatory = True
set_cost_center(si.items, shopify_settings.cost_center)
si.insert(ignore_mandatory=True)
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()
def set_cost_center(items, cost_center):
for item in items:
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
payemnt_entry = get_payment_entry(doc.doctype, doc.name, bank_account=shopify_settings.cash_bank_account)
payemnt_entry.flags.ignore_mandatory = True
payemnt_entry.reference_no = doc.name
payemnt_entry.reference_date = nowdate()
payemnt_entry.insert(ignore_permissions=True)
payemnt_entry.submit()
payment_entry = get_payment_entry(doc.doctype, doc.name, bank_account=shopify_settings.cash_bank_account)
payment_entry.flags.ignore_mandatory = True
payment_entry.reference_no = doc.name
payment_entry.posting_date = posting_date or nowdate()
payment_entry.reference_date = posting_date or nowdate()
payment_entry.insert(ignore_permissions=True)
payment_entry.submit()
def create_delivery_note(shopify_order, shopify_settings, so):
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.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.naming_series = shopify_settings.delivery_note_series or "DN-Shopify-"
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"))
return discounted_amount
def get_order_items(order_items, shopify_settings):
def get_order_items(order_items, shopify_settings, delivery_date):
items = []
all_product_exists = True
product_not_exists = []
@ -205,7 +221,7 @@ def get_order_items(order_items, shopify_settings):
"item_code": item_code,
"item_name": shopify_item.get("name"),
"rate": shopify_item.get("price"),
"delivery_date": nowdate(),
"delivery_date": delivery_date,
"qty": shopify_item.get("quantity"),
"stock_uom": shopify_item.get("uom") or _("Nos"),
"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")))
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",
"doctype": "DocType",
"document_type": "System",
"engine": "InnoDB",
"field_order": [
"status_html",
"enable_shopify",
@ -40,7 +42,16 @@
"sales_invoice_series",
"section_break_22",
"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": [
{
@ -255,10 +266,71 @@
"fieldtype": "Table",
"label": "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,
"modified": "2020-09-18 17:26:09.703215",
"links": [],
"modified": "2020-11-05 20:44:03.664891",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Shopify Settings",
@ -277,4 +349,4 @@
],
"sort_field": "modified",
"sort_order": "DESC"
}
}

View File

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

@ -58,7 +58,7 @@ class ShopifySettings(unittest.TestCase):
}).save(ignore_permissions=True)
self.shopify_settings = shopify_settings
def test_order(self):
### Create Customer ###
with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_customer.json")) as shopify_customer:
@ -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:
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"))})

View File

@ -43,7 +43,7 @@
{
"hidden": 0,
"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",
@ -64,7 +64,7 @@
"idx": 0,
"is_standard": 1,
"label": "Healthcare",
"modified": "2020-06-25 23:50:56.951698",
"modified": "2020-11-23 23:00:48.764377",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare",

View File

@ -85,8 +85,7 @@ frappe.ui.form.on('Clinical Procedure', {
callback: function(r) {
if (r.message) {
frappe.show_alert({
message: __('Stock Entry {0} created',
['<a class="bold" href="#Form/Stock Entry/'+ r.message + '">' + r.message + '</a>']),
message: __('Stock Entry {0} created', ['<a class="bold" href="#Form/Stock Entry/'+ r.message + '">' + r.message + '</a>']),
indicator: 'green'
});
}
@ -105,8 +104,7 @@ frappe.ui.form.on('Clinical Procedure', {
callback: function(r) {
if (!r.exc) {
if (r.message == 'insufficient stock') {
let msg = __('Stock quantity to start the Procedure is not available in the Warehouse {0}. Do you want to record a Stock Entry?',
[frm.doc.warehouse.bold()]);
let msg = __('Stock quantity to start the Procedure is not available in the Warehouse {0}. Do you want to record a Stock Entry?', [frm.doc.warehouse.bold()]);
frappe.confirm(
msg,
function() {

View File

@ -274,4 +274,6 @@ def get_filters(entry):
def get_current_healthcare_service_unit(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'
app_include_js = "assets/js/erpnext.min.js"
app_include_css = "assets/css/erpnext.css"
web_include_js = "assets/js/erpnext-web.min.js"
web_include_css = "assets/css/erpnext-web.css"
app_include_js = "/assets/js/erpnext.min.js"
app_include_css = "/assets/css/erpnext.css"
web_include_js = "/assets/js/erpnext-web.min.js"
web_include_css = "/assets/css/erpnext-web.css"
doctype_js = {
"Address": "public/js/address.js",
@ -237,6 +237,9 @@ doc_events = {
"Website Settings": {
"validate": "erpnext.portal.doctype.products_settings.products_settings.home_page_is_products"
},
"Tax Category": {
"validate": "erpnext.regional.india.utils.validate_tax_category"
},
"Sales Invoice": {
"on_submit": [
"erpnext.regional.create_transaction_log",
@ -250,7 +253,11 @@ doc_events = {
"on_trash": "erpnext.regional.check_deletion_permission"
},
"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": {
"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.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
"erpnext.support.doctype.issue.issue.set_service_level_agreement_variance",
"erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders",
],
"daily": [
"erpnext.stock.reorder_item.reorder_item",
@ -339,14 +347,16 @@ scheduler_events = {
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms",
"erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation",
"erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.automatically_allocate_leaves_based_on_leave_policy",
"erpnext.hr.utils.generate_leave_encashment",
"erpnext.hr.utils.allocate_earned_leaves",
"erpnext.hr.utils.grant_leaves_automatically",
"erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall.create_process_loan_security_shortfall",
"erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
"erpnext.crm.doctype.lead.lead.daily_open_lead"
],
"monthly_long": [
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
"erpnext.hr.utils.allocate_earned_leaves",
"erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_demand_loans"
]
}
@ -390,7 +400,8 @@ regional_overrides = {
'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries'
},
'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': {
'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 = []
department_details = {}
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
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)
if len(approvers) == 0:
frappe.throw(_("Please set {0} for the Employee or for Department: {1}").
format(
field_name, frappe.bold(employee_department),
frappe.bold(employee.name)
),
title=_(field_name + " Missing"))
error_msg = _("Please set {0} for the Employee: {1}").format(field_name, frappe.bold(employee.employee_name))
if department_list:
error_msg += _(" or for Department: {0}").format(frappe.bold(employee_department))
frappe.throw(error_msg, title=_(field_name + " Missing"))
return set(tuple(approver) for approver in approvers)

View File

@ -57,7 +57,6 @@
"column_break_45",
"shift_request_approver",
"attendance_and_leave_details",
"leave_policy",
"attendance_device_id",
"column_break_44",
"holiday_list",
@ -411,14 +410,6 @@
"oldfieldtype": "Link",
"options": "Branch"
},
{
"fetch_from": "grade.default_leave_policy",
"fetch_if_empty": 1,
"fieldname": "leave_policy",
"fieldtype": "Link",
"label": "Leave Policy",
"options": "Leave Policy"
},
{
"description": "Applicable Holiday List",
"fieldname": "holiday_list",
@ -672,10 +663,10 @@
"oldfieldtype": "Date"
},
{
"depends_on": "eval:doc.status == \"Left\"",
"fieldname": "relieving_date",
"fieldtype": "Date",
"label": "Relieving Date",
"mandatory_depends_on": "eval:doc.status == \"Left\"",
"oldfieldname": "relieving_date",
"oldfieldtype": "Date"
},
@ -822,7 +813,7 @@
"idx": 24,
"image_field": "image",
"links": [],
"modified": "2020-10-06 15:58:23.805489",
"modified": "2020-10-16 15:02:04.283657",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",

View File

@ -1,167 +1,69 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 1,
"autoname": "Prompt",
"beta": 0,
"creation": "2018-04-13 16:14:24.174138",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2018-04-13 16:14:24.174138",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"default_salary_structure"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "default_leave_policy",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Default Leave Policy",
"length": 0,
"no_copy": 0,
"options": "Leave Policy",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "default_salary_structure",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Default Salary Structure",
"length": 0,
"no_copy": 0,
"options": "Salary Structure",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Salary Structure"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-09-18 17:17:45.617624",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-08-26 13:12:07.815330",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Grade",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
"track_changes": 1
}

View File

@ -21,6 +21,7 @@
"show_leaves_of_all_department_members_in_calendar",
"auto_leave_encashment",
"restrict_backdated_leave_application",
"automatically_allocate_leaves_based_on_leave_policy",
"hiring_settings",
"check_vacancies"
],
@ -41,7 +42,7 @@
"description": "Employee records are created using the selected field",
"fieldname": "emp_created_by",
"fieldtype": "Select",
"label": "Employee Records to Be Created By",
"label": "Employee Records to be created by",
"options": "Naming Series\nEmployee Number\nFull Name"
},
{
@ -117,7 +118,7 @@
"default": "0",
"fieldname": "restrict_backdated_leave_application",
"fieldtype": "Check",
"label": "Restrict Backdated Leave Applications"
"label": "Restrict Backdated Leave Application"
},
{
"depends_on": "eval:doc.restrict_backdated_leave_application == 1",
@ -125,13 +126,19 @@
"fieldtype": "Link",
"label": "Role Allowed to Create Backdated Leave Application",
"options": "Role"
},
{
"default": "0",
"fieldname": "automatically_allocate_leaves_based_on_leave_policy",
"fieldtype": "Check",
"label": "Automatically Allocate Leaves Based On Leave Policy"
}
],
"icon": "fa fa-cog",
"idx": 1,
"issingle": 1,
"links": [],
"modified": "2020-10-13 11:49:46.168027",
"modified": "2020-08-27 14:30:28.995324",
"modified_by": "Administrator",
"module": "HR",
"name": "HR Settings",

View File

@ -1,4 +1,5 @@
{
"actions": [],
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-02-20 19:10:38",
@ -24,6 +25,7 @@
"compensatory_request",
"leave_period",
"leave_policy",
"leave_policy_assignment",
"carry_forwarded_leaves_count",
"expired",
"amended_from",
@ -160,9 +162,10 @@
"read_only": 1
},
{
"fetch_from": "employee.leave_policy",
"fetch_from": "leave_policy_assignment.leave_policy",
"fieldname": "leave_policy",
"fieldtype": "Link",
"hidden": 1,
"in_standard_filter": 1,
"label": "Leave Policy",
"options": "Leave Policy",
@ -209,12 +212,21 @@
"fieldtype": "Float",
"label": "Carry Forwarded Leaves",
"read_only": 1
},
{
"fieldname": "leave_policy_assignment",
"fieldtype": "Link",
"label": "Leave Policy Assignment",
"options": "Leave Policy Assignment",
"read_only": 1
}
],
"icon": "fa fa-ok",
"idx": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"modified": "2019-08-08 15:08:42.440909",
"links": [],
"modified": "2020-08-20 14:25:10.314323",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Allocation",

View File

@ -51,9 +51,19 @@ class LeaveAllocation(Document):
def on_cancel(self):
self.create_leave_ledger_entry(submit=False)
if self.leave_policy_assignment:
self.update_leave_policy_assignments_when_no_allocations_left()
if self.carry_forward:
self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True)
def update_leave_policy_assignments_when_no_allocations_left(self):
allocations = frappe.db.get_list("Leave Allocation", filters = {
"docstatus": 1,
"leave_policy_assignment": self.leave_policy_assignment
})
if len(allocations) == 0:
frappe.db.set_value("Leave Policy Assignment", self.leave_policy_assignment ,"leaves_allocated", 0)
def validate_period(self):
if date_diff(self.to_date, self.from_date) <= 0:
frappe.throw(_("To date cannot be before from date"))

View File

@ -130,8 +130,7 @@ class LeaveApplication(Document):
if self.status == "Approved":
for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
date = dt.strftime("%Y-%m-%d")
status = "Half Day" if getdate(date) == getdate(self.half_day_date) else "On Leave"
status = "Half Day" if self.half_day_date and getdate(date) == getdate(self.half_day_date) else "On Leave"
attendance_name = frappe.db.exists('Attendance', dict(employee = self.employee,
attendance_date = date, docstatus = ('!=', 2)))
@ -293,7 +292,8 @@ class LeaveApplication(Document):
def set_half_day_date(self):
if self.from_date == self.to_date and self.half_day == 1:
self.half_day_date = self.from_date
elif self.half_day == 0:
if self.half_day == 0:
self.half_day_date = None
def notify_employee(self):

View File

@ -10,6 +10,7 @@ from frappe.permissions import clear_user_permissions_for_doctype
from frappe.utils import add_days, nowdate, now_datetime, getdate, add_months
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees
test_dependencies = ["Leave Allocation", "Leave Block List"]
@ -410,25 +411,39 @@ class TestLeaveApplication(unittest.TestCase):
self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8)), 21)
def test_earned_leaves_creation(self):
frappe.db.sql('''delete from `tabLeave Period`''')
frappe.db.sql('''delete from `tabLeave Policy Assignment`''')
frappe.db.sql('''delete from `tabLeave Allocation`''')
frappe.db.sql('''delete from `tabLeave Ledger Entry`''')
leave_period = get_leave_period()
employee = get_employee()
leave_type = 'Test Earned Leave Type'
if not frappe.db.exists('Leave Type', leave_type):
frappe.get_doc(dict(
leave_type_name = leave_type,
doctype = 'Leave Type',
is_earned_leave = 1,
earned_leave_frequency = 'Monthly',
rounding = 0.5,
max_leaves_allowed = 6
)).insert()
frappe.delete_doc_if_exists("Leave Type", 'Test Earned Leave Type', force=1)
frappe.get_doc(dict(
leave_type_name = leave_type,
doctype = 'Leave Type',
is_earned_leave = 1,
earned_leave_frequency = 'Monthly',
rounding = 0.5,
max_leaves_allowed = 6
)).insert()
leave_policy = frappe.get_doc({
"doctype": "Leave Policy",
"leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}]
}).insert()
frappe.db.set_value("Employee", employee.name, "leave_policy", leave_policy.name)
allocate_leaves(employee, leave_period, leave_type, 0, eligible_leaves = 12)
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
"leave_period": leave_period.name
}
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee()
from erpnext.hr.utils import allocate_earned_leaves
i = 0

View File

@ -32,7 +32,7 @@ class LeaveEncashment(Document):
additional_salary.employee = self.employee
earning_component = frappe.get_value("Leave Type", self.leave_type, "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.payroll_date = self.encashment_date
additional_salary.amount = self.encashment_amount
@ -98,7 +98,11 @@ class LeaveEncashment(Document):
create_leave_ledger_entry(self, args, submit)
# 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()):
args = frappe._dict(
leaves=self.encashable_days,

View File

@ -9,6 +9,7 @@ from frappe.utils import today, add_months
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees
from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy\
test_dependencies = ["Leave Type"]
@ -16,6 +17,7 @@ test_dependencies = ["Leave Type"]
class TestLeaveEncashment(unittest.TestCase):
def setUp(self):
frappe.db.sql('''delete from `tabLeave Period`''')
frappe.db.sql('''delete from `tabLeave Policy Assignment`''')
frappe.db.sql('''delete from `tabLeave Allocation`''')
frappe.db.sql('''delete from `tabLeave Ledger Entry`''')
frappe.db.sql('''delete from `tabAdditional Salary`''')
@ -29,14 +31,22 @@ class TestLeaveEncashment(unittest.TestCase):
# create employee, salary structure and assignment
self.employee = make_employee("test_employee_encashment@example.com")
frappe.db.set_value("Employee", self.employee, "leave_policy", leave_policy.name)
self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
"leave_period": self.leave_period.name
}
leave_policy_assignments = create_assignment_for_multiple_employees([self.employee], frappe._dict(data))
salary_structure = make_salary_structure("Salary Structure for Encashment", "Monthly", self.employee,
other_details={"leave_encashment_amount_per_day": 50})
# create the leave period and assign the leaves
self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
self.leave_period.grant_leave_allocation(employee=self.employee)
#grant Leaves
frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee()
def test_leave_balance_value_and_amount(self):
frappe.db.sql('''delete from `tabLeave Encashment`''')

View File

@ -2,14 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on('Leave Period', {
refresh: (frm)=>{
frm.set_df_property("grant_leaves", "hidden", frm.doc.__islocal ? 1:0);
if(!frm.is_new()) {
frm.add_custom_button(__('Grant Leaves'), function () {
frm.trigger("grant_leaves");
});
}
},
from_date: (frm)=>{
if (frm.doc.from_date && !frm.doc.to_date) {
var a_year_from_start = frappe.datetime.add_months(frm.doc.from_date, 12);
@ -22,73 +14,7 @@ frappe.ui.form.on('Leave Period', {
"filters": {
"company": frm.doc.company,
}
}
})
},
grant_leaves: function(frm) {
var d = new frappe.ui.Dialog({
title: __('Grant Leaves'),
fields: [
{
"label": "Filter Employees By (Optional)",
"fieldname": "sec_break",
"fieldtype": "Section Break",
},
{
"label": "Employee Grade",
"fieldname": "grade",
"fieldtype": "Link",
"options": "Employee Grade"
},
{
"label": "Department",
"fieldname": "department",
"fieldtype": "Link",
"options": "Department"
},
{
"fieldname": "col_break",
"fieldtype": "Column Break",
},
{
"label": "Designation",
"fieldname": "designation",
"fieldtype": "Link",
"options": "Designation"
},
{
"label": "Employee",
"fieldname": "employee",
"fieldtype": "Link",
"options": "Employee"
},
{
"fieldname": "sec_break",
"fieldtype": "Section Break",
},
{
"label": "Add unused leaves from previous allocations",
"fieldname": "carry_forward",
"fieldtype": "Check"
}
],
primary_action: function() {
var data = d.get_values();
frappe.call({
doc: frm.doc,
method: "grant_leave_allocation",
args: data,
callback: function(r) {
if(!r.exc) {
d.hide();
frm.reload_doc();
}
}
});
},
primary_action_label: __('Grant')
};
});
d.show();
}
},
});

View File

@ -7,24 +7,10 @@ import frappe
from frappe import _
from frappe.utils import getdate, cstr, add_days, date_diff, getdate, ceil
from frappe.model.document import Document
from erpnext.hr.utils import validate_overlap, get_employee_leave_policy
from erpnext.hr.utils import validate_overlap
from frappe.utils.background_jobs import enqueue
from six import iteritems
class LeavePeriod(Document):
def get_employees(self, args):
conditions, values = [], []
for field, value in iteritems(args):
if value:
conditions.append("{0}=%s".format(field))
values.append(value)
condition_str = " and " + " and ".join(conditions) if len(conditions) else ""
employees = frappe._dict(frappe.db.sql("select name, date_of_joining from tabEmployee where status='Active' {condition}" #nosec
.format(condition=condition_str), tuple(values)))
return employees
def validate(self):
self.validate_dates()
@ -33,96 +19,3 @@ class LeavePeriod(Document):
def validate_dates(self):
if getdate(self.from_date) >= getdate(self.to_date):
frappe.throw(_("To date can not be equal or less than from date"))
def grant_leave_allocation(self, grade=None, department=None, designation=None,
employee=None, carry_forward=0):
employee_records = self.get_employees({
"grade": grade,
"department": department,
"designation": designation,
"name": employee
})
if employee_records:
if len(employee_records) > 20:
frappe.enqueue(grant_leave_alloc_for_employees, timeout=600,
employee_records=employee_records, leave_period=self, carry_forward=carry_forward)
else:
grant_leave_alloc_for_employees(employee_records, self, carry_forward)
else:
frappe.msgprint(_("No Employee Found"))
def grant_leave_alloc_for_employees(employee_records, leave_period, carry_forward=0):
leave_allocations = []
existing_allocations_for = get_existing_allocations(list(employee_records.keys()), leave_period.name)
leave_type_details = get_leave_type_details()
count = 0
for employee in employee_records.keys():
if employee in existing_allocations_for:
continue
count +=1
leave_policy = get_employee_leave_policy(employee)
if leave_policy:
for leave_policy_detail in leave_policy.leave_policy_details:
if not leave_type_details.get(leave_policy_detail.leave_type).is_lwp:
leave_allocation = create_leave_allocation(employee, leave_policy_detail.leave_type,
leave_policy_detail.annual_allocation, leave_type_details, leave_period, carry_forward, employee_records.get(employee))
leave_allocations.append(leave_allocation)
frappe.db.commit()
frappe.publish_progress(count*100/len(set(employee_records.keys()) - set(existing_allocations_for)), title = _("Allocating leaves..."))
if leave_allocations:
frappe.msgprint(_("Leaves has been granted sucessfully"))
def get_existing_allocations(employees, leave_period):
leave_allocations = frappe.db.sql_list("""
SELECT DISTINCT
employee
FROM `tabLeave Allocation`
WHERE
leave_period=%s
AND employee in (%s)
AND carry_forward=0
AND docstatus=1
""" % ('%s', ', '.join(['%s']*len(employees))), [leave_period] + employees)
if leave_allocations:
frappe.msgprint(_("Skipping Leave Allocation for the following employees, as Leave Allocation records already exists against them. {0}")
.format("\n".join(leave_allocations)))
return leave_allocations
def get_leave_type_details():
leave_type_details = frappe._dict()
leave_types = frappe.get_all("Leave Type",
fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "is_carry_forward", "expire_carry_forwarded_leaves_after_days"])
for d in leave_types:
leave_type_details.setdefault(d.name, d)
return leave_type_details
def create_leave_allocation(employee, leave_type, new_leaves_allocated, leave_type_details, leave_period, carry_forward, date_of_joining):
''' Creates leave allocation for the given employee in the provided leave period '''
if carry_forward and not leave_type_details.get(leave_type).is_carry_forward:
carry_forward = 0
# Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period
if getdate(date_of_joining) > getdate(leave_period.from_date):
remaining_period = ((date_diff(leave_period.to_date, date_of_joining) + 1) / (date_diff(leave_period.to_date, leave_period.from_date) + 1))
new_leaves_allocated = ceil(new_leaves_allocated * remaining_period)
# Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0
if leave_type_details.get(leave_type).is_earned_leave == 1 or leave_type_details.get(leave_type).is_compensatory == 1:
new_leaves_allocated = 0
allocation = frappe.get_doc(dict(
doctype="Leave Allocation",
employee=employee,
leave_type=leave_type,
from_date=leave_period.from_date,
to_date=leave_period.to_date,
new_leaves_allocated=new_leaves_allocated,
leave_period=leave_period.name,
carry_forward=carry_forward
))
allocation.save(ignore_permissions = True)
allocation.submit()
return allocation.name

View File

@ -5,43 +5,11 @@ from __future__ import unicode_literals
import frappe, erpnext
import unittest
from frappe.utils import today, add_months
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.leave_application.leave_application import get_leave_balance_on
test_dependencies = ["Employee", "Leave Type", "Leave Policy"]
class TestLeavePeriod(unittest.TestCase):
def setUp(self):
frappe.db.sql("delete from `tabLeave Period`")
def test_leave_grant(self):
leave_type = "_Test Leave Type"
# create the leave policy
leave_policy = frappe.get_doc({
"doctype": "Leave Policy",
"leave_policy_details": [{
"leave_type": leave_type,
"annual_allocation": 20
}]
}).insert()
leave_policy.submit()
# create employee and assign the leave period
employee = "test_leave_period@employee.com"
employee_doc_name = make_employee(employee)
frappe.db.set_value("Employee", employee_doc_name, "leave_policy", leave_policy.name)
# clear the already allocated leave
frappe.db.sql('''delete from `tabLeave Allocation` where employee=%s''', "test_leave_period@employee.com")
# create the leave period
leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
# test leave_allocation
leave_period.grant_leave_allocation(employee=employee_doc_name)
self.assertEqual(get_leave_balance_on(employee_doc_name, leave_type, today()), 20)
pass
def create_leave_period(from_date, to_date, company=None):
leave_period = frappe.db.get_value('Leave Period',

View File

@ -0,0 +1,72 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Leave Policy Assignment', {
onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"];
},
refresh: function(frm) {
if (frm.doc.docstatus === 1 && frm.doc.leaves_allocated === 0) {
frm.add_custom_button(__("Grant Leave"), function() {
frappe.call({
doc: frm.doc,
method: "grant_leave_alloc_for_employee",
callback: function(r) {
let leave_allocations = r.message;
let msg = frm.events.get_success_message(leave_allocations);
frappe.msgprint(msg);
cur_frm.refresh();
}
});
});
}
},
get_success_message: function(leave_allocations) {
let msg = __("Leaves has been granted successfully");
msg += "<br><table class='table table-bordered'>";
msg += "<tr><th>"+__('Leave Type')+"</th><th>"+__("Leave Allocation")+"</th><th>"+__("Leaves Granted")+"</th><tr>";
for (let key in leave_allocations) {
msg += "<tr><th>"+key+"</th><td>"+leave_allocations[key]["name"]+"</td><td>"+leave_allocations[key]["leaves"]+"</td></tr>";
}
msg += "</table>";
return msg;
},
assignment_based_on: function(frm) {
if (frm.doc.assignment_based_on) {
frm.events.set_effective_date(frm);
} else {
frm.set_value("effective_from", '');
frm.set_value("effective_to", '');
}
},
leave_period: function(frm) {
if (frm.doc.leave_period) {
frm.events.set_effective_date(frm);
}
},
set_effective_date: function(frm) {
if (frm.doc.assignment_based_on == "Leave Period" && frm.doc.leave_period) {
frappe.model.with_doc("Leave Period", frm.doc.leave_period, function () {
let from_date = frappe.model.get_value("Leave Period", frm.doc.leave_period, "from_date");
let to_date = frappe.model.get_value("Leave Period", frm.doc.leave_period, "to_date");
frm.set_value("effective_from", from_date);
frm.set_value("effective_to", to_date);
});
} else if (frm.doc.assignment_based_on == "Joining Date" && frm.doc.employee) {
frappe.model.with_doc("Employee", frm.doc.employee, function () {
let from_date = frappe.model.get_value("Employee", frm.doc.employee, "date_of_joining");
frm.set_value("effective_from", from_date);
frm.set_value("effective_to", frappe.datetime.add_months(frm.doc.effective_from, 12));
});
}
frm.refresh();
}
});

View File

@ -0,0 +1,160 @@
{
"actions": [],
"autoname": "HR-LPOL-ASSGN-.#####",
"creation": "2020-08-19 13:02:43.343666",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"employee",
"employee_name",
"company",
"leave_policy",
"carry_forward",
"column_break_5",
"assignment_based_on",
"leave_period",
"effective_from",
"effective_to",
"leaves_allocated",
"amended_from"
],
"fields": [
{
"fieldname": "employee",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Employee",
"options": "Employee",
"reqd": 1
},
{
"fetch_from": "employee.employee_name",
"fieldname": "employee_name",
"fieldtype": "Data",
"label": "Employee name",
"read_only": 1
},
{
"fieldname": "leave_policy",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Leave Policy",
"options": "Leave Policy",
"reqd": 1
},
{
"fieldname": "assignment_based_on",
"fieldtype": "Select",
"label": "Assignment based on",
"options": "\nLeave Period\nJoining Date"
},
{
"depends_on": "eval:doc.assignment_based_on == \"Leave Period\"",
"fieldname": "leave_period",
"fieldtype": "Link",
"label": "Leave Period",
"mandatory_depends_on": "eval:doc.assignment_based_on == \"Leave Period\"",
"options": "Leave Period"
},
{
"fieldname": "effective_from",
"fieldtype": "Date",
"label": "Effective From",
"read_only_depends_on": "eval:doc.assignment_based_on",
"reqd": 1
},
{
"fieldname": "effective_to",
"fieldtype": "Date",
"label": "Effective To",
"read_only_depends_on": "eval:doc.assignment_based_on == \"Leave Period\"",
"reqd": 1
},
{
"fetch_from": "employee.company",
"fieldname": "company",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"read_only": 1
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Leave Policy Assignment",
"print_hide": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "carry_forward",
"fieldtype": "Check",
"label": "Add unused leaves from previous allocations"
},
{
"default": "0",
"fieldname": "leaves_allocated",
"fieldtype": "Check",
"hidden": 1,
"label": "Leaves Allocated"
}
],
"is_submittable": 1,
"links": [],
"modified": "2020-10-15 15:18:15.227848",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Policy Assignment",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,163 @@
# -*- 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
from frappe.model.document import Document
from frappe import _, bold
from frappe.utils import getdate, date_diff, comma_and, formatdate
from math import ceil
import json
from six import string_types
class LeavePolicyAssignment(Document):
def validate(self):
self.validate_policy_assignment_overlap()
self.set_dates()
def set_dates(self):
if self.assignment_based_on == "Leave Period":
self.effective_from, self.effective_to = frappe.db.get_value("Leave Period", self.leave_period, ["from_date", "to_date"])
elif self.assignment_based_on == "Joining Date":
self.effective_from = frappe.db.get_value("Employee", self.employee, "date_of_joining")
def validate_policy_assignment_overlap(self):
leave_policy_assignments = frappe.get_all("Leave Policy Assignment", filters = {
"employee": self.employee,
"name": ("!=", self.name),
"docstatus": 1,
"effective_to": (">=", self.effective_from),
"effective_from": ("<=", self.effective_to)
})
if len(leave_policy_assignments):
frappe.throw(_("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}")
.format(bold(self.leave_policy), bold(self.employee), bold(formatdate(self.effective_from)), bold(formatdate(self.effective_to))))
def grant_leave_alloc_for_employee(self):
if self.leaves_allocated:
frappe.throw(_("Leave already have been assigned for this Leave Policy Assignment"))
else:
leave_allocations = {}
leave_type_details = get_leave_type_details()
leave_policy = frappe.get_doc("Leave Policy", self.leave_policy)
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
for leave_policy_detail in leave_policy.leave_policy_details:
if not leave_type_details.get(leave_policy_detail.leave_type).is_lwp:
leave_allocation, new_leaves_allocated = self.create_leave_allocation(
leave_policy_detail.leave_type, leave_policy_detail.annual_allocation,
leave_type_details, date_of_joining
)
leave_allocations[leave_policy_detail.leave_type] = {"name": leave_allocation, "leaves": new_leaves_allocated}
self.db_set("leaves_allocated", 1)
return leave_allocations
def create_leave_allocation(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
# Creates leave allocation for the given employee in the provided leave period
carry_forward = self.carry_forward
if self.carry_forward and not leave_type_details.get(leave_type).is_carry_forward:
carry_forward = 0
new_leaves_allocated = self.get_new_leaves(leave_type, new_leaves_allocated,
leave_type_details, date_of_joining)
allocation = frappe.get_doc(dict(
doctype="Leave Allocation",
employee=self.employee,
leave_type=leave_type,
from_date=self.effective_from,
to_date=self.effective_to,
new_leaves_allocated=new_leaves_allocated,
leave_period=self.leave_period or None,
leave_policy_assignment = self.name,
leave_policy = self.leave_policy,
carry_forward=carry_forward
))
allocation.save(ignore_permissions = True)
allocation.submit()
return allocation.name, new_leaves_allocated
def get_new_leaves(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
# Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period
if getdate(date_of_joining) > getdate(self.effective_from):
remaining_period = ((date_diff(self.effective_to, date_of_joining) + 1) / (date_diff(self.effective_to, self.effective_from) + 1))
new_leaves_allocated = ceil(new_leaves_allocated * remaining_period)
# Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0
if leave_type_details.get(leave_type).is_earned_leave == 1 or leave_type_details.get(leave_type).is_compensatory == 1:
new_leaves_allocated = 0
return new_leaves_allocated
@frappe.whitelist()
def grant_leave_for_multiple_employees(leave_policy_assignments):
leave_policy_assignments = json.loads(leave_policy_assignments)
not_granted = []
for assignment in leave_policy_assignments:
try:
frappe.get_doc("Leave Policy Assignment", assignment).grant_leave_alloc_for_employee()
except Exception:
not_granted.append(assignment)
if len(not_granted):
msg = _("Leave not Granted for Assignments:")+ bold(comma_and(not_granted)) + _(". Please Check documents")
else:
msg = _("Leave granted Successfully")
frappe.msgprint(msg)
@frappe.whitelist()
def create_assignment_for_multiple_employees(employees, data):
if isinstance(employees, string_types):
employees= json.loads(employees)
if isinstance(data, string_types):
data = frappe._dict(json.loads(data))
docs_name = []
for employee in employees:
assignment = frappe.new_doc("Leave Policy Assignment")
assignment.employee = employee
assignment.assignment_based_on = data.assignment_based_on or None
assignment.leave_policy = data.leave_policy
assignment.effective_from = getdate(data.effective_from) or None
assignment.effective_to = getdate(data.effective_to) or None
assignment.leave_period = data.leave_period or None
assignment.carry_forward = data.carry_forward
assignment.save()
assignment.submit()
docs_name.append(assignment.name)
return docs_name
def automatically_allocate_leaves_based_on_leave_policy():
today = getdate()
automatically_allocate_leaves_based_on_leave_policy = frappe.db.get_single_value(
'HR Settings', 'automatically_allocate_leaves_based_on_leave_policy'
)
pending_assignments = frappe.get_list(
"Leave Policy Assignment",
filters = {"docstatus": 1, "leaves_allocated": 0, "effective_from": today}
)
if len(pending_assignments) and automatically_allocate_leaves_based_on_leave_policy:
for assignment in pending_assignments:
frappe.get_doc("Leave Policy Assignment", assignment.name).grant_leave_alloc_for_employee()
def get_leave_type_details():
leave_type_details = frappe._dict()
leave_types = frappe.get_all("Leave Type",
fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "is_carry_forward", "expire_carry_forwarded_leaves_after_days"])
for d in leave_types:
leave_type_details.setdefault(d.name, d)
return leave_type_details

View File

@ -0,0 +1,138 @@
frappe.listview_settings['Leave Policy Assignment'] = {
onload: function (list_view) {
let me = this;
list_view.page.add_inner_button(__("Bulk Leave Policy Assignment"), function () {
me.dialog = new frappe.ui.form.MultiSelectDialog({
doctype: "Employee",
target: cur_list,
setters: {
company: '',
department: '',
},
data_fields: [{
fieldname: 'leave_policy',
fieldtype: 'Link',
options: 'Leave Policy',
label: __('Leave Policy'),
reqd: 1
},
{
fieldname: 'assignment_based_on',
fieldtype: 'Select',
options: ["", "Leave Period"],
label: __('Assignment Based On'),
onchange: () => {
if (cur_dialog.fields_dict.assignment_based_on.value === "Leave Period") {
cur_dialog.set_df_property("effective_from", "read_only", 1);
cur_dialog.set_df_property("leave_period", "reqd", 1);
cur_dialog.set_df_property("effective_to", "read_only", 1);
} else {
cur_dialog.set_df_property("effective_from", "read_only", 0);
cur_dialog.set_df_property("leave_period", "reqd", 0);
cur_dialog.set_df_property("effective_to", "read_only", 0);
cur_dialog.set_value("effective_from", "");
cur_dialog.set_value("effective_to", "");
}
}
},
{
fieldname: "leave_period",
fieldtype: 'Link',
options: "Leave Period",
label: __('Leave Period'),
depends_on: doc => {
return doc.assignment_based_on == 'Leave Period';
},
onchange: () => {
if (cur_dialog.fields_dict.leave_period.value) {
me.set_effective_date();
}
}
},
{
fieldtype: "Column Break"
},
{
fieldname: 'effective_from',
fieldtype: 'Date',
label: __('Effective From'),
reqd: 1
},
{
fieldname: 'effective_to',
fieldtype: 'Date',
label: __('Effective To'),
reqd: 1
},
{
fieldname: 'carry_forward',
fieldtype: 'Check',
label: __('Add unused leaves from previous allocations')
}
],
get_query() {
return {
filters: {
status: ['=', 'Active']
}
};
},
add_filters_group: 1,
primary_action_label: "Assign",
action(employees, data) {
frappe.call({
method: 'erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.create_assignment_for_multiple_employees',
async: false,
args: {
employees: employees,
data: data
}
});
cur_dialog.hide();
}
});
});
list_view.page.add_inner_button(__("Grant Leaves"), function () {
me.dialog = new frappe.ui.form.MultiSelectDialog({
doctype: "Leave Policy Assignment",
target: cur_list,
setters: {
company: '',
employee: '',
},
get_query() {
return {
filters: {
docstatus: ['=', 1],
leaves_allocated: ['=', 0]
}
};
},
add_filters_group: 1,
primary_action_label: "Grant Leaves",
action(leave_policy_assignments) {
frappe.call({
method: 'erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.grant_leave_for_multiple_employees',
async: false,
args: {
leave_policy_assignments: leave_policy_assignments
}
});
me.dialog.hide();
}
});
});
},
set_effective_date: function () {
if (cur_dialog.fields_dict.assignment_based_on.value === "Leave Period" && cur_dialog.fields_dict.leave_period.value) {
frappe.model.with_doc("Leave Period", cur_dialog.fields_dict.leave_period.value, function () {
let from_date = frappe.model.get_value("Leave Period", cur_dialog.fields_dict.leave_period.value, "from_date");
let to_date = frappe.model.get_value("Leave Period", cur_dialog.fields_dict.leave_period.value, "to_date");
cur_dialog.set_value("effective_from", from_date);
cur_dialog.set_value("effective_to", to_date);
});
}
}
};

View File

@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from erpnext.hr.doctype.leave_application.test_leave_application import get_leave_period, get_employee
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees
from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy
class TestLeavePolicyAssignment(unittest.TestCase):
def setUp(self):
for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec
def test_grant_leaves(self):
leave_period = get_leave_period()
employee = get_employee()
# create the leave policy with leave type "_Test Leave Type", allocation = 10
leave_policy = create_leave_policy()
leave_policy.submit()
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
"leave_period": leave_period.name
}
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0])
leave_policy_assignment_doc.grant_leave_alloc_for_employee()
leave_policy_assignment_doc.reload()
self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1)
leave_allocation = frappe.get_list("Leave Allocation", filters={
"employee": employee.name,
"leave_policy":leave_policy.name,
"leave_policy_assignment": leave_policy_assignments[0],
"docstatus": 1})[0]
leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation)
self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10)
self.assertEqual(leave_alloc_doc.leave_type, "_Test Leave Type")
self.assertEqual(leave_alloc_doc.from_date, leave_period.from_date)
self.assertEqual(leave_alloc_doc.to_date, leave_period.to_date)
self.assertEqual(leave_alloc_doc.leave_policy, leave_policy.name)
self.assertEqual(leave_alloc_doc.leave_policy_assignment, leave_policy_assignments[0])
def test_allow_to_grant_all_leave_after_cancellation_of_every_leave_allocation(self):
leave_period = get_leave_period()
employee = get_employee()
# create the leave policy with leave type "_Test Leave Type", allocation = 10
leave_policy = create_leave_policy()
leave_policy.submit()
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
"leave_period": leave_period.name
}
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0])
leave_policy_assignment_doc.grant_leave_alloc_for_employee()
leave_policy_assignment_doc.reload()
# every leave is allocated no more leave can be granted now
self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1)
leave_allocation = frappe.get_list("Leave Allocation", filters={
"employee": employee.name,
"leave_policy":leave_policy.name,
"leave_policy_assignment": leave_policy_assignments[0],
"docstatus": 1})[0]
leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation)
# User all allowed to grant leave when there is no allocation against assignment
leave_alloc_doc.cancel()
leave_alloc_doc.delete()
leave_policy_assignment_doc.reload()
# User are now allowed to grant leave
self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 0)
def tearDown(self):
for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec

View File

@ -15,6 +15,8 @@
"column_break_3",
"is_carry_forward",
"is_lwp",
"is_ppl",
"fraction_of_daily_salary_per_leave",
"is_optional_leave",
"allow_negative",
"include_holiday",
@ -31,6 +33,7 @@
"is_earned_leave",
"earned_leave_frequency",
"column_break_22",
"based_on_date_of_joining",
"rounding"
],
"fields": [
@ -77,6 +80,7 @@
},
{
"default": "0",
"depends_on": "eval:doc.is_ppl == 0",
"fieldname": "is_lwp",
"fieldtype": "Check",
"label": "Is Leave Without Pay"
@ -183,12 +187,33 @@
{
"fieldname": "column_break_22",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "eval:doc.is_earned_leave",
"description": "If checked, leave will be granted on the day of joining every month.",
"fieldname": "based_on_date_of_joining",
"fieldtype": "Check",
"label": "Based On Date Of Joining"
},
{
"depends_on": "eval:doc.is_lwp == 0",
"fieldname": "is_ppl",
"fieldtype": "Check",
"label": "Is Partially Paid Leave"
},
{
"depends_on": "eval:doc.is_ppl == 1",
"fieldname": "fraction_of_daily_salary_per_leave",
"fieldtype": "Float",
"label": "Fraction of Daily Salary per Leave",
"mandatory_depends_on": "eval:doc.is_ppl == 1"
}
],
"icon": "fa fa-flag",
"idx": 1,
"links": [],
"modified": "2019-12-12 12:48:37.780254",
"modified": "2020-10-15 15:49:47.555105",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Type",

View File

@ -21,3 +21,9 @@ class LeaveType(Document):
leave_allocation = [l['name'] for l in leave_allocation]
if leave_allocation:
frappe.throw(_('Leave application is linked with leave allocations {0}. Leave application cannot be set as leave without pay').format(", ".join(leave_allocation))) #nosec
if self.is_lwp and self.is_ppl:
frappe.throw(_("Leave Type can be either without pay or partial pay"))
if self.is_ppl and (self.fraction_of_daily_salary_per_leave < 0 or self.fraction_of_daily_salary_per_leave > 1):
frappe.throw(_("The fraction of Daily Salary per Leave should be between 0 and 1"))

View File

@ -18,9 +18,14 @@ def create_leave_type(**args):
"allow_encashment": args.allow_encashment or 0,
"is_earned_leave": args.is_earned_leave or 0,
"is_lwp": args.is_lwp or 0,
"is_ppl":args.is_ppl or 0,
"is_carry_forward": args.is_carry_forward or 0,
"expire_carry_forwarded_leaves_after_days": args.expire_carry_forwarded_leaves_after_days or 0,
"encashment_threshold_days": args.encashment_threshold_days or 5,
"earning_component": "Leave Encashment"
})
if leave_type.is_ppl:
leave_type.fraction_of_daily_salary_per_leave = args.fraction_of_daily_salary_per_leave or 0.5
return leave_type

View File

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

View File

@ -28,7 +28,12 @@ def get_template():
w = UnicodeWriter()
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
frappe.response['result'] = cstr(w.getvalue())

View File

@ -215,19 +215,6 @@ def throw_overlap_error(doc, exists_for, overlap_doc, from_date, to_date):
+ _(") for {0}").format(exists_for)
frappe.throw(msg)
def get_employee_leave_policy(employee):
leave_policy = frappe.db.get_value("Employee", employee, "leave_policy")
if not leave_policy:
employee_grade = frappe.db.get_value("Employee", employee, "grade")
if employee_grade:
leave_policy = frappe.db.get_value("Employee Grade", employee_grade, "default_leave_policy")
if not leave_policy:
frappe.throw(_("Employee {0} of grade {1} have no default leave policy").format(employee, employee_grade))
if leave_policy:
return frappe.get_doc("Leave Policy", leave_policy)
else:
frappe.throw(_("Please set leave policy for employee {0} in Employee / Grade record").format(employee))
def validate_duplicate_exemption_for_payroll_period(doctype, docname, payroll_period, employee):
existing_record = frappe.db.exists(doctype, {
"payroll_period": payroll_period,
@ -300,43 +287,68 @@ def generate_leave_encashment():
def allocate_earned_leaves():
'''Allocate earned leaves to Employees'''
e_leave_types = frappe.get_all("Leave Type",
fields=["name", "max_leaves_allowed", "earned_leave_frequency", "rounding"],
filters={'is_earned_leave' : 1})
e_leave_types = get_earned_leaves()
today = getdate()
divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12}
for e_leave_type in e_leave_types:
leave_allocations = frappe.db.sql("""select name, employee, from_date, to_date from `tabLeave Allocation` where %s
between from_date and to_date and docstatus=1 and leave_type=%s""", (today, e_leave_type.name), as_dict=1)
leave_allocations = get_leave_allocations(today, e_leave_type.name)
for allocation in leave_allocations:
leave_policy = get_employee_leave_policy(allocation.employee)
if not leave_policy:
if not allocation.leave_policy_assignment and not allocation.leave_policy:
continue
if not e_leave_type.earned_leave_frequency == "Monthly":
if not check_frequency_hit(allocation.from_date, today, e_leave_type.earned_leave_frequency):
continue
leave_policy = allocation.leave_policy if allocation.leave_policy else frappe.db.get_value(
"Leave Policy Assignment", allocation.leave_policy_assignment, ["leave_policy"])
annual_allocation = frappe.db.get_value("Leave Policy Detail", filters={
'parent': leave_policy.name,
'parent': leave_policy,
'leave_type': e_leave_type.name
}, fieldname=['annual_allocation'])
if annual_allocation:
earned_leaves = flt(annual_allocation) / divide_by_frequency[e_leave_type.earned_leave_frequency]
if e_leave_type.rounding == "0.5":
earned_leaves = round(earned_leaves * 2) / 2
else:
earned_leaves = round(earned_leaves)
allocation = frappe.get_doc('Leave Allocation', allocation.name)
new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves)
from_date=allocation.from_date
if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0:
new_allocation = e_leave_type.max_leaves_allowed
if e_leave_type.based_on_date_of_joining_date:
from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining")
if new_allocation == allocation.total_leaves_allocated:
continue
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
create_additional_leave_ledger_entry(allocation, earned_leaves, today)
if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining_date):
update_previous_leave_allocation(allocation, annual_allocation, e_leave_type)
def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type):
divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12}
if annual_allocation:
earned_leaves = flt(annual_allocation) / divide_by_frequency[e_leave_type.earned_leave_frequency]
if e_leave_type.rounding == "0.5":
earned_leaves = round(earned_leaves * 2) / 2
else:
earned_leaves = round(earned_leaves)
allocation = frappe.get_doc('Leave Allocation', allocation.name)
new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves)
if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0:
new_allocation = e_leave_type.max_leaves_allowed
if new_allocation != allocation.total_leaves_allocated:
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
today_date = today()
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
def get_leave_allocations(date, leave_type):
return frappe.db.sql("""select name, employee, from_date, to_date, leave_policy_assignment, leave_policy
from `tabLeave Allocation`
where
%s between from_date and to_date and docstatus=1
and leave_type=%s""",
(date, leave_type), as_dict=1)
def get_earned_leaves():
return frappe.get_all("Leave Type",
fields=["name", "max_leaves_allowed", "earned_leave_frequency", "rounding", "based_on_date_of_joining"],
filters={'is_earned_leave' : 1})
def create_additional_leave_ledger_entry(allocation, leaves, date):
''' Create leave ledger entry for leave types '''
@ -345,24 +357,32 @@ def create_additional_leave_ledger_entry(allocation, leaves, date):
allocation.unused_leaves = 0
allocation.create_leave_ledger_entry()
def check_frequency_hit(from_date, to_date, frequency):
'''Return True if current date matches frequency'''
from_dt = get_datetime(from_date)
to_dt = get_datetime(to_date)
def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining_date):
import calendar
from dateutil import relativedelta
rd = relativedelta.relativedelta(to_dt, from_dt)
months = rd.months
if frequency == "Quarterly":
if not months % 3:
from_date = get_datetime(from_date)
to_date = get_datetime(to_date)
rd = relativedelta.relativedelta(to_date, from_date)
#last day of month
last_day = calendar.monthrange(to_date.year, to_date.month)[1]
if (from_date.day == to_date.day and based_on_date_of_joining_date) or (not based_on_date_of_joining_date and to_date.day == last_day):
if frequency == "Monthly":
return True
elif frequency == "Half-Yearly":
if not months % 6:
elif frequency == "Quarterly" and rd.months % 3:
return True
elif frequency == "Yearly":
if not months % 12:
elif frequency == "Half-Yearly" and rd.months % 6:
return True
elif frequency == "Yearly" and rd.months % 12:
return True
if frappe.flags.in_test:
return True
return False
def get_salary_assignment(employee, date):
assignment = frappe.db.sql("""
select * from `tabSalary Structure Assignment`
@ -454,3 +474,10 @@ def get_previous_claimed_amount(employee, payroll_period, non_pro_rata=False, co
if sum_of_claimed_amount and flt(sum_of_claimed_amount[0].total_amount) > 0:
total_claimed_amount = sum_of_claimed_amount[0].total_amount
return total_claimed_amount
def grant_leaves_automatically():
automatically_allocate_leaves_based_on_leave_policy = frappe.db.get_singles_value("HR Settings", "automatically_allocate_leaves_based_on_leave_policy")
if automatically_allocate_leaves_based_on_leave_policy:
lpa = frappe.db.get_all("Leave Policy Assignment", filters={"effective_from": getdate(), "docstatus": 1, "leaves_allocated":0})
for assignment in lpa:
frappe.get_doc("Leave Policy Assignment", assignment.name).grant_leave_alloc_for_employee()

View File

@ -3,7 +3,7 @@
{
"hidden": 0,
"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,
@ -13,7 +13,7 @@
{
"hidden": 0,
"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,
@ -34,10 +34,11 @@
"docstatus": 0,
"doctype": "Desk Page",
"extends_another_page": 0,
"hide_custom": 0,
"idx": 0,
"is_standard": 1,
"label": "Loan",
"modified": "2020-06-07 19:42:14.947902",
"modified": "2020-10-17 12:59:50.336085",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",

View File

@ -7,10 +7,14 @@ frappe.ui.form.on('Loan', {
setup: function(frm) {
frm.make_methods = {
'Loan Disbursement': function() { frm.trigger('make_loan_disbursement') },
'Loan Security Unpledge': function() { frm.trigger('create_loan_security_unpledge') }
'Loan Security Unpledge': function() { frm.trigger('create_loan_security_unpledge') },
'Loan Write Off': function() { frm.trigger('make_loan_write_off_entry') }
}
},
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 () {
return {
"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) {
frm.set_query(field, function () {
return {
@ -49,24 +61,33 @@ frappe.ui.form.on('Loan', {
refresh: function (frm) {
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.trigger("make_loan_disbursement");
},__('Create'));
}
if (["Disbursed", "Partially Disbursed"].includes(frm.doc.status) && (!frm.doc.repay_from_salary)) {
frm.add_custom_button(__('Loan Repayment'), function() {
frm.trigger("make_repayment_entry");
},__('Create'));
}
if (frm.doc.status == "Loan Closure Requested") {
frm.add_custom_button(__('Loan Security Unpledge'), function() {
frm.trigger("create_loan_security_unpledge");
},__('Create'));
}
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");
},
@ -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) {
frappe.call({
method: "erpnext.loan_management.doctype.loan.loan.unpledge_security",

View File

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

View File

@ -9,6 +9,7 @@ from frappe import _
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.controllers.accounts_controller import AccountsController
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts
class Loan(AccountsController):
def validate(self):
@ -137,9 +138,12 @@ class Loan(AccountsController):
})
def unlink_loan_security_pledge(self):
frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET
loan = '', status = 'Unpledged'
where name = %s """, (self.loan_security_pledge))
pledges = frappe.get_all('Loan Security Pledge', fields=['name'], filters={'loan': self.name})
pledge_list = [d.name for d in pledges]
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):
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)
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()
def get_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.company = company
disbursement_entry.disbursement_date = nowdate()
disbursement_entry.posting_date = nowdate()
disbursement_entry.disbursed_amount = pending_amount
if as_dict:
@ -222,6 +245,38 @@ def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as
else:
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()
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

View File

@ -13,7 +13,7 @@ def get_data():
'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']
}
]
}

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