From 1e4c28e99d2b675408bb8b3cda743852b6aedc66 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 23 Apr 2020 15:15:52 +0530 Subject: [PATCH 01/58] fix: quotation have expired status even if sales order exists --- erpnext/patches.txt | 1 + .../v12_0/fix_quotation_expired_status.py | 37 +++++++++++++++++++ .../selling/doctype/quotation/quotation.py | 23 +++++++++--- 3 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 erpnext/patches/v12_0/fix_quotation_expired_status.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9ef0b8d510b..39ae8e74471 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -667,3 +667,4 @@ erpnext.patches.v12_0.update_healthcare_refactored_changes erpnext.patches.v12_0.set_total_batch_quantity erpnext.patches.v12_0.rename_mws_settings_fields erpnext.patches.v12_0.set_updated_purpose_in_pick_list +erpnext.patches.v12_0.fix_quotation_expired_status diff --git a/erpnext/patches/v12_0/fix_quotation_expired_status.py b/erpnext/patches/v12_0/fix_quotation_expired_status.py new file mode 100644 index 00000000000..a0320feb7b3 --- /dev/null +++ b/erpnext/patches/v12_0/fix_quotation_expired_status.py @@ -0,0 +1,37 @@ +import frappe + + +def execute(): + # fixes status of quotations which have status 'Expired' despite having valid sales order created + + # filter out submitted expired quotations which has sales order created + cond = "qo.docstatus = 1 and qo.status = 'Expired'" + invalid_so_against_quo = """ + SELECT + so.name FROM `tabSales Order` so, `tabSales Order Item` so_item + WHERE + so_item.docstatus = 1 and so.docstatus = 1 + and so_item.parent = so.name + and so_item.prevdoc_docname = qo.name + and qo.valid_till < so.transaction_date""" # check if SO was created after quotation expired + + frappe.db.sql( + """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and not exists({invalid_so_against_quo})""" + .format(cond=cond, invalid_so_against_quo=invalid_so_against_quo), + (nowdate()) + ) + + valid_so_against_quo = """ + SELECT + so.name FROM `tabSales Order` so, `tabSales Order Item` so_item + WHERE + so_item.docstatus = 1 and so.docstatus = 1 + and so_item.parent = so.name + and so_item.prevdoc_docname = qo.name + and qo.valid_till >= so.transaction_date""" # check if SO was created before quotation expired + + frappe.db.sql( + """UPDATE `tabQuotation` qo SET qo.status = 'Closed' WHERE {cond} and not exists({valid_so_against_quo})""" + .format(cond=cond, valid_so_against_quo=valid_so_against_quo), + (nowdate()) + ) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 7c47b8ac511..7cfec5a046a 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -193,12 +193,23 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): return doclist def set_expired_status(): - frappe.db.sql(""" - UPDATE - `tabQuotation` SET `status` = 'Expired' - WHERE - `status` not in ('Ordered', 'Expired', 'Lost', 'Cancelled') AND `valid_till` < %s - """, (nowdate())) + # filter out submitted non expired quotations whose validity has been ended + cond = "qo.docstatus = 1 and qo.status != 'Expired' and qo.valid_till < %s" + # check if those QUO have SO against it + so_against_quo = """ + SELECT + so.name FROM `tabSales Order` so, `tabSales Order Item` so_item + WHERE + so_item.docstatus = 1 and so.docstatus = 1 + and so_item.parent = so.name + and so_item.prevdoc_docname = qo.name""" + + # if not exists any SO, set status as Expired + frappe.db.sql( + """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and not exists({so_against_quo})""" + .format(cond=cond, so_against_quo=so_against_quo), + (nowdate()) + ) @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None): From 0f2c64cfefb76ad11b2e322c54563abd0a274fd3 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 23 Apr 2020 19:19:09 +0530 Subject: [PATCH 02/58] fix: travis --- erpnext/patches/v12_0/fix_quotation_expired_status.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches/v12_0/fix_quotation_expired_status.py b/erpnext/patches/v12_0/fix_quotation_expired_status.py index a0320feb7b3..0e4419ac50e 100644 --- a/erpnext/patches/v12_0/fix_quotation_expired_status.py +++ b/erpnext/patches/v12_0/fix_quotation_expired_status.py @@ -1,5 +1,5 @@ import frappe - +from frappe.utils import nowdate def execute(): # fixes status of quotations which have status 'Expired' despite having valid sales order created From f7aa6a835795100dd9c4d1aabee1adfad47ea4ed Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 23 Apr 2020 19:27:50 +0530 Subject: [PATCH 03/58] chore: Stock Entry Form Cleanup --- erpnext/stock/doctype/stock_entry/stock_entry.js | 4 +--- erpnext/stock/doctype/stock_entry/stock_entry.json | 13 +++++++++++-- .../stock_entry_detail/stock_entry_detail.json | 7 ++++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index d1048fc195f..6272e01a602 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -215,9 +215,7 @@ frappe.ui.form.on('Stock Entry', { source_doctype: "Material Request", target: frm, date_field: "schedule_date", - setters: { - company: frm.doc.company, - }, + setters: {}, get_query_filters: { docstatus: 1, material_request_type: ["in", ["Material Transfer", "Material Issue"]], diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index bdd0bd0de11..704ae41bc5f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "autoname": "naming_series:", "creation": "2013-04-09 11:43:55", @@ -12,7 +13,6 @@ "stock_entry_type", "outgoing_stock_entry", "purpose", - "company", "work_order", "purchase_order", "delivery_note_no", @@ -20,6 +20,7 @@ "pick_list", "purchase_receipt_no", "col2", + "company", "posting_date", "posting_time", "set_posting_time", @@ -65,6 +66,7 @@ "dimension_col_break", "printing_settings", "select_print_heading", + "print_settings_col_break", "letter_head", "more_info", "is_opening", @@ -291,6 +293,7 @@ "fieldtype": "Section Break" }, { + "description": "Sets 'Source Warehouse' in each row of the items table.", "fieldname": "from_warehouse", "fieldtype": "Link", "in_list_view": 1, @@ -320,6 +323,7 @@ "fieldtype": "Column Break" }, { + "description": "Sets 'Target Warehouse' in each row of the items table.", "fieldname": "to_warehouse", "fieldtype": "Link", "in_list_view": 1, @@ -622,12 +626,17 @@ "label": "Pick List", "options": "Pick List", "read_only": 1 + }, + { + "fieldname": "print_settings_col_break", + "fieldtype": "Column Break" } ], "icon": "fa fa-file-text", "idx": 1, "is_submittable": 1, - "modified": "2019-09-27 14:38:20.801420", + "links": [], + "modified": "2020-04-23 12:56:52.881752", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index a848c80cf2c..c16a41c24fa 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -14,12 +14,12 @@ "t_warehouse", "sec_break1", "item_code", - "item_group", "col_break2", "item_name", "section_break_8", "description", "column_break_10", + "item_group", "image", "image_view", "quantity_and_rate", @@ -178,6 +178,7 @@ "bold": 1, "fieldname": "basic_rate", "fieldtype": "Currency", + "in_list_view": 1, "label": "Basic Rate (as per Stock UOM)", "oldfieldname": "incoming_rate", "oldfieldtype": "Currency", @@ -420,6 +421,7 @@ "options": "Item" }, { + "collapsible": 1, "fieldname": "reference_section", "fieldtype": "Section Break", "label": "Reference" @@ -466,7 +468,6 @@ "fetch_from": "item_code.item_group", "fieldname": "item_group", "fieldtype": "Data", - "in_list_view": 1, "label": "Item Group" }, { @@ -495,7 +496,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-03-19 12:34:09.836295", + "modified": "2020-04-23 19:19:28.539769", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", From e1e98fe1168a4ce803818835bb4dea7fad194866 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 23 Apr 2020 20:18:52 +0530 Subject: [PATCH 04/58] fix: Issues on qty trigger in Stock Entry Detail --- erpnext/stock/doctype/stock_entry/stock_entry.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index d1048fc195f..aba938663b6 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -511,9 +511,10 @@ frappe.ui.form.on('Stock Entry', { item.amount = flt(item.basic_amount + flt(item.additional_cost), precision("amount", item)); - item.valuation_rate = flt(flt(item.basic_rate) - + (flt(item.additional_cost) / flt(item.transfer_qty)), - precision("valuation_rate", item)); + if (flt(item.transfer_qty)) { + item.valuation_rate = flt(flt(item.basic_rate) + (flt(item.additional_cost) / flt(item.transfer_qty)), + precision("valuation_rate", item)); + } } refresh_field('items'); @@ -539,9 +540,8 @@ frappe.ui.form.on('Stock Entry', { frappe.ui.form.on('Stock Entry Detail', { qty: function(frm, cdt, cdn) { - frm.events.set_serial_no(frm, cdt, cdn, () => { - frm.events.set_basic_rate(frm, cdt, cdn); - }); + frm.events.set_basic_rate(frm, cdt, cdn); + frm.events.set_serial_no(frm, cdt, cdn); }, conversion_factor: function(frm, cdt, cdn) { From 22d2970339cf1f4e46829a892c34b4a80dff025e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 24 Apr 2020 13:47:44 +0530 Subject: [PATCH 05/58] fix: query --- erpnext/patches/v12_0/fix_quotation_expired_status.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/patches/v12_0/fix_quotation_expired_status.py b/erpnext/patches/v12_0/fix_quotation_expired_status.py index 0e4419ac50e..fcc7094b557 100644 --- a/erpnext/patches/v12_0/fix_quotation_expired_status.py +++ b/erpnext/patches/v12_0/fix_quotation_expired_status.py @@ -17,8 +17,7 @@ def execute(): frappe.db.sql( """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and not exists({invalid_so_against_quo})""" - .format(cond=cond, invalid_so_against_quo=invalid_so_against_quo), - (nowdate()) + .format(cond=cond, invalid_so_against_quo=invalid_so_against_quo) ) valid_so_against_quo = """ @@ -32,6 +31,5 @@ def execute(): frappe.db.sql( """UPDATE `tabQuotation` qo SET qo.status = 'Closed' WHERE {cond} and not exists({valid_so_against_quo})""" - .format(cond=cond, valid_so_against_quo=valid_so_against_quo), - (nowdate()) + .format(cond=cond, valid_so_against_quo=valid_so_against_quo) ) From de330293931c58684294a827ee15815f88197821 Mon Sep 17 00:00:00 2001 From: marination Date: Sun, 26 Apr 2020 17:16:21 +0530 Subject: [PATCH 06/58] fix: Remove callback outside if condition --- erpnext/stock/doctype/stock_entry/stock_entry.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index aba938663b6..496a865af72 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -312,10 +312,9 @@ frappe.ui.form.on('Stock Entry', { callback: function(r) { if (!r.exe && r.message){ frappe.model.set_value(cdt, cdn, "serial_no", r.message); - - if (callback) { - callback(); - } + } + if (callback) { + callback(); } } }); @@ -540,8 +539,9 @@ frappe.ui.form.on('Stock Entry', { frappe.ui.form.on('Stock Entry Detail', { qty: function(frm, cdt, cdn) { - frm.events.set_basic_rate(frm, cdt, cdn); - frm.events.set_serial_no(frm, cdt, cdn); + frm.events.set_serial_no(frm, cdt, cdn, () => { + frm.events.set_basic_rate(frm, cdt, cdn); + }); }, conversion_factor: function(frm, cdt, cdn) { From 2fea0735392faf37214fce318ac74abaf7bd449e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sun, 26 Apr 2020 17:30:23 +0530 Subject: [PATCH 07/58] fix: query logic --- erpnext/patches/v12_0/fix_quotation_expired_status.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v12_0/fix_quotation_expired_status.py b/erpnext/patches/v12_0/fix_quotation_expired_status.py index fcc7094b557..c8708d80134 100644 --- a/erpnext/patches/v12_0/fix_quotation_expired_status.py +++ b/erpnext/patches/v12_0/fix_quotation_expired_status.py @@ -1,5 +1,4 @@ import frappe -from frappe.utils import nowdate def execute(): # fixes status of quotations which have status 'Expired' despite having valid sales order created @@ -16,7 +15,7 @@ def execute(): and qo.valid_till < so.transaction_date""" # check if SO was created after quotation expired frappe.db.sql( - """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and not exists({invalid_so_against_quo})""" + """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and exists({invalid_so_against_quo})""" .format(cond=cond, invalid_so_against_quo=invalid_so_against_quo) ) @@ -30,6 +29,6 @@ def execute(): and qo.valid_till >= so.transaction_date""" # check if SO was created before quotation expired frappe.db.sql( - """UPDATE `tabQuotation` qo SET qo.status = 'Closed' WHERE {cond} and not exists({valid_so_against_quo})""" + """UPDATE `tabQuotation` qo SET qo.status = 'Closed' WHERE {cond} and exists({valid_so_against_quo})""" .format(cond=cond, valid_so_against_quo=valid_so_against_quo) ) From 58ee6c1e080d2e4dc754febd36291504bf56b645 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sun, 26 Apr 2020 17:45:57 +0530 Subject: [PATCH 08/58] feat: Income tax slab (#21399) * Feat: Multiple tax as per new taxation rule * patch:for multiple tax slab, fix: payroll and exemption validation * Test: Fixture * feat: income tax slab with other charges and tax exempted deduction components * fix: added missing init file * fix: Patch fixed * fix: Patch fixed * fix: test fixes * fix: validate duplicate exemption declaration * fix: payment entry test case Co-authored-by: Anurag Mishra --- .../doctype/sales_invoice/sales_invoice.js | 10 +- .../doctype/employee_other_income/__init__.py | 0 .../employee_other_income.js | 8 + .../employee_other_income.json | 138 ++++ .../employee_other_income.py | 10 + .../test_employee_other_income.py | 10 + .../employee_tax_exemption_declaration.json | 746 ++++-------------- .../employee_tax_exemption_declaration.py | 20 +- ...test_employee_tax_exemption_declaration.py | 2 +- ...ployee_tax_exemption_proof_submission.json | 14 +- ...employee_tax_exemption_proof_submission.py | 6 +- .../hr/doctype/income_tax_slab/__init__.py | 0 .../income_tax_slab/income_tax_slab.js | 6 + .../income_tax_slab/income_tax_slab.json | 160 ++++ .../income_tax_slab/income_tax_slab.py | 10 + .../income_tax_slab/test_income_tax_slab.py | 10 + .../income_tax_slab_other_charges/__init__.py | 0 .../income_tax_slab_other_charges.json | 75 ++ .../income_tax_slab_other_charges.py | 10 + .../payroll_period/payroll_period.json | 467 ++--------- .../doctype/payroll_period/payroll_period.py | 5 +- .../salary_component/salary_component.json | 525 ++++++------ .../salary_component/test_records.json | 4 - .../salary_component/test_salary_component.py | 1 - .../doctype/salary_detail/salary_detail.json | 15 +- erpnext/hr/doctype/salary_slip/salary_slip.py | 142 +++- .../doctype/salary_slip/test_salary_slip.py | 76 +- .../salary_structure/salary_structure.js | 1 + .../salary_structure/salary_structure.py | 23 +- .../salary_structure/test_salary_structure.py | 20 +- .../salary_structure_assignment.js | 10 + .../salary_structure_assignment.json | 13 +- erpnext/hr/utils.py | 13 + erpnext/patches.txt | 3 +- .../v11_0/set_salary_component_properties.py | 3 +- erpnext/patches/v13_0/__init__.py | 0 ..._from_payroll_period_to_income_tax_slab.py | 99 +++ erpnext/regional/india/setup.py | 18 +- 38 files changed, 1298 insertions(+), 1375 deletions(-) create mode 100644 erpnext/hr/doctype/employee_other_income/__init__.py create mode 100644 erpnext/hr/doctype/employee_other_income/employee_other_income.js create mode 100644 erpnext/hr/doctype/employee_other_income/employee_other_income.json create mode 100644 erpnext/hr/doctype/employee_other_income/employee_other_income.py create mode 100644 erpnext/hr/doctype/employee_other_income/test_employee_other_income.py create mode 100644 erpnext/hr/doctype/income_tax_slab/__init__.py create mode 100644 erpnext/hr/doctype/income_tax_slab/income_tax_slab.js create mode 100644 erpnext/hr/doctype/income_tax_slab/income_tax_slab.json create mode 100644 erpnext/hr/doctype/income_tax_slab/income_tax_slab.py create mode 100644 erpnext/hr/doctype/income_tax_slab/test_income_tax_slab.py create mode 100644 erpnext/hr/doctype/income_tax_slab_other_charges/__init__.py create mode 100644 erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json create mode 100644 erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.py create mode 100644 erpnext/patches/v13_0/__init__.py create mode 100644 erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index a6113cd2bb0..60e41f95536 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -587,7 +587,9 @@ frappe.ui.form.on('Sales Invoice', { frm.set_query("account_for_change_amount", function() { return { filters: { - account_type: ['in', ["Cash", "Bank"]] + account_type: ['in', ["Cash", "Bank"]], + company: frm.doc.company, + is_group: 0 } }; }); @@ -668,7 +670,8 @@ frappe.ui.form.on('Sales Invoice', { frm.fields_dict["loyalty_redemption_account"].get_query = function() { return { filters:{ - "company": frm.doc.company + "company": frm.doc.company, + "is_group": 0 } } }; @@ -677,7 +680,8 @@ frappe.ui.form.on('Sales Invoice', { frm.fields_dict["loyalty_redemption_cost_center"].get_query = function() { return { filters:{ - "company": frm.doc.company + "company": frm.doc.company, + "is_group": 0 } } }; diff --git a/erpnext/hr/doctype/employee_other_income/__init__.py b/erpnext/hr/doctype/employee_other_income/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/doctype/employee_other_income/employee_other_income.js b/erpnext/hr/doctype/employee_other_income/employee_other_income.js new file mode 100644 index 00000000000..c1a74e863ba --- /dev/null +++ b/erpnext/hr/doctype/employee_other_income/employee_other_income.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Employee Other Income', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/hr/doctype/employee_other_income/employee_other_income.json b/erpnext/hr/doctype/employee_other_income/employee_other_income.json new file mode 100644 index 00000000000..2dd6c10988d --- /dev/null +++ b/erpnext/hr/doctype/employee_other_income/employee_other_income.json @@ -0,0 +1,138 @@ +{ + "actions": [], + "autoname": "HR-INCOME-.######", + "creation": "2020-03-18 15:04:40.767434", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "payroll_period", + "column_break_3", + "company", + "source", + "amount", + "amended_from" + ], + "fields": [ + { + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, + { + "fieldname": "payroll_period", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Payroll Period", + "options": "Payroll Period", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "source", + "fieldtype": "Data", + "label": "Source" + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "Company:company:default_currency", + "reqd": 1 + }, + { + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Employee Other Income", + "print_hide": 1, + "read_only": 1 + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-03-19 18:06:45.361830", + "modified_by": "Administrator", + "module": "HR", + "name": "Employee Other Income", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "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": "Employee", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_other_income/employee_other_income.py b/erpnext/hr/doctype/employee_other_income/employee_other_income.py new file mode 100644 index 00000000000..ab63c0de623 --- /dev/null +++ b/erpnext/hr/doctype/employee_other_income/employee_other_income.py @@ -0,0 +1,10 @@ +# -*- 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 + +class EmployeeOtherIncome(Document): + pass diff --git a/erpnext/hr/doctype/employee_other_income/test_employee_other_income.py b/erpnext/hr/doctype/employee_other_income/test_employee_other_income.py new file mode 100644 index 00000000000..2eeca7a23de --- /dev/null +++ b/erpnext/hr/doctype/employee_other_income/test_employee_other_income.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestEmployeeOtherIncome(unittest.TestCase): + pass diff --git a/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json b/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json index e102ff8d705..18fad85c4b3 100644 --- a/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json +++ b/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json @@ -1,620 +1,180 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "HR-TAX-DEC-.YYYY.-.#####", - "beta": 0, - "creation": "2018-04-13 16:53:36.175504", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "HR-TAX-DEC-.YYYY.-.#####", + "creation": "2018-04-13 16:53:36.175504", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "department", + "column_break_2", + "payroll_period", + "company", + "amended_from", + "section_break_8", + "declarations", + "section_break_10", + "total_declared_amount", + "column_break_12", + "total_exemption_amount" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "employee", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Employee", - "length": 0, - "no_copy": 0, - "options": "Employee", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.employee_name", - "fetch_if_empty": 0, - "fieldname": "employee_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Employee Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.department", - "fetch_if_empty": 0, - "fieldname": "department", - "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": "Department", - "length": 0, - "no_copy": 0, - "options": "Department", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "employee.department", + "fieldname": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "payroll_period", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Payroll Period", - "length": 0, - "no_copy": 0, - "options": "Payroll Period", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "payroll_period", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Payroll Period", + "options": "Payroll Period", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.company", - "fetch_if_empty": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "employee.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "amended_from", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Employee Tax Exemption Declaration", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Employee Tax Exemption Declaration", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_8", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_8", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "declarations", - "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": "Declarations", - "length": 0, - "no_copy": 0, - "options": "Employee Tax Exemption Declaration Category", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "declarations", + "fieldtype": "Table", + "label": "Declarations", + "options": "Employee Tax Exemption Declaration Category" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_10", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "total_declared_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Total Declared Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "total_declared_amount", + "fieldtype": "Currency", + "label": "Total Declared Amount", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_12", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "total_exemption_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Total Exemption Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "other_incomes_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Other Incomes", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "income_from_other_sources", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Income From Other Sources", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "total_exemption_amount", + "fieldtype": "Currency", + "label": "Total Exemption Amount", + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-05-11 16:13:50.472670", - "modified_by": "Administrator", - "module": "HR", - "name": "Employee Tax Exemption Declaration", - "name_case": "", - "owner": "Administrator", + ], + "is_submittable": 1, + "links": [], + "modified": "2020-03-18 14:56:25.625717", + "modified_by": "Administrator", + "module": "HR", + "name": "Employee Tax Exemption Declaration", + "owner": "Administrator", "permissions": [ { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, "write": 1 - }, + }, { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "submit": 1, "write": 1 - }, + }, { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "submit": 1, "write": 1 - }, + }, { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Employee", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py b/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py index f2bba7afed7..fb71a2877a1 100644 --- a/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py +++ b/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py @@ -8,31 +8,17 @@ from frappe.model.document import Document from frappe import _ from frappe.utils import flt from frappe.model.mapper import get_mapped_doc -from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, calculate_annual_eligible_hra_exemption - -class DuplicateDeclarationError(frappe.ValidationError): pass +from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, \ + calculate_annual_eligible_hra_exemption, validate_duplicate_exemption_for_payroll_period class EmployeeTaxExemptionDeclaration(Document): def validate(self): validate_tax_declaration(self.declarations) - self.validate_duplicate() + validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee) self.set_total_declared_amount() self.set_total_exemption_amount() self.calculate_hra_exemption() - def validate_duplicate(self): - duplicate = frappe.db.get_value("Employee Tax Exemption Declaration", - filters = { - "employee": self.employee, - "payroll_period": self.payroll_period, - "name": ["!=", self.name], - "docstatus": ["!=", 2] - } - ) - if duplicate: - frappe.throw(_("Duplicate Tax Declaration of {0} for period {1}") - .format(self.employee, self.payroll_period), DuplicateDeclarationError) - def set_total_declared_amount(self): self.total_declared_amount = 0.0 for d in self.declarations: diff --git a/erpnext/hr/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/hr/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py index 9c87bbd1f30..9549fd1b757 100644 --- a/erpnext/hr/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py +++ b/erpnext/hr/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, erpnext import unittest from erpnext.hr.doctype.employee.test_employee import make_employee -from erpnext.hr.doctype.employee_tax_exemption_declaration.employee_tax_exemption_declaration import DuplicateDeclarationError +from erpnext.hr.utils import DuplicateDeclarationError class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): def setUp(self): diff --git a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json b/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json index c170c1693d7..8b117a25b54 100644 --- a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json +++ b/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json @@ -21,8 +21,6 @@ "total_actual_amount", "column_break_12", "exemption_amount", - "other_incomes_section", - "income_from_other_sources", "attachment_section", "attachments", "amended_from" @@ -111,16 +109,6 @@ "label": "Total Exemption Amount", "read_only": 1 }, - { - "fieldname": "other_incomes_section", - "fieldtype": "Section Break", - "label": "Other Incomes" - }, - { - "fieldname": "income_from_other_sources", - "fieldtype": "Currency", - "label": "Income From Other Sources" - }, { "fieldname": "attachment_section", "fieldtype": "Section Break" @@ -142,7 +130,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-03-02 19:02:15.398486", + "modified": "2020-03-18 14:55:51.420016", "modified_by": "Administrator", "module": "HR", "name": "Employee Tax Exemption Proof Submission", diff --git a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py b/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py index 97ceb63476b..5bc33a65f2c 100644 --- a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py +++ b/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py @@ -7,7 +7,8 @@ import frappe from frappe.model.document import Document from frappe import _ from frappe.utils import flt -from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, calculate_hra_exemption_for_period +from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, \ + calculate_hra_exemption_for_period, validate_duplicate_exemption_for_payroll_period class EmployeeTaxExemptionProofSubmission(Document): def validate(self): @@ -15,6 +16,7 @@ class EmployeeTaxExemptionProofSubmission(Document): self.set_total_actual_amount() self.set_total_exemption_amount() self.calculate_hra_exemption() + validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee) def set_total_actual_amount(self): self.total_actual_amount = flt(self.get("house_rent_payment_amount")) @@ -32,4 +34,4 @@ class EmployeeTaxExemptionProofSubmission(Document): self.exemption_amount += hra_exemption["total_eligible_hra_exemption"] self.monthly_hra_exemption = hra_exemption["monthly_exemption"] self.monthly_house_rent = hra_exemption["monthly_house_rent"] - self.total_eligible_hra_exemption = hra_exemption["total_eligible_hra_exemption"] \ No newline at end of file + self.total_eligible_hra_exemption = hra_exemption["total_eligible_hra_exemption"] diff --git a/erpnext/hr/doctype/income_tax_slab/__init__.py b/erpnext/hr/doctype/income_tax_slab/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.js b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.js new file mode 100644 index 00000000000..73a54eb8dd9 --- /dev/null +++ b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.js @@ -0,0 +1,6 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Income Tax Slab', { + +}); diff --git a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json new file mode 100644 index 00000000000..6d89b197d27 --- /dev/null +++ b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json @@ -0,0 +1,160 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2020-03-17 16:50:35.564915", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "effective_from", + "company", + "column_break_3", + "allow_tax_exemption", + "standard_tax_exemption_amount", + "disabled", + "amended_from", + "taxable_salary_slabs_section", + "slabs", + "taxes_and_charges_on_income_tax_section", + "other_taxes_and_charges" + ], + "fields": [ + { + "fieldname": "effective_from", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Effective from", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "If enabled, Tax Exemption Declaration will be considered for income tax calculation.", + "fieldname": "allow_tax_exemption", + "fieldtype": "Check", + "label": "Allow Tax Exemption" + }, + { + "fieldname": "taxable_salary_slabs_section", + "fieldtype": "Section Break", + "label": "Taxable Salary Slabs" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Income Tax Slab", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "slabs", + "fieldtype": "Table", + "label": "Taxable Salary Slabs", + "options": "Taxable Salary Slab", + "reqd": 1 + }, + { + "allow_on_submit": 1, + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "depends_on": "allow_tax_exemption", + "fieldname": "standard_tax_exemption_amount", + "fieldtype": "Currency", + "label": "Standard Tax Exemption Amount", + "options": "Company:company:default_currency" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "collapsible": 1, + "fieldname": "taxes_and_charges_on_income_tax_section", + "fieldtype": "Section Break", + "label": "Taxes and Charges on Income Tax" + }, + { + "fieldname": "other_taxes_and_charges", + "fieldtype": "Table", + "label": "Other Taxes and Charges", + "options": "Income Tax Slab Other Charges" + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-04-24 12:28:36.805904", + "modified_by": "Administrator", + "module": "HR", + "name": "Income Tax Slab", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.py b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.py new file mode 100644 index 00000000000..253f023f68b --- /dev/null +++ b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.py @@ -0,0 +1,10 @@ +# -*- 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 + +class IncomeTaxSlab(Document): + pass diff --git a/erpnext/hr/doctype/income_tax_slab/test_income_tax_slab.py b/erpnext/hr/doctype/income_tax_slab/test_income_tax_slab.py new file mode 100644 index 00000000000..deaaf650a96 --- /dev/null +++ b/erpnext/hr/doctype/income_tax_slab/test_income_tax_slab.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestIncomeTaxSlab(unittest.TestCase): + pass diff --git a/erpnext/hr/doctype/income_tax_slab_other_charges/__init__.py b/erpnext/hr/doctype/income_tax_slab_other_charges/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json b/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json new file mode 100644 index 00000000000..b23fb3dc317 --- /dev/null +++ b/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json @@ -0,0 +1,75 @@ +{ + "actions": [], + "creation": "2020-04-24 11:46:59.041180", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "description", + "column_break_2", + "percent", + "conditions_section", + "min_taxable_income", + "column_break_7", + "max_taxable_income" + ], + "fields": [ + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "columns": 2, + "fieldname": "min_taxable_income", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Min Taxable Income", + "options": "Company:company:default_currency" + }, + { + "columns": 4, + "fieldname": "description", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Description", + "reqd": 1 + }, + { + "columns": 2, + "fieldname": "percent", + "fieldtype": "Percent", + "in_list_view": 1, + "label": "Percent", + "reqd": 1 + }, + { + "fieldname": "conditions_section", + "fieldtype": "Section Break", + "label": "Conditions" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "columns": 2, + "fieldname": "max_taxable_income", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Max Taxable Income", + "options": "Company:company:default_currency" + } + ], + "istable": 1, + "links": [], + "modified": "2020-04-24 13:27:43.598967", + "modified_by": "Administrator", + "module": "HR", + "name": "Income Tax Slab Other Charges", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.py b/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.py new file mode 100644 index 00000000000..b4098ecbf3e --- /dev/null +++ b/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.py @@ -0,0 +1,10 @@ +# -*- 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 + +class IncomeTaxSlabOtherCharges(Document): + pass diff --git a/erpnext/hr/doctype/payroll_period/payroll_period.json b/erpnext/hr/doctype/payroll_period/payroll_period.json index c9bac095f9f..c0fa506e7f0 100644 --- a/erpnext/hr/doctype/payroll_period/payroll_period.json +++ b/erpnext/hr/doctype/payroll_period/payroll_period.json @@ -1,401 +1,102 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "Prompt", - "beta": 0, - "creation": "2018-04-13 15:18:53.698553", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "autoname": "Prompt", + "creation": "2018-04-13 15:18:53.698553", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "column_break_2", + "start_date", + "end_date", + "section_break_5", + "periods" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "start_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Start Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "start_date", + "fieldtype": "Date", + "label": "Start Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "end_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "End Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "end_date", + "fieldtype": "Date", + "label": "End Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_5", - "fieldtype": "Section Break", - "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": "Payroll Periods", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "hidden": 1, + "label": "Payroll Periods" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "periods", - "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": "Payroll Periods", - "length": 0, - "no_copy": 0, - "options": "Payroll Period Date", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_7", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Taxable Salary Slabs", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "taxable_salary_slabs", - "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": "Taxable Salary Slabs", - "length": 0, - "no_copy": 0, - "options": "Taxable Salary Slab", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "standard_tax_exemption_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Standard Tax Exemption Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "periods", + "fieldtype": "Table", + "label": "Payroll Periods", + "options": "Payroll Period Date" } - ], - "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": "2019-04-26 01:45:03.160929", - "modified_by": "Administrator", - "module": "HR", - "name": "Payroll Period", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2020-03-18 18:13:23.859980", + "modified_by": "Administrator", + "module": "HR", + "name": "Payroll Period", + "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, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/payroll_period/payroll_period.py b/erpnext/hr/doctype/payroll_period/payroll_period.py index c1769591eaf..6956c382854 100644 --- a/erpnext/hr/doctype/payroll_period/payroll_period.py +++ b/erpnext/hr/doctype/payroll_period/payroll_period.py @@ -45,8 +45,9 @@ class PayrollPeriod(Document): + _(") for {0}").format(self.company) frappe.throw(msg) -def get_payroll_period_days(start_date, end_date, employee): - company = frappe.db.get_value("Employee", employee, "company") +def get_payroll_period_days(start_date, end_date, employee, company=None): + if not company: + company = frappe.db.get_value("Employee", employee, "company") payroll_period = frappe.db.sql(""" select name, start_date, end_date from `tabPayroll Period` diff --git a/erpnext/hr/doctype/salary_component/salary_component.json b/erpnext/hr/doctype/salary_component/salary_component.json index 986030d8c58..5487e1dee85 100644 --- a/erpnext/hr/doctype/salary_component/salary_component.json +++ b/erpnext/hr/doctype/salary_component/salary_component.json @@ -1,264 +1,263 @@ { - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:salary_component", - "creation": "2016-06-30 15:42:43.631931", - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "salary_component", - "salary_component_abbr", - "type", - "description", - "column_break_4", - "is_payable", - "depends_on_payment_days", - "is_tax_applicable", - "deduct_full_tax_on_selected_payroll_date", - "round_to_the_nearest_integer", - "statistical_component", - "do_not_include_in_total", - "disabled", - "flexible_benefits", - "is_flexible_benefit", - "max_benefit_amount", - "column_break_9", - "pay_against_benefit_claim", - "only_tax_impact", - "create_separate_payment_entry_against_benefit_claim", - "section_break_11", - "variable_based_on_taxable_salary", - "section_break_5", - "accounts", - "condition_and_formula", - "condition", - "amount", - "amount_based_on_formula", - "formula", - "column_break_28", - "help" - ], - "fields": [ - { - "fieldname": "salary_component", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Name", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "salary_component_abbr", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Abbr", - "print_width": "120px", - "reqd": 1, - "width": "120px" - }, - { - "fieldname": "type", - "fieldtype": "Select", - "in_standard_filter": 1, - "label": "Type", - "options": "Earning\nDeduction", - "reqd": 1 - }, - { - "default": "1", - "depends_on": "eval:doc.type == \"Earning\"", - "fieldname": "is_tax_applicable", - "fieldtype": "Check", - "label": "Is Tax Applicable" - }, - { - "default": "1", - "fieldname": "is_payable", - "fieldtype": "Check", - "label": "Is Payable" - }, - { - "default": "1", - "fieldname": "depends_on_payment_days", - "fieldtype": "Check", - "label": "Depends on Payment Days", - "print_hide": 1 - }, - { - "default": "0", - "fieldname": "do_not_include_in_total", - "fieldtype": "Check", - "label": "Do Not Include in Total" - }, - { - "default": "0", - "depends_on": "is_tax_applicable", - "fieldname": "deduct_full_tax_on_selected_payroll_date", - "fieldtype": "Check", - "label": "Deduct Full Tax on Selected Payroll Date" - }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, - { - "default": "0", - "fieldname": "disabled", - "fieldtype": "Check", - "label": "Disabled" - }, - { - "fieldname": "description", - "fieldtype": "Small Text", - "in_list_view": 1, - "label": "Description" - }, - { - "default": "0", - "description": "If selected, the value specified or calculated in this component will not contribute to the earnings or deductions. However, it's value can be referenced by other components that can be added or deducted. ", - "fieldname": "statistical_component", - "fieldtype": "Check", - "label": "Statistical Component" - }, - { - "depends_on": "eval:doc.type==\"Earning\" && doc.statistical_component!=1", - "fieldname": "flexible_benefits", - "fieldtype": "Section Break", - "label": "Flexible Benefits" - }, - { - "default": "0", - "fieldname": "is_flexible_benefit", - "fieldtype": "Check", - "label": "Is Flexible Benefit" - }, - { - "depends_on": "is_flexible_benefit", - "fieldname": "max_benefit_amount", - "fieldtype": "Currency", - "label": "Max Benefit Amount (Yearly)" - }, - { - "fieldname": "column_break_9", - "fieldtype": "Column Break" - }, - { - "default": "0", - "depends_on": "is_flexible_benefit", - "fieldname": "pay_against_benefit_claim", - "fieldtype": "Check", - "label": "Pay Against Benefit Claim" - }, - { - "default": "0", - "depends_on": "eval:doc.is_flexible_benefit == 1 & doc.create_separate_payment_entry_against_benefit_claim !=1", - "fieldname": "only_tax_impact", - "fieldtype": "Check", - "label": "Only Tax Impact (Cannot Claim But Part of Taxable Income)" - }, - { - "default": "0", - "depends_on": "eval:doc.is_flexible_benefit == 1 & doc.only_tax_impact !=1", - "fieldname": "create_separate_payment_entry_against_benefit_claim", - "fieldtype": "Check", - "label": "Create Separate Payment Entry Against Benefit Claim" - }, - { - "depends_on": "eval:doc.type=='Deduction'", - "fieldname": "section_break_11", - "fieldtype": "Section Break" - }, - { - "default": "0", - "fieldname": "variable_based_on_taxable_salary", - "fieldtype": "Check", - "label": "Variable Based On Taxable Salary" - }, - { - "depends_on": "eval:doc.statistical_component != 1", - "fieldname": "section_break_5", - "fieldtype": "Section Break", - "label": "Accounts" - }, - { - "fieldname": "accounts", - "fieldtype": "Table", - "label": "Accounts", - "options": "Salary Component Account" - }, - { - "collapsible": 1, - "depends_on": "eval:doc.is_flexible_benefit != 1 && doc.variable_based_on_taxable_salary != 1", - "fieldname": "condition_and_formula", - "fieldtype": "Section Break", - "label": "Condition and Formula" - }, - { - "fieldname": "condition", - "fieldtype": "Code", - "label": "Condition" - }, - { - "default": "0", - "fieldname": "amount_based_on_formula", - "fieldtype": "Check", - "label": "Amount based on formula" - }, - { - "depends_on": "amount_based_on_formula", - "fieldname": "formula", - "fieldtype": "Code", - "label": "Formula" - }, - { - "depends_on": "eval:doc.amount_based_on_formula!==1", - "fieldname": "amount", - "fieldtype": "Currency", - "label": "Amount" - }, - { - "fieldname": "column_break_28", - "fieldtype": "Column Break" - }, - { - "fieldname": "help", - "fieldtype": "HTML", - "label": "Help", - "options": "

Help

\n\n

Notes:

\n\n
    \n
  1. Use field base for using base salary of the Employee
  2. \n
  3. Use Salary Component abbreviations in conditions and formulas. BS = Basic Salary
  4. \n
  5. Use field name for employee details in conditions and formulas. Employment Type = employment_typeBranch = branch
  6. \n
  7. Use field name from Salary Slip in conditions and formulas. Payment Days = payment_daysLeave without pay = leave_without_pay
  8. \n
  9. Direct Amount can also be entered based on Condtion. See example 3
\n\n

Examples

\n
    \n
  1. Calculating Basic Salary based on base\n
    Condition: base < 10000
    \n
    Formula: base * .2
  2. \n
  3. Calculating HRA based on Basic SalaryBS \n
    Condition: BS > 2000
    \n
    Formula: BS * .1
  4. \n
  5. Calculating TDS based on Employment Typeemployment_type \n
    Condition: employment_type==\"Intern\"
    \n
    Amount: 1000
  6. \n
" - }, - { - "default": "0", - "fieldname": "round_to_the_nearest_integer", - "fieldtype": "Check", - "label": "Round to the Nearest Integer" - } - ], - "icon": "fa fa-flag", - "modified": "2019-06-05 11:34:14.231228", - "modified_by": "Administrator", - "module": "HR", - "name": "Salary Component", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "HR User", - "share": 1, - "write": 1 - }, - { - "read": 1, - "role": "Employee" - } - ], - "sort_field": "modified", - "sort_order": "DESC" - } \ No newline at end of file + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:salary_component", + "creation": "2016-06-30 15:42:43.631931", + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "salary_component", + "salary_component_abbr", + "type", + "description", + "column_break_4", + "depends_on_payment_days", + "is_tax_applicable", + "deduct_full_tax_on_selected_payroll_date", + "variable_based_on_taxable_salary", + "exempted_from_income_tax", + "round_to_the_nearest_integer", + "statistical_component", + "do_not_include_in_total", + "disabled", + "flexible_benefits", + "is_flexible_benefit", + "max_benefit_amount", + "column_break_9", + "pay_against_benefit_claim", + "only_tax_impact", + "create_separate_payment_entry_against_benefit_claim", + "section_break_5", + "accounts", + "condition_and_formula", + "condition", + "amount", + "amount_based_on_formula", + "formula", + "column_break_28", + "help" + ], + "fields": [ + { + "fieldname": "salary_component", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "salary_component_abbr", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Abbr", + "print_width": "120px", + "reqd": 1, + "width": "120px" + }, + { + "fieldname": "type", + "fieldtype": "Select", + "in_standard_filter": 1, + "label": "Type", + "options": "Earning\nDeduction", + "reqd": 1 + }, + { + "default": "1", + "depends_on": "eval:doc.type == \"Earning\"", + "fieldname": "is_tax_applicable", + "fieldtype": "Check", + "label": "Is Tax Applicable" + }, + { + "default": "1", + "fieldname": "depends_on_payment_days", + "fieldtype": "Check", + "label": "Depends on Payment Days", + "print_hide": 1 + }, + { + "default": "0", + "fieldname": "do_not_include_in_total", + "fieldtype": "Check", + "label": "Do Not Include in Total" + }, + { + "default": "0", + "depends_on": "eval:doc.is_tax_applicable && doc.type=='Earning'", + "fieldname": "deduct_full_tax_on_selected_payroll_date", + "fieldtype": "Check", + "label": "Deduct Full Tax on Selected Payroll Date" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Description" + }, + { + "default": "0", + "description": "If selected, the value specified or calculated in this component will not contribute to the earnings or deductions. However, it's value can be referenced by other components that can be added or deducted. ", + "fieldname": "statistical_component", + "fieldtype": "Check", + "label": "Statistical Component" + }, + { + "depends_on": "eval:doc.type==\"Earning\" && doc.statistical_component!=1", + "fieldname": "flexible_benefits", + "fieldtype": "Section Break", + "label": "Flexible Benefits" + }, + { + "default": "0", + "fieldname": "is_flexible_benefit", + "fieldtype": "Check", + "label": "Is Flexible Benefit" + }, + { + "depends_on": "is_flexible_benefit", + "fieldname": "max_benefit_amount", + "fieldtype": "Currency", + "label": "Max Benefit Amount (Yearly)" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "is_flexible_benefit", + "fieldname": "pay_against_benefit_claim", + "fieldtype": "Check", + "label": "Pay Against Benefit Claim" + }, + { + "default": "0", + "depends_on": "eval:doc.is_flexible_benefit == 1 & doc.create_separate_payment_entry_against_benefit_claim !=1", + "fieldname": "only_tax_impact", + "fieldtype": "Check", + "label": "Only Tax Impact (Cannot Claim But Part of Taxable Income)" + }, + { + "default": "0", + "depends_on": "eval:doc.is_flexible_benefit == 1 & doc.only_tax_impact !=1", + "fieldname": "create_separate_payment_entry_against_benefit_claim", + "fieldtype": "Check", + "label": "Create Separate Payment Entry Against Benefit Claim" + }, + { + "default": "0", + "depends_on": "eval:doc.type == \"Deduction\"", + "fieldname": "variable_based_on_taxable_salary", + "fieldtype": "Check", + "label": "Variable Based On Taxable Salary" + }, + { + "depends_on": "eval:doc.statistical_component != 1", + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "label": "Accounts" + }, + { + "fieldname": "accounts", + "fieldtype": "Table", + "label": "Accounts", + "options": "Salary Component Account" + }, + { + "collapsible": 1, + "depends_on": "eval:doc.is_flexible_benefit != 1 && doc.variable_based_on_taxable_salary != 1", + "fieldname": "condition_and_formula", + "fieldtype": "Section Break", + "label": "Condition and Formula" + }, + { + "fieldname": "condition", + "fieldtype": "Code", + "label": "Condition" + }, + { + "default": "0", + "fieldname": "amount_based_on_formula", + "fieldtype": "Check", + "label": "Amount based on formula" + }, + { + "depends_on": "amount_based_on_formula", + "fieldname": "formula", + "fieldtype": "Code", + "label": "Formula" + }, + { + "depends_on": "eval:doc.amount_based_on_formula!==1", + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount" + }, + { + "fieldname": "column_break_28", + "fieldtype": "Column Break" + }, + { + "fieldname": "help", + "fieldtype": "HTML", + "label": "Help", + "options": "

Help

\n\n

Notes:

\n\n
    \n
  1. Use field base for using base salary of the Employee
  2. \n
  3. Use Salary Component abbreviations in conditions and formulas. BS = Basic Salary
  4. \n
  5. Use field name for employee details in conditions and formulas. Employment Type = employment_typeBranch = branch
  6. \n
  7. Use field name from Salary Slip in conditions and formulas. Payment Days = payment_daysLeave without pay = leave_without_pay
  8. \n
  9. Direct Amount can also be entered based on Condtion. See example 3
\n\n

Examples

\n
    \n
  1. Calculating Basic Salary based on base\n
    Condition: base < 10000
    \n
    Formula: base * .2
  2. \n
  3. Calculating HRA based on Basic SalaryBS \n
    Condition: BS > 2000
    \n
    Formula: BS * .1
  4. \n
  5. Calculating TDS based on Employment Typeemployment_type \n
    Condition: employment_type==\"Intern\"
    \n
    Amount: 1000
  6. \n
" + }, + { + "default": "0", + "fieldname": "round_to_the_nearest_integer", + "fieldtype": "Check", + "label": "Round to the Nearest Integer" + }, + { + "default": "0", + "depends_on": "eval:doc.type == \"Deduction\" && !doc.variable_based_on_taxable_salary", + "description": "If checked, the full amount will be deducted from taxable income before calculating income tax. Otherwise, it can be exempted via Employee Tax Exemption Declaration.", + "fieldname": "exempted_from_income_tax", + "fieldtype": "Check", + "label": "Exempted from Income Tax" + } + ], + "icon": "fa fa-flag", + "links": [], + "modified": "2020-04-24 14:50:28.994054", + "modified_by": "Administrator", + "module": "HR", + "name": "Salary Component", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "Employee" + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/hr/doctype/salary_component/test_records.json b/erpnext/hr/doctype/salary_component/test_records.json index 7b22b481f36..104b44ffa1b 100644 --- a/erpnext/hr/doctype/salary_component/test_records.json +++ b/erpnext/hr/doctype/salary_component/test_records.json @@ -3,14 +3,12 @@ "doctype": "Salary Component", "salary_component": "_Test Basic Salary", "type": "Earning", - "is_payable": 1, "is_tax_applicable": 1 }, { "doctype": "Salary Component", "salary_component": "_Test Allowance", "type": "Earning", - "is_payable": 1, "is_tax_applicable": 1 }, { @@ -27,14 +25,12 @@ "doctype": "Salary Component", "salary_component": "Basic", "type": "Earning", - "is_payable": 1, "is_tax_applicable": 1 }, { "doctype": "Salary Component", "salary_component": "Leave Encashment", "type": "Earning", - "is_payable": 1, "is_tax_applicable": 1 } ] \ No newline at end of file diff --git a/erpnext/hr/doctype/salary_component/test_salary_component.py b/erpnext/hr/doctype/salary_component/test_salary_component.py index 965cc9e9ffd..4f7db0c71ca 100644 --- a/erpnext/hr/doctype/salary_component/test_salary_component.py +++ b/erpnext/hr/doctype/salary_component/test_salary_component.py @@ -18,6 +18,5 @@ def create_salary_component(component_name, **args): "doctype": "Salary Component", "salary_component": component_name, "type": args.get("type") or "Earning", - "is_payable": args.get("is_payable") or 1, "is_tax_applicable": args.get("is_tax_applicable") or 1 }).insert() diff --git a/erpnext/hr/doctype/salary_detail/salary_detail.json b/erpnext/hr/doctype/salary_detail/salary_detail.json index bde735d3bc9..545f56a0b60 100644 --- a/erpnext/hr/doctype/salary_detail/salary_detail.json +++ b/erpnext/hr/doctype/salary_detail/salary_detail.json @@ -12,6 +12,7 @@ "deduct_full_tax_on_selected_payroll_date", "depends_on_payment_days", "is_tax_applicable", + "exempted_from_income_tax", "is_flexible_benefit", "variable_based_on_taxable_salary", "section_break_2", @@ -62,6 +63,7 @@ }, { "default": "0", + "depends_on": "eval:doc.parentfield=='earnings'", "fetch_from": "salary_component.is_tax_applicable", "fieldname": "is_tax_applicable", "fieldtype": "Check", @@ -71,6 +73,7 @@ }, { "default": "0", + "depends_on": "eval:doc.parentfield=='earnings'", "fetch_from": "salary_component.is_flexible_benefit", "fieldname": "is_flexible_benefit", "fieldtype": "Check", @@ -80,6 +83,7 @@ }, { "default": "0", + "depends_on": "eval:doc.parentfield=='deductions'", "fetch_from": "salary_component.variable_based_on_taxable_salary", "fieldname": "variable_based_on_taxable_salary", "fieldtype": "Check", @@ -187,11 +191,20 @@ "fieldtype": "HTML", "label": "Condition and Formula Help", "options": "

Condition and Formula Help

\n\n

Notes:

\n\n
    \n
  1. Use field base for using base salary of the Employee
  2. \n
  3. Use Salary Component abbreviations in conditions and formulas. BS = Basic Salary
  4. \n
  5. Use field name for employee details in conditions and formulas. Employment Type = employment_typeBranch = branch
  6. \n
  7. Use field name from Salary Slip in conditions and formulas. Payment Days = payment_daysLeave without pay = leave_without_pay
  8. \n
  9. Direct Amount can also be entered based on Condtion. See example 3
\n\n

Examples

\n
    \n
  1. Calculating Basic Salary based on base\n
    Condition: base < 10000
    \n
    Formula: base * .2
  2. \n
  3. Calculating HRA based on Basic SalaryBS \n
    Condition: BS > 2000
    \n
    Formula: BS * .1
  4. \n
  5. Calculating TDS based on Employment Typeemployment_type \n
    Condition: employment_type==\"Intern\"
    \n
    Amount: 1000
  6. \n
" + }, + { + "default": "0", + "depends_on": "eval:doc.parentfield=='deductions'", + "fetch_from": "salary_component.exempted_from_income_tax", + "fieldname": "exempted_from_income_tax", + "fieldtype": "Check", + "label": "Exempted from Income Tax", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2019-12-31 17:15:25.646689", + "modified": "2020-04-24 20:00:16.475295", "modified_by": "Administrator", "module": "HR", "name": "Salary Detail", diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index 223c4e3e3bf..40fe572d753 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -451,7 +451,8 @@ class SalarySlip(TransactionBase): 'is_flexible_benefit': struct_row.is_flexible_benefit, 'variable_based_on_taxable_salary': struct_row.variable_based_on_taxable_salary, 'deduct_full_tax_on_selected_payroll_date': struct_row.deduct_full_tax_on_selected_payroll_date, - 'additional_amount': amount if struct_row.get("is_additional_component") else 0 + 'additional_amount': amount if struct_row.get("is_additional_component") else 0, + 'exempted_from_income_tax': struct_row.exempted_from_income_tax }) else: if struct_row.get("is_additional_component"): @@ -482,10 +483,12 @@ class SalarySlip(TransactionBase): return self.calculate_variable_tax(payroll_period, tax_component) def calculate_variable_tax(self, payroll_period, tax_component): + # get Tax slab from salary structure assignment for the employee and payroll period + tax_slab = self.get_income_tax_slabs(payroll_period) + # get remaining numbers of sub-period (period for which one salary is processed) remaining_sub_periods = get_period_factor(self.employee, self.start_date, self.end_date, self.payroll_frequency, payroll_period)[1] - # get taxable_earnings, paid_taxes for previous period previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date, self.start_date) previous_total_paid_taxes = self.get_tax_paid_in_period(payroll_period.start_date, self.start_date, tax_component) @@ -507,23 +510,27 @@ class SalarySlip(TransactionBase): unclaimed_taxable_benefits += current_taxable_earnings_for_payment_days.flexi_benefits # Total exemption amount based on tax exemption declaration - total_exemption_amount, other_incomes = self.get_total_exemption_amount_and_other_incomes(payroll_period) + total_exemption_amount = self.get_total_exemption_amount(payroll_period, tax_slab) + + #Employee Other Incomes + other_incomes = self.get_income_form_other_sources(payroll_period) or 0.0 # Total taxable earnings including additional and other incomes total_taxable_earnings = previous_taxable_earnings + current_structured_taxable_earnings + future_structured_taxable_earnings \ + current_additional_earnings + other_incomes + unclaimed_taxable_benefits - total_exemption_amount - + # Total taxable earnings without additional earnings with full tax total_taxable_earnings_without_full_tax_addl_components = total_taxable_earnings - current_additional_earnings_with_full_tax # Structured tax amount - total_structured_tax_amount = self.calculate_tax_by_tax_slab(payroll_period, total_taxable_earnings_without_full_tax_addl_components) + total_structured_tax_amount = self.calculate_tax_by_tax_slab( + total_taxable_earnings_without_full_tax_addl_components, tax_slab) current_structured_tax_amount = (total_structured_tax_amount - previous_total_paid_taxes) / remaining_sub_periods - + # Total taxable earnings with additional earnings with full tax full_tax_on_additional_earnings = 0.0 if current_additional_earnings_with_full_tax: - total_tax_amount = self.calculate_tax_by_tax_slab(payroll_period, total_taxable_earnings) + total_tax_amount = self.calculate_tax_by_tax_slab(total_taxable_earnings, tax_slab) full_tax_on_additional_earnings = total_tax_amount - total_structured_tax_amount current_tax_amount = current_structured_tax_amount + full_tax_on_additional_earnings @@ -532,12 +539,30 @@ class SalarySlip(TransactionBase): return current_tax_amount + def get_income_tax_slabs(self, payroll_period): + income_tax_slab, ss_assignment_name = frappe.db.get_value("Salary Structure Assignment", + {"employee": self.employee, "salary_structure": self.salary_structure, "docstatus": 1}, ["income_tax_slab", 'name']) + + if not income_tax_slab: + frappe.throw(_("Income Tax Slab not set in Salary Structure Assignment: {0}").format(ss_assignment_name)) + + income_tax_slab_doc = frappe.get_doc("Income Tax Slab", income_tax_slab) + if income_tax_slab_doc.disabled: + frappe.throw(_("Income Tax Slab: {0} is disabled").format(income_tax_slab)) + + if getdate(income_tax_slab_doc.effective_from) > getdate(payroll_period.start_date): + frappe.throw(_("Income Tax Slab must be effective on or before Payroll Period Start Date: {0}") + .format(payroll_period.start_date)) + + return income_tax_slab_doc + + def get_taxable_earnings_for_prev_period(self, start_date, end_date): taxable_earnings = frappe.db.sql(""" select sum(sd.amount) from `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name - where + where sd.parentfield='earnings' and sd.is_tax_applicable=1 and is_flexible_benefit=0 @@ -550,7 +575,28 @@ class SalarySlip(TransactionBase): "from_date": start_date, "to_date": end_date }) - return flt(taxable_earnings[0][0]) if taxable_earnings else 0 + taxable_earnings = flt(taxable_earnings[0][0]) if taxable_earnings else 0 + + exempted_amount = frappe.db.sql(""" + select sum(sd.amount) + from + `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name + where + sd.parentfield='deductions' + and sd.exempted_from_income_tax=1 + and is_flexible_benefit=0 + and ss.docstatus=1 + and ss.employee=%(employee)s + and ss.start_date between %(from_date)s and %(to_date)s + and ss.end_date between %(from_date)s and %(to_date)s + """, { + "employee": self.employee, + "from_date": start_date, + "to_date": end_date + }) + exempted_amount = flt(exempted_amount[0][0]) if exempted_amount else 0 + + return taxable_earnings - exempted_amount def get_tax_paid_in_period(self, start_date, end_date, tax_component): # find total_tax_paid, tax paid for benefit, additional_salary @@ -610,6 +656,13 @@ class SalarySlip(TransactionBase): else: taxable_earnings += amount + for ded in self.deductions: + if ded.exempted_from_income_tax: + amount = ded.amount + if based_on_payment_days: + amount = self.get_amount_based_on_payment_days(ded, joining_date, relieving_date)[0] + taxable_earnings -= flt(amount) + return frappe._dict({ "taxable_earnings": taxable_earnings, "additional_income": additional_income, @@ -672,40 +725,63 @@ class SalarySlip(TransactionBase): return total_benefits_paid - total_benefits_claimed - def get_total_exemption_amount_and_other_incomes(self, payroll_period): - total_exemption_amount, other_incomes = 0, 0 - if self.deduct_tax_for_unsubmitted_tax_exemption_proof: - exemption_proof = frappe.db.get_value("Employee Tax Exemption Proof Submission", - {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1}, - ["exemption_amount", "income_from_other_sources"]) - if exemption_proof: - total_exemption_amount, other_incomes = exemption_proof - else: - declaration = frappe.db.get_value("Employee Tax Exemption Declaration", - {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1}, - ["total_exemption_amount", "income_from_other_sources"]) - if declaration: - total_exemption_amount, other_incomes = declaration + def get_total_exemption_amount(self, payroll_period, tax_slab): + total_exemption_amount = 0 + if tax_slab.allow_tax_exemption: + if self.deduct_tax_for_unsubmitted_tax_exemption_proof: + exemption_proof = frappe.db.get_value("Employee Tax Exemption Proof Submission", + {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1}, + ["exemption_amount"]) + if exemption_proof: + total_exemption_amount = exemption_proof + else: + declaration = frappe.db.get_value("Employee Tax Exemption Declaration", + {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1}, + ["total_exemption_amount"]) + if declaration: + total_exemption_amount = declaration - return total_exemption_amount, other_incomes + total_exemption_amount += flt(tax_slab.standard_tax_exemption_amount) - def calculate_tax_by_tax_slab(self, payroll_period, annual_taxable_earning): - payroll_period_obj = frappe.get_doc("Payroll Period", payroll_period) - annual_taxable_earning -= flt(payroll_period_obj.standard_tax_exemption_amount) + return total_exemption_amount + + def get_income_form_other_sources(self, payroll_period): + return frappe.get_all("Employee Other Income", + filters={ + "employee": self.employee, + "payroll_period": payroll_period.name, + "company": self.company, + "docstatus": 1 + }, + fields="SUM(amount) as total_amount" + )[0].total_amount + + def calculate_tax_by_tax_slab(self, annual_taxable_earning, tax_slab): data = self.get_data_for_eval() data.update({"annual_taxable_earning": annual_taxable_earning}) - taxable_amount = 0 - for slab in payroll_period_obj.taxable_salary_slabs: + tax_amount = 0 + for slab in tax_slab.slabs: if slab.condition and not self.eval_tax_slab_condition(slab.condition, data): continue if not slab.to_amount and annual_taxable_earning > slab.from_amount: - taxable_amount += (annual_taxable_earning - slab.from_amount) * slab.percent_deduction *.01 + tax_amount += (annual_taxable_earning - slab.from_amount) * slab.percent_deduction *.01 continue if annual_taxable_earning > slab.from_amount and annual_taxable_earning < slab.to_amount: - taxable_amount += (annual_taxable_earning - slab.from_amount) * slab.percent_deduction *.01 + tax_amount += (annual_taxable_earning - slab.from_amount) * slab.percent_deduction *.01 elif annual_taxable_earning > slab.from_amount and annual_taxable_earning > slab.to_amount: - taxable_amount += (slab.to_amount - slab.from_amount) * slab.percent_deduction * .01 - return taxable_amount + tax_amount += (slab.to_amount - slab.from_amount) * slab.percent_deduction * .01 + + # other taxes and charges on income tax + for d in tax_slab.other_taxes_and_charges: + if flt(d.min_taxable_income) and flt(d.min_taxable_income) > tax_amount: + continue + + if flt(d.max_taxable_income) and flt(d.max_taxable_income) < tax_amount: + continue + + tax_amount += tax_amount * flt(d.percent) / 100 + + return tax_amount def eval_tax_slab_condition(self, condition, data): try: diff --git a/erpnext/hr/doctype/salary_slip/test_salary_slip.py b/erpnext/hr/doctype/salary_slip/test_salary_slip.py index ecccac7d416..73bb19e9eea 100644 --- a/erpnext/hr/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/test_salary_slip.py @@ -47,10 +47,7 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.payment_days, no_of_days[0]) self.assertEqual(ss.earnings[0].amount, 50000) self.assertEqual(ss.earnings[1].amount, 3000) - self.assertEqual(ss.deductions[0].amount, 5000) - self.assertEqual(ss.deductions[1].amount, 5000) self.assertEqual(ss.gross_pay, 78000) - self.assertEqual(ss.net_pay, 68000.0) def test_salary_slip_with_holidays_excluded(self): no_of_days = self.get_no_of_days() @@ -67,10 +64,7 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.earnings[0].amount, 50000) self.assertEqual(ss.earnings[0].default_amount, 50000) self.assertEqual(ss.earnings[1].amount, 3000) - self.assertEqual(ss.deductions[0].amount, 5000) - self.assertEqual(ss.deductions[1].amount, 5000) self.assertEqual(ss.gross_pay, 78000) - self.assertEqual(ss.net_pay, 68000.0) def test_payment_days(self): no_of_days = self.get_no_of_days() @@ -80,8 +74,8 @@ class TestSalarySlip(unittest.TestCase): # set joinng date in the same month make_employee("test_employee@salary.com") if getdate(nowdate()).day >= 15: - date_of_joining = getdate(add_days(nowdate(),-10)) relieving_date = getdate(add_days(nowdate(),-10)) + date_of_joining = getdate(add_days(nowdate(),-10)) elif getdate(nowdate()).day < 15 and getdate(nowdate()).day >= 5: date_of_joining = getdate(add_days(nowdate(),-3)) relieving_date = getdate(add_days(nowdate(),-3)) @@ -131,9 +125,7 @@ class TestSalarySlip(unittest.TestCase): def test_email_salary_slip(self): frappe.db.sql("delete from `tabEmail Queue`") - hr_settings = frappe.get_doc("HR Settings", "HR Settings") - hr_settings.email_salary_slip_to_employee = 1 - hr_settings.save() + frappe.db.set_value("HR Settings", None, "email_salary_slip_to_employee", 1) make_employee("test_employee@salary.com") ss = make_employee_salary_slip("test_employee@salary.com", "Monthly") @@ -203,8 +195,11 @@ class TestSalarySlip(unittest.TestCase): # as per assigned salary structure 40500 in monthly salary so 236000*5/100/12 frappe.db.sql("""delete from `tabPayroll Period`""") frappe.db.sql("""delete from `tabSalary Component`""") + payroll_period = create_payroll_period() - create_tax_slab(payroll_period) + + create_tax_slab(payroll_period, allow_tax_exemption=True) + employee = make_employee("test_tax@salary.slip") delete_docs = [ "Salary Slip", @@ -230,8 +225,7 @@ class TestSalarySlip(unittest.TestCase): payroll_period, deduct_random=False) tax_paid = get_tax_paid_in_period(employee) - # total taxable income 586000, 250000 @ 5%, 86000 @ 20% ie. 12500 + 17200 - annual_tax = 113568 + annual_tax = 113589.0 try: self.assertEqual(tax_paid, annual_tax) except AssertionError: @@ -255,8 +249,7 @@ class TestSalarySlip(unittest.TestCase): raise # Submit proof for total 120000 - data["proof-1"] = create_proof_submission(employee, payroll_period, 50000) - data["proof-2"] = create_proof_submission(employee, payroll_period, 70000) + data["proof"] = create_proof_submission(employee, payroll_period, 120000) # Submit benefit claim for total 50000 data["benefit-1"] = create_benefit_claim(employee, payroll_period, 15000, "Medical Allowance") @@ -270,7 +263,7 @@ class TestSalarySlip(unittest.TestCase): # total taxable income 416000, 166000 @ 5% ie. 8300 try: - self.assertEqual(tax_paid, 88608) + self.assertEqual(tax_paid, 82389.0) except AssertionError: print("\nSalary Slip - Tax calculation failed on following case\n", data, "\n") raise @@ -285,7 +278,7 @@ class TestSalarySlip(unittest.TestCase): # total taxable income 566000, 250000 @ 5%, 66000 @ 20%, 12500 + 13200 tax_paid = get_tax_paid_in_period(employee) try: - self.assertEqual(tax_paid, 121211) + self.assertEqual(tax_paid, annual_tax) except AssertionError: print("\nSalary Slip - Tax calculation failed on following case\n", data, "\n") raise @@ -327,6 +320,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): from erpnext.hr.doctype.salary_structure.test_salary_structure import make_salary_structure if not salary_structure: salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" + employee = frappe.db.get_value("Employee", {"user_id": user}) salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee) salary_slip = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) @@ -456,17 +450,15 @@ def make_deduction_salary_component(setup=False, test_tax=False, company_list=No { "salary_component": 'Professional Tax', "abbr":'PT', - "condition": 'base > 10000', - "formula": 'base*.1', "type": "Deduction", - "amount_based_on_formula": 1 + "amount": 200, + "exempted_from_income_tax": 1 + }, { "salary_component": 'TDS', "abbr":'T', - "formula": 'base*.1', "type": "Deduction", - "amount_based_on_formula": 1, "depends_on_payment_days": 0, "variable_based_on_taxable_salary": 1, "round_to_the_nearest_integer": 1 @@ -477,9 +469,7 @@ def make_deduction_salary_component(setup=False, test_tax=False, company_list=No "salary_component": 'TDS', "abbr":'T', "condition": 'employment_type=="Intern"', - "formula": 'base*.1', "type": "Deduction", - "amount_based_on_formula": 1, "round_to_the_nearest_integer": 1 }) if setup or test_tax: @@ -535,29 +525,47 @@ def create_benefit_claim(employee, payroll_period, amount, component): }).submit() return claim_date -def create_tax_slab(payroll_period): - data = [ +def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False): + if frappe.db.exists("Income Tax Slab", "Tax Slab: " + payroll_period.name): + return + + slabs = [ { "from_amount": 250000, "to_amount": 500000, - "percent_deduction": 5.2, + "percent_deduction": 5, "condition": "annual_taxable_earning > 500000" }, { "from_amount": 500001, "to_amount": 1000000, - "percent_deduction": 20.8 + "percent_deduction": 20 }, { "from_amount": 1000001, - "percent_deduction": 31.2 + "percent_deduction": 30 } ] - payroll_period.taxable_salary_slabs = [] - for item in data: - payroll_period.append("taxable_salary_slabs", item) - payroll_period.standard_tax_exemption_amount = 52500 - payroll_period.save() + + income_tax_slab = frappe.new_doc("Income Tax Slab") + income_tax_slab.name = "Tax Slab: " + payroll_period.name + income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2) + + if allow_tax_exemption: + income_tax_slab.allow_tax_exemption = 1 + income_tax_slab.standard_tax_exemption_amount = 50000 + + for item in slabs: + income_tax_slab.append("slabs", item) + + income_tax_slab.append("other_taxes_and_charges", { + "description": "cess", + "percent": 4 + }) + + income_tax_slab.save() + if not dont_submit: + income_tax_slab.submit() def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True): deducted_dates = [] diff --git a/erpnext/hr/doctype/salary_structure/salary_structure.js b/erpnext/hr/doctype/salary_structure/salary_structure.js index 71204481006..7748403513f 100755 --- a/erpnext/hr/doctype/salary_structure/salary_structure.js +++ b/erpnext/hr/doctype/salary_structure/salary_structure.js @@ -82,6 +82,7 @@ frappe.ui.form.on('Salary Structure', { {fieldname:"employee", fieldtype: "Link", options: "Employee", label: __("Employee")}, {fieldname:'base_variable', fieldtype:'Section Break'}, {fieldname:'from_date', fieldtype:'Date', label: __('From Date'), "reqd": 1}, + {fieldname:'income_tax_slab', fieldtype:'Link', label: __('Income Tax Slab'), options: 'Income Tax Slab'}, {fieldname:'base_col_br', fieldtype:'Column Break'}, {fieldname:'base', fieldtype:'Currency', label: __('Base')}, {fieldname:'variable', fieldtype:'Currency', label: __('Variable')} diff --git a/erpnext/hr/doctype/salary_structure/salary_structure.py b/erpnext/hr/doctype/salary_structure/salary_structure.py index 568277f8a73..df76458fe02 100644 --- a/erpnext/hr/doctype/salary_structure/salary_structure.py +++ b/erpnext/hr/doctype/salary_structure/salary_structure.py @@ -16,6 +16,7 @@ class SalaryStructure(Document): self.validate_amount() self.strip_condition_and_formula_fields() self.validate_max_benefits_with_flexi() + self.validate_component_based_on_tax_slab() def set_missing_values(self): overwritten_fields = ["depends_on_payment_days", "variable_based_on_taxable_salary", "is_tax_applicable", "is_flexible_benefit"] @@ -34,6 +35,12 @@ class SalaryStructure(Document): for fieldname in overwritten_fields_if_missing: d.set(fieldname, component_default_value.get(fieldname)) + def validate_component_based_on_tax_slab(self): + for row in self.deductions: + if row.variable_based_on_taxable_salary and (row.amount or row.formula): + frappe.throw(_("Row #{0}: Cannot set amount or formula for Salary Component {1} with Variable Based On Taxable Salary") + .format(row.idx, row.salary_component)) + def validate_amount(self): if flt(self.net_pay) < 0 and self.salary_slip_based_on_timesheet: frappe.throw(_("Net pay cannot be negative")) @@ -82,21 +89,23 @@ class SalaryStructure(Document): @frappe.whitelist() def assign_salary_structure(self, company=None, grade=None, department=None, designation=None,employee=None, - from_date=None, base=None,variable=None): + from_date=None, base=None, variable=None, income_tax_slab=None): employees = self.get_employees(company= company, grade= grade,department= department,designation= designation,name=employee) if employees: if len(employees) > 20: frappe.enqueue(assign_salary_structure_for_employees, timeout=600, - employees=employees, salary_structure=self,from_date=from_date, base=base,variable=variable) + employees=employees, salary_structure=self,from_date=from_date, + base=base, variable=variable, income_tax_slab=income_tax_slab) else: - assign_salary_structure_for_employees(employees, self, from_date=from_date, base=base,variable=variable) + assign_salary_structure_for_employees(employees, self, from_date=from_date, + base=base, variable=variable, income_tax_slab=income_tax_slab) else: frappe.msgprint(_("No Employee Found")) -def assign_salary_structure_for_employees(employees, salary_structure, from_date=None, base=None,variable=None): +def assign_salary_structure_for_employees(employees, salary_structure, from_date=None, base=None, variable=None, income_tax_slab=None): salary_structures_assignments = [] existing_assignments_for = get_existing_assignments(employees, salary_structure, from_date) count=0 @@ -105,7 +114,8 @@ def assign_salary_structure_for_employees(employees, salary_structure, from_date continue count +=1 - salary_structures_assignment = create_salary_structures_assignment(employee, salary_structure, from_date, base, variable) + salary_structures_assignment = create_salary_structures_assignment(employee, + salary_structure, from_date, base, variable, income_tax_slab) salary_structures_assignments.append(salary_structures_assignment) frappe.publish_progress(count*100/len(set(employees) - set(existing_assignments_for)), title = _("Assigning Structures...")) @@ -113,7 +123,7 @@ def assign_salary_structure_for_employees(employees, salary_structure, from_date frappe.msgprint(_("Structures have been assigned successfully")) -def create_salary_structures_assignment(employee, salary_structure, from_date, base, variable): +def create_salary_structures_assignment(employee, salary_structure, from_date, base, variable, income_tax_slab=None): assignment = frappe.new_doc("Salary Structure Assignment") assignment.employee = employee assignment.salary_structure = salary_structure.name @@ -121,6 +131,7 @@ def create_salary_structures_assignment(employee, salary_structure, from_date, b assignment.from_date = from_date assignment.base = base assignment.variable = variable + assignment.income_tax_slab = income_tax_slab assignment.save(ignore_permissions = True) assignment.submit() return assignment.name diff --git a/erpnext/hr/doctype/salary_structure/test_salary_structure.py b/erpnext/hr/doctype/salary_structure/test_salary_structure.py index 6ca6dfd2c02..c1869f05d7d 100644 --- a/erpnext/hr/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/hr/doctype/salary_structure/test_salary_structure.py @@ -9,8 +9,9 @@ from frappe.utils.make_random import get_random from frappe.utils import nowdate, add_days, add_years, getdate, add_months from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.hr.doctype.salary_slip.test_salary_slip import make_earning_salary_component,\ - make_deduction_salary_component, make_employee_salary_slip + make_deduction_salary_component, make_employee_salary_slip, create_tax_slab from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import create_payroll_period test_dependencies = ["Fiscal Year"] @@ -70,10 +71,8 @@ class TestSalaryStructure(unittest.TestCase): self.assertEqual(sal_slip.get("earnings")[1].amount, 3000) self.assertEqual(sal_slip.get("earnings")[2].amount, 25000) self.assertEqual(sal_slip.get("gross_pay"), 78000) - self.assertEqual(sal_slip.get("deductions")[0].amount, 5000) - self.assertEqual(sal_slip.get("deductions")[1].amount, 5000) - self.assertEqual(sal_slip.get("total_deduction"), 10000) - self.assertEqual(sal_slip.get("net_pay"), 68000) + self.assertEqual(sal_slip.get("deductions")[0].amount, 200) + self.assertEqual(sal_slip.get("net_pay"), 78000 - sal_slip.get("total_deduction")) def test_whitespaces_in_formula_conditions_fields(self): salary_structure = make_salary_structure("Salary Structure Sample", "Monthly", dont_submit=True) @@ -112,6 +111,7 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do test_tax=False, company=None): if test_tax: frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure)) + if not frappe.db.exists('Salary Structure', salary_structure): details = { "doctype": "Salary Structure", @@ -124,7 +124,8 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do } if other_details and isinstance(other_details, dict): details.update(other_details) - salary_structure_doc = frappe.get_doc(details).insert() + salary_structure_doc = frappe.get_doc(details) + salary_structure_doc.insert() if not dont_submit: salary_structure_doc.submit() else: @@ -139,13 +140,18 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None): if frappe.db.exists("Salary Structure Assignment", {"employee": employee}): frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee)) + + payroll_period = create_payroll_period() + create_tax_slab(payroll_period, allow_tax_exemption=True) + salary_structure_assignment = frappe.new_doc("Salary Structure Assignment") salary_structure_assignment.employee = employee salary_structure_assignment.base = 50000 salary_structure_assignment.variable = 5000 - salary_structure_assignment.from_date = from_date or add_months(nowdate(), -1) + salary_structure_assignment.from_date = from_date or add_days(nowdate(), -1) salary_structure_assignment.salary_structure = salary_structure salary_structure_assignment.company = company or erpnext.get_default_company() salary_structure_assignment.save(ignore_permissions=True) + salary_structure_assignment.income_tax_slab = "Tax Slab: _Test Payroll Period" salary_structure_assignment.submit() return salary_structure_assignment diff --git a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js b/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js index 56a05e04956..818e853154d 100644 --- a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js +++ b/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js @@ -20,6 +20,16 @@ frappe.ui.form.on('Salary Structure Assignment', { } } }); + + frm.set_query("income_tax_slab", function() { + return { + filters: { + company: frm.doc.company, + docstatus: 1, + disabled: 0 + } + } + }); }, employee: function(frm) { if(frm.doc.employee){ diff --git a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.json b/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.json index 380c889477f..0098aa8ec80 100644 --- a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.json +++ b/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.json @@ -10,11 +10,12 @@ "employee", "employee_name", "department", - "designation", + "company", "column_break_6", + "designation", "salary_structure", "from_date", - "company", + "income_tax_slab", "section_break_7", "base", "column_break_9", @@ -113,11 +114,17 @@ "options": "Salary Structure Assignment", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "income_tax_slab", + "fieldtype": "Link", + "label": "Income Tax Slab", + "options": "Income Tax Slab" } ], "is_submittable": 1, "links": [], - "modified": "2019-12-31 16:35:34.415099", + "modified": "2020-04-25 18:24:23.617088", "modified_by": "Administrator", "module": "HR", "name": "Salary Structure Assignment", diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index ef276001c58..cd125108c61 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -9,6 +9,8 @@ from frappe.model.document import Document from frappe.desk.form import assign_to from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee +class DuplicateDeclarationError(frappe.ValidationError): pass + class EmployeeBoardingController(Document): ''' Create the project and the task for the boarding process @@ -226,6 +228,17 @@ def get_employee_leave_policy(employee): 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, + "employee": employee, + 'docstatus': ['<', 2], + 'name': ['!=', docname] + }) + if existing_record: + frappe.throw(_("{0} already exists for employee {1} and period {2}") + .format(doctype, employee, payroll_period), DuplicateDeclarationError) + def validate_tax_declaration(declarations): subcategories = [] for d in declarations: diff --git a/erpnext/patches.txt b/erpnext/patches.txt index eb2b35cc8e7..a73e8c19263 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -669,4 +669,5 @@ erpnext.patches.v12_0.set_total_batch_quantity erpnext.patches.v12_0.rename_mws_settings_fields erpnext.patches.v12_0.set_updated_purpose_in_pick_list erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse -erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign \ No newline at end of file +erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign +erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123 diff --git a/erpnext/patches/v11_0/set_salary_component_properties.py b/erpnext/patches/v11_0/set_salary_component_properties.py index fa3605ba5f1..83fb53d2a73 100644 --- a/erpnext/patches/v11_0/set_salary_component_properties.py +++ b/erpnext/patches/v11_0/set_salary_component_properties.py @@ -5,8 +5,7 @@ def execute(): frappe.reload_doc('hr', 'doctype', 'salary_detail') frappe.reload_doc('hr', 'doctype', 'salary_component') - frappe.db.sql("update `tabSalary Component` set is_payable=1, is_tax_applicable=1 where type='Earning'") - frappe.db.sql("update `tabSalary Component` set is_payable=0 where type='Deduction'") + frappe.db.sql("update `tabSalary Component` set is_tax_applicable=1 where type='Earning'") frappe.db.sql("""update `tabSalary Component` set variable_based_on_taxable_salary=1 where type='Deduction' and name in ('TDS', 'Tax Deducted at Source')""") diff --git a/erpnext/patches/v13_0/__init__.py b/erpnext/patches/v13_0/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py new file mode 100644 index 00000000000..a6aefac12ad --- /dev/null +++ b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py @@ -0,0 +1,99 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe +from frappe.model.utils.rename_field import rename_field + +def execute(): + if not frappe.db.table_exists("Payroll Period"): + return + + for doctype in ("income_tax_slab", "salary_structure_assignment", "employee_other_income"): + frappe.reload_doc("hr", "doctype", doctype) + + + for company in frappe.get_all("Company"): + payroll_periods = frappe.db.sql(""" + SELECT + name, start_date, end_date, standard_tax_exemption_amount + FROM + `tabPayroll Period` + WHERE company=%s + ORDER BY start_date DESC + """, company.name, as_dict = 1) + + for i, period in enumerate(payroll_periods): + income_tax_slab = frappe.new_doc("Income Tax Slab") + income_tax_slab.name = "Tax Slab:" + period.name + + if i == 0: + income_tax_slab.disabled = 0 + else: + income_tax_slab.disabled = 1 + + income_tax_slab.effective_from = period.start_date + income_tax_slab.company = company.name + income_tax_slab.allow_tax_exemption = 1 + income_tax_slab.standard_tax_exemption_amount = period.standard_tax_exemption_amount + + income_tax_slab.flags.ignore_mandatory = True + income_tax_slab.submit() + + frappe.db.sql( + """ UPDATE `tabTaxable Salary Slab` + SET parent = %s , parentfield = 'slabs' , parenttype = "Income Tax Slab" + WHERE parent = %s + """, (income_tax_slab.name, period.name), as_dict = 1) + + if i == 0: + frappe.db.sql(""" + UPDATE + `tabSalary Structure Assignment` + set + income_tax_slab = %s + where + company = %s + and from_date >= %s + and docstatus < 2 + """, (income_tax_slab.name, company.name, period.start_date)) + + # move other incomes to separate document + migrated = [] + proofs = frappe.get_all("Employee Tax Exemption Proof Submission", + filters = {'docstatus': 1}, + fields =['payroll_period', 'employee', 'company', 'income_from_other_sources'] + ) + for proof in proofs: + if proof.income_from_other_sources: + employee_other_income = frappe.new_doc("Employee Other Income") + employee_other_income.employee = proof.employee + employee_other_income.payroll_period = proof.payroll_period + employee_other_income.company = proof.company + employee_other_income.amount = proof.income_from_other_sources + + try: + employee_other_income.submit() + migrated.append([proof.employee, proof.payroll_period]) + except: + pass + + declerations = frappe.get_all("Employee Tax Exemption Declaration", + filters = {'docstatus': 1}, + fields =['payroll_period', 'employee', 'company', 'income_from_other_sources'] + ) + + for declaration in declerations: + if declaration.income_from_other_sources \ + and [declaration.employee, declaration.payroll_period] not in migrated: + employee_other_income = frappe.new_doc("Employee Other Income") + employee_other_income.employee = declaration.employee + employee_other_income.payroll_period = declaration.payroll_period + employee_other_income.company = declaration.company + employee_other_income.amount = declaration.income_from_other_sources + + try: + employee_other_income.submit() + except: + pass diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 4be6804db56..b4e3558af67 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -531,12 +531,18 @@ def make_fixtures(company=None): def set_salary_components(docs): docs.extend([ - {'doctype': 'Salary Component', 'salary_component': 'Professional Tax', 'description': 'Professional Tax', 'type': 'Deduction'}, - {'doctype': 'Salary Component', 'salary_component': 'Provident Fund', 'description': 'Provident fund', 'type': 'Deduction'}, - {'doctype': 'Salary Component', 'salary_component': 'House Rent Allowance', 'description': 'House Rent Allowance', 'type': 'Earning'}, - {'doctype': 'Salary Component', 'salary_component': 'Basic', 'description': 'Basic', 'type': 'Earning'}, - {'doctype': 'Salary Component', 'salary_component': 'Arrear', 'description': 'Arrear', 'type': 'Earning'}, - {'doctype': 'Salary Component', 'salary_component': 'Leave Encashment', 'description': 'Leave Encashment', 'type': 'Earning'} + {'doctype': 'Salary Component', 'salary_component': 'Professional Tax', + 'description': 'Professional Tax', 'type': 'Deduction', 'exempted_from_income_tax': 1}, + {'doctype': 'Salary Component', 'salary_component': 'Provident Fund', + 'description': 'Provident fund', 'type': 'Deduction', 'is_tax_applicable': 1}, + {'doctype': 'Salary Component', 'salary_component': 'House Rent Allowance', + 'description': 'House Rent Allowance', 'type': 'Earning', 'is_tax_applicable': 1}, + {'doctype': 'Salary Component', 'salary_component': 'Basic', + 'description': 'Basic', 'type': 'Earning', 'is_tax_applicable': 1}, + {'doctype': 'Salary Component', 'salary_component': 'Arrear', + 'description': 'Arrear', 'type': 'Earning', 'is_tax_applicable': 1}, + {'doctype': 'Salary Component', 'salary_component': 'Leave Encashment', + 'description': 'Leave Encashment', 'type': 'Earning', 'is_tax_applicable': 1} ]) def set_tax_withholding_category(company): From 5d5454ef1f0a074397a4163d85eef484d4944451 Mon Sep 17 00:00:00 2001 From: Marica Date: Sun, 26 Apr 2020 20:07:22 +0530 Subject: [PATCH 09/58] fix [ux]: Purchase Order Form Cleanup (#20932) * chore: Purchase Order Form Cleanup * fix: Get Items from popups cleanup * fix: Shift Set Target Warehouse next to Supply Raw Materials. Co-authored-by: Nabin Hait --- .../purchase_taxes_and_charges.json | 23 ++++----- .../doctype/purchase_order/purchase_order.js | 6 +-- .../purchase_order/purchase_order.json | 44 ++++++++++------- .../purchase_order_item.json | 42 ++++++++++------- .../purchase_order_item_supplied.json | 47 ++++++++++++++----- 5 files changed, 100 insertions(+), 62 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index 17418693616..0e748f84bb5 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-05-21 16:16:04", "doctype": "DocType", @@ -14,11 +15,11 @@ "col_break1", "account_head", "description", + "section_break_10", + "rate", "accounting_dimensions_section", "cost_center", "dimension_col_break", - "section_break_10", - "rate", "section_break_9", "tax_amount", "tax_amount_after_discount_amount", @@ -27,8 +28,7 @@ "base_tax_amount", "base_total", "base_tax_amount_after_discount_amount", - "item_wise_tax_detail", - "parenttype" + "item_wise_tax_detail" ], "fields": [ { @@ -53,6 +53,7 @@ }, { "columns": 2, + "default": "On Net Total", "fieldname": "charge_type", "fieldtype": "Select", "in_list_view": 1, @@ -196,15 +197,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "parenttype", - "fieldtype": "Data", - "hidden": 1, - "label": "Parenttype", - "oldfieldname": "parenttype", - "oldfieldtype": "Data", - "print_hide": 1 - }, { "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", @@ -217,11 +209,14 @@ ], "idx": 1, "istable": 1, - "modified": "2019-05-25 23:08:38.281025", + "links": [], + "modified": "2020-03-12 14:53:47.679439", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Taxes and Charges", "owner": "Administrator", "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 3111a3a7d53..ed054aedb5a 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -365,9 +365,7 @@ 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: { - company: me.frm.doc.company - }, + setters: {}, get_query_filters: { material_request_type: "Purchase", docstatus: 1, @@ -384,7 +382,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( source_doctype: "Supplier Quotation", target: me.frm, setters: { - company: me.frm.doc.company + supplier: me.frm.doc.supplier }, get_query_filters: { docstatus: 1, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 578858ca520..a4f60fbba5c 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -54,11 +54,6 @@ "items_section", "scan_barcode", "items", - "section_break_48", - "pricing_rules", - "raw_material_details", - "set_reserve_warehouse", - "supplied_items", "sb_last_purchase", "total_qty", "base_total", @@ -67,6 +62,11 @@ "total_net_weight", "total", "net_total", + "section_break_48", + "pricing_rules", + "raw_material_details", + "set_reserve_warehouse", + "supplied_items", "taxes_section", "tax_category", "column_break_50", @@ -105,23 +105,25 @@ "payment_schedule_section", "payment_terms_template", "payment_schedule", + "tracking_section", + "per_billed", + "column_break_75", + "per_received", "terms_section_break", "tc_name", "terms", "more_info", "status", "ref_sq", + "column_break_74", "party_account_currency", "inter_company_order_reference", - "column_break_74", - "per_received", - "per_billed", "column_break5", "letter_head", "select_print_heading", "column_break_86", - "group_same_items", "language", + "group_same_items", "subscription_section", "from_date", "to_date", @@ -220,7 +222,7 @@ "allow_on_submit": 1, "fieldname": "schedule_date", "fieldtype": "Date", - "label": "Reqd By Date" + "label": "Required By" }, { "allow_on_submit": 1, @@ -432,6 +434,7 @@ "fieldtype": "Section Break" }, { + "description": "Sets 'Warehouse' in each row of the Items table.", "fieldname": "set_warehouse", "fieldtype": "Link", "label": "Set Target Warehouse", @@ -827,6 +830,7 @@ "read_only": 1 }, { + "collapsible": 1, "fieldname": "payment_schedule_section", "fieldtype": "Section Break", "label": "Payment Terms" @@ -917,7 +921,8 @@ "fieldname": "inter_company_order_reference", "fieldtype": "Link", "label": "Inter Company Order Reference", - "options": "Sales Order" + "options": "Sales Order", + "read_only": 1 }, { "fieldname": "column_break_74", @@ -930,8 +935,6 @@ "in_list_view": 1, "label": "% Received", "no_copy": 1, - "oldfieldname": "per_received", - "oldfieldtype": "Currency", "print_hide": 1, "read_only": 1 }, @@ -942,8 +945,6 @@ "in_list_view": 1, "label": "% Billed", "no_copy": 1, - "oldfieldname": "per_billed", - "oldfieldtype": "Currency", "print_hide": 1, "read_only": 1 }, @@ -998,6 +999,7 @@ "print_hide": 1 }, { + "collapsible": 1, "fieldname": "subscription_section", "fieldtype": "Section Break", "label": "Subscription Section" @@ -1050,13 +1052,23 @@ "fieldtype": "Link", "label": "Set Reserve Warehouse", "options": "Warehouse" + }, + { + "collapsible": 1, + "fieldname": "tracking_section", + "fieldtype": "Section Break", + "label": "Tracking" + }, + { + "fieldname": "column_break_75", + "fieldtype": "Column Break" } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2020-04-17 13:04:28.185197", + "modified": "2020-04-24 12:13:14.186280", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index e37e1dd99d8..7a52c28a0ee 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -18,10 +18,6 @@ "col_break1", "image", "image_view", - "manufacture_details", - "manufacturer", - "column_break_14", - "manufacturer_part_no", "quantity_and_rate", "qty", "stock_uom", @@ -44,7 +40,6 @@ "base_amount", "pricing_rules", "is_free_item", - "is_fixed_asset", "section_break_29", "net_rate", "net_amount", @@ -52,11 +47,6 @@ "base_net_rate", "base_net_amount", "billed_amt", - "item_weight_details", - "weight_per_unit", - "total_weight", - "column_break_40", - "weight_uom", "warehouse_and_reference", "warehouse", "delivered_by_supplier", @@ -80,20 +70,31 @@ "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", "accounting_details", "expense_account", "column_break_68", + "item_weight_details", + "weight_per_unit", + "total_weight", + "column_break_40", + "weight_uom", "accounting_dimensions_section", "cost_center", "dimension_col_break", "section_break_72", - "page_break", - "item_tax_rate" + "page_break" ], "fields": [ { "bold": 1, - "columns": 3, + "columns": 2, "fieldname": "item_code", "fieldtype": "Link", "in_list_view": 1, @@ -133,7 +134,7 @@ "fieldname": "schedule_date", "fieldtype": "Date", "in_list_view": 1, - "label": "Reqd By Date", + "label": "Required By", "oldfieldname": "schedule_date", "oldfieldtype": "Date", "print_hide": 1, @@ -216,15 +217,16 @@ "print_hide": 1 }, { + "columns": 1, "fieldname": "uom", "fieldtype": "Link", + "in_list_view": 1, "label": "UOM", "oldfieldname": "uom", "oldfieldtype": "Link", "options": "UOM", "print_width": "100px", - "reqd": 1, - "width": "100px" + "reqd": 1 }, { "fieldname": "conversion_factor", @@ -685,6 +687,7 @@ "fieldtype": "Column Break" }, { + "collapsible": 1, "fieldname": "manufacture_details", "fieldtype": "Section Break", "label": "Manufacture" @@ -717,12 +720,17 @@ "fieldtype": "Check", "label": "Is Fixed Asset", "read_only": 1 + }, + { + "fieldname": "more_info_section_break", + "fieldtype": "Section Break", + "label": "More Information" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-04-07 18:35:17.558928", + "modified": "2020-04-21 11:55:58.643393", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json index 8435bbb06e8..c3e1bf53030 100644 --- a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json +++ b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json @@ -1,20 +1,26 @@ { + "actions": [], "creation": "2013-02-22 01:27:42", "doctype": "DocType", "editable_grid": 1, + "engine": "InnoDB", "field_order": [ "main_item_code", - "rm_item_code", - "required_qty", - "supplied_qty", - "rate", - "amount", - "column_break_6", "bom_detail_no", - "reference_name", - "conversion_factor", "stock_uom", - "reserve_warehouse" + "conversion_factor", + "column_break_6", + "rm_item_code", + "reference_name", + "reserve_warehouse", + "section_break2", + "rate", + "col_break2", + "amount", + "section_break1", + "required_qty", + "col_break1", + "supplied_qty" ], "fields": [ { @@ -120,15 +126,34 @@ "in_list_view": 1, "label": "Supplied Qty", "read_only": 1 + }, + { + "fieldname": "section_break1", + "fieldtype": "Section Break" + }, + { + "fieldname": "col_break1", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break2", + "fieldtype": "Section Break" + }, + { + "fieldname": "col_break2", + "fieldtype": "Column Break" } ], "hide_toolbar": 1, "idx": 1, "istable": 1, - "modified": "2019-08-20 13:37:32.702068", + "links": [], + "modified": "2020-03-12 15:43:53.862897", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item Supplied", "owner": "dhanalekshmi@webnotestech.com", - "permissions": [] + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file From d78cf9725098af1ebe0f08df177d2a54bb1062e9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sun, 26 Apr 2020 20:08:52 +0530 Subject: [PATCH 10/58] feat: Allow tax withholding category selection at invoice level (#20870) * feat: Allow tax withholding category selection at invoice level * fix: Linitng fixes * feat: TDS calculation using common PAN * fix: Add provision to deduct Lower TDS in purchase invoice * fix: Consider only ref docs company while computing TDS * fix: Default permission fixes * fix: Add validation for dates in fiscal year * fix: Undefined variable --- .../purchase_invoice/purchase_invoice.js | 15 +- .../purchase_invoice/purchase_invoice.json | 11 +- .../purchase_invoice/purchase_invoice.py | 2 +- .../tax_withholding_category.py | 128 +++++++++++++--- .../tds_computation_summary.py | 11 +- erpnext/patches.txt | 1 + .../add_permission_in_lower_deduction.py | 13 ++ .../lower_deduction_certificate/__init__.py | 0 .../lower_deduction_certificate.js | 8 + .../lower_deduction_certificate.json | 138 ++++++++++++++++++ .../lower_deduction_certificate.py | 26 ++++ .../test_lower_deduction_certificate.py | 10 ++ erpnext/regional/india/setup.py | 2 +- 13 files changed, 334 insertions(+), 31 deletions(-) create mode 100644 erpnext/patches/v12_0/add_permission_in_lower_deduction.py create mode 100644 erpnext/regional/doctype/lower_deduction_certificate/__init__.py create mode 100644 erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.js create mode 100644 erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json create mode 100644 erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py create mode 100644 erpnext/regional/doctype/lower_deduction_certificate/test_lower_deduction_certificate.py diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 9292b633fc3..3cf4d5994a5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -261,12 +261,25 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ price_list: this.frm.doc.buying_price_list }, function() { me.apply_pricing_rule(); - me.frm.doc.apply_tds = me.frm.supplier_tds ? 1 : 0; + me.frm.doc.tax_withholding_category = me.frm.supplier_tds; me.frm.set_df_property("apply_tds", "read_only", me.frm.supplier_tds ? 0 : 1); + me.frm.set_df_property("tax_withholding_category", "hidden", me.frm.supplier_tds ? 0 : 1); }) }, + apply_tds: function(frm) { + var me = this; + + if (!me.frm.doc.apply_tds) { + me.frm.set_value("tax_withholding_category", ''); + me.frm.set_df_property("tax_withholding_category", "hidden", 1); + } else { + me.frm.set_value("tax_withholding_category", me.frm.supplier_tds); + me.frm.set_df_property("tax_withholding_category", "hidden", 0); + } + }, + credit_to: function() { var me = this; if(this.frm.doc.credit_to) { diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 0e0945454c9..98ba5c72ae2 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -13,6 +13,7 @@ "supplier_name", "tax_id", "due_date", + "tax_withholding_category", "column_break1", "company", "posting_date", @@ -1294,13 +1295,21 @@ "fieldtype": "Check", "label": "Is Internal Supplier", "read_only": 1 + }, + { + "fieldname": "tax_withholding_category", + "fieldtype": "Link", + "hidden": 1, + "label": "Tax Withholding Category", + "options": "Tax Withholding Category", + "print_hide": 1 } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2020-04-17 13:05:25.199832", + "modified": "2020-04-18 13:05:25.199832", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 0283d304d7e..b1ae194301c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1002,7 +1002,7 @@ class PurchaseInvoice(BuyingController): if not self.apply_tds: return - tax_withholding_details = get_party_tax_withholding_details(self) + tax_withholding_details = get_party_tax_withholding_details(self, self.tax_withholding_category) if not tax_withholding_details: return diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 6c31e9efed1..dd6b4fdc603 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -6,23 +6,42 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import flt +from frappe.utils import flt, getdate from erpnext.accounts.utils import get_fiscal_year class TaxWithholdingCategory(Document): pass -def get_party_tax_withholding_details(ref_doc): - tax_withholding_category = frappe.db.get_value('Supplier', ref_doc.supplier, 'tax_withholding_category') +def get_party_tax_withholding_details(ref_doc, tax_withholding_category=None): + + pan_no = '' + suppliers = [] + + if not tax_withholding_category: + tax_withholding_category, pan_no = frappe.db.get_value('Supplier', ref_doc.supplier, ['tax_withholding_category', 'pan']) + if not tax_withholding_category: return + if not pan_no: + pan_no = frappe.db.get_value('Supplier', ref_doc.supplier, 'pan') + + # Get others suppliers with the same PAN No + if pan_no: + suppliers = [d.name for d in frappe.get_all('Supplier', fields=['name'], filters={'pan': pan_no})] + + if not suppliers: + suppliers.append(ref_doc.supplier) + fy = get_fiscal_year(ref_doc.posting_date, company=ref_doc.company) tax_details = get_tax_withholding_details(tax_withholding_category, fy[0], ref_doc.company) if not tax_details: frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}') .format(tax_withholding_category, ref_doc.company)) - tds_amount = get_tds_amount(ref_doc, tax_details, fy) + + tds_amount = get_tds_amount(suppliers, ref_doc.net_total, ref_doc.company, + tax_details, fy, ref_doc.posting_date, pan_no) + tax_row = get_tax_row(tax_details, tds_amount) return tax_row @@ -51,6 +70,7 @@ def get_tax_withholding_rates(tax_withholding, fiscal_year): frappe.throw(_("No Tax Withholding data found for the current Fiscal Year.")) def get_tax_row(tax_details, tds_amount): + return { "category": "Total", "add_deduct_tax": "Deduct", @@ -60,25 +80,36 @@ def get_tax_row(tax_details, tds_amount): "tax_amount": tds_amount } -def get_tds_amount(ref_doc, tax_details, fiscal_year_details): +def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_details, posting_date, pan_no=None): fiscal_year, year_start_date, year_end_date = fiscal_year_details tds_amount = 0 tds_deducted = 0 - def _get_tds(amount): + def _get_tds(amount, rate): if amount <= 0: return 0 - return amount * tax_details.rate / 100 + return amount * rate / 100 + + ldc_name = frappe.db.get_value('Lower Deduction Certificate', + { + 'pan_no': pan_no, + 'fiscal_year': fiscal_year + }, 'name') + ldc = '' + + if ldc_name: + ldc = frappe.get_doc('Lower Deduction Certificate', ldc_name) entries = frappe.db.sql(""" select voucher_no, credit from `tabGL Entry` - where party=%s and fiscal_year=%s and credit > 0 - """, (ref_doc.supplier, fiscal_year), as_dict=1) + where company = %s and + party in %s and fiscal_year=%s and credit > 0 + """, (company, tuple(suppliers), fiscal_year), as_dict=1) vouchers = [d.voucher_no for d in entries] - advance_vouchers = get_advance_vouchers(ref_doc.supplier, fiscal_year) + advance_vouchers = get_advance_vouchers(suppliers, fiscal_year=fiscal_year, company=company) tds_vouchers = vouchers + advance_vouchers @@ -93,7 +124,20 @@ def get_tds_amount(ref_doc, tax_details, fiscal_year_details): tds_deducted = tds_deducted[0][0] if tds_deducted and tds_deducted[0][0] else 0 if tds_deducted: - tds_amount = _get_tds(ref_doc.net_total) + if ldc: + limit_consumed = frappe.db.get_value('Purchase Invoice', + { + 'supplier': ('in', suppliers), + 'apply_tds': 1, + 'docstatus': 1 + }, 'sum(net_total)') + + if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, limit_consumed, net_total, + ldc.certificate_limit): + + tds_amount = get_ltds_amount(net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details) + else: + tds_amount = _get_tds(net_total, tax_details.rate) else: supplier_credit_amount = frappe.get_all('Purchase Invoice Item', fields = ['sum(net_amount)'], @@ -106,43 +150,79 @@ def get_tds_amount(ref_doc, tax_details, fiscal_year_details): fields = ['sum(credit_in_account_currency)'], filters = { 'parent': ('in', vouchers), 'docstatus': 1, - 'party': ref_doc.supplier, + 'party': ('in', suppliers), 'reference_type': ('not in', ['Purchase Invoice']) }, as_list=1) supplier_credit_amount += (jv_supplier_credit_amt[0][0] if jv_supplier_credit_amt and jv_supplier_credit_amt[0][0] else 0) - supplier_credit_amount += ref_doc.net_total + supplier_credit_amount += net_total - debit_note_amount = get_debit_note_amount(ref_doc.supplier, year_start_date, year_end_date) + debit_note_amount = get_debit_note_amount(suppliers, year_start_date, year_end_date) supplier_credit_amount -= debit_note_amount if ((tax_details.get('threshold', 0) and supplier_credit_amount >= tax_details.threshold) or (tax_details.get('cumulative_threshold', 0) and supplier_credit_amount >= tax_details.cumulative_threshold)): - tds_amount = _get_tds(supplier_credit_amount) + + if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, tds_deducted, net_total, + ldc.certificate_limit): + tds_amount = get_ltds_amount(supplier_credit_amount, 0, ldc.certificate_limit, ldc.rate, + tax_details) + else: + tds_amount = _get_tds(supplier_credit_amount, tax_details.rate) return tds_amount -def get_advance_vouchers(supplier, fiscal_year=None, company=None, from_date=None, to_date=None): +def get_advance_vouchers(suppliers, fiscal_year=None, company=None, from_date=None, to_date=None): condition = "fiscal_year=%s" % fiscal_year + + if company: + condition += "and company =%s" % (company) if from_date and to_date: - condition = "company=%s and posting_date between %s and %s" % (company, from_date, to_date) + condition += "and posting_date between %s and %s" % (company, from_date, to_date) + + ## Appending the same supplier again if length of suppliers list is 1 + ## since tuple of single element list contains None, For example ('Test Supplier 1', ) + ## and the below query fails + if len(suppliers) == 1: + suppliers.append(suppliers[0]) return frappe.db.sql_list(""" select distinct voucher_no from `tabGL Entry` - where party=%s and %s and debit > 0 - """, (supplier, condition)) or [] + where party in %s and %s and debit > 0 + """, (tuple(suppliers), condition)) or [] -def get_debit_note_amount(supplier, year_start_date, year_end_date, company=None): - condition = "" +def get_debit_note_amount(suppliers, year_start_date, year_end_date, company=None): + condition = "and 1=1" if company: condition = " and company=%s " % company + if len(suppliers) == 1: + suppliers.append(suppliers[0]) + return flt(frappe.db.sql(""" select abs(sum(net_total)) from `tabPurchase Invoice` - where supplier=%s %s and is_return=1 and docstatus=1 - and posting_date between %s and %s - """, (supplier, condition, year_start_date, year_end_date))) \ No newline at end of file + where supplier in %s and is_return=1 and docstatus=1 + and posting_date between %s and %s %s + """, (tuple(suppliers), year_start_date, year_end_date, condition))) + +def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details): + if current_amount < (certificate_limit - deducted_amount): + return current_amount * rate/100 + else: + ltds_amount = (certificate_limit - deducted_amount) + tds_amount = current_amount - ltds_amount + + return ltds_amount * rate/100 + tds_amount * tax_details.rate/100 + +def is_valid_certificate(valid_from, valid_upto, posting_date, deducted_amount, current_amount, certificate_limit): + valid = False + + if ((getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and + certificate_limit > deducted_amount): + valid = True + + return valid \ No newline at end of file diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index 2e805f8d3f5..c7cfee74cb0 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -44,9 +44,14 @@ def get_result(filters): out = [] for supplier in filters.supplier: tds = frappe.get_doc("Tax Withholding Category", supplier.tax_withholding_category) - rate = [d.tax_withholding_rate for d in tds.rates if d.fiscal_year == filters.fiscal_year][0] + rate = [d.tax_withholding_rate for d in tds.rates if d.fiscal_year == filters.fiscal_year] + + if rate: + rate = rate[0] + try: account = [d.account for d in tds.accounts if d.company == filters.company][0] + except IndexError: account = [] total_invoiced_amount, tds_deducted = get_invoice_and_tds_amount(supplier.name, account, @@ -76,7 +81,7 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date): supplier_credit_amount = flt(sum([d.credit for d in entries])) vouchers = [d.voucher_no for d in entries] - vouchers += get_advance_vouchers(supplier, company=company, + vouchers += get_advance_vouchers([supplier], company=company, from_date=from_date, to_date=to_date) tds_deducted = 0 @@ -89,7 +94,7 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date): """.format(', '.join(["'%s'" % d for d in vouchers])), (account, from_date, to_date, company))[0][0]) - debit_note_amount = get_debit_note_amount(supplier, from_date, to_date, company=company) + debit_note_amount = get_debit_note_amount([supplier], from_date, to_date, company=company) total_invoiced_amount = supplier_credit_amount + tds_deducted - debit_note_amount diff --git a/erpnext/patches.txt b/erpnext/patches.txt index a73e8c19263..801d583ab15 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -661,6 +661,7 @@ erpnext.patches.v12_0.set_job_offer_applicant_email erpnext.patches.v12_0.create_irs_1099_field_united_states erpnext.patches.v12_0.move_bank_account_swift_number_to_bank erpnext.patches.v12_0.rename_bank_reconciliation_fields # 2020-01-22 +erpnext.patches.v12_0.add_permission_in_lower_deduction erpnext.patches.v12_0.set_received_qty_in_material_request_as_per_stock_uom erpnext.patches.v12_0.rename_account_type_doctype erpnext.patches.v12_0.recalculate_requested_qty_in_bin diff --git a/erpnext/patches/v12_0/add_permission_in_lower_deduction.py b/erpnext/patches/v12_0/add_permission_in_lower_deduction.py new file mode 100644 index 00000000000..af9bf74f30e --- /dev/null +++ b/erpnext/patches/v12_0/add_permission_in_lower_deduction.py @@ -0,0 +1,13 @@ +import frappe +from frappe.permissions import add_permission, update_permission_property + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + frappe.reload_doc('regional', 'doctype', 'Lower Deduction Certificate') + + add_permission('Lower Deduction Certificate', 'Accounts Manager', 0) + update_permission_property('Lower Deduction Certificate', 'Accounts Manager', 0, 'write', 1) + update_permission_property('Lower Deduction Certificate', 'Accounts Manager', 0, 'create', 1) \ No newline at end of file diff --git a/erpnext/regional/doctype/lower_deduction_certificate/__init__.py b/erpnext/regional/doctype/lower_deduction_certificate/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.js b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.js new file mode 100644 index 00000000000..8257bf8a969 --- /dev/null +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Lower Deduction Certificate', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json new file mode 100644 index 00000000000..f48fe6f4763 --- /dev/null +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json @@ -0,0 +1,138 @@ +{ + "actions": [], + "autoname": "field:certificate_no", + "creation": "2020-03-10 23:12:10.072631", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "certificate_details_section", + "section_code", + "fiscal_year", + "column_break_3", + "certificate_no", + "section_break_3", + "supplier", + "column_break_7", + "pan_no", + "validity_details_section", + "valid_from", + "column_break_10", + "valid_upto", + "section_break_9", + "rate", + "column_break_14", + "certificate_limit" + ], + "fields": [ + { + "fieldname": "certificate_no", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Certificate No", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "section_code", + "fieldtype": "Select", + "label": "Section Code", + "options": "192\n193\n194\n194A\n194C\n194D\n194H\n194I\n194J\n194LA\n194LBB\n194LBC\n195", + "reqd": 1 + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break", + "label": "Deductee Details" + }, + { + "fieldname": "supplier", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Supplier", + "options": "Supplier", + "reqd": 1 + }, + { + "fetch_from": "supplier.pan", + "fetch_if_empty": 1, + "fieldname": "pan_no", + "fieldtype": "Data", + "in_list_view": 1, + "label": "PAN No", + "reqd": 1 + }, + { + "fieldname": "validity_details_section", + "fieldtype": "Section Break", + "label": "Validity Details" + }, + { + "fieldname": "valid_upto", + "fieldtype": "Date", + "label": "Valid Upto", + "reqd": 1 + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break" + }, + { + "fieldname": "rate", + "fieldtype": "Percent", + "label": "Rate Of TDS As Per Certificate", + "reqd": 1 + }, + { + "fieldname": "certificate_limit", + "fieldtype": "Currency", + "label": "Certificate Limit", + "reqd": 1 + }, + { + "fieldname": "certificate_details_section", + "fieldtype": "Section Break", + "label": "Certificate Details" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "fieldname": "valid_from", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Valid From", + "reqd": 1 + }, + { + "fieldname": "fiscal_year", + "fieldtype": "Link", + "label": "Fiscal Year", + "options": "Fiscal Year", + "reqd": 1 + } + ], + "links": [], + "modified": "2020-04-23 23:04:41.203721", + "modified_by": "Administrator", + "module": "Regional", + "name": "Lower Deduction Certificate", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py new file mode 100644 index 00000000000..e8a8ed87505 --- /dev/null +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py @@ -0,0 +1,26 @@ +# -*- 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 import _ +from frappe.utils import getdate +from frappe.model.document import Document +from erpnext.accounts.utils import get_fiscal_year + +class LowerDeductionCertificate(Document): + def validate(self): + if getdate(self.valid_upto) < getdate(self.valid_from): + frappe.throw(_("Valid Upto date cannot be before Valid From date")) + + fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True) + + if not (fiscal_year.year_start_date <= getdate(self.valid_from) \ + <= fiscal_year.year_end_date): + frappe.throw(_("Valid From date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) + + if not (fiscal_year.year_start_date <= getdate(self.valid_upto) \ + <= fiscal_year.year_end_date): + frappe.throw(_("Valid Upto date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) + diff --git a/erpnext/regional/doctype/lower_deduction_certificate/test_lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/test_lower_deduction_certificate.py new file mode 100644 index 00000000000..7e950206fcc --- /dev/null +++ b/erpnext/regional/doctype/lower_deduction_certificate/test_lower_deduction_certificate.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLowerDeductionCertificate(unittest.TestCase): + pass diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index b4e3558af67..8593966cc3a 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -61,7 +61,7 @@ def add_custom_roles_for_reports(): )).insert() def add_permissions(): - for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report'): + for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'): add_permission(doctype, 'All', 0) for role in ('Accounts Manager', 'Accounts User', 'System Manager'): add_permission(doctype, role, 0) From ba70e7e8bce2ccfb0c31699a8d0dcb2505f1453b Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sun, 26 Apr 2020 20:17:48 +0530 Subject: [PATCH 11/58] Payroll based on attendance (#21258) * feat: Payroll based on attendance and leave * test: salary slip based 0n attendance * feat: Payroll based on attendance * fix: Codacy issues Co-authored-by: Anurag Mishra --- erpnext/hr/doctype/attendance/attendance.json | 7 +- erpnext/hr/doctype/attendance/attendance.py | 95 +++++++++------ .../hr/doctype/hr_settings/hr_settings.json | 20 +++- erpnext/hr/doctype/hr_settings/hr_settings.py | 3 + erpnext/hr/doctype/salary_slip/salary_slip.js | 15 ++- .../hr/doctype/salary_slip/salary_slip.json | 12 +- erpnext/hr/doctype/salary_slip/salary_slip.py | 111 ++++++++++++++---- .../doctype/salary_slip/test_salary_slip.py | 106 ++++++++++++++++- erpnext/patches.txt | 1 + .../v12_0/set_default_payroll_based_on.py | 6 + 10 files changed, 291 insertions(+), 85 deletions(-) create mode 100644 erpnext/patches/v12_0/set_default_payroll_based_on.py diff --git a/erpnext/hr/doctype/attendance/attendance.json b/erpnext/hr/doctype/attendance/attendance.json index eaca9f6ebe5..906f6f77f21 100644 --- a/erpnext/hr/doctype/attendance/attendance.json +++ b/erpnext/hr/doctype/attendance/attendance.json @@ -87,11 +87,12 @@ "search_index": 1 }, { - "depends_on": "eval:doc.status==\"On Leave\"", + "depends_on": "eval:in_list([\"On Leave\", \"Half Day\"], doc.status)", "fieldname": "leave_type", "fieldtype": "Link", "in_standard_filter": 1, "label": "Leave Type", + "mandatory_depends_on": "eval:in_list([\"On Leave\", \"Half Day\"], doc.status)", "oldfieldname": "leave_type", "oldfieldtype": "Link", "options": "Leave Type" @@ -100,6 +101,7 @@ "fieldname": "leave_application", "fieldtype": "Link", "label": "Leave Application", + "no_copy": 1, "options": "Leave Application", "read_only": 1 }, @@ -175,7 +177,8 @@ "icon": "fa fa-ok", "idx": 1, "is_submittable": 1, - "modified": "2020-02-19 14:25:32.945842", + "links": [], + "modified": "2020-04-11 11:40:14.319496", "modified_by": "Administrator", "module": "HR", "name": "Attendance", diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index 9e965dbc392..7355a56128a 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -7,33 +7,15 @@ import frappe from frappe.utils import getdate, nowdate from frappe import _ from frappe.model.document import Document -from frappe.utils import cstr, get_datetime, get_datetime_str -from frappe.utils import update_progress_bar +from frappe.utils import cstr, get_datetime, format_date class Attendance(Document): - def validate_duplicate_record(self): - res = frappe.db.sql("""select name from `tabAttendance` where employee = %s and attendance_date = %s - and name != %s and docstatus != 2""", - (self.employee, getdate(self.attendance_date), self.name)) - if res: - frappe.throw(_("Attendance for employee {0} is already marked").format(self.employee)) - - def check_leave_record(self): - leave_record = frappe.db.sql("""select leave_type, half_day, half_day_date from `tabLeave Application` - where employee = %s and %s between from_date and to_date and status = 'Approved' - and docstatus = 1""", (self.employee, self.attendance_date), as_dict=True) - if leave_record: - for d in leave_record: - if d.half_day_date == getdate(self.attendance_date): - self.status = 'Half Day' - frappe.msgprint(_("Employee {0} on Half day on {1}").format(self.employee, self.attendance_date)) - else: - self.status = 'On Leave' - self.leave_type = d.leave_type - frappe.msgprint(_("Employee {0} is on Leave on {1}").format(self.employee, self.attendance_date)) - - if self.status == "On Leave" and not leave_record: - frappe.throw(_("No leave record found for employee {0} for {1}").format(self.employee, self.attendance_date)) + def validate(self): + from erpnext.controllers.status_updater import validate_status + validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) + self.validate_attendance_date() + self.validate_duplicate_record() + self.check_leave_record() def validate_attendance_date(self): date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining") @@ -44,19 +26,52 @@ class Attendance(Document): elif date_of_joining and getdate(self.attendance_date) < getdate(date_of_joining): frappe.throw(_("Attendance date can not be less than employee's joining date")) + def validate_duplicate_record(self): + res = frappe.db.sql(""" + select name from `tabAttendance` + where employee = %s + and attendance_date = %s + and name != %s + and docstatus != 2 + """, (self.employee, getdate(self.attendance_date), self.name)) + if res: + frappe.throw(_("Attendance for employee {0} is already marked").format(self.employee)) + + def check_leave_record(self): + leave_record = frappe.db.sql(""" + select leave_type, half_day, half_day_date + from `tabLeave Application` + where employee = %s + and %s between from_date and to_date + and status = 'Approved' + and docstatus = 1 + """, (self.employee, self.attendance_date), as_dict=True) + if leave_record: + for d in leave_record: + self.leave_type = d.leave_type + if d.half_day_date == getdate(self.attendance_date): + self.status = 'Half Day' + frappe.msgprint(_("Employee {0} on Half day on {1}") + .format(self.employee, format_date(self.attendance_date))) + else: + self.status = 'On Leave' + frappe.msgprint(_("Employee {0} is on Leave on {1}") + .format(self.employee, format_date(self.attendance_date))) + + if self.status in ("On Leave", "Half Day"): + if not leave_record: + frappe.msgprint(_("No leave record found for employee {0} on {1}") + .format(self.employee, format_date(self.attendance_date)), alert=1) + elif self.leave_type: + self.leave_type = None + self.leave_application = None + def validate_employee(self): emp = frappe.db.sql("select name from `tabEmployee` where name = %s and status = 'Active'", self.employee) if not emp: frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee)) - def validate(self): - from erpnext.controllers.status_updater import validate_status - validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) - self.validate_attendance_date() - self.validate_duplicate_record() - self.check_leave_record() - @frappe.whitelist() def get_events(start, end, filters=None): events = [] @@ -90,18 +105,20 @@ def add_attendance(events, start, end, conditions=None): if e not in events: events.append(e) -def mark_attendance(employee, attendance_date, status, shift=None): - employee_doc = frappe.get_doc('Employee', employee) +def mark_attendance(employee, attendance_date, status, shift=None, leave_type=None, ignore_validate=False): if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date, 'docstatus':('!=', '2')}): - doc_dict = { + company = frappe.db.get_value('Employee', employee, 'company') + attendance = frappe.get_doc({ 'doctype': 'Attendance', 'employee': employee, 'attendance_date': attendance_date, 'status': status, - 'company': employee_doc.company, - 'shift': shift - } - attendance = frappe.get_doc(doc_dict).insert() + 'company': company, + 'shift': shift, + 'leave_type': leave_type + }) + attendance.flags.ignore_validate = ignore_validate + attendance.insert() attendance.submit() return attendance.name diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index 90f49886f84..9161ed822ac 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -13,10 +13,12 @@ "stop_birthday_reminders", "expense_approver_mandatory_in_expense_claim", "payroll_settings", + "payroll_based_on", + "max_working_hours_against_timesheet", "include_holidays_in_total_working_days", "disable_rounded_total", - "max_working_hours_against_timesheet", "column_break_11", + "daily_wages_fraction_for_half_day", "email_salary_slip_to_employee", "encrypt_salary_slips_in_emails", "password_policy", @@ -184,13 +186,27 @@ "fieldtype": "Link", "label": "Role Allowed to Create Backdated Leave Application", "options": "Role" + }, + { + "default": "Leave", + "fieldname": "payroll_based_on", + "fieldtype": "Select", + "label": "Calculate Working Days in Payroll based on", + "options": "Leave\nAttendance" + }, + { + "default": "0.5", + "description": "The fraction of daily wages to be paid for half-day attendance", + "fieldname": "daily_wages_fraction_for_half_day", + "fieldtype": "Float", + "label": "Daily Wages Fraction for Half Day" } ], "icon": "fa fa-cog", "idx": 1, "issingle": 1, "links": [], - "modified": "2020-01-06 18:46:30.189815", + "modified": "2020-04-13 21:20:59.382394", "modified_by": "Administrator", "module": "HR", "name": "HR Settings", diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.py b/erpnext/hr/doctype/hr_settings/hr_settings.py index bf919067cae..5ed4c87c62f 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.py +++ b/erpnext/hr/doctype/hr_settings/hr_settings.py @@ -15,6 +15,9 @@ class HRSettings(Document): self.set_naming_series() self.validate_password_policy() + if not self.daily_wages_fraction_for_half_day: + self.daily_wages_fraction_for_half_day = 0.5 + def set_naming_series(self): from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series set_by_naming_series("Employee", "employee_number", diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.js b/erpnext/hr/doctype/salary_slip/salary_slip.js index f430eeed4ee..1c4d4e34c56 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.js +++ b/erpnext/hr/doctype/salary_slip/salary_slip.js @@ -51,7 +51,7 @@ frappe.ui.form.on("Salary Slip", { }, end_date: function(frm) { - frm.events.get_emp_and_leave_details(frm); + frm.events.get_emp_and_working_day_details(frm); }, set_end_date: function(frm){ @@ -86,7 +86,7 @@ frappe.ui.form.on("Salary Slip", { salary_slip_based_on_timesheet: function(frm) { frm.trigger("toggle_fields"); - frm.events.get_emp_and_leave_details(frm); + frm.events.get_emp_and_working_day_details(frm); }, payroll_frequency: function(frm) { @@ -95,15 +95,14 @@ frappe.ui.form.on("Salary Slip", { }, employee: function(frm) { - frm.events.get_emp_and_leave_details(frm); + frm.events.get_emp_and_working_day_details(frm); }, leave_without_pay: function(frm){ if (frm.doc.employee && frm.doc.start_date && frm.doc.end_date) { return frappe.call({ - method: 'process_salary_based_on_leave', + method: 'process_salary_based_on_working_days', doc: frm.doc, - args: {"lwp": frm.doc.leave_without_pay}, callback: function(r, rt) { frm.refresh(); } @@ -115,12 +114,12 @@ frappe.ui.form.on("Salary Slip", { frm.toggle_display(['hourly_wages', 'timesheets'], cint(frm.doc.salary_slip_based_on_timesheet)===1); frm.toggle_display(['payment_days', 'total_working_days', 'leave_without_pay'], - frm.doc.payroll_frequency!=""); + frm.doc.payroll_frequency != ""); }, - get_emp_and_leave_details: function(frm) { + get_emp_and_working_day_details: function(frm) { return frappe.call({ - method: 'get_emp_and_leave_details', + method: 'get_emp_and_working_day_details', doc: frm.doc, callback: function(r, rt) { frm.refresh(); diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.json b/erpnext/hr/doctype/salary_slip/salary_slip.json index 097d3a096b0..54a8164587e 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.json +++ b/erpnext/hr/doctype/salary_slip/salary_slip.json @@ -11,20 +11,20 @@ "employee_name", "department", "designation", + "branch", "column_break1", - "company", + "status", "journal_entry", "payroll_entry", + "company", "letter_head", - "branch", - "status", "section_break_10", "salary_slip_based_on_timesheet", - "payroll_frequency", "start_date", "end_date", "column_break_15", "salary_structure", + "payroll_frequency", "total_working_days", "leave_without_pay", "payment_days", @@ -309,6 +309,7 @@ { "fieldname": "earning", "fieldtype": "Column Break", + "label": "Earning", "oldfieldtype": "Column Break", "width": "50%" }, @@ -323,6 +324,7 @@ { "fieldname": "deduction", "fieldtype": "Column Break", + "label": "Deduction", "oldfieldtype": "Column Break", "width": "50%" }, @@ -463,7 +465,7 @@ "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-04-09 20:02:53.159827", + "modified": "2020-04-14 20:02:53.159827", "modified_by": "Administrator", "module": "HR", "name": "Salary Slip", diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index 40fe572d753..916b64a83da 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe, erpnext import datetime, math -from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words +from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, format_date from frappe.model.naming import make_autoname from frappe import msgprint, _ @@ -44,9 +44,9 @@ class SalarySlip(TransactionBase): if not (len(self.get("earnings")) or len(self.get("deductions"))): # get details from salary structure - self.get_emp_and_leave_details() + self.get_emp_and_working_day_details() else: - self.get_leave_details(lwp = self.leave_without_pay) + self.get_working_days_details(lwp = self.leave_without_pay) self.calculate_net_pay() @@ -117,7 +117,7 @@ class SalarySlip(TransactionBase): self.start_date = date_details.start_date self.end_date = date_details.end_date - def get_emp_and_leave_details(self): + def get_emp_and_working_day_details(self): '''First time, load all the components from salary structure''' if self.employee: self.set("earnings", []) @@ -129,7 +129,8 @@ class SalarySlip(TransactionBase): joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, ["date_of_joining", "relieving_date"]) - self.get_leave_details(joining_date, relieving_date) + #getin leave details + self.get_working_days_details(joining_date, relieving_date) struct = self.check_sal_struct(joining_date, relieving_date) if struct: @@ -188,10 +189,9 @@ class SalarySlip(TransactionBase): make_salary_slip(self._salary_structure_doc.name, self) - def get_leave_details(self, joining_date=None, relieving_date=None, lwp=None, for_preview=0): - if not joining_date: - joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, - ["date_of_joining", "relieving_date"]) + def get_working_days_details(self, joining_date=None, relieving_date=None, lwp=None, for_preview=0): + payroll_based_on = frappe.db.get_value("HR Settings", None, "payroll_based_on") + include_holidays_in_total_working_days = frappe.db.get_single_value("HR Settings", "include_holidays_in_total_working_days") working_days = date_diff(self.end_date, self.start_date) + 1 if for_preview: @@ -200,24 +200,42 @@ class SalarySlip(TransactionBase): return holidays = self.get_holidays_for_employee(self.start_date, self.end_date) - actual_lwp = self.calculate_lwp(holidays, working_days) - if not cint(frappe.db.get_value("HR Settings", None, "include_holidays_in_total_working_days")): + + if not cint(include_holidays_in_total_working_days): working_days -= len(holidays) if working_days < 0: frappe.throw(_("There are more holidays than working days this month.")) + if not payroll_based_on: + frappe.throw(_("Please set Payroll based on in HR settings")) + + if payroll_based_on == "Attendance": + actual_lwp = self.calculate_lwp_based_on_attendance(holidays) + else: + actual_lwp = self.calculate_lwp_based_on_leave_application(holidays, working_days) + if not lwp: lwp = actual_lwp elif lwp != actual_lwp: - frappe.msgprint(_("Leave Without Pay does not match with approved Leave Application records")) + frappe.msgprint(_("Leave Without Pay does not match with approved {} records") + .format(payroll_based_on)) - self.total_working_days = working_days self.leave_without_pay = lwp + self.total_working_days = working_days - payment_days = flt(self.get_payment_days(joining_date, relieving_date)) - flt(lwp) - self.payment_days = payment_days > 0 and payment_days or 0 + payment_days = self.get_payment_days(joining_date, + relieving_date, include_holidays_in_total_working_days) + + if flt(payment_days) > flt(lwp): + self.payment_days = flt(payment_days) - flt(lwp) + else: + self.payment_days = 0 + + def get_payment_days(self, joining_date, relieving_date, include_holidays_in_total_working_days): + if not joining_date: + joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, + ["date_of_joining", "relieving_date"]) - def get_payment_days(self, joining_date, relieving_date): start_date = getdate(self.start_date) if joining_date: if getdate(self.start_date) <= joining_date <= getdate(self.end_date): @@ -235,9 +253,10 @@ class SalarySlip(TransactionBase): payment_days = date_diff(end_date, start_date) + 1 - if not cint(frappe.db.get_value("HR Settings", None, "include_holidays_in_total_working_days")): + if not cint(include_holidays_in_total_working_days): holidays = self.get_holidays_for_employee(start_date, end_date) payment_days -= len(holidays) + return payment_days def get_holidays_for_employee(self, start_date, end_date): @@ -256,27 +275,67 @@ class SalarySlip(TransactionBase): return holidays - def calculate_lwp(self, holidays, working_days): + def calculate_lwp_based_on_leave_application(self, holidays, working_days): lwp = 0 holidays = "','".join(holidays) + daily_wages_fraction_for_half_day = \ + flt(frappe.db.get_value("HR Settings", None, "daily_wages_fraction_for_half_day")) or 0.5 + for d in range(working_days): dt = add_days(cstr(getdate(self.start_date)), d) leave = frappe.db.sql(""" SELECT t1.name, - CASE WHEN t1.half_day_date = %(dt)s or t1.to_date = t1.from_date + CASE WHEN (t1.half_day_date = %(dt)s or t1.to_date = t1.from_date) THEN t1.half_day else 0 END FROM `tabLeave Application` t1, `tabLeave Type` t2 WHERE t2.name = t1.leave_type AND t2.is_lwp = 1 AND t1.docstatus = 1 AND t1.employee = %(employee)s - AND CASE WHEN t2.include_holiday != 1 THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date and ifnull(t1.salary_slip, '') = '' - WHEN t2.include_holiday THEN %(dt)s between from_date and to_date and ifnull(t1.salary_slip, '') = '' - END + AND ifnull(t1.salary_slip, '') = '' + AND CASE + WHEN t2.include_holiday != 1 + THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date + WHEN t2.include_holiday + THEN %(dt)s between from_date and to_date + END """.format(holidays), {"employee": self.employee, "dt": dt}) if leave: - lwp = cint(leave[0][1]) and (lwp + 0.5) or (lwp + 1) + is_half_day_leave = cint(leave[0][1]) + lwp += (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1 + + return lwp + + def calculate_lwp_based_on_attendance(self, holidays): + lwp = 0 + + daily_wages_fraction_for_half_day = \ + flt(frappe.db.get_value("HR Settings", None, "daily_wages_fraction_for_half_day")) or 0.5 + + lwp_leave_types = dict(frappe.get_all("Leave Type", {"is_lwp": 1}, ["name", "include_holiday"], as_list=1)) + + attendances = frappe.db.sql(''' + SELECT attendance_date, status, leave_type + FROM `tabAttendance` + WHERE + status in ("Absent", "Half Day", "On leave") + AND employee = %s + AND docstatus = 1 + AND attendance_date between %s and %s + ''', values=(self.employee, self.start_date, self.end_date), as_dict=1) + + for d in attendances: + if d.status in ('Half Day', 'On Leave') and d.leave_type and d.leave_type not in lwp_leave_types: + continue + + if format_date(d.attendance_date, "yyyy-mm-dd") in holidays: + if d.status == "Absent" or \ + (d.leave_type and d.leave_type in lwp_leave_types and not lwp_leave_types[d.leave_type]): + continue + + lwp += (1 - daily_wages_fraction_for_half_day) if d.status == "Half Day" else 1 + return lwp def add_earning_for_hourly_wages(self, doc, salary_component, amount): @@ -945,7 +1004,7 @@ class SalarySlip(TransactionBase): if not self.salary_slip_based_on_timesheet: self.get_date_details() self.pull_emp_details() - self.get_leave_details(for_preview=for_preview) + self.get_working_days_details(for_preview=for_preview) self.calculate_net_pay() def pull_emp_details(self): @@ -954,8 +1013,8 @@ class SalarySlip(TransactionBase): self.bank_name = emp.bank_name self.bank_account_no = emp.bank_ac_no - def process_salary_based_on_leave(self, lwp=0): - self.get_leave_details(lwp=lwp) + def process_salary_based_on_working_days(self): + self.get_working_days_details(lwp=self.leave_without_pay) self.calculate_net_pay() def unlink_ref_doc_from_salary_slip(ref_no): diff --git a/erpnext/hr/doctype/salary_slip/test_salary_slip.py b/erpnext/hr/doctype/salary_slip/test_salary_slip.py index 73bb19e9eea..fc687a355c0 100644 --- a/erpnext/hr/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/test_salary_slip.py @@ -21,18 +21,105 @@ class TestSalarySlip(unittest.TestCase): make_earning_salary_component(setup=True, company_list=["_Test Company"]) make_deduction_salary_component(setup=True, company_list=["_Test Company"]) - for dt in ["Leave Application", "Leave Allocation", "Salary Slip"]: + for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Attendance"]: frappe.db.sql("delete from `tab%s`" % dt) self.make_holiday_list() frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List") frappe.db.set_value("HR Settings", None, "email_salary_slip_to_employee", 0) - + frappe.db.set_value('HR Settings', None, 'leave_status_notification_template', None) + frappe.db.set_value('HR Settings', None, 'leave_approval_notification_template', None) + def tearDown(self): frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 0) frappe.set_user("Administrator") + def test_payment_days_based_on_attendance(self): + from erpnext.hr.doctype.attendance.attendance import mark_attendance + no_of_days = self.get_no_of_days() + + # Payroll based on attendance + frappe.db.set_value("HR Settings", None, "payroll_based_on", "Attendance") + frappe.db.set_value("HR Settings", None, "daily_wages_fraction_for_half_day", 0.75) + + emp_id = make_employee("test_for_attendance@salary.com") + frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) + + frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0) + + month_start_date = get_first_day(nowdate()) + month_end_date = get_last_day(nowdate()) + + first_sunday = frappe.db.sql(""" + select holiday_date from `tabHoliday` + where parent = 'Salary Slip Test Holiday List' + and holiday_date between %s and %s + order by holiday_date + """, (month_start_date, month_end_date))[0][0] + + mark_attendance(emp_id, first_sunday, 'Absent', ignore_validate=True) # invalid lwp + mark_attendance(emp_id, add_days(first_sunday, 1), 'Absent', ignore_validate=True) # valid lwp + mark_attendance(emp_id, add_days(first_sunday, 2), 'Half Day', leave_type='Leave Without Pay', ignore_validate=True) # valid 0.75 lwp + mark_attendance(emp_id, add_days(first_sunday, 3), 'On Leave', leave_type='Leave Without Pay', ignore_validate=True) # valid lwp + mark_attendance(emp_id, add_days(first_sunday, 4), 'On Leave', leave_type='Casual Leave', ignore_validate=True) # invalid lwp + mark_attendance(emp_id, add_days(first_sunday, 7), 'On Leave', leave_type='Leave Without Pay', ignore_validate=True) # invalid lwp + + ss = make_employee_salary_slip("test_for_attendance@salary.com", "Monthly") + + self.assertEqual(ss.leave_without_pay, 2.25) + + days_in_month = no_of_days[0] + no_of_holidays = no_of_days[1] + + self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 2.25) + + #Gross pay calculation based on attendances + gross_pay = 78000 - ((78000 / (days_in_month - no_of_holidays)) * flt(ss.leave_without_pay)) + + self.assertEqual(ss.gross_pay, gross_pay) + + frappe.db.set_value("HR Settings", None, "payroll_based_on", "Leave") + + def test_payment_days_based_on_leave_application(self): + no_of_days = self.get_no_of_days() + + # Payroll based on attendance + frappe.db.set_value("HR Settings", None, "payroll_based_on", "Leave") + + emp_id = make_employee("test_for_attendance@salary.com") + frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) + + frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0) + + month_start_date = get_first_day(nowdate()) + month_end_date = get_last_day(nowdate()) + + first_sunday = frappe.db.sql(""" + select holiday_date from `tabHoliday` + where parent = 'Salary Slip Test Holiday List' + and holiday_date between %s and %s + order by holiday_date + """, (month_start_date, month_end_date))[0][0] + + make_leave_application(emp_id, first_sunday, add_days(first_sunday, 3), "Leave Without Pay") + + ss = make_employee_salary_slip("test_for_attendance@salary.com", "Monthly") + + self.assertEqual(ss.leave_without_pay, 3) + + days_in_month = no_of_days[0] + no_of_holidays = no_of_days[1] + + self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 3) + + #Gross pay calculation based on attendances + gross_pay = 78000 - ((78000 / (days_in_month - no_of_holidays)) * flt(ss.leave_without_pay)) + + self.assertEqual(ss.gross_pay, gross_pay) + + frappe.db.set_value("HR Settings", None, "payroll_based_on", "Leave") + def test_salary_slip_with_holidays_included(self): no_of_days = self.get_no_of_days() frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 1) @@ -315,7 +402,6 @@ class TestSalarySlip(unittest.TestCase): return [no_of_days_in_month[1], no_of_holidays_in_month] - def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): from erpnext.hr.doctype.salary_structure.test_salary_structure import make_salary_structure if not salary_structure: @@ -603,3 +689,17 @@ def create_additional_salary(employee, payroll_period, amount): "type": "Earning" }).submit() return salary_date + +def make_leave_application(employee, from_date, to_date, leave_type, company=None): + leave_application = frappe.get_doc(dict( + doctype = 'Leave Application', + employee = employee, + leave_type = leave_type, + from_date = from_date, + to_date = to_date, + company = company or erpnext.get_default_company() or "_Test Company", + docstatus = 1, + status = "Approved", + leave_approver = 'test@example.com' + )) + leave_application.submit() \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 801d583ab15..0ea83fd7bc8 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -669,6 +669,7 @@ erpnext.patches.v12_0.update_healthcare_refactored_changes erpnext.patches.v12_0.set_total_batch_quantity erpnext.patches.v12_0.rename_mws_settings_fields erpnext.patches.v12_0.set_updated_purpose_in_pick_list +erpnext.patches.v12_0.set_default_payroll_based_on erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123 diff --git a/erpnext/patches/v12_0/set_default_payroll_based_on.py b/erpnext/patches/v12_0/set_default_payroll_based_on.py new file mode 100644 index 00000000000..04b54a6cf61 --- /dev/null +++ b/erpnext/patches/v12_0/set_default_payroll_based_on.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc("hr", "doctype", "hr_settings") + frappe.db.set_value("HR Settings", None, "payroll_based_on", "Leave") \ No newline at end of file From ded418e31d859c4aa940b8d3dec5c1ded9f3a4b6 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Sun, 26 Apr 2020 21:04:58 +0530 Subject: [PATCH 12/58] feat: employee leave balance reports (#20754) * feat: Employee leave balance summary report new design * feat: Employee leave balance report new design * fix: leave based on multiple holiday list Co-authored-by: Nabin Hait --- .../leave_application/leave_application.py | 20 +- .../leave_ledger_entry.json | 11 +- .../employee_leave_balance.py | 213 +++++++++++++----- .../employee_leave_balance_summary.js | 13 +- .../employee_leave_balance_summary.py | 159 +++++-------- 5 files changed, 234 insertions(+), 182 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index c4417515253..47b1bb7684e 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -374,7 +374,8 @@ class LeaveApplication(Document): leaves=self.total_leave_days * -1, from_date=self.from_date, to_date=self.to_date, - is_lwp=lwp + is_lwp=lwp, + holiday_list=get_holiday_list_for_employee(self.employee) ) create_leave_ledger_entry(self, args, submit) @@ -384,7 +385,9 @@ class LeaveApplication(Document): from_date=self.from_date, to_date=expiry_date, leaves=(date_diff(expiry_date, self.from_date) + 1) * -1, - is_lwp=lwp + is_lwp=lwp, + holiday_list=get_holiday_list_for_employee(self.employee), + ) create_leave_ledger_entry(self, args, submit) @@ -410,7 +413,7 @@ def get_allocation_expiry(employee, leave_type, to_date, from_date): return expiry[0]['to_date'] if expiry else None @frappe.whitelist() -def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day = None, half_day_date = None): +def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day = None, half_day_date = None, holiday_list = None): number_of_days = 0 if cint(half_day) == 1: if from_date == to_date: @@ -424,7 +427,7 @@ def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day number_of_days = date_diff(to_date, from_date) + 1 if not frappe.db.get_value("Leave Type", leave_type, "include_holiday"): - number_of_days = flt(number_of_days) - flt(get_holidays(employee, from_date, to_date)) + number_of_days = flt(number_of_days) - flt(get_holidays(employee, from_date, to_date, holiday_list=holiday_list)) return number_of_days @frappe.whitelist() @@ -575,7 +578,7 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date): {'name': leave_entry.transaction_name}, ['half_day_date']) leave_days += get_number_of_leave_days(employee, leave_type, - leave_entry.from_date, leave_entry.to_date, half_day, half_day_date) * -1 + leave_entry.from_date, leave_entry.to_date, half_day, half_day_date, holiday_list=leave_entry.holiday_list) * -1 return leave_days @@ -589,7 +592,7 @@ def get_leave_entries(employee, leave_type, from_date, to_date): ''' Returns leave entries between from_date and to_date. ''' return frappe.db.sql(""" SELECT - employee, leave_type, from_date, to_date, leaves, transaction_name, transaction_type, + employee, leave_type, from_date, to_date, leaves, transaction_name, transaction_type, holiday_list, is_carry_forward, is_expired FROM `tabLeave Ledger Entry` WHERE employee=%(employee)s AND leave_type=%(leave_type)s @@ -607,9 +610,10 @@ def get_leave_entries(employee, leave_type, from_date, to_date): }, as_dict=1) @frappe.whitelist() -def get_holidays(employee, from_date, to_date): +def get_holidays(employee, from_date, to_date, holiday_list = None): '''get holidays between two dates for the given employee''' - holiday_list = get_holiday_list_for_employee(employee) + if not holiday_list: + holiday_list = get_holiday_list_for_employee(employee) 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 diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json index 771e706bbb3..a5ac3f3d471 100644 --- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-05-09 15:47:39.760406", "doctype": "DocType", "engine": "InnoDB", @@ -12,6 +13,7 @@ "column_break_7", "from_date", "to_date", + "holiday_list", "is_carry_forward", "is_expired", "is_lwp", @@ -98,11 +100,18 @@ "fieldname": "is_lwp", "fieldtype": "Check", "label": "Is Leave Without Pay" + }, + { + "fieldname": "holiday_list", + "fieldtype": "Link", + "label": "Holiday List", + "options": "Holiday List" } ], "in_create": 1, "is_submittable": 1, - "modified": "2019-08-20 14:40:04.130799", + "links": [], + "modified": "2020-02-27 14:40:10.502605", "modified_by": "Administrator", "module": "HR", "name": "Leave Ledger Entry", diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index 35c8630e8e2..97be5cd813c 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -1,85 +1,186 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt from __future__ import unicode_literals import frappe -from frappe import _ from frappe.utils import flt -from erpnext.hr.doctype.leave_application.leave_application \ - import get_leave_balance_on, get_leaves_for_period - -from erpnext.hr.report.employee_leave_balance_summary.employee_leave_balance_summary \ - import get_department_leave_approver_map +from frappe import _ +from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period, get_leave_balance_on, get_leave_allocation_records def execute(filters=None): - leave_types = frappe.db.sql_list("select name from `tabLeave Type` order by name asc") + if filters.to_date <= filters.from_date: + frappe.throw(_('From date can not be greater than than To date')) - columns = get_columns(leave_types) - data = get_data(filters, leave_types) + columns = get_columns() + data = get_data(filters) return columns, data -def get_columns(leave_types): - columns = [ - _("Employee") + ":Link.Employee:150", - _("Employee Name") + "::200", - _("Department") +"::150" - ] - - for leave_type in leave_types: - columns.append(_(leave_type) + " " + _("Opening") + ":Float:160") - columns.append(_(leave_type) + " " + _("Taken") + ":Float:160") - columns.append(_(leave_type) + " " + _("Balance") + ":Float:160") +def get_columns(): + columns = [{ + 'label': _('Leave Type'), + 'fieldtype': 'Link', + 'fieldname': 'leave_type', + 'width': 200, + 'options': 'Leave Type' + }, { + 'label': _('Employee'), + 'fieldtype': 'Link', + 'fieldname': 'employee', + 'width': 100, + 'options': 'Employee' + }, { + 'label': _('Employee Name'), + 'fieldtype': 'Data', + 'fieldname': 'employee_name', + 'width': 100, + }, { + 'label': _('Opening Balance'), + 'fieldtype': 'float', + 'fieldname': 'opening_balance', + 'width': 130, + }, { + 'label': _('Leaves Allocated'), + 'fieldtype': 'float', + 'fieldname': 'leaves_allocated', + 'width': 130, + }, { + 'label': _('Leaves Taken'), + 'fieldtype': 'float', + 'fieldname': 'leaves_taken', + 'width': 130, + }, { + 'label': _('Leaves Expired'), + 'fieldtype': 'float', + 'fieldname': 'leaves_expired', + 'width': 130, + }, { + 'label': _('Closing Balance'), + 'fieldtype': 'float', + 'fieldname': 'closing_balance', + 'width': 130, + }] return columns -def get_conditions(filters): - conditions = { - "status": "Active", - "company": filters.company, - } - if filters.get("department"): - conditions.update({"department": filters.get("department")}) - if filters.get("employee"): - conditions.update({"employee": filters.get("employee")}) +def get_data(filters): + leave_types = frappe.db.sql_list("SELECT `name` FROM `tabLeave Type` ORDER BY `name` ASC") - return conditions - -def get_data(filters, leave_types): - user = frappe.session.user conditions = get_conditions(filters) - if filters.to_date <= filters.from_date: - frappe.throw(_("From date can not be greater than than To date")) - - active_employees = frappe.get_all("Employee", - filters=conditions, - fields=["name", "employee_name", "department", "user_id", "leave_approver"]) - + user = frappe.session.user department_approver_map = get_department_leave_approver_map(filters.get('department')) + active_employees = frappe.get_list('Employee', + filters=conditions, + fields=['name', 'employee_name', 'department', 'user_id', 'leave_approver']) + data = [] - for employee in active_employees: - leave_approvers = department_approver_map.get(employee.department_name, []) - if employee.leave_approver: - leave_approvers.append(employee.leave_approver) - if (len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) or ("HR Manager" in frappe.get_roles(user)): - row = [employee.name, employee.employee_name, employee.department] + for leave_type in leave_types: + if len(active_employees) > 1: + data.append({ + 'leave_type': leave_type + }) + else: + row = frappe._dict({ + 'leave_type': leave_type + }) + + for employee in active_employees: + + leave_approvers = department_approver_map.get(employee.department_name, []).append(employee.leave_approver) + + if (leave_approvers and len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) \ + or ("HR Manager" in frappe.get_roles(user)): + if len(active_employees) > 1: + row = frappe._dict() + row.employee = employee.name, + row.employee_name = employee.employee_name - for leave_type in leave_types: - # leaves taken leaves_taken = get_leaves_for_period(employee.name, leave_type, filters.from_date, filters.to_date) * -1 - # opening balance + new_allocation, expired_leaves = get_allocated_and_expired_leaves(filters.from_date, filters.to_date, employee.name, leave_type) + + opening = get_leave_balance_on(employee.name, leave_type, filters.from_date) + closing = get_leave_balance_on(employee.name, leave_type, filters.to_date) - # closing balance - closing = max(opening - leaves_taken, 0) + row.leaves_allocated = new_allocation + row.leaves_expired = expired_leaves - leaves_taken if expired_leaves - leaves_taken > 0 else 0 + row.opening_balance = opening + row.leaves_taken = leaves_taken + row.closing_balance = closing + row.indent = 1 + data.append(row) + new_leaves_allocated = 0 - row += [opening, leaves_taken, closing] - data.append(row) + return data - return data \ No newline at end of file +def get_conditions(filters): + conditions={ + 'status': 'Active', + } + if filters.get('employee'): + conditions['name'] = filters.get('employee') + + if filters.get('employee'): + conditions['name'] = filters.get('employee') + + return conditions + +def get_department_leave_approver_map(department=None): + conditions='' + if department: + conditions="and (department_name = '%(department)s' or parent_department = '%(department)s')"%{'department': department} + + # get current department and all its child + department_list = frappe.db.sql_list(""" SELECT name FROM `tabDepartment` WHERE disabled=0 {0}""".format(conditions)) #nosec + + # retrieve approvers list from current department and from its subsequent child departments + approver_list = frappe.get_all('Department Approver', filters={ + 'parentfield': 'leave_approvers', + 'parent': ('in', department_list) + }, fields=['parent', 'approver'], as_list=1) + + approvers = {} + + for k, v in approver_list: + approvers.setdefault(k, []).append(v) + + return approvers + +def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type): + + from frappe.utils import getdate + + new_allocation = 0 + expired_leaves = 0 + + records= frappe.db.sql(""" + SELECT + employee, leave_type, from_date, to_date, leaves, transaction_name, + is_carry_forward, is_expired + FROM `tabLeave Ledger Entry` + WHERE employee=%(employee)s AND leave_type=%(leave_type)s + AND docstatus=1 AND leaves>0 + AND (from_date between %(from_date)s AND %(to_date)s + OR to_date between %(from_date)s AND %(to_date)s + OR (from_date < %(from_date)s AND to_date > %(to_date)s)) + """, { + "from_date": from_date, + "to_date": to_date, + "employee": employee, + "leave_type": leave_type + }, as_dict=1) + + for record in records: + if record.to_date <= getdate(to_date): + expired_leaves += record.leaves + + if record.from_date >= getdate(from_date): + new_allocation += record.leaves + + return new_allocation, expired_leaves diff --git a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.js b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.js index 3fb8f6e9c1a..cb05d1138f8 100644 --- a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.js +++ b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.js @@ -5,18 +5,11 @@ frappe.query_reports['Employee Leave Balance Summary'] = { filters: [ { - fieldname:'from_date', - label: __('From Date'), + fieldname:'date', + label: __('Date'), fieldtype: 'Date', reqd: 1, - default: frappe.defaults.get_default('year_start_date') - }, - { - fieldname:'to_date', - label: __('To Date'), - fieldtype: 'Date', - reqd: 1, - default: frappe.defaults.get_default('year_end_date') + default: frappe.datetime.now_date() }, { fieldname:'company', diff --git a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py index 777de022387..a5cdecf36a7 100644 --- a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py +++ b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py @@ -1,130 +1,75 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt from __future__ import unicode_literals import frappe -from frappe.utils import flt from frappe import _ -from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period, get_leave_balance_on +from frappe.utils import flt +from erpnext.hr.doctype.leave_application.leave_application \ + import get_leave_balance_on, get_leaves_for_period + +from erpnext.hr.report.employee_leave_balance.employee_leave_balance \ + import get_department_leave_approver_map def execute(filters=None): - if filters.to_date <= filters.from_date: - frappe.throw(_('From date can not be greater than than To date')) + leave_types = frappe.db.sql_list("select name from `tabLeave Type` order by name asc") - columns = get_columns() - data = get_data(filters) + columns = get_columns(leave_types) + data = get_data(filters, leave_types) return columns, data -def get_columns(): - columns = [{ - 'label': _('Leave Type'), - 'fieldtype': 'Link', - 'fieldname': 'leave_type', - 'width': 300, - 'options': 'Leave Type' - }, { - 'label': _('Employee'), - 'fieldtype': 'Link', - 'fieldname': 'employee', - 'width': 100, - 'options': 'Employee' - }, { - 'label': _('Employee Name'), - 'fieldtype': 'Data', - 'fieldname': 'employee_name', - 'width': 100, - }, { - 'label': _('Opening Balance'), - 'fieldtype': 'float', - 'fieldname': 'opening_balance', - 'width': 160, - }, { - 'label': _('Leaves Taken'), - 'fieldtype': 'float', - 'fieldname': 'leaves_taken', - 'width': 160, - }, { - 'label': _('Closing Balance'), - 'fieldtype': 'float', - 'fieldname': 'closing_balance', - 'width': 160, - }] +def get_columns(leave_types): + columns = [ + _("Employee") + ":Link.Employee:150", + _("Employee Name") + "::200", + _("Department") +"::150" + ] + + for leave_type in leave_types: + columns.append(_(leave_type) + ":Float:160") return columns -def get_data(filters): - leave_types = frappe.db.sql_list("SELECT `name` FROM `tabLeave Type` ORDER BY `name` ASC") - - conditions = get_conditions(filters) - - user = frappe.session.user - department_approver_map = get_department_leave_approver_map(filters.get('department')) - - active_employees = frappe.get_list('Employee', - filters=conditions, - fields=['name', 'employee_name', 'department', 'user_id', 'leave_approver']) - - data = [] - - for leave_type in leave_types: - data.append({ - 'leave_type': leave_type - }) - for employee in active_employees: - - leave_approvers = department_approver_map.get(employee.department_name, []).append(employee.leave_approver) - - if (leave_approvers and len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) \ - or ("HR Manager" in frappe.get_roles(user)): - row = frappe._dict({ - 'employee': employee.name, - 'employee_name': employee.employee_name - }) - - leaves_taken = get_leaves_for_period(employee.name, leave_type, - filters.from_date, filters.to_date) * -1 - - opening = get_leave_balance_on(employee.name, leave_type, filters.from_date) - closing = get_leave_balance_on(employee.name, leave_type, filters.to_date) - - row.opening_balance = opening - row.leaves_taken = leaves_taken - row.closing_balance = closing - row.indent = 1 - data.append(row) - - return data - def get_conditions(filters): - conditions={ - 'status': 'Active', + conditions = { + "status": "Active", + "company": filters.company, } - if filters.get('employee'): - conditions['name'] = filters.get('employee') - - if filters.get('employee'): - conditions['name'] = filters.get('employee') + if filters.get("department"): + conditions.update({"department": filters.get("department")}) + if filters.get("employee"): + conditions.update({"employee": filters.get("employee")}) return conditions -def get_department_leave_approver_map(department=None): - conditions='' - if department: - conditions="and (department_name = '%(department)s' or parent_department = '%(department)s')"%{'department': department} +def get_data(filters, leave_types): + user = frappe.session.user + conditions = get_conditions(filters) - # get current department and all its child - department_list = frappe.db.sql_list(""" SELECT name FROM `tabDepartment` WHERE disabled=0 {0}""".format(conditions)) #nosec + active_employees = frappe.get_all("Employee", + filters=conditions, + fields=["name", "employee_name", "department", "user_id", "leave_approver"]) - # retrieve approvers list from current department and from its subsequent child departments - approver_list = frappe.get_all('Department Approver', filters={ - 'parentfield': 'leave_approvers', - 'parent': ('in', department_list) - }, fields=['parent', 'approver'], as_list=1) + department_approver_map = get_department_leave_approver_map(filters.get('department')) - approvers = {} + data = [] + for employee in active_employees: + leave_approvers = department_approver_map.get(employee.department_name, []) + if employee.leave_approver: + leave_approvers.append(employee.leave_approver) - for k, v in approver_list: - approvers.setdefault(k, []).append(v) + if (len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) or ("HR Manager" in frappe.get_roles(user)): + row = [employee.name, employee.employee_name, employee.department] - return approvers + for leave_type in leave_types: + + # opening balance + opening = get_leave_balance_on(employee.name, leave_type, filters.date) + + + row += [opening] + + data.append(row) + + return data \ No newline at end of file From 3e3a793567813047aece67753019f09cff56c349 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Sun, 26 Apr 2020 23:25:40 +0530 Subject: [PATCH 13/58] fix: Set barcode field empty only if it has value (#21425) - To avoid unnecessary form dirty trigger --- erpnext/public/js/controllers/transaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 42964474b0a..5843034543b 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -288,7 +288,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ this.setup_sms(); this.setup_quality_inspection(); let scan_barcode_field = this.frm.get_field('scan_barcode'); - if (scan_barcode_field) { + if (scan_barcode_field && scan_barcode_field.get_value()) { scan_barcode_field.set_value(""); scan_barcode_field.set_new_description(""); From c9a1aa8eaa91880c9e6f77354eeb3c9761c16b2d Mon Sep 17 00:00:00 2001 From: Himanshu Date: Sun, 26 Apr 2020 23:28:33 +0530 Subject: [PATCH 14/58] fix: add dashboard to quality inspection template (#21423) --- .../quality_inspection_template.json | 250 ++++++------------ 1 file changed, 80 insertions(+), 170 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.json b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.json index eab08e2216f..9646f2d8e8f 100644 --- a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.json +++ b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.json @@ -1,186 +1,96 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:quality_inspection_template_name", - "beta": 0, - "creation": "2018-01-24 16:23:41.691127", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:quality_inspection_template_name", + "creation": "2018-01-24 16:23:41.691127", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "quality_inspection_template_name", + "item_quality_inspection_parameter" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "quality_inspection_template_name", - "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": "Quality Inspection Template Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "quality_inspection_template_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Quality Inspection Template Name", + "reqd": 1, + "unique": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_quality_inspection_parameter", - "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": "Item Quality Inspection Parameter", - "length": 0, - "no_copy": 0, - "options": "Item Quality Inspection Parameter", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "item_quality_inspection_parameter", + "fieldtype": "Table", + "label": "Item Quality Inspection Parameter", + "options": "Item Quality Inspection Parameter", + "reqd": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-02-21 12:05:29.304432", - "modified_by": "Administrator", - "module": "Stock", - "name": "Quality Inspection Template", - "name_case": "", - "owner": "Administrator", + ], + "links": [ + { + "group": "Quality Inspection", + "link_doctype": "Quality Inspection", + "link_fieldname": "quality_inspection_template" + } + ], + "modified": "2020-04-26 20:13:02.810132", + "modified_by": "Administrator", + "module": "Stock", + "name": "Quality Inspection Template", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Stock User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Quality Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Quality Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Manufacturing User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file From 2305d00e0ba78195d7f8d5f29a6e96c6ce6d97dd Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sun, 26 Apr 2020 23:29:13 +0530 Subject: [PATCH 15/58] fix: Procurement tracker report (#21421) * fix: procurement report data was not coming * fix: leave allocation minor issue --- .../procurement_tracker.py | 74 +++++++++---------- .../leave_allocation/leave_allocation.py | 6 +- 2 files changed, 39 insertions(+), 41 deletions(-) diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py index 866bf0c7332..39668795cba 100644 --- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py @@ -141,19 +141,18 @@ def get_conditions(filters): conditions = "" if filters.get("company"): - conditions += " AND company=%s"% frappe.db.escape(filters.get('company')) + conditions += " AND par.company=%s" % frappe.db.escape(filters.get('company')) if filters.get("cost_center") or filters.get("project"): conditions += """ - AND (cost_center=%s - OR project=%s) - """% (frappe.db.escape(filters.get('cost_center')), frappe.db.escape(filters.get('project'))) + AND (child.`cost_center`=%s OR child.`project`=%s) + """ % (frappe.db.escape(filters.get('cost_center')), frappe.db.escape(filters.get('project'))) if filters.get("from_date"): - conditions += " AND transaction_date>=%s"% filters.get('from_date') + conditions += " AND par.transaction_date>='%s'" % filters.get('from_date') if filters.get("to_date"): - conditions += " AND transaction_date<=%s"% filters.get('to_date') + conditions += " AND par.transaction_date<='%s'" % filters.get('to_date') return conditions def get_data(filters): @@ -162,7 +161,6 @@ def get_data(filters): mr_records, procurement_record_against_mr = get_mapped_mr_details(conditions) pr_records = get_mapped_pr_records() pi_records = get_mapped_pi_records() - print(pi_records) procurement_record=[] if procurement_record_against_mr: @@ -198,16 +196,16 @@ def get_mapped_mr_details(conditions): mr_records = {} mr_details = frappe.db.sql(""" SELECT - mr.transaction_date, - mr.per_ordered, - mr_item.name, - mr_item.parent, - mr_item.amount - FROM `tabMaterial Request` mr, `tabMaterial Request Item` mr_item + par.transaction_date, + par.per_ordered, + child.name, + child.parent, + child.amount + FROM `tabMaterial Request` par, `tabMaterial Request Item` child WHERE - mr.per_ordered>=0 - AND mr.name=mr_item.parent - AND mr.docstatus=1 + par.per_ordered>=0 + AND par.name=child.parent + AND par.docstatus=1 {conditions} """.format(conditions=conditions), as_dict=1) #nosec @@ -254,29 +252,29 @@ def get_mapped_pr_records(): def get_po_entries(conditions): return frappe.db.sql(""" SELECT - po_item.name, - po_item.parent, - po_item.cost_center, - po_item.project, - po_item.warehouse, - po_item.material_request, - po_item.material_request_item, - po_item.description, - po_item.stock_uom, - po_item.qty, - po_item.amount, - po_item.base_amount, - po_item.schedule_date, - po.transaction_date, - po.supplier, - po.status, - po.owner - FROM `tabPurchase Order` po, `tabPurchase Order Item` po_item + child.name, + child.parent, + child.cost_center, + child.project, + child.warehouse, + child.material_request, + child.material_request_item, + child.description, + child.stock_uom, + child.qty, + child.amount, + child.base_amount, + child.schedule_date, + par.transaction_date, + par.supplier, + par.status, + par.owner + FROM `tabPurchase Order` par, `tabPurchase Order Item` child WHERE - po.docstatus = 1 - AND po.name = po_item.parent - AND po.status not in ("Closed","Completed","Cancelled") + par.docstatus = 1 + AND par.name = child.parent + AND par.status not in ("Closed","Completed","Cancelled") {conditions} GROUP BY - po.name,po_item.item_code + par.name, child.item_code """.format(conditions=conditions), as_dict=1) #nosec \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index d13bb4577cd..03fe3fa035c 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -30,16 +30,16 @@ class LeaveAllocation(Document): def validate_leave_allocation_days(self): company = frappe.db.get_value("Employee", self.employee, "company") leave_period = get_leave_period(self.from_date, self.to_date, company) - max_leaves_allowed = frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed") + max_leaves_allowed = flt(frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed")) if max_leaves_allowed > 0: leave_allocated = 0 if leave_period: leave_allocated = get_leave_allocation_for_period(self.employee, self.leave_type, leave_period[0].from_date, leave_period[0].to_date) - leave_allocated += self.new_leaves_allocated + leave_allocated += flt(self.new_leaves_allocated) if leave_allocated > max_leaves_allowed: frappe.throw(_("Total allocated leaves are more days than maximum allocation of {0} leave type for employee {1} in the period") - .format(self.leave_type, self.employee)) + .format(self.leave_type, self.employee)) def on_submit(self): self.create_leave_ledger_entry() From e34ec70d441d13aede056669990d7906cbcd542a Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 26 Apr 2020 23:40:37 +0530 Subject: [PATCH 16/58] fix(Desk Page): Number of Open Leads not visible on Shortcut Card --- erpnext/crm/desk_page/crm/crm.json | 4 ++-- erpnext/selling/desk_page/retail/retail.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/desk_page/crm/crm.json b/erpnext/crm/desk_page/crm/crm.json index 4a599fe478d..747c8e3a41a 100644 --- a/erpnext/crm/desk_page/crm/crm.json +++ b/erpnext/crm/desk_page/crm/crm.json @@ -33,7 +33,7 @@ "idx": 0, "is_standard": 1, "label": "CRM", - "modified": "2020-04-01 11:28:51.219999", + "modified": "2020-04-26 22:31:15.865799", "modified_by": "Administrator", "module": "CRM", "name": "CRM", @@ -42,7 +42,7 @@ "pin_to_top": 0, "shortcuts": [ { - "format": "Open", + "format": "{} Open", "label": "Lead", "link_to": "Lead", "stats_filter": "{\"status\":\"Open\"}", diff --git a/erpnext/selling/desk_page/retail/retail.json b/erpnext/selling/desk_page/retail/retail.json index 9f3912db4c2..7b30af20ccb 100644 --- a/erpnext/selling/desk_page/retail/retail.json +++ b/erpnext/selling/desk_page/retail/retail.json @@ -17,7 +17,7 @@ "idx": 0, "is_standard": 1, "label": "Retail", - "modified": "2020-04-01 11:28:50.966145", + "modified": "2020-04-26 22:42:39.346750", "modified_by": "Administrator", "module": "Selling", "name": "Retail", From 9979ceb96b2a074f9d4c496197c2a9d860aa9231 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 27 Apr 2020 10:50:03 +0530 Subject: [PATCH 17/58] fix: E-way bill fix in sales invoice --- .../doctype/sales_invoice/regional/india.js | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india.js b/erpnext/accounts/doctype/sales_invoice/regional/india.js index ba6c03b95f8..6336db16ebc 100644 --- a/erpnext/accounts/doctype/sales_invoice/regional/india.js +++ b/erpnext/accounts/doctype/sales_invoice/regional/india.js @@ -26,16 +26,24 @@ frappe.ui.form.on("Sales Invoice", { && !frm.doc.is_return && !frm.doc.ewaybill) { frm.add_custom_button('E-Way Bill JSON', () => { - var w = window.open( - frappe.urllib.get_full_url( - "/api/method/erpnext.regional.india.utils.generate_ewb_json?" - + "dt=" + encodeURIComponent(frm.doc.doctype) - + "&dn=" + encodeURIComponent(frm.doc.name) - ) - ); - if (!w) { - frappe.msgprint(__("Please enable pop-ups")); return; - } + frappe.call({ + method: 'erpnext.regional.india.utils.generate_ewb_json', + args: { + 'dt': frm.doc.doctype, + 'dn': [frm.doc.name] + }, + callback: function(r) { + if (r.message) { + const args = { + cmd: 'erpnext.regional.india.utils.download_ewb_json', + data: r.message, + docname: frm.doc.name + }; + open_url_post(frappe.request.url, args); + } + } + }); + }, __("Create")); } } From 10aff8e11818c43ef2a74ad3ed78afd13ca1ebf7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 27 Apr 2020 10:50:17 +0530 Subject: [PATCH 18/58] fix: E-way bill fix in List view --- .../sales_invoice/regional/india_list.js | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india_list.js b/erpnext/accounts/doctype/sales_invoice/regional/india_list.js index d17582769cc..3e1c5228ead 100644 --- a/erpnext/accounts/doctype/sales_invoice/regional/india_list.js +++ b/erpnext/accounts/doctype/sales_invoice/regional/india_list.js @@ -16,17 +16,23 @@ frappe.listview_settings['Sales Invoice'].onload = function (doclist) { } } - var w = window.open( - frappe.urllib.get_full_url( - "/api/method/erpnext.regional.india.utils.generate_ewb_json?" - + "dt=" + encodeURIComponent(doclist.doctype) - + "&dn=" + encodeURIComponent(docnames) - ) - ); - if (!w) { - frappe.msgprint(__("Please enable pop-ups")); return; - } - + frappe.call({ + method: 'erpnext.regional.india.utils.generate_ewb_json', + args: { + 'dt': doclist.doctype, + 'dn': docnames + }, + callback: function(r) { + if (r.message) { + const args = { + cmd: 'erpnext.regional.india.utils.download_ewb_json', + data: r.message, + docname: docnames + }; + open_url_post(frappe.request.url, args); + } + } + }); }; doclist.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false); From 00ea59b447c52ef879c6709233d1b7bcaac5a772 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 27 Apr 2020 10:50:40 +0530 Subject: [PATCH 19/58] fix: Utils messsage cleanup --- erpnext/regional/india/utils.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 02823821c41..badb4b4dabd 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -372,7 +372,6 @@ def calculate_hra_exemption_for_period(doc): return exemptions def get_ewb_data(dt, dn): - dn = dn.split(',') ewaybills = [] for doc_name in dn: @@ -453,18 +452,24 @@ def get_ewb_data(dt, dn): @frappe.whitelist() def generate_ewb_json(dt, dn): + dn = json.loads(dn) + return get_ewb_data(dt, dn) - data = get_ewb_data(dt, dn) +@frappe.whitelist() +def download_ewb_json(): + data = frappe._dict(frappe.local.form_dict) - frappe.local.response.filecontent = json.dumps(data, indent=4, sort_keys=True) + frappe.local.response.filecontent = json.dumps(data['data'], indent=4, sort_keys=True) frappe.local.response.type = 'download' - if len(data['billLists']) > 1: + billList = json.loads(data['data'])['billLists'] + + if len(billList) > 1: doc_name = 'Bulk' else: - doc_name = dn + doc_name = data['docname'] - frappe.local.response.filename = '{0}_e-WayBill_Data_{1}.json'.format(doc_name, frappe.utils.random_string(5)) + frappe.local.response.filename = '{0}_e-WayBill_Data_{1}.json'.format(data['docname'], frappe.utils.random_string(5)) @frappe.whitelist() def get_gstins_for_company(company): From 4eecc65cff6742e2e1a8465cf12b81e5f36096c3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 27 Apr 2020 10:50:59 +0530 Subject: [PATCH 20/58] fix: E-way bill fix in Delivery Note --- .../doctype/delivery_note/regional/india.js | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/regional/india.js b/erpnext/stock/doctype/delivery_note/regional/india.js index 0c1ca5caaa8..5e1ff980009 100644 --- a/erpnext/stock/doctype/delivery_note/regional/india.js +++ b/erpnext/stock/doctype/delivery_note/regional/india.js @@ -3,21 +3,28 @@ erpnext.setup_auto_gst_taxation('Delivery Note'); frappe.ui.form.on('Delivery Note', { - refresh: function(frm) { - if(frm.doc.docstatus == 1 && !frm.is_dirty() && !frm.doc.ewaybill) { + refresh: function(frm) { + if(frm.doc.docstatus == 1 && !frm.is_dirty() && !frm.doc.ewaybill) { frm.add_custom_button('E-Way Bill JSON', () => { - var w = window.open( - frappe.urllib.get_full_url( - "/api/method/erpnext.regional.india.utils.generate_ewb_json?" - + "dt=" + encodeURIComponent(frm.doc.doctype) - + "&dn=" + encodeURIComponent(frm.doc.name) - ) - ); - if (!w) { - frappe.msgprint(__("Please enable pop-ups")); return; - } + frappe.call({ + method: 'erpnext.regional.india.utils.generate_ewb_json', + args: { + 'dt': frm.doc.doctype, + 'dn': [frm.doc.name] + }, + callback: function(r) { + if (r.message) { + const args = { + cmd: 'erpnext.regional.india.utils.download_ewb_json', + data: r.message, + docname: frm.doc.name + }; + open_url_post(frappe.request.url, args); + } + } + }); }, __("Create")); } - } + } }) From 7bbe3dd8a0789ee2a8e420a81cb12ecd75403f5f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 27 Apr 2020 10:51:46 +0530 Subject: [PATCH 21/58] fix: Patch for updating Appointment Reminder method in Scheduled Job Type (#21431) --- erpnext/patches.txt | 1 + .../v12_0/update_appointment_reminder_scheduler_entry.py | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 erpnext/patches/v12_0/update_appointment_reminder_scheduler_entry.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index eaeebcf0e9c..a216f53a8ba 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -674,3 +674,4 @@ erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123 erpnext.patches.v12_0.fix_quotation_expired_status +erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry \ No newline at end of file diff --git a/erpnext/patches/v12_0/update_appointment_reminder_scheduler_entry.py b/erpnext/patches/v12_0/update_appointment_reminder_scheduler_entry.py new file mode 100644 index 00000000000..91931eeb3bc --- /dev/null +++ b/erpnext/patches/v12_0/update_appointment_reminder_scheduler_entry.py @@ -0,0 +1,7 @@ +import frappe + +def execute(): + job = frappe.db.exists('Scheduled Job Type', 'patient_appointment.send_appointment_reminder') + if job: + method = 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder' + frappe.db.set_value('Scheduled Job Type', job, 'method', method) \ No newline at end of file From 131452ca9425795fadd78023c0bd3dfb1f8a5d45 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 27 Apr 2020 10:52:38 +0530 Subject: [PATCH 22/58] fix: Lab Test Invoicing (#21435) --- erpnext/healthcare/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py index 9a32c737cf2..a7565323585 100644 --- a/erpnext/healthcare/utils.py +++ b/erpnext/healthcare/utils.py @@ -43,7 +43,7 @@ def validate_customer_created(patient): def get_fee_validity(patient_appointments): if not frappe.db.get_single_value('Healthcare Settings', 'enable_free_follow_ups'): - return + return [] items_to_invoice = [] for appointment in patient_appointments: @@ -110,7 +110,7 @@ def get_lab_tests_to_invoice(patient): filters={'patient': patient.name, 'invoiced': False, 'docstatus': 1} ) for lab_test in lab_tests: - item, is_billable = frappe.get_cached_value('Lab Test Template', lab_test.lab_test_code, ['item', 'is_billable']) + item, is_billable = frappe.get_cached_value('Lab Test Template', lab_test.template, ['item', 'is_billable']) if is_billable: lab_tests_to_invoice.append({ 'reference_type': 'Lab Test', From f34faa91818334581df7c9c12cc104c0a9032675 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Mon, 27 Apr 2020 10:27:02 +0500 Subject: [PATCH 23/58] Module Import Fix (#21433) --- erpnext/hr/doctype/attendance/attendance.py | 8 ++++---- erpnext/hr/doctype/salary_slip/salary_slip.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index 7355a56128a..b6c80655c2e 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -7,7 +7,7 @@ import frappe from frappe.utils import getdate, nowdate from frappe import _ from frappe.model.document import Document -from frappe.utils import cstr, get_datetime, format_date +from frappe.utils import cstr, get_datetime, formatdate class Attendance(Document): def validate(self): @@ -52,16 +52,16 @@ class Attendance(Document): if d.half_day_date == getdate(self.attendance_date): self.status = 'Half Day' frappe.msgprint(_("Employee {0} on Half day on {1}") - .format(self.employee, format_date(self.attendance_date))) + .format(self.employee, formatdate(self.attendance_date))) else: self.status = 'On Leave' frappe.msgprint(_("Employee {0} is on Leave on {1}") - .format(self.employee, format_date(self.attendance_date))) + .format(self.employee, formatdate(self.attendance_date))) if self.status in ("On Leave", "Half Day"): if not leave_record: frappe.msgprint(_("No leave record found for employee {0} on {1}") - .format(self.employee, format_date(self.attendance_date)), alert=1) + .format(self.employee, formatdate(self.attendance_date)), alert=1) elif self.leave_type: self.leave_type = None self.leave_application = None diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index 916b64a83da..8a4da7e7d38 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe, erpnext import datetime, math -from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, format_date +from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate from frappe.model.naming import make_autoname from frappe import msgprint, _ @@ -329,7 +329,7 @@ class SalarySlip(TransactionBase): if d.status in ('Half Day', 'On Leave') and d.leave_type and d.leave_type not in lwp_leave_types: continue - if format_date(d.attendance_date, "yyyy-mm-dd") in holidays: + if formatdate(d.attendance_date, "yyyy-mm-dd") in holidays: if d.status == "Absent" or \ (d.leave_type and d.leave_type in lwp_leave_types and not lwp_leave_types[d.leave_type]): continue From f40a431d8c4d47e5002db91f5fe84754db72f607 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 27 Apr 2020 11:29:56 +0530 Subject: [PATCH 24/58] fix: delivery trip form --- erpnext/stock/doctype/delivery_trip/delivery_trip.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.js b/erpnext/stock/doctype/delivery_trip/delivery_trip.js index a025f06711b..a6fbb66aa2b 100755 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.js +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.js @@ -95,8 +95,6 @@ frappe.ui.form.on('Delivery Trip', { }; }, - }, - optimize_route: function (frm) { if (!frm.doc.driver_address) { frappe.throw(__("Cannot Optimize Route as Driver Address is Missing.")); From fe7e6f5f530823550e783018352dc5cb6e44ceab Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 27 Apr 2020 14:05:45 +0530 Subject: [PATCH 25/58] fix: Test --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 2 +- erpnext/regional/india/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index a2819af5086..88b54fec8f2 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1892,7 +1892,7 @@ class TestSalesInvoice(unittest.TestCase): si.submit() - data = get_ewb_data("Sales Invoice", si.name) + data = get_ewb_data("Sales Invoice", [si.name]) self.assertEqual(data['version'], '1.0.1118') self.assertEqual(data['billLists'][0]['fromGstin'], '27AAECE4835E1ZR') diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index badb4b4dabd..094f01017b9 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -469,7 +469,7 @@ def download_ewb_json(): else: doc_name = data['docname'] - frappe.local.response.filename = '{0}_e-WayBill_Data_{1}.json'.format(data['docname'], frappe.utils.random_string(5)) + frappe.local.response.filename = '{0}_e-WayBill_Data_{1}.json'.format(doc_name, frappe.utils.random_string(5)) @frappe.whitelist() def get_gstins_for_company(company): From cc9dbb912e1c223494c6064e3f19c4bd8486edd5 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Mon, 27 Apr 2020 22:37:02 +0530 Subject: [PATCH 26/58] Adding campaign card in CRM Desk --- erpnext/crm/desk_page/crm/crm.json | 17 +++++++++----- .../twitter_settings/twitter_settings.js | 22 +++++++++++-------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/erpnext/crm/desk_page/crm/crm.json b/erpnext/crm/desk_page/crm/crm.json index 747c8e3a41a..19c89eb3e23 100644 --- a/erpnext/crm/desk_page/crm/crm.json +++ b/erpnext/crm/desk_page/crm/crm.json @@ -12,13 +12,18 @@ }, { "hidden": 0, - "label": "Settings", - "links": "[\n {\n \"description\": \"Manage Customer Group Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Customer Group\",\n \"link\": \"Tree/Customer Group\",\n \"name\": \"Customer Group\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Manage Territory Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Territory\",\n \"link\": \"Tree/Territory\",\n \"name\": \"Territory\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Manage Sales Person Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Sales Person\",\n \"link\": \"Tree/Sales Person\",\n \"name\": \"Sales Person\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Sales campaigns.\",\n \"label\": \"Campaign\",\n \"name\": \"Campaign\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Sends Mails to lead or contact based on a Campaign schedule\",\n \"label\": \"Email Campaign\",\n \"name\": \"Email Campaign\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Send mass SMS to your contacts\",\n \"label\": \"SMS Center\",\n \"name\": \"SMS Center\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Logs for maintaining sms delivery status\",\n \"label\": \"SMS Log\",\n \"name\": \"SMS Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup SMS gateway settings\",\n \"label\": \"SMS Settings\",\n \"name\": \"SMS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Email Group\",\n \"name\": \"Email Group\",\n \"type\": \"doctype\"\n }\n]" + "label": "Maintenance", + "links": "[\n {\n \"description\": \"Plan for maintenance visits.\",\n \"label\": \"Maintenance Schedule\",\n \"name\": \"Maintenance Schedule\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Visit report for maintenance call.\",\n \"label\": \"Maintenance Visit\",\n \"name\": \"Maintenance Visit\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Warranty Claim against Serial No.\",\n \"label\": \"Warranty Claim\",\n \"name\": \"Warranty Claim\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, - "label": "Maintenance", - "links": "[\n {\n \"description\": \"Plan for maintenance visits.\",\n \"label\": \"Maintenance Schedule\",\n \"name\": \"Maintenance Schedule\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Visit report for maintenance call.\",\n \"label\": \"Maintenance Visit\",\n \"name\": \"Maintenance Visit\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Warranty Claim against Serial No.\",\n \"label\": \"Warranty Claim\",\n \"name\": \"Warranty Claim\",\n \"type\": \"doctype\"\n }\n]" + "label": "Campaign", + "links": "[\n {\n \"description\": \"Sales campaigns.\",\n \"label\": \"Campaign\",\n \"name\": \"Campaign\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Sends Mails to lead or contact based on a Campaign schedule\",\n \"label\": \"Email Campaign\",\n \"name\": \"Email Campaign\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Create and Schedule social media posts\",\n \"label\": \"Social Media Post\",\n \"name\": \"Social Media Post\",\n \"type\": \"doctype\"\n }\n]" + }, + { + "hidden": 0, + "label": "Settings", + "links": "[\n {\n \"description\": \"Manage Customer Group Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Customer Group\",\n \"link\": \"Tree/Customer Group\",\n \"name\": \"Customer Group\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Manage Territory Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Territory\",\n \"link\": \"Tree/Territory\",\n \"name\": \"Territory\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Manage Sales Person Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Sales Person\",\n \"link\": \"Tree/Sales Person\",\n \"name\": \"Sales Person\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Send mass SMS to your contacts\",\n \"label\": \"SMS Center\",\n \"name\": \"SMS Center\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Logs for maintaining sms delivery status\",\n \"label\": \"SMS Log\",\n \"name\": \"SMS Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup SMS gateway settings\",\n \"label\": \"SMS Settings\",\n \"name\": \"SMS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Email Group\",\n \"name\": \"Email Group\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Twitter Settings\",\n \"name\": \"Twitter Settings\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"LinkedIn Settings\",\n \"name\": \"LinkedIn Settings\",\n \"type\": \"doctype\"\n }\n]" } ], "category": "Modules", @@ -33,7 +38,7 @@ "idx": 0, "is_standard": 1, "label": "CRM", - "modified": "2020-04-26 22:31:15.865799", + "modified": "2020-04-27 22:32:26.682911", "modified_by": "Administrator", "module": "CRM", "name": "CRM", @@ -42,7 +47,7 @@ "pin_to_top": 0, "shortcuts": [ { - "format": "{} Open", + "format": "Open", "label": "Lead", "link_to": "Lead", "stats_filter": "{\"status\":\"Open\"}", diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.js b/erpnext/crm/doctype/twitter_settings/twitter_settings.js index 8f9c419062f..b55946a8bd8 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.js +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.js @@ -16,23 +16,27 @@ frappe.ui.form.on('Twitter Settings', { } }, refresh: function(frm){ - let msg,color; + let msg, color, flag=false; if (frm.doc.session_status == "Active"){ msg = __("Session Active"); color = 'green'; + flag = true; } - else { + else if(frm.doc.consumer_key && frm.doc.consumer_secret) { msg = __("Session Not Active. Save doc to login."); color = 'red'; + flag = true; } - frm.dashboard.set_headline_alert( - `
-
- -
-
` - ); + if (flag){ + frm.dashboard.set_headline_alert( + `
+
+ +
+
` + ); + } }, login: function(frm){ if (frm.doc.consumer_key && frm.doc.consumer_secret){ From 01d0b373e05835346fbd64e385405c81b2e2a9f6 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Tue, 28 Apr 2020 00:21:10 +0530 Subject: [PATCH 27/58] Review changes --- erpnext/crm/desk_page/crm/crm.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/desk_page/crm/crm.json b/erpnext/crm/desk_page/crm/crm.json index 19c89eb3e23..ca13d6abb66 100644 --- a/erpnext/crm/desk_page/crm/crm.json +++ b/erpnext/crm/desk_page/crm/crm.json @@ -47,7 +47,7 @@ "pin_to_top": 0, "shortcuts": [ { - "format": "Open", + "format": "{} Open", "label": "Lead", "link_to": "Lead", "stats_filter": "{\"status\":\"Open\"}", From 504a5f3a3ae00d842c7724f435791e259ca8c583 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 28 Apr 2020 11:16:04 +0530 Subject: [PATCH 28/58] fix: reload income_tax_slab_other_charges in patch (#21447) * fix: reload income_tax_slab_other_charges in patch * fix: reload lower_deduction_certificate in patch --- erpnext/patches/v11_0/add_permissions_in_gst_settings.py | 1 + .../move_tax_slabs_from_payroll_period_to_income_tax_slab.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v11_0/add_permissions_in_gst_settings.py b/erpnext/patches/v11_0/add_permissions_in_gst_settings.py index 121a20288c3..d7936110edb 100644 --- a/erpnext/patches/v11_0/add_permissions_in_gst_settings.py +++ b/erpnext/patches/v11_0/add_permissions_in_gst_settings.py @@ -6,4 +6,5 @@ def execute(): if not company: return + frappe.reload_doc("regional", "doctype", "lower_deduction_certificate") add_permissions() \ No newline at end of file diff --git a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py index a6aefac12ad..179be2cfde5 100644 --- a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py +++ b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py @@ -10,7 +10,7 @@ def execute(): if not frappe.db.table_exists("Payroll Period"): return - for doctype in ("income_tax_slab", "salary_structure_assignment", "employee_other_income"): + for doctype in ("income_tax_slab", "salary_structure_assignment", "employee_other_income", "income_tax_slab_other_charges"): frappe.reload_doc("hr", "doctype", doctype) From 142af4b58aafc7ea7dc4814a8e01374afb2f7580 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 28 Apr 2020 11:17:45 +0530 Subject: [PATCH 29/58] fix: Default column width in Gross profit report --- .../report/gross_profit/gross_profit.py | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 6ef6d6eea03..4e22b05a81d 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -55,27 +55,27 @@ def get_columns(group_wise_columns, filters): columns = [] column_map = frappe._dict({ "parent": _("Sales Invoice") + ":Link/Sales Invoice:120", - "posting_date": _("Posting Date") + ":Date", - "posting_time": _("Posting Time"), - "item_code": _("Item Code") + ":Link/Item", - "item_name": _("Item Name"), - "item_group": _("Item Group") + ":Link/Item Group", - "brand": _("Brand"), - "description": _("Description"), - "warehouse": _("Warehouse") + ":Link/Warehouse", - "qty": _("Qty") + ":Float", - "base_rate": _("Avg. Selling Rate") + ":Currency/currency", - "buying_rate": _("Valuation Rate") + ":Currency/currency", - "base_amount": _("Selling Amount") + ":Currency/currency", - "buying_amount": _("Buying Amount") + ":Currency/currency", - "gross_profit": _("Gross Profit") + ":Currency/currency", - "gross_profit_percent": _("Gross Profit %") + ":Percent", - "project": _("Project") + ":Link/Project", + "posting_date": _("Posting Date") + ":Date:100", + "posting_time": _("Posting Time") + ":Data:100", + "item_code": _("Item Code") + ":Link/Item:100", + "item_name": _("Item Name") + ":Data:100", + "item_group": _("Item Group") + ":Link/Item Group:100", + "brand": _("Brand") + ":Link/Brand:100", + "description": _("Description") +":Data:100", + "warehouse": _("Warehouse") + ":Link/Warehouse:100", + "qty": _("Qty") + ":Float:80", + "base_rate": _("Avg. Selling Rate") + ":Currency/currency:100", + "buying_rate": _("Valuation Rate") + ":Currency/currency:100", + "base_amount": _("Selling Amount") + ":Currency/currency:100", + "buying_amount": _("Buying Amount") + ":Currency/currency:100", + "gross_profit": _("Gross Profit") + ":Currency/currency:100", + "gross_profit_percent": _("Gross Profit %") + ":Percent:100", + "project": _("Project") + ":Link/Project:100", "sales_person": _("Sales person"), - "allocated_amount": _("Allocated Amount") + ":Currency/currency", - "customer": _("Customer") + ":Link/Customer", - "customer_group": _("Customer Group") + ":Link/Customer Group", - "territory": _("Territory") + ":Link/Territory" + "allocated_amount": _("Allocated Amount") + ":Currency/currency:100", + "customer": _("Customer") + ":Link/Customer:100", + "customer_group": _("Customer Group") + ":Link/Customer Group:100", + "territory": _("Territory") + ":Link/Territory:100" }) for col in group_wise_columns.get(scrub(filters.group_by)): @@ -85,7 +85,8 @@ def get_columns(group_wise_columns, filters): "fieldname": "currency", "label" : _("Currency"), "fieldtype": "Link", - "options": "Currency" + "options": "Currency", + "hidden": 1 }) return columns @@ -277,7 +278,7 @@ class GrossProfitGenerator(object): from `tabPurchase Invoice Item` a where a.item_code = %s and a.docstatus=1 and modified <= %s - order by a.modified desc limit 1""", (item_code,self.filters.to_date)) + order by a.modified desc limit 1""", (item_code, self.filters.to_date)) else: last_purchase_rate = frappe.db.sql(""" select (a.base_rate / a.conversion_factor) From daf37e7570432c1f27e99c7a86b586813e2e708a Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 28 Apr 2020 12:48:22 +0530 Subject: [PATCH 30/58] fix: better validation message for the expense claim and set default cost center in the expenses table (#21454) --- .../hr/doctype/expense_claim/expense_claim.js | 3 ++- .../hr/doctype/expense_claim/expense_claim.py | 21 ++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index 88f3865434b..fb2310396b7 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -17,7 +17,7 @@ erpnext.hr.ExpenseClaimController = frappe.ui.form.Controller.extend({ return; } return frappe.call({ - method: "erpnext.hr.doctype.expense_claim.expense_claim.get_expense_claim_account", + method: "erpnext.hr.doctype.expense_claim.expense_claim.get_expense_claim_account_and_cost_center", args: { "expense_claim_type": d.expense_type, "company": doc.company @@ -25,6 +25,7 @@ erpnext.hr.ExpenseClaimController = frappe.ui.form.Controller.extend({ callback: function(r) { if (r.message) { d.default_account = r.message.account; + d.cost_center = r.message.cost_center; } } }); diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index fe8afdf8734..ad9d86b66e2 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -2,9 +2,9 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -import frappe +import frappe, erpnext from frappe import _ -from frappe.utils import get_fullname, flt, cstr +from frappe.utils import get_fullname, flt, cstr, get_link_to_form from frappe.model.document import Document from erpnext.hr.utils import set_employee_name from erpnext.accounts.party import get_party_account @@ -192,7 +192,8 @@ class ExpenseClaim(AccountsController): def validate_account_details(self): for data in self.expenses: if not data.cost_center: - frappe.throw(_("Cost center is required to book an expense claim")) + frappe.throw(_("Row {0}: {1} is required in the expenses table to book an expense claim.") + .format(data.idx, frappe.bold("Cost Center"))) if self.is_paid: if not self.mode_of_payment: @@ -308,13 +309,23 @@ def make_bank_entry(dt, dn): return je.as_dict() +@frappe.whitelist() +def get_expense_claim_account_and_cost_center(expense_claim_type, company): + data = get_expense_claim_account(expense_claim_type, company) + cost_center = erpnext.get_default_cost_center(company) + + return { + "account": data.get("account"), + "cost_center": cost_center + } + @frappe.whitelist() def get_expense_claim_account(expense_claim_type, company): account = frappe.db.get_value("Expense Claim Account", {"parent": expense_claim_type, "company": company}, "default_account") if not account: - frappe.throw(_("Please set default account in Expense Claim Type {0}") - .format(expense_claim_type)) + frappe.throw(_("Set the default account for the {0} {1}") + .format(frappe.bold("Expense Claim Type"), get_link_to_form("Expense Claim Type", expense_claim_type))) return { "account": account From 299e21766805f6fcc705928fbfe3f7ca3a4bf8a6 Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 28 Apr 2020 13:00:04 +0530 Subject: [PATCH 31/58] fix: Blanket Order in SO/PO child tables (#21442) --- .../doctype/purchase_order/purchase_order.js | 9 --------- erpnext/controllers/queries.py | 13 +++++++++++++ erpnext/public/js/controllers/transaction.js | 14 ++++++++++++++ erpnext/selling/doctype/sales_order/sales_order.js | 11 +---------- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index ed054aedb5a..4a8146a797a 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -27,15 +27,6 @@ frappe.ui.form.on("Purchase Order", { frm.set_indicator_formatter('item_code', function(doc) { return (doc.qty<=doc.received_qty) ? "green" : "orange" }) - frm.set_query("blanket_order", "items", function() { - return { - filters: { - "company": frm.doc.company, - "docstatus": 1 - } - } - }); - frm.set_query("expense_account", "items", function() { return { query: "erpnext.controllers.queries.get_expense_account", diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index c14bb669a4d..5febfd6bf28 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -371,6 +371,19 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters): fields = ["name", "parent_account"], limit_start=start, limit_page_length=page_len, as_list=True) +def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters): + return frappe.db.sql("""select distinct bo.name, bo.blanket_order_type, bo.to_date + from `tabBlanket Order` bo, `tabBlanket Order Item` boi + where + boi.parent = bo.name + and boi.item_code = {item_code} + and bo.blanket_order_type = '{blanket_order_type}' + and bo.company = {company} + and bo.docstatus = 1""" + .format(item_code = frappe.db.escape(filters.get("item")), + blanket_order_type = filters.get("blanket_order_type"), + company = frappe.db.escape(filters.get("company")) + )) @frappe.whitelist() def get_income_account(doctype, txt, searchfield, start, page_len, filters): diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 5843034543b..c9d77285215 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -175,6 +175,20 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }; } + if (this.frm.fields_dict["items"].grid.get_field('blanket_order')) { + this.frm.set_query("blanket_order", "items", function(doc, cdt, cdn) { + var item = locals[cdt][cdn]; + return { + query: "erpnext.controllers.queries.get_blanket_orders", + filters: { + "company": doc.company, + "blanket_order_type": doc.doctype === "Sales Order" ? "Selling" : "Purchasing", + "item": item.item_code + } + } + }); + } + }, onload: function() { var me = this; diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 3c1ffe95960..45a43c5e7e9 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -65,15 +65,6 @@ frappe.ui.form.on("Sales Order", { } }); - frm.set_query("blanket_order", "items", function() { - return { - filters: { - "company": frm.doc.company, - "docstatus": 1 - } - } - }); - erpnext.queries.setup_warehouse_query(frm); }, @@ -148,7 +139,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create')); - + const order_is_a_sale = ["Sales", "Shopping Cart"].indexOf(doc.order_type) !== -1; const order_is_maintenance = ["Maintenance"].indexOf(doc.order_type) !== -1; // order type has been customised then show all the action buttons From 28a4880b48fdfccabfe044c89e6d61e13cd04a56 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 28 Apr 2020 13:01:43 +0530 Subject: [PATCH 32/58] fix: added validation to not allow to select expired batch (#21455) --- erpnext/controllers/stock_controller.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 55a2c435a12..9d453af2ace 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import frappe, erpnext -from frappe.utils import cint, flt, cstr +from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate from frappe import _ import frappe.defaults from erpnext.accounts.utils import get_fiscal_year @@ -55,6 +55,13 @@ class StockController(AccountsController): frappe.throw(_("Row #{0}: Serial No {1} does not belong to Batch {2}") .format(d.idx, serial_no_data.name, d.batch_no)) + if d.qty > 0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2: + expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date") + + if expiry_date and getdate(expiry_date) < getdate(self.posting_date): + frappe.throw(_("Row #{0}: The batch {1} has already expired.") + .format(d.idx, get_link_to_form("Batch", d.get("batch_no")))) + def get_gl_entries(self, warehouse_account=None, default_expense_account=None, default_cost_center=None): From fa2eecc5fc7520fe6f274dbc2ec9b42227f83fc9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 28 Apr 2020 13:12:22 +0530 Subject: [PATCH 33/58] fix: Report summary fix in consolidated financial statement for report type Profit and Loss --- .../consolidated_financial_statement.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index b62238b59bb..c2c7207e377 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -84,6 +84,7 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters): def get_profit_loss_data(fiscal_year, companies, columns, filters): income, expense, net_profit_loss = get_income_expense_data(companies, fiscal_year, filters) + company_currency = get_company_currency(filters) data = [] data.extend(income or []) @@ -93,7 +94,7 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters): chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss) - report_summary = get_pl_summary(companies, '', income, expense, net_profit_loss, True) + report_summary = get_pl_summary(companies, '', income, expense, net_profit_loss, company_currency, True) return data, None, chart, report_summary From 7b14721e2f61060d60284f8066a17c4fd6b42f4b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 28 Apr 2020 14:25:36 +0530 Subject: [PATCH 34/58] fix: Allow rename for Loan Security --- .../doctype/loan_security/loan_security.json | 21 +++++++++++++------ .../loan_security_type.json | 12 +++++++---- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_security/loan_security.json b/erpnext/loan_management/doctype/loan_security/loan_security.json index e6984ee7f15..e879b17a43d 100644 --- a/erpnext/loan_management/doctype/loan_security/loan_security.json +++ b/erpnext/loan_management/doctype/loan_security/loan_security.json @@ -1,15 +1,17 @@ { + "actions": [], + "allow_rename": 1, "autoname": "field:loan_security_name", "creation": "2019-09-02 15:07:08.885593", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "loan_security_type", - "loan_security_code", "loan_security_name", "unit_of_measure", + "loan_security_code", "column_break_3", + "loan_security_type", "haircut", "disabled" ], @@ -17,7 +19,9 @@ { "fieldname": "loan_security_name", "fieldtype": "Data", + "in_list_view": 1, "label": "Loan Security Name", + "reqd": 1, "unique": 1 }, { @@ -33,8 +37,10 @@ { "fieldname": "loan_security_type", "fieldtype": "Link", + "in_list_view": 1, "label": "Loan Security Type", - "options": "Loan Security Type" + "options": "Loan Security Type", + "reqd": 1 }, { "fieldname": "loan_security_code", @@ -52,11 +58,15 @@ "fetch_from": "loan_security_type.unit_of_measure", "fieldname": "unit_of_measure", "fieldtype": "Link", + "in_list_view": 1, "label": "Unit Of Measure", - "options": "UOM" + "options": "UOM", + "read_only": 1, + "reqd": 1 } ], - "modified": "2019-11-16 11:36:37.901656", + "links": [], + "modified": "2020-04-28 14:07:54.506896", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security", @@ -87,7 +97,6 @@ "write": 1 } ], - "quick_entry": 1, "search_fields": "loan_security_code", "sort_field": "modified", "sort_order": "DESC", diff --git a/erpnext/loan_management/doctype/loan_security_type/loan_security_type.json b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.json index 5f296093a4b..f46b88cbca6 100644 --- a/erpnext/loan_management/doctype/loan_security_type/loan_security_type.json +++ b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.json @@ -9,9 +9,9 @@ "loan_security_type", "unit_of_measure", "haircut", - "disabled", "column_break_5", - "loan_to_value_ratio" + "loan_to_value_ratio", + "disabled" ], "fields": [ { @@ -23,7 +23,9 @@ { "fieldname": "loan_security_type", "fieldtype": "Data", + "in_list_view": 1, "label": "Loan Security Type", + "reqd": 1, "unique": 1 }, { @@ -34,8 +36,10 @@ { "fieldname": "unit_of_measure", "fieldtype": "Link", + "in_list_view": 1, "label": "Unit Of Measure", - "options": "UOM" + "options": "UOM", + "reqd": 1 }, { "fieldname": "column_break_5", @@ -48,7 +52,7 @@ } ], "links": [], - "modified": "2020-02-28 12:43:20.364447", + "modified": "2020-04-28 14:06:49.046177", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Type", From 76825ab2bd73927a2a2e446b165df1105c8013b2 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Tue, 28 Apr 2020 16:08:52 +0530 Subject: [PATCH 35/58] fix: update lead if contact details are changed --- erpnext/crm/utils.py | 24 ++++++++++++++++++++++++ erpnext/hooks.py | 5 +++-- 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 erpnext/crm/utils.py diff --git a/erpnext/crm/utils.py b/erpnext/crm/utils.py new file mode 100644 index 00000000000..72d778e9568 --- /dev/null +++ b/erpnext/crm/utils.py @@ -0,0 +1,24 @@ +import frappe + + +def update_lead_phone_numbers(contact, method): + if contact.phone_nos: + contact_lead = contact.get_link_for("Lead") + if contact_lead: + phone = mobile_no = contact.phone_nos[0].phone + + if len(contact.phone_nos) > 1: + # get the default phone number + primary_phones = [phone.phone for phone in contact.phone_nos if phone.is_primary_phone] + if primary_phones: + phone = primary_phones[0] + + # get the default mobile number + primary_mobile_nos = [phone.phone for phone in contact.phone_nos if phone.is_primary_mobile_no] + if primary_mobile_nos: + mobile_no = primary_mobile_nos[0] + + lead = frappe.get_doc("Lead", contact_lead) + lead.phone = phone + lead.mobile_no = mobile_no + lead.save() diff --git a/erpnext/hooks.py b/erpnext/hooks.py index e6f6c8e47a4..6b198e744cc 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -250,7 +250,8 @@ doc_events = { }, "Contact": { "on_trash": "erpnext.support.doctype.issue.issue.update_issue", - "after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information" + "after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information", + "validate": "erpnext.crm.utils.update_lead_phone_numbers" }, "Lead": { "after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information" @@ -537,4 +538,4 @@ global_search_doctypes = { {'doctype': 'Hotel Room Package', 'index': 3}, {'doctype': 'Hotel Room Type', 'index': 4} ] -} \ No newline at end of file +} From 8705df628394e7826fa97a96139179d450ab06d2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 28 Apr 2020 20:19:26 +0530 Subject: [PATCH 36/58] fix: Remove duplicate code from accounting dimension --- .../public/js/utils/dimension_tree_filter.js | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index 75c5a820b46..b223fc557be 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -24,7 +24,7 @@ doctypes_with_dimensions.forEach((doctype) => { onload: function(frm) { erpnext.dimension_filters.forEach((dimension) => { frappe.model.with_doctype(dimension['document_type'], () => { - if (frappe.meta.has_field(dimension['document_type'], 'is_group')) { + if(frappe.meta.has_field(dimension['document_type'], 'is_group')) { frm.set_query(dimension['fieldname'], { "is_group": 0 }); @@ -42,19 +42,21 @@ doctypes_with_dimensions.forEach((doctype) => { update_dimension: function(frm) { erpnext.dimension_filters.forEach((dimension) => { - if (frm.is_new()) { - if (frm.doc.company && Object.keys(default_dimensions || {}).length > 0 + if(frm.is_new()) { + if(frm.doc.company && Object.keys(default_dimensions || {}).length > 0 && default_dimensions[frm.doc.company]) { - if (frappe.meta.has_field(doctype, dimension['fieldname'])) { - frm.set_value(dimension['fieldname'], - default_dimensions[frm.doc.company][dimension['document_type']]); - } + let default_dimension = default_dimensions[frm.doc.company][dimension['document_type']]; - $.each(frm.doc.items || frm.doc.accounts || [], function(i, row) { - frappe.model.set_value(row.doctype, row.name, dimension['fieldname'], - default_dimensions[frm.doc.company][dimension['document_type']]) - }); + if(default_dimension) { + if (frappe.meta.has_field(doctype, dimension['fieldname'])) { + frm.set_value(dimension['fieldname'], default_dimension); + } + + $.each(frm.doc.items || frm.doc.accounts || [], function(i, row) { + frappe.model.set_value(row.doctype, row.name, dimension['fieldname'], default_dimension); + }); + } } } }); @@ -71,20 +73,6 @@ child_docs.forEach((doctype) => { }); }, - accounts_add: function(frm, cdt, cdn) { - erpnext.dimension_filters.forEach((dimension) => { - var row = frappe.get_doc(cdt, cdn); - frm.script_manager.copy_from_first_row("accounts", row, [dimension['fieldname']]); - }); - }, - - items_add: function(frm, cdt, cdn) { - erpnext.dimension_filters.forEach((dimension) => { - var row = frappe.get_doc(cdt, cdn); - frm.script_manager.copy_from_first_row("items", row, [dimension['fieldname']]); - }); - }, - accounts_add: function(frm, cdt, cdn) { erpnext.dimension_filters.forEach((dimension) => { var row = frappe.get_doc(cdt, cdn); From 6c871d6bcf931884e0f0b9e699790bd63d9f7e89 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 28 Apr 2020 21:28:53 +0530 Subject: [PATCH 37/58] fix: Removed Finished Product and Finished Qty columns from Stock Ledger Report --- erpnext/stock/report/stock_ledger/stock_ledger.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 28d72084de8..0190f09f3d4 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -46,19 +46,6 @@ def execute(filters=None): "out_qty": min(sle.actual_qty, 0) }) - # get the name of the item that was produced using this item - if sle.voucher_type == "Stock Entry": - purpose, work_order, fg_completed_qty = frappe.db.get_value(sle.voucher_type, sle.voucher_no, ["purpose", "work_order", "fg_completed_qty"]) - - if purpose == "Manufacture" and work_order: - finished_product = frappe.db.get_value("Work Order", work_order, "item_name") - finished_qty = fg_completed_qty - - sle.update({ - "finished_product": finished_product, - "finished_qty": finished_qty, - }) - data.append(sle) if include_uom: @@ -77,8 +64,6 @@ def get_columns(): {"label": _("In Qty"), "fieldname": "in_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"}, {"label": _("Out Qty"), "fieldname": "out_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"}, {"label": _("Balance Qty"), "fieldname": "qty_after_transaction", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Finished Product"), "fieldname": "finished_product", "width": 100}, - {"label": _("Finished Qty"), "fieldname": "finished_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, {"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 150}, {"label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", "width": 150}, {"label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100}, From 49bb8ccd2e65c730d674cb3d421f31e486216183 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <> Date: Tue, 28 Apr 2020 18:56:47 +0200 Subject: [PATCH 38/58] feat: add Bank Account to dashboards --- erpnext/buying/doctype/supplier/supplier_dashboard.py | 8 ++------ erpnext/education/doctype/student/student_dashboard.py | 7 +++++-- erpnext/hr/doctype/employee/employee_dashboard.py | 5 ++++- erpnext/non_profit/doctype/member/member_dashboard.py | 9 ++++++++- erpnext/selling/doctype/customer/customer_dashboard.py | 5 +++-- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/erpnext/buying/doctype/supplier/supplier_dashboard.py b/erpnext/buying/doctype/supplier/supplier_dashboard.py index d0d5b73984f..16251035bd3 100644 --- a/erpnext/buying/doctype/supplier/supplier_dashboard.py +++ b/erpnext/buying/doctype/supplier/supplier_dashboard.py @@ -23,15 +23,11 @@ def get_data(): }, { 'label': _('Payments'), - 'items': ['Payment Entry'] - }, - { - 'label': _('Bank'), - 'items': ['Bank Account'] + 'items': ['Payment Entry', 'Bank Account'] }, { 'label': _('Pricing'), 'items': ['Pricing Rule'] } ] - } \ No newline at end of file + } diff --git a/erpnext/education/doctype/student/student_dashboard.py b/erpnext/education/doctype/student/student_dashboard.py index 0cbd17b8a4a..d2614628b10 100644 --- a/erpnext/education/doctype/student/student_dashboard.py +++ b/erpnext/education/doctype/student/student_dashboard.py @@ -6,6 +6,9 @@ def get_data(): 'heatmap': True, 'heatmap_message': _('This is based on the attendance of this Student'), 'fieldname': 'student', + 'non_standard_fieldnames': { + 'Bank Account': 'party' + }, 'transactions': [ { 'label': _('Admission'), @@ -29,7 +32,7 @@ def get_data(): }, { 'label': _('Fee'), - 'items': ['Fees'] + 'items': ['Fees', 'Bank Account'] } ] - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py index 11ad83ba37e..02033321648 100644 --- a/erpnext/hr/doctype/employee/employee_dashboard.py +++ b/erpnext/hr/doctype/employee/employee_dashboard.py @@ -6,6 +6,9 @@ def get_data(): 'heatmap': True, 'heatmap_message': _('This is based on the attendance of this Employee'), 'fieldname': 'employee', + 'non_standard_fieldnames': { + 'Bank Account': 'party' + }, 'transactions': [ { 'label': _('Leave and Attendance'), @@ -33,7 +36,7 @@ def get_data(): }, { 'label': _('Payroll'), - 'items': ['Salary Structure Assignment', 'Salary Slip', 'Additional Salary', 'Timesheet','Employee Incentive', 'Retention Bonus'] + 'items': ['Salary Structure Assignment', 'Salary Slip', 'Additional Salary', 'Timesheet','Employee Incentive', 'Retention Bonus', 'Bank Account'] }, { 'label': _('Training'), diff --git a/erpnext/non_profit/doctype/member/member_dashboard.py b/erpnext/non_profit/doctype/member/member_dashboard.py index 945fb7b7d31..743db2513af 100644 --- a/erpnext/non_profit/doctype/member/member_dashboard.py +++ b/erpnext/non_profit/doctype/member/member_dashboard.py @@ -6,10 +6,17 @@ def get_data(): 'heatmap': True, 'heatmap_message': _('Member Activity'), 'fieldname': 'member', + 'non_standard_fieldnames': { + 'Bank Account': 'party' + }, 'transactions': [ { 'label': _('Membership Details'), 'items': ['Membership'] + }, + { + 'label': _('Fee'), + 'items': ['Bank Account'] } ] - } \ No newline at end of file + } diff --git a/erpnext/selling/doctype/customer/customer_dashboard.py b/erpnext/selling/doctype/customer/customer_dashboard.py index 654dd48c669..22e30e31139 100644 --- a/erpnext/selling/doctype/customer/customer_dashboard.py +++ b/erpnext/selling/doctype/customer/customer_dashboard.py @@ -11,7 +11,8 @@ def get_data(): 'non_standard_fieldnames': { 'Payment Entry': 'party', 'Quotation': 'party_name', - 'Opportunity': 'party_name' + 'Opportunity': 'party_name', + 'Bank Account': 'party' }, 'dynamic_links': { 'party_name': ['Customer', 'quotation_to'] @@ -27,7 +28,7 @@ def get_data(): }, { 'label': _('Payments'), - 'items': ['Payment Entry'] + 'items': ['Payment Entry', 'Bank Account'] }, { 'label': _('Support'), From 50b4106d1dece03412c57fca596e73c1adf14857 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 29 Apr 2020 02:27:47 +0530 Subject: [PATCH 39/58] fix: payment request not able to make against fees --- erpnext/accounts/doctype/payment_request/payment_request.py | 6 +++--- erpnext/education/doctype/fees/fees.js | 2 ++ erpnext/education/doctype/fees/fees.py | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 53ff2225d31..68aeb6d1d65 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -326,7 +326,7 @@ def make_payment_request(**args): "reference_doctype": args.dt, "reference_name": args.dn, "party_type": args.get("party_type") or "Customer", - "party": args.get("party") or ref_doc.customer, + "party": args.get("party") or ref_doc.get("customer"), "bank_account": bank_account }) @@ -420,7 +420,7 @@ def make_payment_entry(docname): def update_payment_req_status(doc, method): from erpnext.accounts.doctype.payment_entry.payment_entry import get_reference_details - + for ref in doc.references: payment_request_name = frappe.db.get_value("Payment Request", {"reference_doctype": ref.reference_doctype, "reference_name": ref.reference_name, @@ -430,7 +430,7 @@ def update_payment_req_status(doc, method): ref_details = get_reference_details(ref.reference_doctype, ref.reference_name, doc.party_account_currency) pay_req_doc = frappe.get_doc('Payment Request', payment_request_name) status = pay_req_doc.status - + if status != "Paid" and not ref_details.outstanding_amount: status = 'Paid' elif status != "Partially Paid" and ref_details.outstanding_amount != ref_details.total_amount: diff --git a/erpnext/education/doctype/fees/fees.js b/erpnext/education/doctype/fees/fees.js index e2c6f1d8565..17ef44954b1 100644 --- a/erpnext/education/doctype/fees/fees.js +++ b/erpnext/education/doctype/fees/fees.js @@ -112,6 +112,8 @@ frappe.ui.form.on("Fees", { args: { "dt": frm.doc.doctype, "dn": frm.doc.name, + "party_type": "Student", + "party": frm.doc.student, "recipient_id": frm.doc.student_email }, callback: function(r) { diff --git a/erpnext/education/doctype/fees/fees.py b/erpnext/education/doctype/fees/fees.py index aa616e6206a..f31003bf326 100644 --- a/erpnext/education/doctype/fees/fees.py +++ b/erpnext/education/doctype/fees/fees.py @@ -75,7 +75,8 @@ class Fees(AccountsController): self.make_gl_entries() if self.send_payment_request and self.student_email: - pr = make_payment_request(dt="Fees", dn=self.name, recipient_id=self.student_email, + pr = make_payment_request(party_type="Student", party=self.student, dt="Fees", + dn=self.name, recipient_id=self.student_email, submit_doc=True, use_dummy_message=True) frappe.msgprint(_("Payment request {0} created").format(getlink("Payment Request", pr.name))) From 9bf733d82e2fae97fd033086c6834b727fcf83fd Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 29 Apr 2020 11:16:34 +0530 Subject: [PATCH 40/58] fix: reload procedure doc on completion --- .../doctype/clinical_procedure/clinical_procedure.js | 8 +++++--- .../doctype/clinical_procedure/clinical_procedure.py | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js index 5f36bdd95c6..87c22ccf6f3 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js +++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js @@ -80,6 +80,7 @@ frappe.ui.form.on('Clinical Procedure', { frappe.call({ method: 'complete_procedure', doc: frm.doc, + freeze: true, callback: function(r) { if (r.message) { frappe.show_alert({ @@ -87,8 +88,8 @@ frappe.ui.form.on('Clinical Procedure', { ['' + r.message + '']), indicator: 'green' }); - frm.reload_doc(); } + frm.reload_doc(); } }); } @@ -111,9 +112,10 @@ frappe.ui.form.on('Clinical Procedure', { frappe.call({ doc: frm.doc, method: 'make_material_receipt', + freeze: true, callback: function(r) { if (!r.exc) { - cur_frm.reload_doc(); + frm.reload_doc(); let doclist = frappe.model.sync(r.message); frappe.set_route('Form', doclist[0].doctype, doclist[0].name); } @@ -122,7 +124,7 @@ frappe.ui.form.on('Clinical Procedure', { } ); } else { - cur_frm.reload_doc(); + frm.reload_doc(); } } } diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py index db3afc8807e..d6c08939143 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py +++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py @@ -87,7 +87,8 @@ class ClinicalProcedure(Document): else: frappe.throw(_('Please set Customer in Patient {0}').format(frappe.bold(self.patient)), title=_('Customer Not Found')) - frappe.db.set_value('Clinical Procedure', self.name, 'status', 'Completed') + self.db_set('status', 'Completed') + if self.consume_stock and self.items: return stock_entry From 33793d4e0df0355e779b90bac7deed294911fe67 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Wed, 29 Apr 2020 11:48:41 +0530 Subject: [PATCH 41/58] fix: Permission issue Employee Tax exemption (#21490) --- erpnext/hr/doctype/salary_structure/salary_structure.py | 4 ++-- erpnext/regional/india/utils.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/salary_structure/salary_structure.py b/erpnext/hr/doctype/salary_structure/salary_structure.py index df76458fe02..5ba7f1c4327 100644 --- a/erpnext/hr/doctype/salary_structure/salary_structure.py +++ b/erpnext/hr/doctype/salary_structure/salary_structure.py @@ -149,7 +149,7 @@ def get_existing_assignments(employees, salary_structure, from_date): return salary_structures_assignments @frappe.whitelist() -def make_salary_slip(source_name, target_doc = None, employee = None, as_print = False, print_format = None, for_preview=0): +def make_salary_slip(source_name, target_doc = None, employee = None, as_print = False, print_format = None, for_preview=0, ignore_permissions=False): def postprocess(source, target): if employee: employee_details = frappe.db.get_value("Employee", employee, @@ -169,7 +169,7 @@ def make_salary_slip(source_name, target_doc = None, employee = None, as_print = "name": "salary_structure" } } - }, target_doc, postprocess, ignore_child_tables=True) + }, target_doc, postprocess, ignore_child_tables=True, ignore_permissions=ignore_permissions) if cint(as_print): doc.name = 'Preview for {0}'.format(employee) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 094f01017b9..33098587c2a 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -288,7 +288,7 @@ def calculate_annual_eligible_hra_exemption(doc): }) def get_component_amt_from_salary_slip(employee, salary_structure, basic_component, hra_component): - salary_slip = make_salary_slip(salary_structure, employee=employee, for_preview=1) + salary_slip = make_salary_slip(salary_structure, employee=employee, for_preview=1, ignore_permissions=True) basic_amt, hra_amt = 0, 0 for earning in salary_slip.earnings: if earning.salary_component == basic_component: From 3d8dadaab6e075814284dba0742bc4a690c6d75a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 29 Apr 2020 12:05:56 +0530 Subject: [PATCH 42/58] fix: Group by filter fix in item wise sales and purchase register --- .../item_wise_purchase_register/item_wise_purchase_register.py | 2 +- .../report/item_wise_sales_register/item_wise_sales_register.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index 127f3133f5b..1f78c7a006f 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -102,7 +102,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum data.append(row) - if filters.get('group_by'): + if filters.get('group_by') and item_list: total_row = total_row_map.get(prev_group_by_value or d.get('item_name')) total_row['percent_gt'] = flt(total_row['total']/grand_total * 100) data.append(total_row) diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 0c8957ae441..92a22e62f14 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -111,7 +111,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum data.append(row) - if filters.get('group_by'): + if filters.get('group_by') and item_list: total_row = total_row_map.get(prev_group_by_value or d.get('item_name')) total_row['percent_gt'] = flt(total_row['total']/grand_total * 100) data.append(total_row) From 708bceba6e7dd303c0e3f258ba347af0b0408124 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 29 Apr 2020 13:06:52 +0530 Subject: [PATCH 43/58] fix: change Patient Medical Record subject fieldtype to Text Editor --- .../clinical_procedure/clinical_procedure.py | 2 +- .../healthcare/doctype/lab_test/lab_test.py | 18 +++++++------- .../patient_encounter/patient_encounter.py | 23 +++++++++++------- .../patient_medical_record.json | 4 ++-- .../doctype/vital_signs/vital_signs.py | 24 +++++++++---------- 5 files changed, 39 insertions(+), 32 deletions(-) diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py index d6c08939143..297f1b9f4cf 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py +++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py @@ -248,7 +248,7 @@ def make_procedure(source_name, target_doc=None): def insert_clinical_procedure_to_medical_record(doc): subject = cstr(doc.procedure_template) if doc.practitioner: - subject += ' ' + doc.practitioner + subject += frappe.bold(_('Healthcare Practitioner: ')) + doc.practitioner if subject and doc.notes: subject += '
' + doc.notes diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.py b/erpnext/healthcare/doctype/lab_test/lab_test.py index 4e4015d2f09..ea8ce25c975 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/lab_test.py @@ -288,23 +288,23 @@ def insert_lab_test_to_medical_record(doc): table_row = False subject = cstr(doc.lab_test_name) if doc.practitioner: - subject += " "+ doc.practitioner + subject += frappe.bold(_("Healthcare Practitioner: "))+ doc.practitioner + "
" if doc.normal_test_items: item = doc.normal_test_items[0] comment = "" if item.lab_test_comment: comment = str(item.lab_test_comment) - table_row = item.lab_test_name + table_row = frappe.bold(_("Lab Test Conducted: ")) + item.lab_test_name if item.lab_test_event: - table_row += " " + item.lab_test_event + table_row += frappe.bold(_("Lab Test Event: ")) + item.lab_test_event if item.result_value: - table_row += " " + item.result_value + table_row += " " + frappe.bold(_("Lab Test Result: ")) + item.result_value if item.normal_range: - table_row += " normal_range("+item.normal_range+")" - table_row += " "+comment + table_row += " " + _("Normal Range:") + item.normal_range + table_row += " " + comment elif doc.special_test_items: item = doc.special_test_items[0] @@ -316,12 +316,12 @@ def insert_lab_test_to_medical_record(doc): item = doc.sensitivity_test_items[0] if item.antibiotic and item.antibiotic_sensitivity: - table_row = item.antibiotic +" "+ item.antibiotic_sensitivity + table_row = item.antibiotic + " " + item.antibiotic_sensitivity if table_row: - subject += "
"+table_row + subject += "
" + table_row if doc.lab_test_comment: - subject += "
"+ cstr(doc.lab_test_comment) + subject += "
" + cstr(doc.lab_test_comment) medical_record = frappe.new_doc("Patient Medical Record") medical_record.patient = doc.patient diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py index 767643bc73a..1734c28e52b 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py @@ -18,6 +18,9 @@ class PatientEncounter(Document): def after_insert(self): insert_encounter_to_medical_record(self) + def on_submit(self): + update_encounter_medical_record(self) + def on_cancel(self): if self.appointment: frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Open') @@ -66,22 +69,26 @@ def delete_medical_record(encounter): frappe.db.delete_doc_if_exists('Patient Medical Record', 'reference_name', encounter.name) def set_subject_field(encounter): - subject = encounter.practitioner + '\n' + subject = frappe.bold(_('Healthcare Practitioner: ')) + encounter.practitioner + '
' if encounter.symptoms: - subject += _('Symptoms: ') + cstr(encounter.symptoms) + '\n' + subject += frappe.bold(_('Symptoms: ')) + '
' + for entry in encounter.symptoms: + subject += cstr(entry.complaint) + '
' else: - subject += _('No Symptoms') + '\n' + subject += frappe.bold(_('No Symptoms')) + '
' if encounter.diagnosis: - subject += _('Diagnosis: ') + cstr(encounter.diagnosis) + '\n' + subject += frappe.bold(_('Diagnosis: ')) + '
' + for entry in encounter.diagnosis: + subject += cstr(entry.diagnosis) + '
' else: - subject += _('No Diagnosis') + '\n' + subject += frappe.bold(_('No Diagnosis')) + '
' if encounter.drug_prescription: - subject += '\n' + _('Drug(s) Prescribed.') + subject += '
' + _('Drug(s) Prescribed.') if encounter.lab_test_prescription: - subject += '\n' + _('Test(s) Prescribed.') + subject += '
' + _('Test(s) Prescribed.') if encounter.procedure_prescription: - subject += '\n' + _('Procedure(s) Prescribed.') + subject += '
' + _('Procedure(s) Prescribed.') return subject diff --git a/erpnext/healthcare/doctype/patient_medical_record/patient_medical_record.json b/erpnext/healthcare/doctype/patient_medical_record/patient_medical_record.json index 3655e24cb98..ed82355f33a 100644 --- a/erpnext/healthcare/doctype/patient_medical_record/patient_medical_record.json +++ b/erpnext/healthcare/doctype/patient_medical_record/patient_medical_record.json @@ -57,7 +57,7 @@ }, { "fieldname": "subject", - "fieldtype": "Small Text", + "fieldtype": "Text Editor", "ignore_xss_filter": 1, "label": "Subject" }, @@ -125,7 +125,7 @@ ], "in_create": 1, "links": [], - "modified": "2020-03-23 19:26:59.308383", + "modified": "2020-04-29 12:26:57.679402", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient Medical Record", diff --git a/erpnext/healthcare/doctype/vital_signs/vital_signs.py b/erpnext/healthcare/doctype/vital_signs/vital_signs.py index 959e8504c47..b0e78e8eb94 100644 --- a/erpnext/healthcare/doctype/vital_signs/vital_signs.py +++ b/erpnext/healthcare/doctype/vital_signs/vital_signs.py @@ -35,17 +35,17 @@ def delete_vital_signs_from_medical_record(doc): def set_subject_field(doc): subject = '' - if(doc.temperature): - subject += _('Temperature: ') + '\n'+ cstr(doc.temperature) + '. ' - if(doc.pulse): - subject += _('Pulse: ') + '\n' + cstr(doc.pulse) + '. ' - if(doc.respiratory_rate): - subject += _('Respiratory Rate: ') + '\n' + cstr(doc.respiratory_rate) + '. ' - if(doc.bp): - subject += _('BP: ') + '\n' + cstr(doc.bp) + '. ' - if(doc.bmi): - subject += _('BMI: ') + '\n' + cstr(doc.bmi) + '. ' - if(doc.nutrition_note): - subject += _('Note: ') + '\n' + cstr(doc.nutrition_note) + '. ' + if doc.temperature: + subject += frappe.bold(_('Temperature: ')) + cstr(doc.temperature) + '
' + if doc.pulse: + subject += frappe.bold(_('Pulse: ')) + cstr(doc.pulse) + '
' + if doc.respiratory_rate: + subject += frappe.bold(_('Respiratory Rate: ')) + cstr(doc.respiratory_rate) + '
' + if doc.bp: + subject += frappe.bold(_('BP: ')) + cstr(doc.bp) + '
' + if doc.bmi: + subject += frappe.bold(_('BMI: ')) + cstr(doc.bmi) + '
' + if doc.nutrition_note: + subject += frappe.bold(_('Note: ')) + cstr(doc.nutrition_note) + '
' return subject From 7835bf91e4cba9c4332bf084b9554c1b57ffb1c0 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Wed, 29 Apr 2020 13:09:16 +0530 Subject: [PATCH 44/58] fix: AttributeError --- erpnext/crm/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/utils.py b/erpnext/crm/utils.py index 72d778e9568..38bf79e5fcb 100644 --- a/erpnext/crm/utils.py +++ b/erpnext/crm/utils.py @@ -9,12 +9,12 @@ def update_lead_phone_numbers(contact, method): if len(contact.phone_nos) > 1: # get the default phone number - primary_phones = [phone.phone for phone in contact.phone_nos if phone.is_primary_phone] + primary_phones = [phone_doc.phone for phone_doc in contact.phone_nos if phone_doc.is_primary_phone] if primary_phones: phone = primary_phones[0] # get the default mobile number - primary_mobile_nos = [phone.phone for phone in contact.phone_nos if phone.is_primary_mobile_no] + primary_mobile_nos = [phone_doc.phone for phone_doc in contact.phone_nos if phone_doc.is_primary_mobile_no] if primary_mobile_nos: mobile_no = primary_mobile_nos[0] From 91e94587b612e82a8a1e12105b724e420295a982 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 29 Apr 2020 13:43:26 +0530 Subject: [PATCH 45/58] fix: Autoname for loan security --- .../doctype/loan_security/loan_security.json | 7 +++---- .../loan_management/doctype/loan_security/loan_security.py | 3 ++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_security/loan_security.json b/erpnext/loan_management/doctype/loan_security/loan_security.json index e879b17a43d..1d0bb309104 100644 --- a/erpnext/loan_management/doctype/loan_security/loan_security.json +++ b/erpnext/loan_management/doctype/loan_security/loan_security.json @@ -1,18 +1,17 @@ { "actions": [], "allow_rename": 1, - "autoname": "field:loan_security_name", "creation": "2019-09-02 15:07:08.885593", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ "loan_security_name", - "unit_of_measure", + "haircut", "loan_security_code", "column_break_3", "loan_security_type", - "haircut", + "unit_of_measure", "disabled" ], "fields": [ @@ -66,7 +65,7 @@ } ], "links": [], - "modified": "2020-04-28 14:07:54.506896", + "modified": "2020-04-29 13:21:26.043492", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security", diff --git a/erpnext/loan_management/doctype/loan_security/loan_security.py b/erpnext/loan_management/doctype/loan_security/loan_security.py index 800ad129571..8858c818362 100644 --- a/erpnext/loan_management/doctype/loan_security/loan_security.py +++ b/erpnext/loan_management/doctype/loan_security/loan_security.py @@ -7,4 +7,5 @@ from __future__ import unicode_literals from frappe.model.document import Document class LoanSecurity(Document): - pass + def autoname(self): + self.name = self.loan_security_name From 7a0f425674afff94c9942a2c261ddd23e253cbbb Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 29 Apr 2020 13:47:14 +0530 Subject: [PATCH 46/58] feat: create medical record for therapy sessions --- .../clinical_procedure/clinical_procedure.py | 2 +- .../doctype/therapy_plan/therapy_plan.py | 10 ++++- .../therapy_session/therapy_session.js | 11 ++--- .../therapy_session/therapy_session.json | 26 +++++++++++- .../therapy_session/therapy_session.py | 40 ++++++++++++++++++- 5 files changed, 76 insertions(+), 13 deletions(-) diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py index 297f1b9f4cf..b7d7a62a950 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py +++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py @@ -246,7 +246,7 @@ def make_procedure(source_name, target_doc=None): def insert_clinical_procedure_to_medical_record(doc): - subject = cstr(doc.procedure_template) + subject = frappe.bold(_("Clinical Procedure conducted: ")) + cstr(doc.procedure_template) + "
" if doc.practitioner: subject += frappe.bold(_('Healthcare Practitioner: ')) + doc.practitioner if subject and doc.notes: diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py index 201264f8294..c19be17ba8d 100644 --- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py +++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py @@ -21,8 +21,14 @@ class TherapyPlan(Document): self.status = 'Completed' def set_totals(self): - total_sessions = sum([int(d.no_of_sessions) for d in self.get('therapy_plan_details')]) - total_sessions_completed = sum([int(d.sessions_completed) for d in self.get('therapy_plan_details')]) + total_sessions = 0 + total_sessions_completed = 0 + for entry in self.therapy_plan_details: + if entry.no_of_sessions: + total_sessions += entry.no_of_sessions + if entry.sessions_completed: + total_sessions_completed += entry.sessions_completed + self.db_set('total_sessions', total_sessions) self.db_set('total_sessions_completed', total_sessions_completed) diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.js b/erpnext/healthcare/doctype/therapy_session/therapy_session.js index bb675752bbc..80fca39661f 100644 --- a/erpnext/healthcare/doctype/therapy_session/therapy_session.js +++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.js @@ -13,14 +13,9 @@ frappe.ui.form.on('Therapy Session', { refresh: function(frm) { if (!frm.doc.__islocal) { - let target = 0; - let completed = 0; - $.each(frm.doc.exercises, function(_i, e) { - target += e.counts_target; - completed += e.counts_completed; - }); - frm.dashboard.add_indicator(__('Counts Targetted: {0}', [target]), 'blue'); - frm.dashboard.add_indicator(__('Counts Completed: {0}', [completed]), (completed < target) ? 'orange' : 'green'); + frm.dashboard.add_indicator(__('Counts Targeted: {0}', [frm.doc.total_counts_targeted]), 'blue'); + frm.dashboard.add_indicator(__('Counts Completed: {0}', [frm.doc.total_counts_completed]), + (frm.doc.total_counts_completed < frm.doc.total_counts_targeted) ? 'orange' : 'green'); } if (frm.doc.docstatus === 1) { diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.json b/erpnext/healthcare/doctype/therapy_session/therapy_session.json index 5ff719672fb..d8c4b994357 100644 --- a/erpnext/healthcare/doctype/therapy_session/therapy_session.json +++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.json @@ -28,6 +28,10 @@ "invoiced", "exercises_section", "exercises", + "section_break_23", + "total_counts_targeted", + "column_break_25", + "total_counts_completed", "amended_from" ], "fields": [ @@ -173,11 +177,31 @@ "fieldtype": "Data", "label": "Patient Age", "read_only": 1 + }, + { + "fieldname": "total_counts_targeted", + "fieldtype": "Int", + "label": "Total Counts Targeted", + "read_only": 1 + }, + { + "fieldname": "total_counts_completed", + "fieldtype": "Int", + "label": "Total Counts Completed", + "read_only": 1 + }, + { + "fieldname": "section_break_23", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_25", + "fieldtype": "Column Break" } ], "is_submittable": 1, "links": [], - "modified": "2020-04-21 13:16:46.378798", + "modified": "2020-04-29 13:22:13.190353", "modified_by": "Administrator", "module": "Healthcare", "name": "Therapy Session", diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.py b/erpnext/healthcare/doctype/therapy_session/therapy_session.py index 45d2ee60e61..7e240955cf4 100644 --- a/erpnext/healthcare/doctype/therapy_session/therapy_session.py +++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.py @@ -6,10 +6,16 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc +from frappe import _ +from frappe.utils import cstr class TherapySession(Document): + def validate(self): + self.set_total_counts() + def on_submit(self): self.update_sessions_count_in_therapy_plan() + insert_session_medical_record(self) def on_cancel(self): self.update_sessions_count_in_therapy_plan(on_cancel=True) @@ -24,6 +30,18 @@ class TherapySession(Document): entry.sessions_completed += 1 therapy_plan.save() + def set_total_counts(self): + target_total = 0 + counts_completed = 0 + for entry in self.exercises: + if entry.counts_target: + target_total += entry.counts_target + if entry.counts_completed: + counts_completed += entry.counts_completed + + self.db_set('total_counts_targeted', target_total) + self.db_set('total_counts_completed', counts_completed) + @frappe.whitelist() def create_therapy_session(source_name, target_doc=None): @@ -52,4 +70,24 @@ def create_therapy_session(source_name, target_doc=None): } }, target_doc, set_missing_values) - return doc \ No newline at end of file + return doc + + +def insert_session_medical_record(doc): + subject = frappe.bold(_('Therapy: ')) + cstr(doc.therapy_type) + '
' + if doc.therapy_plan: + subject += frappe.bold(_('Therapy Plan: ')) + cstr(doc.therapy_plan) + '
' + if doc.practitioner: + subject += frappe.bold(_('Healthcare Practitioner: ')) + doc.practitioner + subject += frappe.bold(_('Total Counts Targeted: ')) + cstr(doc.total_counts_targeted) + '
' + subject += frappe.bold(_('Total Counts Completed: ')) + cstr(doc.total_counts_completed) + '
' + + medical_record = frappe.new_doc('Patient Medical Record') + medical_record.patient = doc.patient + medical_record.subject = subject + medical_record.status = 'Open' + medical_record.communication_date = doc.start_date + medical_record.reference_doctype = 'Therapy Session' + medical_record.reference_name = doc.name + medical_record.reference_owner = doc.owner + medical_record.save(ignore_permissions=True) \ No newline at end of file From 706f6b0030b2ddeb5873cfa6f154b27c2aa871b4 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 29 Apr 2020 14:19:27 +0530 Subject: [PATCH 47/58] fix: mark form as dirty when editing or deleting exercise card --- .../doctype/exercise_type/exercise_type.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/erpnext/healthcare/doctype/exercise_type/exercise_type.js b/erpnext/healthcare/doctype/exercise_type/exercise_type.js index f450c9bccb0..ff99dc2d727 100644 --- a/erpnext/healthcare/doctype/exercise_type/exercise_type.js +++ b/erpnext/healthcare/doctype/exercise_type/exercise_type.js @@ -24,6 +24,8 @@ erpnext.ExerciseEditor = Class.extend({ this.exercise_cards = $('
').appendTo(this.wrapper); + this.row = $('
').appendTo(this.exercise_cards); + let me = this; this.exercise_toolbar.find(".btn-add") @@ -32,7 +34,7 @@ erpnext.ExerciseEditor = Class.extend({ me.show_add_card_dialog(frm); }); - if (frm.doc.steps_table.length > 0) { + if (frm.doc.steps_table && frm.doc.steps_table.length > 0) { this.make_cards(frm); this.make_buttons(frm); } @@ -41,7 +43,6 @@ erpnext.ExerciseEditor = Class.extend({ make_cards: function(frm) { var me = this; $(me.exercise_cards).empty(); - this.row = $('
').appendTo(me.exercise_cards); $.each(frm.doc.steps_table, function(i, step) { $(repl(` @@ -78,6 +79,7 @@ erpnext.ExerciseEditor = Class.extend({ frm.doc.steps_table.pop(id); frm.refresh_field('steps_table'); $('#col-'+id).remove(); + frm.dirty(); }, 300); }); }, @@ -106,7 +108,10 @@ erpnext.ExerciseEditor = Class.extend({ ], primary_action: function() { let data = d.get_values(); - let i = frm.doc.steps_table.length; + let i = 0; + if (frm.doc.steps_table) { + i = frm.doc.steps_table.length; + } $(repl(`
@@ -165,9 +170,10 @@ erpnext.ExerciseEditor = Class.extend({ frm.doc.steps_table[id].image = data.image; frm.doc.steps_table[id].description = data.step_description; refresh_field('steps_table'); + frm.dirty(); new_dialog.hide(); }, - primary_action_label: __("Save"), + primary_action_label: __("Edit"), }); new_dialog.set_values({ From b4b3872ff27311bcf346859a4a531d43a880e7e2 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 29 Apr 2020 17:34:36 +0530 Subject: [PATCH 48/58] feat: add create Sales Invoice option from Therapy Session --- .../therapy_session/therapy_session.js | 78 ++++++++++++++++++- .../therapy_session/therapy_session.json | 15 +++- .../therapy_session/therapy_session.py | 41 +++++++++- 3 files changed, 129 insertions(+), 5 deletions(-) diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.js b/erpnext/healthcare/doctype/therapy_session/therapy_session.js index 80fca39661f..abe4defaf93 100644 --- a/erpnext/healthcare/doctype/therapy_session/therapy_session.js +++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.js @@ -19,12 +19,86 @@ frappe.ui.form.on('Therapy Session', { } if (frm.doc.docstatus === 1) { - frm.add_custom_button(__('Patient Assessment'),function() { + frm.add_custom_button(__('Patient Assessment'), function() { frappe.model.open_mapped_doc({ method: 'erpnext.healthcare.doctype.patient_assessment.patient_assessment.create_patient_assessment', frm: frm, }) }, 'Create'); + + frm.add_custom_button(__('Sales Invoice'), function() { + frappe.model.open_mapped_doc({ + method: 'erpnext.healthcare.doctype.therapy_session.therapy_session.invoice_therapy_session', + frm: frm, + }) + }, 'Create'); + } + }, + + patient: function(frm) { + if (frm.doc.patient) { + frappe.call({ + 'method': 'erpnext.healthcare.doctype.patient.patient.get_patient_detail', + args: { + patient: frm.doc.patient + }, + callback: function (data) { + let age = ''; + if (data.message.dob) { + age = calculate_age(data.message.dob); + } else if (data.message.age) { + age = data.message.age; + if (data.message.age_as_on) { + age = __('{0} as on {1}', [age, data.message.age_as_on]); + } + } + frm.set_value('patient_age', age); + frm.set_value('gender', data.message.sex); + frm.set_value('patient_name', data.message.patient_name); + } + }); + } else { + frm.set_value('patient_age', ''); + frm.set_value('gender', ''); + frm.set_value('patient_name', ''); + } + }, + + appointment: function(frm) { + if (frm.doc.appointment) { + frappe.call({ + 'method': 'frappe.client.get', + args: { + doctype: 'Patient Appointment', + name: frm.doc.appointment + }, + callback: function(data) { + let values = { + 'patient':data.message.patient, + 'therapy_type': data.message.therapy_type, + 'therapy_plan': data.message.therapy_plan, + 'practitioner': data.message.practitioner, + 'department': data.message.department, + 'start_date': data.message.appointment_date, + 'start_time': data.message.appointment_time, + 'service_unit': data.message.service_unit, + 'company': data.message.company + }; + frm.set_value(values); + } + }); + } else { + let values = { + 'patient': '', + 'therapy_type': '', + 'therapy_plan': '', + 'practitioner': '', + 'department': '', + 'start_date': '', + 'start_time': '', + 'service_unit': '', + }; + frm.set_value(values); } }, @@ -39,6 +113,8 @@ frappe.ui.form.on('Therapy Session', { callback: function(data) { frm.set_value('duration', data.message.default_duration); frm.set_value('rate', data.message.rate); + frm.set_value('service_unit', data.message.healthcare_service_unit); + frm.set_value('department', data.message.medical_department); frm.doc.exercises = []; $.each(data.message.exercises, function(_i, e) { let exercise = frm.add_child('exercises'); diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.json b/erpnext/healthcare/doctype/therapy_session/therapy_session.json index d8c4b994357..00d74a09495 100644 --- a/erpnext/healthcare/doctype/therapy_session/therapy_session.json +++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.json @@ -9,9 +9,11 @@ "naming_series", "appointment", "patient", + "patient_name", "patient_age", "gender", "column_break_5", + "company", "therapy_plan", "therapy_type", "practitioner", @@ -20,7 +22,6 @@ "duration", "rate", "location", - "company", "column_break_12", "service_unit", "start_date", @@ -163,7 +164,8 @@ "fieldname": "company", "fieldtype": "Link", "label": "Company", - "options": "Company" + "options": "Company", + "reqd": 1 }, { "default": "0", @@ -197,11 +199,18 @@ { "fieldname": "column_break_25", "fieldtype": "Column Break" + }, + { + "fetch_from": "patient.patient_name", + "fieldname": "patient_name", + "fieldtype": "Data", + "label": "Patient Name", + "read_only": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-04-29 13:22:13.190353", + "modified": "2020-04-29 16:49:16.286006", "modified_by": "Administrator", "module": "Healthcare", "name": "Therapy Session", diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.py b/erpnext/healthcare/doctype/therapy_session/therapy_session.py index 7e240955cf4..96501837120 100644 --- a/erpnext/healthcare/doctype/therapy_session/therapy_session.py +++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.py @@ -7,7 +7,8 @@ import frappe from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc from frappe import _ -from frappe.utils import cstr +from frappe.utils import cstr, getdate +from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account, get_income_account class TherapySession(Document): def validate(self): @@ -73,6 +74,44 @@ def create_therapy_session(source_name, target_doc=None): return doc +@frappe.whitelist() +def invoice_therapy_session(source_name, target_doc=None): + def set_missing_values(source, target): + target.customer = frappe.db.get_value('Patient', source.patient, 'customer') + target.due_date = getdate() + target.debit_to = get_receivable_account(source.company) + item = target.append('items', {}) + item = get_therapy_item(source, item) + target.set_missing_values(for_validate=True) + + doc = get_mapped_doc('Therapy Session', source_name, { + 'Therapy Session': { + 'doctype': 'Sales Invoice', + 'field_map': [ + ['patient', 'patient'], + ['referring_practitioner', 'practitioner'], + ['company', 'company'], + ['due_date', 'start_date'] + ] + } + }, target_doc, set_missing_values) + + return doc + + +def get_therapy_item(therapy, item): + item.item_code = frappe.db.get_value('Therapy Type', therapy.therapy_type, 'item') + item.description = _('Therapy Session Charges: {0}').format(therapy.practitioner) + item.income_account = get_income_account(therapy.practitioner, therapy.company) + item.cost_center = frappe.get_cached_value('Company', therapy.company, 'cost_center') + item.rate = therapy.rate + item.amount = therapy.rate + item.qty = 1 + item.reference_dt = 'Therapy Session' + item.reference_dn = therapy.name + return item + + def insert_session_medical_record(doc): subject = frappe.bold(_('Therapy: ')) + cstr(doc.therapy_type) + '
' if doc.therapy_plan: From 298c7e5be5cf8ef2795728d344aa12e21b703e97 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 29 Apr 2020 19:23:16 +0530 Subject: [PATCH 49/58] fix: exercise type --- erpnext/healthcare/doctype/exercise_type/exercise_type.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/healthcare/doctype/exercise_type/exercise_type.js b/erpnext/healthcare/doctype/exercise_type/exercise_type.js index ff99dc2d727..68db0477c2d 100644 --- a/erpnext/healthcare/doctype/exercise_type/exercise_type.js +++ b/erpnext/healthcare/doctype/exercise_type/exercise_type.js @@ -24,7 +24,7 @@ erpnext.ExerciseEditor = Class.extend({ this.exercise_cards = $('
').appendTo(this.wrapper); - this.row = $('
').appendTo(this.exercise_cards); + this.row = $('
').appendTo(this.wrapper); let me = this; From 0a6450115f139fc4337ce175eeac891a43722980 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 29 Apr 2020 22:14:45 +0530 Subject: [PATCH 50/58] fix: get_period_list API change fixes --- erpnext/hr/report/vehicle_expenses/vehicle_expenses.py | 3 ++- .../item_group_wise_sales_target_variance.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py index e5622b7ae12..eab58ffbbcf 100644 --- a/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py +++ b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py @@ -12,7 +12,8 @@ def execute(filters=None): columns, data, chart = [], [], [] if filters.get('fiscal_year'): company = erpnext.get_default_company() - period_list = get_period_list(filters.get('fiscal_year'), filters.get('fiscal_year'),"Monthly", company) + period_list = get_period_list(filters.get('fiscal_year'), filters.get('fiscal_year'), + '', '', 'Fiscal Year', 'Monthly', company=company) columns=get_columns() data=get_log_data(filters) chart=get_chart_data(data,period_list) diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py index 0cb606b2777..2d0d60ac06f 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py @@ -11,9 +11,10 @@ from erpnext.accounts.doctype.monthly_distribution.monthly_distribution import g def get_data_column(filters, partner_doctype): data = [] - period_list = get_period_list(filters.fiscal_year, filters.fiscal_year, - filters.period, company=filters.company) + period_list = get_period_list(filters.fiscal_year, filters.fiscal_year, '', '', + 'Fiscal Year', filters.period, company=filters.company) + print(period_list) rows = get_data(filters, period_list, partner_doctype) columns = get_columns(filters, period_list, partner_doctype) From 8113294d6e08d4c03c9f471a89c560335ebab573 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 29 Apr 2020 22:16:47 +0530 Subject: [PATCH 51/58] fix: Remove print --- .../item_group_wise_sales_target_variance.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py index 2d0d60ac06f..857b9823e03 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py @@ -14,7 +14,6 @@ def get_data_column(filters, partner_doctype): period_list = get_period_list(filters.fiscal_year, filters.fiscal_year, '', '', 'Fiscal Year', filters.period, company=filters.company) - print(period_list) rows = get_data(filters, period_list, partner_doctype) columns = get_columns(filters, period_list, partner_doctype) From 2a9c5badc6da3514706d98f5684e81f95cb09a04 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Thu, 30 Apr 2020 10:38:58 +0530 Subject: [PATCH 52/58] feat: Immutable ledger (#18740) * fix: Reverse GL entry on cancellation of document * fix: Removed set posting time field for multiple docs * fix: Stop future reposting and reverse entry for purchase receipt and delivery note * fix: Change is_cancelled field from select to check * Revert "fix: Removed set posting time field for multiple docs" This reverts commit 81fb808db7da69ab1e58ba226c0cd0b77e5b90c8. * fix: Multiple fixes in GL Entry * fix: Remove future reporting from doctypes * fix: Canceled entry filters in Stock Ledger and General Ledger Report * fix: Remove print statement * fix: Validation for back dated entries * fix: Codacy fixes * fix: Add ignore links to multiple doctypes * fix: Codacy Fixes * fix: Ignore GL Entry and Stock Ledger entry while cancel * fix: Test case fixes * fix: Patch * fix: Codacy * fix: Budget Test Cases * fix: Patch * fix: Patch * fix: Multiple test cases * fix: changes in make_reverse_entry function * fix: Update patch * fix: Test Cases * fix: Test Case fixes * fix: Move patch upward in patches.txt * fix: Budget Test Cases * fix: Test Case and codacy * fix: Patch * fix: Minor label and UX fixes * fix: Move freezing date check * fix: Test Cases * fix: Test cases * fix: Test Cases * fix: Test Case * fix: Remove update_gl_entries_after function * fix: Remove update_gl_entries_after function * fix: Test Cases * fix: Fiscal Year wise backdated entry * fix: Update entries only for current SLE * fix: Remove is_cancelled * fix: Test Cases * fix: Test cases * fix: Test Cases * fix: Uncomment account and stock balance sync logic * fix: Stock balance and Account balance out of sync fixes * fix: Test Cases * fix: Test cases for POS, Stock Reco and Purchase Receipt * fix: Stock Reco tests * fix: Test stock reco precision * fix: Test stock reco for fifo precision * fix: Test stock reco for fifo precision * fix: Stock Entry test case Co-authored-by: Nabin Hait --- .../accounts/doctype/budget/test_budget.py | 95 ++++++----- .../accounts/doctype/gl_entry/gl_entry.json | 11 +- erpnext/accounts/doctype/gl_entry/gl_entry.py | 31 +--- .../doctype/journal_entry/journal_entry.py | 3 +- .../doctype/payment_entry/payment_entry.py | 3 +- .../payment_entry/test_payment_entry.py | 4 - .../period_closing_voucher.js | 8 +- .../period_closing_voucher.py | 5 +- .../purchase_invoice/purchase_invoice.js | 10 +- .../purchase_invoice/purchase_invoice.py | 19 +-- .../purchase_invoice/test_records.json | 8 +- .../doctype/sales_invoice/sales_invoice.js | 62 +++---- .../doctype/sales_invoice/sales_invoice.py | 21 ++- .../sales_invoice/test_sales_invoice.py | 37 +++-- erpnext/accounts/general_ledger.py | 152 +++++++++++------- .../report/general_ledger/general_ledger.js | 8 +- .../report/general_ledger/general_ledger.py | 3 + erpnext/accounts/utils.py | 58 ++++++- erpnext/assets/doctype/asset/asset.py | 5 +- erpnext/assets/doctype/asset/test_asset.py | 3 - erpnext/controllers/accounts_controller.py | 7 +- erpnext/controllers/stock_controller.py | 80 ++------- erpnext/education/doctype/fees/fees.py | 5 +- .../hr/doctype/expense_claim/expense_claim.py | 14 +- .../production_plan/test_production_plan.py | 8 +- erpnext/patches.txt | 3 +- ...r_purchase_receipts_with_rejected_items.py | 4 +- erpnext/patches/v10_0/taxes_issue_with_pos.py | 2 +- .../patches/v12_0/set_total_batch_quantity.py | 2 +- .../v12_0/update_is_cancelled_field.py | 15 ++ .../fix_gl_entries_for_stock_transactions.py | 4 +- ...ost_valuation_rate_for_serialized_items.py | 28 ---- .../repost_future_gle_for_purchase_invoice.py | 24 --- .../repost_gle_for_pi_with_update_stock.py | 12 +- .../public/js/controllers/stock_controller.js | 4 +- .../doctype/sales_order/test_sales_order.py | 7 +- erpnext/stock/doctype/bin/bin.py | 9 +- .../doctype/delivery_note/delivery_note.js | 2 +- .../doctype/delivery_note/delivery_note.py | 1 + .../delivery_note/test_delivery_note.py | 95 +++++------ .../landed_cost_voucher.py | 2 +- .../test_landed_cost_voucher.py | 35 ++-- .../purchase_receipt/purchase_receipt.js | 2 +- .../purchase_receipt/purchase_receipt.py | 1 + .../purchase_receipt/test_purchase_receipt.py | 59 +++---- .../purchase_receipt/test_records.json | 32 ++++ erpnext/stock/doctype/serial_no/serial_no.py | 34 ++-- .../stock/doctype/stock_entry/stock_entry.py | 7 +- .../doctype/stock_entry/test_records.json | 2 - .../doctype/stock_entry/test_stock_entry.py | 19 +-- .../stock_ledger_entry.json | 9 +- .../stock_ledger_entry/stock_ledger_entry.py | 4 +- .../stock_reconciliation.js | 2 +- .../stock_reconciliation.py | 55 +++---- .../test_stock_reconciliation.py | 40 ++--- .../stock/report/stock_ledger/stock_ledger.js | 13 +- .../stock/report/stock_ledger/stock_ledger.py | 3 + erpnext/stock/stock_balance.py | 24 +-- erpnext/stock/stock_ledger.py | 71 +++++--- erpnext/stock/utils.py | 14 +- erpnext/utilities/transaction_base.py | 29 +++- 61 files changed, 689 insertions(+), 640 deletions(-) create mode 100644 erpnext/patches/v12_0/update_is_cancelled_field.py delete mode 100644 erpnext/patches/v6_24/repost_valuation_rate_for_serialized_items.py delete mode 100644 erpnext/patches/v7_0/repost_future_gle_for_purchase_invoice.py diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py index 9c19791d29a..61c48c74990 100644 --- a/erpnext/accounts/doctype/budget/test_budget.py +++ b/erpnext/accounts/doctype/budget/test_budget.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.utils import nowdate +from frappe.utils import nowdate, now_datetime from erpnext.accounts.utils import get_fiscal_year from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.accounts.doctype.budget.budget import get_actual_expense, BudgetError @@ -13,27 +13,28 @@ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journ class TestBudget(unittest.TestCase): def test_monthly_budget_crossed_ignore(self): - set_total_expense_zero("2013-02-28", "cost_center") + set_total_expense_zero(nowdate(), "cost_center") budget = make_budget(budget_against="Cost Center") jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date="2013-02-28", submit=True) + "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True) self.assertTrue(frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name})) budget.cancel() + jv.cancel() def test_monthly_budget_crossed_stop1(self): - set_total_expense_zero("2013-02-28", "cost_center") + set_total_expense_zero(nowdate(), "cost_center") budget = make_budget(budget_against="Cost Center") frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date="2013-02-28") + "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date=nowdate()) self.assertRaises(BudgetError, jv.submit) @@ -41,14 +42,14 @@ class TestBudget(unittest.TestCase): budget.cancel() def test_exception_approver_role(self): - set_total_expense_zero("2013-02-28", "cost_center") + set_total_expense_zero(nowdate(), "cost_center") budget = make_budget(budget_against="Cost Center") frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date="2013-03-02") + "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date=nowdate()) self.assertRaises(BudgetError, jv.submit) @@ -112,16 +113,17 @@ class TestBudget(unittest.TestCase): budget.load_from_db() budget.cancel() + po.cancel() def test_monthly_budget_crossed_stop2(self): - set_total_expense_zero("2013-02-28", "project") + set_total_expense_zero(nowdate(), "project") budget = make_budget(budget_against="Project") frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project="_Test Project", posting_date="2013-02-28") + "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project="_Test Project", posting_date=nowdate()) self.assertRaises(BudgetError, jv.submit) @@ -129,86 +131,76 @@ class TestBudget(unittest.TestCase): budget.cancel() def test_yearly_budget_crossed_stop1(self): - set_total_expense_zero("2013-02-28", "cost_center") + set_total_expense_zero(nowdate(), "cost_center") budget = make_budget(budget_against="Cost Center") jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 150000, "_Test Cost Center - _TC", posting_date="2013-03-28") + "_Test Bank - _TC", 250000, "_Test Cost Center - _TC", posting_date=nowdate()) self.assertRaises(BudgetError, jv.submit) budget.cancel() def test_yearly_budget_crossed_stop2(self): - set_total_expense_zero("2013-02-28", "project") + set_total_expense_zero(nowdate(), "project") budget = make_budget(budget_against="Project") jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 150000, "_Test Cost Center - _TC", project="_Test Project", posting_date="2013-03-28") + "_Test Bank - _TC", 250000, "_Test Cost Center - _TC", project="_Test Project", posting_date=nowdate()) self.assertRaises(BudgetError, jv.submit) budget.cancel() def test_monthly_budget_on_cancellation1(self): - set_total_expense_zero("2013-02-28", "cost_center") + set_total_expense_zero(nowdate(), "cost_center") budget = make_budget(budget_against="Cost Center") - jv1 = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date="2013-02-28", submit=True) + for i in range(now_datetime().month): + jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", + "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True) - self.assertTrue(frappe.db.get_value("GL Entry", - {"voucher_type": "Journal Entry", "voucher_no": jv1.name})) - - jv2 = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date="2013-02-28", submit=True) - - self.assertTrue(frappe.db.get_value("GL Entry", - {"voucher_type": "Journal Entry", "voucher_no": jv2.name})) + self.assertTrue(frappe.db.get_value("GL Entry", + {"voucher_type": "Journal Entry", "voucher_no": jv.name})) frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") - self.assertRaises(BudgetError, jv1.cancel) + self.assertRaises(BudgetError, jv.cancel) budget.load_from_db() budget.cancel() def test_monthly_budget_on_cancellation2(self): - set_total_expense_zero("2013-02-28", "project") + set_total_expense_zero(nowdate(), "project") budget = make_budget(budget_against="Project") - jv1 = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date="2013-02-28", submit=True, project="_Test Project") + for i in range(now_datetime().month): + jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", + "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, project="_Test Project") - self.assertTrue(frappe.db.get_value("GL Entry", - {"voucher_type": "Journal Entry", "voucher_no": jv1.name})) - - jv2 = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date="2013-02-28", submit=True, project="_Test Project") - - self.assertTrue(frappe.db.get_value("GL Entry", - {"voucher_type": "Journal Entry", "voucher_no": jv2.name})) + self.assertTrue(frappe.db.get_value("GL Entry", + {"voucher_type": "Journal Entry", "voucher_no": jv.name})) frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") - self.assertRaises(BudgetError, jv1.cancel) + self.assertRaises(BudgetError, jv.cancel) budget.load_from_db() budget.cancel() def test_monthly_budget_against_group_cost_center(self): - set_total_expense_zero("2013-02-28", "cost_center") - set_total_expense_zero("2013-02-28", "cost_center", "_Test Cost Center 2 - _TC") + set_total_expense_zero(nowdate(), "cost_center") + set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center 2 - _TC") budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC") frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 40000, "_Test Cost Center 2 - _TC", posting_date="2013-02-28") + "_Test Bank - _TC", 40000, "_Test Cost Center 2 - _TC", posting_date=nowdate()) self.assertRaises(BudgetError, jv.submit) @@ -231,7 +223,7 @@ class TestBudget(unittest.TestCase): frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 40000, cost_center, posting_date="2013-02-28") + "_Test Bank - _TC", 40000, cost_center, posting_date=nowdate()) self.assertRaises(BudgetError, jv.submit) @@ -246,12 +238,14 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again else: budget_against = budget_against_CC or "_Test Cost Center - _TC" + fiscal_year = get_fiscal_year(nowdate())[0] + args = frappe._dict({ "account": "_Test Account Cost for Goods Sold - _TC", "cost_center": "_Test Cost Center - _TC", "monthly_end_date": posting_date, "company": "_Test Company", - "fiscal_year": "_Test Fiscal Year 2013", + "fiscal_year": fiscal_year, "budget_against_field": budget_against_field, }) @@ -263,10 +257,10 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again if existing_expense: if budget_against_field == "cost_center": make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", posting_date="2013-02-28", submit=True) + "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True) elif budget_against_field == "project": make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project="_Test Project", posting_date="2013-02-28") + "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project="_Test Project", posting_date=nowdate()) def make_budget(**args): args = frappe._dict(args) @@ -274,10 +268,13 @@ def make_budget(**args): budget_against=args.budget_against cost_center=args.cost_center + fiscal_year = get_fiscal_year(nowdate())[0] + if budget_against == "Project": - budget_list = frappe.get_all("Budget", fields=["name"], filters = {"name": ("like", "_Test Project/_Test Fiscal Year 2013%")}) + project_name = "{0}%".format("_Test Project/" + fiscal_year) + budget_list = frappe.get_all("Budget", fields=["name"], filters = {"name": ("like", project_name)}) else: - cost_center_name = "{0}%".format(cost_center or "_Test Cost Center - _TC/_Test Fiscal Year 2013") + cost_center_name = "{0}%".format(cost_center or "_Test Cost Center - _TC/" + fiscal_year) budget_list = frappe.get_all("Budget", fields=["name"], filters = {"name": ("like", cost_center_name)}) for d in budget_list: frappe.db.sql("delete from `tabBudget` where name = %(name)s", d) @@ -290,8 +287,10 @@ def make_budget(**args): else: budget.cost_center =cost_center or "_Test Cost Center - _TC" + monthly_distribution = frappe.get_doc("Monthly Distribution", "_Test Distribution") + monthly_distribution.fiscal_year = fiscal_year - budget.fiscal_year = "_Test Fiscal Year 2013" + budget.fiscal_year = fiscal_year budget.monthly_distribution = "_Test Distribution" budget.company = "_Test Company" budget.applicable_on_booking_actual_expenses = 1 @@ -300,7 +299,7 @@ def make_budget(**args): budget.budget_against = budget_against budget.append("accounts", { "account": "_Test Account Cost for Goods Sold - _TC", - "budget_amount": 100000 + "budget_amount": 200000 }) if args.applicable_on_material_request: diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.json b/erpnext/accounts/doctype/gl_entry/gl_entry.json index 2214811d8b3..0d753290395 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.json +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.json @@ -30,7 +30,8 @@ "company", "finance_book", "to_rename", - "due_date" + "due_date", + "is_cancelled" ], "fields": [ { @@ -245,12 +246,18 @@ "fieldname": "due_date", "fieldtype": "Date", "label": "Due Date" + }, + { + "default": "0", + "fieldname": "is_cancelled", + "fieldtype": "Check", + "label": "Is Cancelled" } ], "icon": "fa fa-list", "idx": 1, "in_create": 1, - "modified": "2020-03-28 16:22:33.766994", + "modified": "2020-04-07 16:22:33.766994", "modified_by": "Administrator", "module": "Accounts", "name": "GL Entry", diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 14d05312718..efab5801e8b 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -30,23 +30,20 @@ class GLEntry(Document): self.pl_must_have_cost_center() self.validate_cost_center() - if not self.flags.from_repost: - self.check_pl_account() - self.validate_party() - self.validate_currency() + self.check_pl_account() + self.validate_party() + self.validate_currency() - def on_update_with_args(self, adv_adj, update_outstanding = 'Yes', from_repost=False): - if not from_repost: - self.validate_account_details(adv_adj) - self.validate_dimensions_for_pl_and_bs() - check_freezing_date(self.posting_date, adv_adj) + def on_update_with_args(self, adv_adj, update_outstanding = 'Yes'): + self.validate_account_details(adv_adj) + self.validate_dimensions_for_pl_and_bs() validate_frozen_account(self.account, adv_adj) validate_balance_type(self.account, adv_adj) # Update outstanding amt on against voucher if self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] \ - and self.against_voucher and update_outstanding == 'Yes' and not from_repost: + and self.against_voucher and update_outstanding == 'Yes': update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher) @@ -159,7 +156,6 @@ class GLEntry(Document): if self.party_type and self.party: validate_party_gle_currency(self.party_type, self.party, self.company, self.account_currency) - def validate_and_set_fiscal_year(self): if not self.fiscal_year: self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0] @@ -176,19 +172,6 @@ def validate_balance_type(account, adv_adj=False): (balance_must_be=="Credit" and flt(balance) > 0): frappe.throw(_("Balance for Account {0} must always be {1}").format(account, _(balance_must_be))) -def check_freezing_date(posting_date, adv_adj=False): - """ - Nobody can do GL Entries where posting date is before freezing date - except authorized person - """ - if not adv_adj: - acc_frozen_upto = frappe.db.get_value('Accounts Settings', None, 'acc_frozen_upto') - if acc_frozen_upto: - frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,'frozen_accounts_modifier') - if getdate(posting_date) <= getdate(acc_frozen_upto) \ - and not frozen_accounts_modifier in frappe.get_roles(): - frappe.throw(_("You are not authorized to add or update entries before {0}").format(formatdate(acc_frozen_upto))) - def update_outstanding_amt(account, party_type, party, against_voucher_type, against_voucher, on_cancel=False): if party_type and party: party_condition = " and party_type={0} and party={1}"\ diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index eb3017a46b5..d6ffdb69ed2 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -57,6 +57,7 @@ class JournalEntry(AccountsController): from erpnext.hr.doctype.salary_slip.salary_slip import unlink_ref_doc_from_salary_slip unlink_ref_doc_from_payment_entries(self) unlink_ref_doc_from_salary_slip(self.name) + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') self.make_gl_entries(1) self.update_advance_paid() self.update_expense_claim() @@ -594,7 +595,7 @@ class JournalEntry(AccountsController): for d in self.accounts: if d.reference_type=="Expense Claim" and d.reference_name: doc = frappe.get_doc("Expense Claim", d.reference_name) - update_reimbursed_amount(doc) + update_reimbursed_amount(doc, jv=self.name) def validate_expense_claim(self): diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index b53e68ff735..8b7c0960334 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -75,6 +75,7 @@ class PaymentEntry(AccountsController): self.set_status() def on_cancel(self): + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') self.setup_party_account_field() self.make_gl_entries(cancel=1) self.update_outstanding_amounts() @@ -571,7 +572,7 @@ class PaymentEntry(AccountsController): for d in self.get("references"): if d.reference_doctype=="Expense Claim" and d.reference_name: doc = frappe.get_doc("Expense Claim", d.reference_name) - update_reimbursed_amount(doc) + update_reimbursed_amount(doc, self.name) def on_recurring(self, reference_doc, auto_repeat_doc): self.reference_no = reference_doc.name diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 4c7d933476f..8bb741f0b25 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -35,8 +35,6 @@ class TestPaymentEntry(unittest.TestCase): pe.cancel() - self.assertFalse(self.get_gle(pe.name)) - so_advance_paid = frappe.db.get_value("Sales Order", so.name, "advance_paid") self.assertEqual(so_advance_paid, 0) @@ -124,7 +122,6 @@ class TestPaymentEntry(unittest.TestCase): self.assertEqual(outstanding_amount, 0) pe.cancel() - self.assertFalse(self.get_gle(pe.name)) outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount")) self.assertEqual(outstanding_amount, 100) @@ -381,7 +378,6 @@ class TestPaymentEntry(unittest.TestCase): self.assertEqual(outstanding_amount, 0) pe3.cancel() - self.assertFalse(self.get_gle(pe3.name)) outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount")) self.assertEqual(outstanding_amount, -100) diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js index 03b8f932a9e..87e02fef1b1 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js @@ -5,7 +5,7 @@ frappe.ui.form.on('Period Closing Voucher', { onload: function(frm) { if (!frm.doc.transaction_date) frm.doc.transaction_date = frappe.datetime.obj_to_str(new Date()); }, - + setup: function(frm) { frm.set_query("closing_account_head", function() { return { @@ -18,9 +18,9 @@ frappe.ui.form.on('Period Closing Voucher', { } }); }, - + refresh: function(frm) { - if(frm.doc.docstatus==1) { + if(frm.doc.docstatus > 0) { frm.add_custom_button(__('Ledger'), function() { frappe.route_options = { "voucher_no": frm.doc.name, @@ -33,5 +33,5 @@ frappe.ui.form.on('Period Closing Voucher', { }, "fa fa-table"); } } - + }) diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index eb95e458dc8..0bd9a90b3ee 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -17,8 +17,9 @@ class PeriodClosingVoucher(AccountsController): self.make_gl_entries() def on_cancel(self): - frappe.db.sql("""delete from `tabGL Entry` - where voucher_type = 'Period Closing Voucher' and voucher_no=%s""", self.name) + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + from erpnext.accounts.general_ledger import make_reverse_gl_entries + make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name, cancel=True) def validate_account_head(self): closing_account_type = frappe.db.get_value("Account", self.closing_account_head, "root_type") diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 3cf4d5994a5..4f6be59c65e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -382,11 +382,6 @@ function hide_fields(doc) { cur_frm.refresh_fields(); } -cur_frm.cscript.update_stock = function(doc, dt, dn) { - hide_fields(doc, dt, dn); - this.frm.fields_dict.items.grid.toggle_reqd("item_code", doc.update_stock? true: false) -} - cur_frm.fields_dict.cash_bank_account.get_query = function(doc) { return { filters: [ @@ -528,5 +523,10 @@ frappe.ui.form.on("Purchase Invoice", { erpnext.buying.get_default_bom(frm); } frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted==="Yes"); + }, + + update_stock: function(frm) { + hide_fields(frm.doc); + frm.fields_dict.items.grid.toggle_reqd("item_code", frm.doc.update_stock? true: false); } }) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index b1ae194301c..3aa24df16d3 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -14,7 +14,7 @@ from erpnext.accounts.party import get_party_account, get_due_date from erpnext.accounts.utils import get_account_currency, get_fiscal_year from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billed_amount_based_on_po from erpnext.stock import get_warehouse_account_map -from erpnext.accounts.general_ledger import make_gl_entries, merge_similar_entries, delete_gl_entries +from erpnext.accounts.general_ledger import make_gl_entries, merge_similar_entries, make_reverse_gl_entries from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center @@ -382,7 +382,7 @@ class PurchaseInvoice(BuyingController): self.update_project() update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) - def make_gl_entries(self, gl_entries=None, repost_future_gle=True, from_repost=False): + def make_gl_entries(self, gl_entries=None): if not self.grand_total: return if not gl_entries: @@ -391,21 +391,17 @@ class PurchaseInvoice(BuyingController): if gl_entries: update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes" - make_gl_entries(gl_entries, cancel=(self.docstatus == 2), - update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost) + if self.docstatus == 1: + make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False) + elif self.docstatus == 2: + make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) if update_outstanding == "No": update_outstanding_amt(self.credit_to, "Supplier", self.supplier, self.doctype, self.return_against if cint(self.is_return) and self.return_against else self.name) - if (repost_future_gle or self.flags.repost_future_gle) and cint(self.update_stock) and self.auto_accounting_for_stock: - from erpnext.controllers.stock_controller import update_gl_entries_after - items, warehouses = self.get_items_and_warehouses() - update_gl_entries_after(self.posting_date, self.posting_time, - warehouses, items, company = self.company) - elif self.docstatus == 2 and cint(self.update_stock) and self.auto_accounting_for_stock: - delete_gl_entries(voucher_type=self.doctype, voucher_no=self.name) + make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) def get_gl_entries(self, warehouse_account=None): self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company) @@ -934,6 +930,7 @@ class PurchaseInvoice(BuyingController): frappe.db.set(self, 'status', 'Cancelled') unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') def update_project(self): project_list = [] diff --git a/erpnext/accounts/doctype/purchase_invoice/test_records.json b/erpnext/accounts/doctype/purchase_invoice/test_records.json index 171927c1822..7030faf2b73 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_records.json +++ b/erpnext/accounts/doctype/purchase_invoice/test_records.json @@ -138,13 +138,12 @@ "row_id": 7 } ], - "posting_date": "2013-02-03", "supplier": "_Test Supplier", "supplier_name": "_Test Supplier" }, - - - + + + { "bill_no": "NA", "buying_price_list": "_Test Price List", @@ -204,7 +203,6 @@ "tax_amount": 150.0 } ], - "posting_date": "2013-02-03", "supplier": "_Test Supplier", "supplier_name": "_Test Supplier" } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 60e41f95536..f248276e5bb 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -345,7 +345,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte set_dynamic_labels: function() { this._super(); - this.hide_fields(this.frm.doc); + this.frm.events.hide_fields(this.frm) }, items_on_form_rendered: function() { @@ -404,7 +404,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte if(r.message && r.message.print_format) { me.frm.pos_print_format = r.message.print_format; } - me.frm.script_manager.trigger("update_stock"); + me.frm.trigger("update_stock"); if(me.frm.doc.taxes_and_charges) { me.frm.script_manager.trigger("taxes_and_charges"); } @@ -446,35 +446,6 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte // for backward compatibility: combine new and previous states $.extend(cur_frm.cscript, new erpnext.accounts.SalesInvoiceController({frm: cur_frm})); -// Hide Fields -// ------------ -cur_frm.cscript.hide_fields = function(doc) { - var parent_fields = ['project', 'due_date', 'is_opening', 'source', 'total_advance', 'get_advances', - 'advances', 'from_date', 'to_date']; - - if(cint(doc.is_pos) == 1) { - hide_field(parent_fields); - } else { - for (var i in parent_fields) { - var docfield = frappe.meta.docfield_map[doc.doctype][parent_fields[i]]; - if(!docfield.hidden) unhide_field(parent_fields[i]); - } - } - - // India related fields - if (frappe.boot.sysdefaults.country == 'India') unhide_field(['c_form_applicable', 'c_form_no']); - else hide_field(['c_form_applicable', 'c_form_no']); - - this.frm.toggle_enable("write_off_amount", !!!cint(doc.write_off_outstanding_amount_automatically)); - - cur_frm.refresh_fields(); -} - -cur_frm.cscript.update_stock = function(doc, dt, dn) { - cur_frm.cscript.hide_fields(doc, dt, dn); - this.frm.fields_dict.items.grid.toggle_reqd("item_code", doc.update_stock? true: false) -} - cur_frm.cscript['Make Delivery Note'] = function() { frappe.model.open_mapped_doc({ method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_delivery_note", @@ -719,6 +690,12 @@ frappe.ui.form.on('Sales Invoice', { frm.redemption_conversion_factor = null; }, + update_stock: function(frm, dt, dn) { + frm.events.hide_fields(frm); + frm.fields_dict.items.grid.toggle_reqd("item_code", frm.doc.update_stock); + frm.trigger('reset_posting_time'); + }, + redeem_loyalty_points: function(frm) { frm.events.get_loyalty_details(frm); }, @@ -742,6 +719,29 @@ frappe.ui.form.on('Sales Invoice', { } }, + hide_fields: function(frm) { + let doc = frm.doc; + var parent_fields = ['project', 'due_date', 'is_opening', 'source', 'total_advance', 'get_advances', + 'advances', 'from_date', 'to_date']; + + if(cint(doc.is_pos) == 1) { + hide_field(parent_fields); + } else { + for (var i in parent_fields) { + var docfield = frappe.meta.docfield_map[doc.doctype][parent_fields[i]]; + if(!docfield.hidden) unhide_field(parent_fields[i]); + } + } + + // India related fields + if (frappe.boot.sysdefaults.country == 'India') unhide_field(['c_form_applicable', 'c_form_no']); + else hide_field(['c_form_applicable', 'c_form_no']); + + frm.toggle_enable("write_off_amount", !!!cint(doc.write_off_outstanding_amount_automatically)); + + frm.refresh_fields(); + }, + get_loyalty_details: function(frm) { if (frm.doc.customer && frm.doc.redeem_loyalty_points) { frappe.call({ diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 3c40112ae6f..3b0fade0e53 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -7,7 +7,6 @@ import frappe.defaults from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate from frappe import _, msgprint, throw from erpnext.accounts.party import get_party_account, get_due_date -from erpnext.controllers.stock_controller import update_gl_entries_after from frappe.model.mapper import get_mapped_doc from erpnext.accounts.doctype.sales_invoice.pos import update_multi_mode_option @@ -282,6 +281,8 @@ class SalesInvoice(SellingController): if "Healthcare" in active_domains: manage_invoice_submit_cancel(self, "on_cancel") + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + def update_status_updater_args(self): if cint(self.update_stock): self.status_updater.append({ @@ -717,7 +718,9 @@ class SalesInvoice(SellingController): if d.delivery_note and frappe.db.get_value("Delivery Note", d.delivery_note, "docstatus") != 1: throw(_("Delivery Note {0} is not submitted").format(d.delivery_note)) - def make_gl_entries(self, gl_entries=None, repost_future_gle=True, from_repost=False): + def make_gl_entries(self, gl_entries=None): + from erpnext.accounts.general_ledger import make_reverse_gl_entries + auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company) if not gl_entries: gl_entries = self.get_gl_entries() @@ -729,23 +732,19 @@ class SalesInvoice(SellingController): update_outstanding = "No" if (cint(self.is_pos) or self.write_off_account or cint(self.redeem_loyalty_points)) else "Yes" - make_gl_entries(gl_entries, cancel=(self.docstatus == 2), - update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost) + if self.docstatus == 1: + make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False) + elif self.docstatus == 2: + make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) if update_outstanding == "No": from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt update_outstanding_amt(self.debit_to, "Customer", self.customer, self.doctype, self.return_against if cint(self.is_return) and self.return_against else self.name) - if (repost_future_gle or self.flags.repost_future_gle) and cint(self.update_stock) \ - and cint(auto_accounting_for_stock): - items, warehouses = self.get_items_and_warehouses() - update_gl_entries_after(self.posting_date, self.posting_time, - warehouses, items, company = self.company) elif self.docstatus == 2 and cint(self.update_stock) \ and cint(auto_accounting_for_stock): - from erpnext.accounts.general_ledger import delete_gl_entries - delete_gl_entries(voucher_type=self.doctype, voucher_no=self.name) + make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) def get_gl_entries(self, warehouse_account=None): from erpnext.accounts.general_ledger import merge_similar_entries diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 88b54fec8f2..dd727a49b08 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -364,7 +364,7 @@ class TestSalesInvoice(unittest.TestCase): gle = frappe.db.sql("""select * from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s""", si.name) - self.assertFalse(gle) + self.assertTrue(gle) def test_tax_calculation_with_multiple_items(self): si = create_sales_invoice(qty=84, rate=4.6, do_not_save=True) @@ -678,14 +678,15 @@ class TestSalesInvoice(unittest.TestCase): gle = frappe.db.sql("""select * from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s""", si.name) - self.assertFalse(gle) + self.assertTrue(gle) def test_pos_gl_entry_with_perpetual_inventory(self): make_pos_profile() - pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",supplier_warehouse= "Work In Progress - TCP1", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1") + pr = make_purchase_receipt(company= "_Test Company with perpetual inventory", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1") - pos = create_sales_invoice(company= "_Test Company with perpetual inventory", debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", cost_center = "Main - TCP1", do_not_save=True) + pos = create_sales_invoice(company= "_Test Company with perpetual inventory", debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1", + income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", cost_center = "Main - TCP1", do_not_save=True) pos.is_pos = 1 pos.update_stock = 1 @@ -766,9 +767,13 @@ class TestSalesInvoice(unittest.TestCase): def test_pos_change_amount(self): make_pos_profile() - pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",supplier_warehouse= "Work In Progress - TCP1", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1") + pr = make_purchase_receipt(company= "_Test Company with perpetual inventory", + item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1") - pos = create_sales_invoice(company= "_Test Company with perpetual inventory", debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", cost_center = "Main - TCP1", do_not_save=True) + pos = create_sales_invoice(company= "_Test Company with perpetual inventory", + debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1", + income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", + cost_center = "Main - TCP1", do_not_save=True) pos.is_pos = 1 pos.update_stock = 1 @@ -787,8 +792,15 @@ class TestSalesInvoice(unittest.TestCase): from erpnext.accounts.doctype.sales_invoice.pos import make_invoice pos_profile = make_pos_profile() - pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",supplier_warehouse= "Work In Progress - TCP1", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1") - pos = create_sales_invoice(company= "_Test Company with perpetual inventory", debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", cost_center = "Main - TCP1", do_not_save=True) + + pr = make_purchase_receipt(company= "_Test Company with perpetual inventory", + item_code= "_Test FG Item", + warehouse= "Stores - TCP1", cost_center= "Main - TCP1") + + pos = create_sales_invoice(company= "_Test Company with perpetual inventory", + debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1", + income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", + cost_center = "Main - TCP1", do_not_save=True) pos.is_pos = 1 pos.update_stock = 1 @@ -891,11 +903,9 @@ class TestSalesInvoice(unittest.TestCase): gle = frappe.db.sql("""select * from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s""", si.name) - self.assertFalse(gle) - + self.assertTrue(gle) frappe.db.sql("delete from `tabPOS Profile`") - si.delete() def test_pos_si_without_payment(self): set_perpetual_inventory() @@ -1012,9 +1022,6 @@ class TestSalesInvoice(unittest.TestCase): si.cancel() - self.assertTrue(not frappe.db.sql("""select name from `tabJournal Entry Account` - where reference_name=%s""", si.name)) - def test_serialized(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -1230,7 +1237,7 @@ class TestSalesInvoice(unittest.TestCase): gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s""", si.name) - self.assertFalse(gle) + self.assertTrue(gle) def test_invalid_currency(self): # Customer currency = USD diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 5ba455c1315..fb1a4f4dba2 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import frappe, erpnext -from frappe.utils import flt, cstr, cint, comma_and +from frappe.utils import flt, cstr, cint, comma_and, today, getdate, formatdate, now from frappe import _ from erpnext.accounts.utils import get_stock_and_account_balance from frappe.model.meta import get_field_precision @@ -15,17 +15,17 @@ class ClosedAccountingPeriod(frappe.ValidationError): pass class StockAccountInvalidTransaction(frappe.ValidationError): pass class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): pass -def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes', from_repost=False): +def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes'): if gl_map: if not cancel: validate_accounting_period(gl_map) gl_map = process_gl_map(gl_map, merge_entries) if gl_map and len(gl_map) > 1: - save_entries(gl_map, adv_adj, update_outstanding, from_repost) + save_entries(gl_map, adv_adj, update_outstanding) else: frappe.throw(_("Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction.")) else: - delete_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding) + make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding) def validate_accounting_period(gl_map): accounting_periods = frappe.db.sql(""" SELECT @@ -119,33 +119,36 @@ def check_if_in_list(gle, gl_map, dimensions=None): if same_head: return e -def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): - if not from_repost: - validate_cwip_accounts(gl_map) +def save_entries(gl_map, adv_adj, update_outstanding): + validate_cwip_accounts(gl_map) round_off_debit_credit(gl_map) + + if gl_map: + check_freezing_date(gl_map[0]["posting_date"], adv_adj) + for entry in gl_map: - make_entry(entry, adv_adj, update_outstanding, from_repost) + make_entry(entry, adv_adj, update_outstanding) # check against budget - if not from_repost: - validate_expense_against_budget(entry) + validate_expense_against_budget(entry) - if not from_repost: - validate_account_for_perpetual_inventory(gl_map) + validate_account_for_perpetual_inventory(gl_map) -def make_entry(args, adv_adj, update_outstanding, from_repost=False): +def make_entry(args, adv_adj, update_outstanding): gle = frappe.new_doc("GL Entry") gle.update(args) gle.flags.ignore_permissions = 1 - gle.flags.from_repost = from_repost gle.validate() gle.db_insert() - gle.run_method("on_update_with_args", adv_adj, update_outstanding, from_repost) + gle.run_method("on_update_with_args", adv_adj, update_outstanding) gle.flags.ignore_validate = True gle.submit() + # check against budget + validate_expense_against_budget(args) + def validate_account_for_perpetual_inventory(gl_map): if cint(erpnext.is_perpetual_inventory_enabled(gl_map[0].company)): account_list = [gl_entries.account for gl_entries in gl_map] @@ -169,33 +172,33 @@ def validate_account_for_perpetual_inventory(gl_map): .format(account), StockAccountInvalidTransaction) # This has been comment for a temporary, will add this code again on release of immutable ledger - # elif account_bal != stock_bal: - # precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), - # currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency")) + elif account_bal != stock_bal: + precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), + currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency")) - # diff = flt(stock_bal - account_bal, precision) - # error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.").format( - # stock_bal, account_bal, frappe.bold(account)) - # error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(diff)) - # stock_adjustment_account = frappe.db.get_value("Company",gl_map[0].company,"stock_adjustment_account") + diff = flt(stock_bal - account_bal, precision) + error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.").format( + stock_bal, account_bal, frappe.bold(account)) + error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(diff)) + stock_adjustment_account = frappe.db.get_value("Company",gl_map[0].company,"stock_adjustment_account") - # db_or_cr_warehouse_account =('credit_in_account_currency' if diff < 0 else 'debit_in_account_currency') - # db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if diff < 0 else 'credit_in_account_currency') + db_or_cr_warehouse_account =('credit_in_account_currency' if diff < 0 else 'debit_in_account_currency') + db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if diff < 0 else 'credit_in_account_currency') - # journal_entry_args = { - # 'accounts':[ - # {'account': account, db_or_cr_warehouse_account : abs(diff)}, - # {'account': stock_adjustment_account, db_or_cr_stock_adjustment_account : abs(diff) }] - # } + journal_entry_args = { + 'accounts':[ + {'account': account, db_or_cr_warehouse_account : abs(diff)}, + {'account': stock_adjustment_account, db_or_cr_stock_adjustment_account : abs(diff) }] + } - # frappe.msgprint(msg="""{0}

{1}

""".format(error_reason, error_resolution), - # raise_exception=StockValueAndAccountBalanceOutOfSync, - # title=_('Values Out Of Sync'), - # primary_action={ - # 'label': _('Make Journal Entry'), - # 'client_action': 'erpnext.route_to_adjustment_jv', - # 'args': journal_entry_args - # }) + frappe.msgprint(msg="""{0}

{1}

""".format(error_reason, error_resolution), + raise_exception=StockValueAndAccountBalanceOutOfSync, + title=_('Values Out Of Sync'), + primary_action={ + 'label': _('Make Journal Entry'), + 'client_action': 'erpnext.route_to_adjustment_jv', + 'args': journal_entry_args + }) def validate_cwip_accounts(gl_map): cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")]) @@ -282,31 +285,64 @@ def get_round_off_account_and_cost_center(company): return round_off_account, round_off_cost_center -def delete_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None, - adv_adj=False, update_outstanding="Yes"): - - from erpnext.accounts.doctype.gl_entry.gl_entry import validate_balance_type, \ - check_freezing_date, update_outstanding_amt, validate_frozen_account +def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None, + adv_adj=False, update_outstanding="Yes"): + """ + Get original gl entries of the voucher + and make reverse gl entries by swapping debit and credit + """ if not gl_entries: - gl_entries = frappe.db.sql(""" - select account, posting_date, party_type, party, cost_center, fiscal_year,voucher_type, - voucher_no, against_voucher_type, against_voucher, cost_center, company - from `tabGL Entry` - where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no), as_dict=True) + gl_entries = frappe.get_all("GL Entry", + fields = ["*"], + filters = { + "voucher_type": voucher_type, + "voucher_no": voucher_no + }) if gl_entries: + set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no']) check_freezing_date(gl_entries[0]["posting_date"], adv_adj) - frappe.db.sql("""delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s""", - (voucher_type or gl_entries[0]["voucher_type"], voucher_no or gl_entries[0]["voucher_no"])) + for entry in gl_entries: + entry['name'] = None + debit = entry.get('debit', 0) + credit = entry.get('credit', 0) - for entry in gl_entries: - validate_frozen_account(entry["account"], adv_adj) - validate_balance_type(entry["account"], adv_adj) - if not adv_adj: - validate_expense_against_budget(entry) + debit_in_account_currency = entry.get('debit_in_account_currency', 0) + credit_in_account_currency = entry.get('credit_in_account_currency', 0) - if entry.get("against_voucher") and update_outstanding == 'Yes' and not adv_adj: - update_outstanding_amt(entry["account"], entry.get("party_type"), entry.get("party"), entry.get("against_voucher_type"), - entry.get("against_voucher"), on_cancel=True) + entry['debit'] = credit + entry['credit'] = debit + entry['debit_in_account_currency'] = credit_in_account_currency + entry['credit_in_account_currency'] = debit_in_account_currency + + entry['remarks'] = "On cancellation of " + entry['voucher_no'] + entry['is_cancelled'] = 1 + entry['posting_date'] = today() + + if entry['debit'] or entry['credit']: + make_entry(entry, adv_adj, "Yes") + + +def check_freezing_date(posting_date, adv_adj=False): + """ + Nobody can do GL Entries where posting date is before freezing date + except authorized person + """ + if not adv_adj: + acc_frozen_upto = frappe.db.get_value('Accounts Settings', None, 'acc_frozen_upto') + if acc_frozen_upto: + frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,'frozen_accounts_modifier') + if getdate(posting_date) <= getdate(acc_frozen_upto) \ + and not frozen_accounts_modifier in frappe.get_roles(): + frappe.throw(_("You are not authorized to add or update entries before {0}").format(formatdate(acc_frozen_upto))) + +def set_as_cancel(voucher_type, voucher_no): + """ + Set is_cancelled=1 in all original gl entries for the voucher + """ + frappe.db.sql("""update `tabGL Entry` set is_cancelled = 1, + modified=%s, modified_by=%s + where voucher_type=%s and voucher_no=%s and is_cancelled = 0""", + (now(), frappe.session.user, voucher_type, voucher_no)) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index ac49d373d47..1188beaa0f8 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -154,8 +154,12 @@ frappe.query_reports["General Ledger"] = { { "fieldname": "include_default_book_entries", "label": __("Include Default Book Entries"), - "fieldtype": "Check", - "default": 1 + "fieldtype": "Check" + }, + { + "fieldname": "show_cancelled_entries", + "label": __("Show Cancelled Entries"), + "fieldtype": "Check" } ] } diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index f776d933014..7af5fa8eaa7 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -188,6 +188,9 @@ def get_conditions(filters): else: conditions.append("finance_book in (%(finance_book)s)") + if not filters.get("show_cancelled_entries"): + conditions.append("is_cancelled = 0") + from frappe.desk.reportview import build_match_conditions match_conditions = build_match_conditions("GL Entry") diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 4789063ba50..b5d6ca9bbce 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -897,4 +897,60 @@ def get_stock_accounts(company): return frappe.get_all("Account", filters = { "account_type": "Stock", "company": company - }) \ No newline at end of file + }) + +def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None, + warehouse_account=None, company=None): + def _delete_gl_entries(voucher_type, voucher_no): + frappe.db.sql("""delete from `tabGL Entry` + where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no)) + + if not warehouse_account: + warehouse_account = get_warehouse_account_map(company) + + future_stock_vouchers = get_future_stock_vouchers(posting_date, posting_time, for_warehouses, for_items) + gle = get_voucherwise_gl_entries(future_stock_vouchers, posting_date) + + for voucher_type, voucher_no in future_stock_vouchers: + existing_gle = gle.get((voucher_type, voucher_no), []) + voucher_obj = frappe.get_doc(voucher_type, voucher_no) + expected_gle = voucher_obj.get_gl_entries(warehouse_account) + if expected_gle: + if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle): + _delete_gl_entries(voucher_type, voucher_no) + voucher_obj.make_gl_entries(gl_entries=expected_gle, repost_future_gle=False, from_repost=True) + else: + _delete_gl_entries(voucher_type, voucher_no) + +def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None): + future_stock_vouchers = [] + + values = [] + condition = "" + if for_items: + condition += " and item_code in ({})".format(", ".join(["%s"] * len(for_items))) + values += for_items + + if for_warehouses: + condition += " and warehouse in ({})".format(", ".join(["%s"] * len(for_warehouses))) + values += for_warehouses + + for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no + from `tabStock Ledger Entry` sle + where timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s) {condition} + order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""".format(condition=condition), + tuple([posting_date, posting_time] + values), as_dict=True): + future_stock_vouchers.append([d.voucher_type, d.voucher_no]) + + return future_stock_vouchers + +def get_voucherwise_gl_entries(future_stock_vouchers, posting_date): + gl_entries = {} + if future_stock_vouchers: + for d in frappe.db.sql("""select * from `tabGL Entry` + where posting_date >= %s and voucher_no in (%s)""" % + ('%s', ', '.join(['%s']*len(future_stock_vouchers))), + tuple([posting_date] + [d[1] for d in future_stock_vouchers]), as_dict=1): + gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d) + + return gl_entries \ No newline at end of file diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 759d42a5427..ecbfeb7f140 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -11,7 +11,7 @@ from frappe.model.document import Document from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset.depreciation \ import get_disposal_account_and_cost_center, get_depreciation_accounts -from erpnext.accounts.general_ledger import make_gl_entries, delete_gl_entries +from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries from erpnext.accounts.utils import get_account_currency from erpnext.controllers.accounts_controller import AccountsController @@ -41,7 +41,8 @@ class Asset(AccountsController): self.validate_cancellation() self.delete_depreciation_entries() self.set_status() - delete_gl_entries(voucher_type='Asset', voucher_no=self.name) + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + make_reverse_gl_entries(voucher_type='Asset', voucher_no=self.name) self.db_set('booked_fixed_asset', 0) def validate_asset_and_reference(self): diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index a56440de3d3..050b30d89a8 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -66,9 +66,6 @@ class TestAsset(unittest.TestCase): pr.cancel() self.assertEqual(asset.docstatus, 2) - self.assertFalse(frappe.db.get_value("GL Entry", - {"voucher_type": "Purchase Invoice", "voucher_no": pi.name})) - def test_is_fixed_asset_set(self): asset = create_asset(is_existing_asset = 1) doc = frappe.new_doc('Purchase Invoice') diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 3e97f76f7b9..eecb143d556 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -664,23 +664,26 @@ class AccountsController(TransactionBase): def set_total_advance_paid(self): if self.doctype == "Sales Order": dr_or_cr = "credit_in_account_currency" + rev_dr_or_cr = "debit_in_account_currency" party = self.customer else: dr_or_cr = "debit_in_account_currency" + rev_dr_or_cr = "credit_in_account_currency" party = self.supplier advance = frappe.db.sql(""" select - account_currency, sum({dr_or_cr}) as amount + account_currency, sum({dr_or_cr}) - sum({rev_dr_cr}) as amount from `tabGL Entry` where against_voucher_type = %s and against_voucher = %s and party=%s and docstatus = 1 - """.format(dr_or_cr=dr_or_cr), (self.doctype, self.name, party), as_dict=1) + """.format(dr_or_cr=dr_or_cr, rev_dr_cr=rev_dr_or_cr), (self.doctype, self.name, party), as_dict=1) #nosec if advance: advance = advance[0] + advance_paid = flt(advance.amount, self.precision("advance_paid")) formatted_advance_paid = fmt_money(advance_paid, precision=self.precision("advance_paid"), currency=advance.account_currency) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 9d453af2ace..86de80815db 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -7,7 +7,7 @@ from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate from frappe import _ import frappe.defaults from erpnext.accounts.utils import get_fiscal_year -from erpnext.accounts.general_ledger import make_gl_entries, delete_gl_entries, process_gl_map +from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map from erpnext.controllers.accounts_controller import AccountsController from erpnext.stock.stock_ledger import get_valuation_rate from erpnext.stock import get_warehouse_account_map @@ -23,9 +23,9 @@ class StockController(AccountsController): self.validate_serialized_batch() self.validate_customer_provided_item() - def make_gl_entries(self, gl_entries=None, repost_future_gle=True, from_repost=False): + def make_gl_entries(self, gl_entries=None): if self.docstatus == 2: - delete_gl_entries(voucher_type=self.doctype, voucher_no=self.name) + make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) if cint(erpnext.is_perpetual_inventory_enabled(self.company)): warehouse_account = get_warehouse_account_map(self.company) @@ -33,16 +33,12 @@ class StockController(AccountsController): if self.docstatus==1: if not gl_entries: gl_entries = self.get_gl_entries(warehouse_account) - make_gl_entries(gl_entries, from_repost=from_repost) + make_gl_entries(gl_entries) - if (repost_future_gle or self.flags.repost_future_gle): - items, warehouses = self.get_items_and_warehouses() - update_gl_entries_after(self.posting_date, self.posting_time, warehouses, items, - warehouse_account, company=self.company) elif self.doctype in ['Purchase Receipt', 'Purchase Invoice'] and self.docstatus == 1: gl_entries = [] gl_entries = self.get_asset_gl_entry(gl_entries) - make_gl_entries(gl_entries, from_repost=from_repost) + make_gl_entries(gl_entries) def validate_serialized_batch(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -274,21 +270,21 @@ class StockController(AccountsController): "batch_no": cstr(d.get("batch_no")).strip(), "serial_no": d.get("serial_no"), "project": d.get("project") or self.get('project'), - "is_cancelled": self.docstatus==2 and "Yes" or "No" + "is_cancelled": 1 if self.docstatus==2 else 0 }) sl_dict.update(args) return sl_dict - def make_sl_entries(self, sl_entries, is_amended=None, allow_negative_stock=False, + def make_sl_entries(self, sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): from erpnext.stock.stock_ledger import make_sl_entries - make_sl_entries(sl_entries, is_amended, allow_negative_stock, via_landed_cost_voucher) + make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher) - def make_gl_entries_on_cancel(self, repost_future_gle=True): + def make_gl_entries_on_cancel(self): if frappe.db.sql("""select name from `tabGL Entry` where voucher_type=%s and voucher_no=%s""", (self.doctype, self.name)): - self.make_gl_entries(repost_future_gle=repost_future_gle) + self.make_gl_entries() def get_serialized_items(self): serialized_items = [] @@ -391,29 +387,6 @@ class StockController(AccountsController): if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'): d.allow_zero_valuation_rate = 1 -def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None, - warehouse_account=None, company=None): - def _delete_gl_entries(voucher_type, voucher_no): - frappe.db.sql("""delete from `tabGL Entry` - where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no)) - - if not warehouse_account: - warehouse_account = get_warehouse_account_map(company) - - future_stock_vouchers = get_future_stock_vouchers(posting_date, posting_time, for_warehouses, for_items) - gle = get_voucherwise_gl_entries(future_stock_vouchers, posting_date) - - for voucher_type, voucher_no in future_stock_vouchers: - existing_gle = gle.get((voucher_type, voucher_no), []) - voucher_obj = frappe.get_doc(voucher_type, voucher_no) - expected_gle = voucher_obj.get_gl_entries(warehouse_account) - if expected_gle: - if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle): - _delete_gl_entries(voucher_type, voucher_no) - voucher_obj.make_gl_entries(gl_entries=expected_gle, repost_future_gle=False, from_repost=True) - else: - _delete_gl_entries(voucher_type, voucher_no) - def compare_existing_and_expected_gle(existing_gle, expected_gle): matched = True for entry in expected_gle: @@ -430,36 +403,3 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle): matched = False break return matched - -def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None): - future_stock_vouchers = [] - - values = [] - condition = "" - if for_items: - condition += " and item_code in ({})".format(", ".join(["%s"] * len(for_items))) - values += for_items - - if for_warehouses: - condition += " and warehouse in ({})".format(", ".join(["%s"] * len(for_warehouses))) - values += for_warehouses - - for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no - from `tabStock Ledger Entry` sle - where timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s) {condition} - order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""".format(condition=condition), - tuple([posting_date, posting_time] + values), as_dict=True): - future_stock_vouchers.append([d.voucher_type, d.voucher_no]) - - return future_stock_vouchers - -def get_voucherwise_gl_entries(future_stock_vouchers, posting_date): - gl_entries = {} - if future_stock_vouchers: - for d in frappe.db.sql("""select * from `tabGL Entry` - where posting_date >= %s and voucher_no in (%s)""" % - ('%s', ', '.join(['%s']*len(future_stock_vouchers))), - tuple([posting_date] + [d[1] for d in future_stock_vouchers]), as_dict=1): - gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d) - - return gl_entries diff --git a/erpnext/education/doctype/fees/fees.py b/erpnext/education/doctype/fees/fees.py index f31003bf326..01f7b872494 100644 --- a/erpnext/education/doctype/fees/fees.py +++ b/erpnext/education/doctype/fees/fees.py @@ -10,7 +10,7 @@ from frappe.utils import money_in_words from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request from frappe.utils.csvutils import getlink from erpnext.controllers.accounts_controller import AccountsController -from erpnext.accounts.general_ledger import delete_gl_entries +from erpnext.accounts.general_ledger import make_reverse_gl_entries class Fees(AccountsController): @@ -81,7 +81,8 @@ class Fees(AccountsController): frappe.msgprint(_("Payment request {0} created").format(getlink("Payment Request", pr.name))) def on_cancel(self): - delete_gl_entries(voucher_type=self.doctype, voucher_no=self.name) + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name, cancel=True) # frappe.db.set(self, 'status', 'Cancelled') diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index ad9d86b66e2..ac1bfa1a391 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -76,6 +76,7 @@ class ExpenseClaim(AccountsController): def on_cancel(self): self.update_task_and_project() + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') if self.payable_account: self.make_gl_entries(cancel=True) @@ -260,10 +261,17 @@ class ExpenseClaim(AccountsController): if not expense.default_account or not validate: expense.default_account = get_expense_claim_account(expense.expense_type, self.company)["account"] -def update_reimbursed_amount(doc): - amt = frappe.db.sql("""select ifnull(sum(debit_in_account_currency), 0) as amt +def update_reimbursed_amount(doc, jv=None): + + condition = "" + + if jv: + condition += "and voucher_no = '{0}'".format(jv) + + amt = frappe.db.sql("""select ifnull(sum(debit_in_account_currency), 0) - ifnull(sum(credit_in_account_currency), 0)as amt from `tabGL Entry` where against_voucher_type = 'Expense Claim' and against_voucher = %s - and party = %s """, (doc.name, doc.employee) ,as_dict=1)[0].amt + and party = %s {condition}""".format(condition=condition), #nosec + (doc.name, doc.employee) ,as_dict=1)[0].amt doc.total_amount_reimbursed = amt frappe.db.set_value("Expense Claim", doc.name , "total_amount_reimbursed", amt) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 26f580db339..ca67d71bb0c 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -62,9 +62,9 @@ class TestProductionPlan(unittest.TestCase): def test_production_plan_for_existing_ordered_qty(self): sr1 = create_stock_reconciliation(item_code="Raw Material Item 1", - target="_Test Warehouse - _TC", qty=1, rate=100) + target="_Test Warehouse - _TC", qty=1, rate=110) sr2 = create_stock_reconciliation(item_code="Raw Material Item 2", - target="_Test Warehouse - _TC", qty=1, rate=100) + target="_Test Warehouse - _TC", qty=1, rate=120) pln = create_production_plan(item_code='Test Production Item 1', ignore_existing_ordered_qty=0) self.assertTrue(len(pln.mr_items), 1) @@ -86,9 +86,9 @@ class TestProductionPlan(unittest.TestCase): def test_production_plan_without_multi_level_for_existing_ordered_qty(self): sr1 = create_stock_reconciliation(item_code="Raw Material Item 1", - target="_Test Warehouse - _TC", qty=1, rate=100) + target="_Test Warehouse - _TC", qty=1, rate=130) sr2 = create_stock_reconciliation(item_code="Subassembly Item 1", - target="_Test Warehouse - _TC", qty=1, rate=100) + target="_Test Warehouse - _TC", qty=1, rate=140) pln = create_production_plan(item_code='Test Production Item 1', use_multi_level_bom=0, ignore_existing_ordered_qty=0) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index a216f53a8ba..b903bcae91c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -1,6 +1,7 @@ execute:import unidecode # new requirement erpnext.patches.v8_0.move_perpetual_inventory_setting erpnext.patches.v8_9.set_print_zero_amount_taxes +erpnext.patches.v12_0.update_is_cancelled_field erpnext.patches.v11_0.rename_production_order_to_work_order erpnext.patches.v11_0.refactor_naming_series erpnext.patches.v11_0.refactor_autoname_naming @@ -261,7 +262,6 @@ erpnext.patches.v6_19.comment_feed_communication erpnext.patches.v6_21.fix_reorder_level erpnext.patches.v6_21.rename_material_request_fields erpnext.patches.v6_23.update_stopped_status_to_closed -erpnext.patches.v6_24.repost_valuation_rate_for_serialized_items erpnext.patches.v6_24.set_recurring_id erpnext.patches.v6_20x.set_compact_print execute:frappe.delete_doc_if_exists("Web Form", "contact") #2016-03-10 @@ -315,7 +315,6 @@ erpnext.patches.v7_0.set_material_request_type_in_item erpnext.patches.v7_0.rename_examination_to_assessment erpnext.patches.v7_0.set_portal_settings erpnext.patches.v7_0.update_change_amount_account -erpnext.patches.v7_0.repost_future_gle_for_purchase_invoice erpnext.patches.v7_0.fix_duplicate_icons erpnext.patches.v7_0.repost_gle_for_pos_sales_return erpnext.patches.v7_1.update_total_billing_hours diff --git a/erpnext/patches/v10_0/repost_gle_for_purchase_receipts_with_rejected_items.py b/erpnext/patches/v10_0/repost_gle_for_purchase_receipts_with_rejected_items.py index 68c06ef62b2..e6546e386b9 100644 --- a/erpnext/patches/v10_0/repost_gle_for_purchase_receipts_with_rejected_items.py +++ b/erpnext/patches/v10_0/repost_gle_for_purchase_receipts_with_rejected_items.py @@ -24,9 +24,9 @@ def execute(): doc = frappe.get_doc("Purchase Receipt", d.name) doc.docstatus = 2 - doc.make_gl_entries_on_cancel(repost_future_gle=False) + doc.make_gl_entries_on_cancel() # update gl entries for submit state of PR doc.docstatus = 1 - doc.make_gl_entries(repost_future_gle=False) + doc.make_gl_entries() diff --git a/erpnext/patches/v10_0/taxes_issue_with_pos.py b/erpnext/patches/v10_0/taxes_issue_with_pos.py index 9b54297e220..2a3275ac2ce 100644 --- a/erpnext/patches/v10_0/taxes_issue_with_pos.py +++ b/erpnext/patches/v10_0/taxes_issue_with_pos.py @@ -19,7 +19,7 @@ def execute(): doc.db_update() delete_gle_for_voucher(doc.name) - doc.make_gl_entries(repost_future_gle=False) + doc.make_gl_entries() def delete_gle_for_voucher(voucher_no): frappe.db.sql("""delete from `tabGL Entry` where voucher_no = %(voucher_no)s""", diff --git a/erpnext/patches/v12_0/set_total_batch_quantity.py b/erpnext/patches/v12_0/set_total_batch_quantity.py index d373275c253..7296eaa33d8 100644 --- a/erpnext/patches/v12_0/set_total_batch_quantity.py +++ b/erpnext/patches/v12_0/set_total_batch_quantity.py @@ -6,6 +6,6 @@ def execute(): for batch in frappe.get_all("Batch", fields=["name", "batch_id"]): batch_qty = frappe.db.get_value("Stock Ledger Entry", - {"docstatus": 1, "batch_no": batch.batch_id, "is_cancelled": "No"}, + {"docstatus": 1, "batch_no": batch.batch_id, "is_cancelled": 0}, "sum(actual_qty)") or 0.0 frappe.db.set_value("Batch", batch.name, "batch_qty", batch_qty, update_modified=False) diff --git a/erpnext/patches/v12_0/update_is_cancelled_field.py b/erpnext/patches/v12_0/update_is_cancelled_field.py new file mode 100644 index 00000000000..0b2e82750b2 --- /dev/null +++ b/erpnext/patches/v12_0/update_is_cancelled_field.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + try: + frappe.db.sql("UPDATE `tabStock Ledger Entry` SET is_cancelled = 0 where is_cancelled in ('', NULL, 'No')") + frappe.db.sql("UPDATE `tabSerial No` SET is_cancelled = 0 where is_cancelled in ('', NULL, 'No')") + + frappe.db.sql("UPDATE `tabStock Ledger Entry` SET is_cancelled = 1 where is_cancelled = 'Yes'") + frappe.db.sql("UPDATE `tabSerial No` SET is_cancelled = 1 where is_cancelled = 'Yes'") + + frappe.reload_doc("stock", "doctype", "stock_ledger_entry") + frappe.reload_doc("stock", "doctype", "serial_no") + except: + pass \ No newline at end of file diff --git a/erpnext/patches/v4_2/fix_gl_entries_for_stock_transactions.py b/erpnext/patches/v4_2/fix_gl_entries_for_stock_transactions.py index 16932af3d66..c6c94d41797 100644 --- a/erpnext/patches/v4_2/fix_gl_entries_for_stock_transactions.py +++ b/erpnext/patches/v4_2/fix_gl_entries_for_stock_transactions.py @@ -8,7 +8,7 @@ from frappe.utils import flt def execute(): from erpnext.stock.stock_balance import repost repost(allow_zero_rate=True, only_actual=True) - + frappe.reload_doctype("Account") warehouse_account = frappe.db.sql("""select name, master_name from tabAccount @@ -43,7 +43,7 @@ def execute(): where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no)) voucher = frappe.get_doc(voucher_type, voucher_no) - voucher.make_gl_entries(repost_future_gle=False) + voucher.make_gl_entries() frappe.db.commit() except Exception as e: print(frappe.get_traceback()) diff --git a/erpnext/patches/v6_24/repost_valuation_rate_for_serialized_items.py b/erpnext/patches/v6_24/repost_valuation_rate_for_serialized_items.py deleted file mode 100644 index 3b157a3e365..00000000000 --- a/erpnext/patches/v6_24/repost_valuation_rate_for_serialized_items.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -from __future__ import unicode_literals -import frappe -from frappe.utils import today -from erpnext.accounts.utils import get_fiscal_year -from erpnext.stock.stock_ledger import update_entries_after - -def execute(): - try: - year_start_date = get_fiscal_year(today())[1] - except: - return - - if year_start_date: - items = frappe.db.sql("""select distinct item_code, warehouse from `tabStock Ledger Entry` - where ifnull(serial_no, '') != '' and actual_qty > 0 and incoming_rate=0""", as_dict=1) - - for d in items: - try: - update_entries_after({ - "item_code": d.item_code, - "warehouse": d.warehouse, - "posting_date": year_start_date - }, allow_zero_rate=True) - except: - pass \ No newline at end of file diff --git a/erpnext/patches/v7_0/repost_future_gle_for_purchase_invoice.py b/erpnext/patches/v7_0/repost_future_gle_for_purchase_invoice.py deleted file mode 100644 index 9e21fb699b9..00000000000 --- a/erpnext/patches/v7_0/repost_future_gle_for_purchase_invoice.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -from __future__ import unicode_literals -import frappe -from frappe.utils import cint -from erpnext.stock import get_warehouse_account_map -from erpnext.controllers.stock_controller import update_gl_entries_after - -def execute(): - company_list = frappe.db.sql_list("""Select name from tabCompany where enable_perpetual_inventory = 1""") - frappe.reload_doc('accounts', 'doctype', 'sales_invoice') - - frappe.reload_doctype("Purchase Invoice") - wh_account = get_warehouse_account_map() - - for pi in frappe.get_all("Purchase Invoice", fields=["name", "company"], filters={"docstatus": 1, "update_stock": 1}): - if pi.company in company_list: - pi_doc = frappe.get_doc("Purchase Invoice", pi.name) - items, warehouses = pi_doc.get_items_and_warehouses() - update_gl_entries_after(pi_doc.posting_date, pi_doc.posting_time, - warehouses, items, wh_account, company = pi.company) - - frappe.db.commit() \ No newline at end of file diff --git a/erpnext/patches/v7_0/repost_gle_for_pi_with_update_stock.py b/erpnext/patches/v7_0/repost_gle_for_pi_with_update_stock.py index 2d1a15181b6..b864e597b82 100644 --- a/erpnext/patches/v7_0/repost_gle_for_pi_with_update_stock.py +++ b/erpnext/patches/v7_0/repost_gle_for_pi_with_update_stock.py @@ -8,13 +8,13 @@ from frappe.utils import cint def execute(): frappe.reload_doctype("Purchase Invoice") - for pi in frappe.db.sql("""select name from `tabPurchase Invoice` - where company in(select name from tabCompany where enable_perpetual_inventory = 1) and + for pi in frappe.db.sql("""select name from `tabPurchase Invoice` + where company in(select name from tabCompany where enable_perpetual_inventory = 1) and update_stock=1 and docstatus=1 order by posting_date asc""", as_dict=1): - - frappe.db.sql("""delete from `tabGL Entry` + + frappe.db.sql("""delete from `tabGL Entry` where voucher_type = 'Purchase Invoice' and voucher_no = %s""", pi.name) - + pi_doc = frappe.get_doc("Purchase Invoice", pi.name) - pi_doc.make_gl_entries(repost_future_gle=False) + pi_doc.make_gl_entries() frappe.db.commit() \ No newline at end of file diff --git a/erpnext/public/js/controllers/stock_controller.js b/erpnext/public/js/controllers/stock_controller.js index 1c12c352ed4..2ce49e766b9 100644 --- a/erpnext/public/js/controllers/stock_controller.js +++ b/erpnext/public/js/controllers/stock_controller.js @@ -50,7 +50,7 @@ erpnext.stock.StockController = frappe.ui.form.Controller.extend({ show_stock_ledger: function() { var me = this; - if(this.frm.doc.docstatus===1) { + if(this.frm.doc.docstatus > 0) { cur_frm.add_custom_button(__("Stock Ledger"), function() { frappe.route_options = { voucher_no: me.frm.doc.name, @@ -66,7 +66,7 @@ erpnext.stock.StockController = frappe.ui.form.Controller.extend({ show_general_ledger: function() { var me = this; - if(this.frm.doc.docstatus===1) { + if(this.frm.doc.docstatus > 0) { cur_frm.add_custom_button(__('Accounting Ledger'), function() { frappe.route_options = { voucher_no: me.frm.doc.name, diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index d8e9a635b3a..b8b0d404e50 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -335,7 +335,7 @@ class TestSalesOrder(unittest.TestCase): self.assertEqual(so.get("items")[-1].qty, 7) self.assertEqual(so.get("items")[-1].amount, 1400) self.assertEqual(so.status, 'To Deliver and Bill') - + def test_remove_item_in_update_child_qty_rate(self): so = make_sales_order(**{ "item_list": [{ @@ -373,7 +373,7 @@ class TestSalesOrder(unittest.TestCase): "docname": so.get("items")[0].name }]) update_child_qty_rate('Sales Order', trans_item, so.name) - + so.reload() self.assertEqual(len(so.get("items")), 1) self.assertEqual(so.status, 'To Deliver and Bill') @@ -760,10 +760,9 @@ class TestSalesOrder(unittest.TestCase): self.assertEqual(reserved_serial_no, dn.get("items")[0].serial_no) item_line = dn.get("items")[0] item_line.serial_no = item_serial_no.name - self.assertRaises(frappe.ValidationError, dn.submit) item_line = dn.get("items")[0] item_line.serial_no = reserved_serial_no - self.assertTrue(dn.submit) + dn.submit() dn.load_from_db() dn.cancel() si = make_sales_invoice(so.name) diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 73b36e3d852..7acdec728b6 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -23,22 +23,19 @@ class Bin(Document): if not args.get("posting_date"): args["posting_date"] = nowdate() - # update valuation and qty after transaction for post dated entry - if args.get("is_cancelled") == "Yes" and via_landed_cost_voucher: - return update_entries_after({ "item_code": self.item_code, "warehouse": self.warehouse, "posting_date": args.get("posting_date"), "posting_time": args.get("posting_time"), - "voucher_no": args.get("voucher_no") + "voucher_no": args.get("voucher_no"), + "sle_id": args.sle_id }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) def update_qty(self, args): # update the stock values (for current quantities) if args.get("voucher_type")=="Stock Reconciliation": - if args.get('is_cancelled') == 'No': - self.actual_qty = args.get("qty_after_transaction") + self.actual_qty = args.get("qty_after_transaction") else: self.actual_qty = flt(self.actual_qty) + flt(args.get("actual_qty")) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index f8608d8ac0e..68836b4053a 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -188,7 +188,7 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( } } - if (doc.docstatus==1) { + if (doc.docstatus > 0) { this.show_stock_ledger(); if (erpnext.is_perpetual_inventory_enabled(doc.company)) { this.show_general_ledger(); diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index dc96e7bd493..37f90979376 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -222,6 +222,7 @@ class DeliveryNote(SellingController): self.cancel_packing_slips() self.make_gl_entries_on_cancel() + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') def check_credit_limit(self): from erpnext.selling.doctype.customer.customer import check_credit_limit diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index d7a93fb6917..bf7007abeeb 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -61,54 +61,55 @@ class TestDeliveryNote(unittest.TestCase): self.assertFalse(get_gl_entries("Delivery Note", dn.name)) - def test_delivery_note_gl_entry(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + # def test_delivery_note_gl_entry(self): + # company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') - set_valuation_method("_Test Item", "FIFO") + # set_valuation_method("_Test Item", "FIFO") - make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) + # make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) - stock_in_hand_account = get_inventory_account('_Test Company with perpetual inventory') - prev_bal = get_balance_on(stock_in_hand_account) + # stock_in_hand_account = get_inventory_account('_Test Company with perpetual inventory') + # prev_bal = get_balance_on(stock_in_hand_account) - dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1") + # dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1") - gl_entries = get_gl_entries("Delivery Note", dn.name) - self.assertTrue(gl_entries) + # gl_entries = get_gl_entries("Delivery Note", dn.name) + # self.assertTrue(gl_entries) - stock_value_difference = abs(frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Delivery Note", "voucher_no": dn.name}, "stock_value_difference")) + # stock_value_difference = abs(frappe.db.get_value("Stock Ledger Entry", + # {"voucher_type": "Delivery Note", "voucher_no": dn.name}, "stock_value_difference")) - expected_values = { - stock_in_hand_account: [0.0, stock_value_difference], - "Cost of Goods Sold - TCP1": [stock_value_difference, 0.0] - } - for i, gle in enumerate(gl_entries): - self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account)) + # expected_values = { + # stock_in_hand_account: [0.0, stock_value_difference], + # "Cost of Goods Sold - TCP1": [stock_value_difference, 0.0] + # } + # for i, gle in enumerate(gl_entries): + # self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account)) - # check stock in hand balance - bal = get_balance_on(stock_in_hand_account) - self.assertEqual(bal, prev_bal - stock_value_difference) + # # check stock in hand balance + # bal = get_balance_on(stock_in_hand_account) + # self.assertEqual(bal, prev_bal - stock_value_difference) - # back dated incoming entry - make_stock_entry(posting_date=add_days(nowdate(), -2), target="Stores - TCP1", - qty=5, basic_rate=100) + # # back dated incoming entry + # make_stock_entry(posting_date=add_days(nowdate(), -2), target="Stores - TCP1", + # qty=5, basic_rate=100) - gl_entries = get_gl_entries("Delivery Note", dn.name) - self.assertTrue(gl_entries) + # gl_entries = get_gl_entries("Delivery Note", dn.name) + # self.assertTrue(gl_entries) - stock_value_difference = abs(frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Delivery Note", "voucher_no": dn.name}, "stock_value_difference")) + # stock_value_difference = abs(frappe.db.get_value("Stock Ledger Entry", + # {"voucher_type": "Delivery Note", "voucher_no": dn.name}, "stock_value_difference")) - expected_values = { - stock_in_hand_account: [0.0, stock_value_difference], - "Cost of Goods Sold - TCP1": [stock_value_difference, 0.0] - } - for i, gle in enumerate(gl_entries): - self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account)) + # expected_values = { + # stock_in_hand_account: [0.0, stock_value_difference], + # "Cost of Goods Sold - TCP1": [stock_value_difference, 0.0] + # } + # for i, gle in enumerate(gl_entries): + # self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account)) - dn.cancel() - self.assertFalse(get_gl_entries("Delivery Note", dn.name)) + # dn.cancel() + # self.assertTrue(get_gl_entries("Delivery Note", dn.name)) + # set_perpetual_inventory(0, company) def test_delivery_note_gl_entry_packing_item(self): company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') @@ -147,7 +148,6 @@ class TestDeliveryNote(unittest.TestCase): self.assertEqual(flt(bal, 2), flt(prev_bal - stock_value_diff, 2)) dn.cancel() - self.assertFalse(get_gl_entries("Delivery Note", dn.name)) def test_serialized(self): se = make_serialized_item() @@ -464,27 +464,19 @@ class TestDeliveryNote(unittest.TestCase): frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) dn1 = make_delivery_note(so.name) - dn1.set_posting_time = 1 - dn1.posting_time = "10:00" dn1.get("items")[0].qty = 2 dn1.submit() + dn2 = make_delivery_note(so.name) + dn2.get("items")[0].qty = 3 + dn2.submit() + + dn1.load_from_db() self.assertEqual(dn1.get("items")[0].billed_amt, 200) self.assertEqual(dn1.per_billed, 100) self.assertEqual(dn1.status, "Completed") - dn2 = make_delivery_note(so.name) - dn2.set_posting_time = 1 - dn2.posting_time = "08:00" - dn2.get("items")[0].qty = 4 - dn2.submit() - - dn1.load_from_db() - self.assertEqual(dn1.get("items")[0].billed_amt, 100) - self.assertEqual(dn1.per_billed, 50) - self.assertEqual(dn1.status, "To Bill") - - self.assertEqual(dn2.get("items")[0].billed_amt, 400) + self.assertEqual(dn2.get("items")[0].billed_amt, 300) self.assertEqual(dn2.per_billed, 100) self.assertEqual(dn2.status, "Completed") @@ -497,8 +489,6 @@ class TestDeliveryNote(unittest.TestCase): so = make_sales_order() dn1 = make_delivery_note(so.name) - dn1.set_posting_time = 1 - dn1.posting_time = "10:00" dn1.get("items")[0].qty = 2 dn1.submit() @@ -513,7 +503,6 @@ class TestDeliveryNote(unittest.TestCase): si2.submit() dn2 = make_delivery_note(so.name) - dn2.posting_time = "08:00" dn2.get("items")[0].qty = 5 dn2.submit() diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 5ad0e13db9a..bc3d3266add 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -137,7 +137,7 @@ class LandedCostVoucher(Document): # update stock & gl entries for cancelled state of PR doc.docstatus = 2 doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True) - doc.make_gl_entries_on_cancel(repost_future_gle=False) + doc.make_gl_entries_on_cancel() # update stock & gl entries for submit state of PR doc.docstatus = 1 diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 62d369cb9d9..3f2c5daf669 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -15,8 +15,9 @@ class TestLandedCostVoucher(unittest.TestCase): def test_landed_cost_voucher(self): frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", get_multiple_items = True, get_taxes_and_charges = True) - + pr = make_purchase_receipt(company="_Test Company with perpetual inventory", + warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", + get_multiple_items = True, get_taxes_and_charges = True) last_sle = frappe.db.get_value("Stock Ledger Entry", { "voucher_type": pr.doctype, @@ -26,7 +27,7 @@ class TestLandedCostVoucher(unittest.TestCase): }, fieldname=["qty_after_transaction", "stock_value"], as_dict=1) - submit_landed_cost_voucher("Purchase Receipt", pr.name) + submit_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) pr_lc_value = frappe.db.get_value("Purchase Receipt Item", {"parent": pr.name}, "landed_cost_voucher_amount") self.assertEqual(pr_lc_value, 25.0) @@ -67,8 +68,9 @@ class TestLandedCostVoucher(unittest.TestCase): } for gle in gl_entries: - self.assertEqual(expected_values[gle.account][0], gle.debit) - self.assertEqual(expected_values[gle.account][1], gle.credit) + if not gle.get('is_cancelled'): + self.assertEqual(expected_values[gle.account][0], gle.debit) + self.assertEqual(expected_values[gle.account][1], gle.credit) def test_landed_cost_voucher_against_purchase_invoice(self): @@ -87,7 +89,7 @@ class TestLandedCostVoucher(unittest.TestCase): }, fieldname=["qty_after_transaction", "stock_value"], as_dict=1) - submit_landed_cost_voucher("Purchase Invoice", pi.name) + submit_landed_cost_voucher("Purchase Invoice", pi.name, pi.company) pi_lc_value = frappe.db.get_value("Purchase Invoice Item", {"parent": pi.name}, "landed_cost_voucher_amount") @@ -118,8 +120,9 @@ class TestLandedCostVoucher(unittest.TestCase): } for gle in gl_entries: - self.assertEqual(expected_values[gle.account][0], gle.debit) - self.assertEqual(expected_values[gle.account][1], gle.credit) + if not gle.get('is_cancelled'): + self.assertEqual(expected_values[gle.account][0], gle.debit) + self.assertEqual(expected_values[gle.account][1], gle.credit) def test_landed_cost_voucher_for_serialized_item(self): @@ -134,7 +137,7 @@ class TestLandedCostVoucher(unittest.TestCase): serial_no_rate = frappe.db.get_value("Serial No", "SN001", "purchase_rate") - submit_landed_cost_voucher("Purchase Receipt", pr.name) + submit_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) serial_no = frappe.db.get_value("Serial No", "SN001", ["warehouse", "purchase_rate"], as_dict=1) @@ -157,13 +160,13 @@ class TestLandedCostVoucher(unittest.TestCase): }) pr.submit() - lcv = submit_landed_cost_voucher("Purchase Receipt", pr.name, 123.22) + lcv = submit_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, 123.22) self.assertEqual(lcv.items[0].applicable_charges, 41.07) self.assertEqual(lcv.items[2].applicable_charges, 41.08) def test_multiple_landed_cost_voucher_against_pr(self): - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", + pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Stores - TCP1", do_not_save=True) pr.append("items", { @@ -176,7 +179,7 @@ class TestLandedCostVoucher(unittest.TestCase): pr.submit() - lcv1 = make_landed_cost_voucher(receipt_document_type = 'Purchase Receipt', + lcv1 = make_landed_cost_voucher(company = pr.company, receipt_document_type = 'Purchase Receipt', receipt_document=pr.name, charges=100, do_not_save=True) lcv1.insert() @@ -187,7 +190,7 @@ class TestLandedCostVoucher(unittest.TestCase): lcv1.submit() - lcv2 = make_landed_cost_voucher(receipt_document_type = 'Purchase Receipt', + lcv2 = make_landed_cost_voucher(company = pr.company, receipt_document_type = 'Purchase Receipt', receipt_document=pr.name, charges=100, do_not_save=True) lcv2.insert() @@ -208,7 +211,7 @@ def make_landed_cost_voucher(** args): ref_doc = frappe.get_doc(args.receipt_document_type, args.receipt_document) lcv = frappe.new_doc('Landed Cost Voucher') - lcv.company = '_Test Company' + lcv.company = args.company or '_Test Company' lcv.distribute_charges_based_on = 'Amount' lcv.set('purchase_receipts', [{ @@ -233,11 +236,11 @@ def make_landed_cost_voucher(** args): return lcv -def submit_landed_cost_voucher(receipt_document_type, receipt_document, charges=50): +def submit_landed_cost_voucher(receipt_document_type, receipt_document, company, charges=50): ref_doc = frappe.get_doc(receipt_document_type, receipt_document) lcv = frappe.new_doc("Landed Cost Voucher") - lcv.company = "_Test Company" + lcv.company = company lcv.distribute_charges_based_on = 'Amount' lcv.set("purchase_receipts", [{ diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index f3020e04ffd..e9568eeacc0 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -92,7 +92,7 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend refresh: function() { var me = this; this._super(); - if(this.frm.doc.docstatus===1) { + if(this.frm.doc.docstatus > 0) { this.show_stock_ledger(); //removed for temporary this.show_general_ledger(); diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index c2b38927f75..8dfe1d10302 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -196,6 +196,7 @@ class PurchaseReceipt(BuyingController): # because updating ordered qty in bin depends upon updated ordered qty in PO self.update_stock_ledger() self.make_gl_entries_on_cancel() + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') self.delete_auto_created_batches() def get_current_stock(self): diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 40d7cc2537c..3d42590e4c6 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -51,7 +51,7 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(current_bin_stock_value, existing_bin_stock_value + 250) self.assertFalse(get_gl_entries("Purchase Receipt", pr.name)) - + def test_batched_serial_no_purchase(self): item = frappe.db.exists("Item", {'item_name': 'Batched Serialized Item'}) if not item: @@ -68,7 +68,7 @@ class TestPurchaseReceipt(unittest.TestCase): pr = make_purchase_receipt(item_code=item.name, qty=5, rate=500) self.assertTrue(frappe.db.get_value('Batch', {'item': item.name, 'reference_name': pr.name})) - + pr.load_from_db() batch_no = pr.items[0].batch_no pr.cancel() @@ -106,7 +106,7 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(expected_values[gle.account][1], gle.credit) pr.cancel() - self.assertFalse(get_gl_entries("Purchase Receipt", pr.name)) + self.assertTrue(get_gl_entries("Purchase Receipt", pr.name)) def test_subcontracting(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -375,7 +375,7 @@ class TestPurchaseReceipt(unittest.TestCase): location = frappe.db.get_value('Asset', assets[0].name, 'location') self.assertEquals(location, "Test Location") - + def test_purchase_return_with_submitted_asset(self): from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return @@ -397,10 +397,10 @@ class TestPurchaseReceipt(unittest.TestCase): pr_return = make_purchase_return(pr.name) self.assertRaises(frappe.exceptions.ValidationError, pr_return.submit) - + asset.load_from_db() asset.cancel() - + pr_return.submit() def test_purchase_receipt_for_enable_allow_cost_center_in_entry_of_bs_account(self): @@ -505,10 +505,13 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEquals(pi2.items[1].qty, 1) def test_stock_transfer_from_purchase_receipt(self): - set_perpetual_inventory(1) - pr = make_purchase_receipt(do_not_save=1) + pr1 = make_purchase_receipt(warehouse = 'Work In Progress - TCP1', company="_Test Company with perpetual inventory") + + pr = make_purchase_receipt(company="_Test Company with perpetual inventory", + warehouse = "Stores - TCP1", do_not_save=1) + pr.supplier_warehouse = '' - pr.items[0].from_warehouse = '_Test Warehouse 2 - _TC' + pr.items[0].from_warehouse = 'Work In Progress - TCP1' pr.submit() @@ -518,31 +521,33 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertFalse(gl_entries) expected_sle = { - '_Test Warehouse 2 - _TC': -5, - '_Test Warehouse - _TC': 5 + 'Work In Progress - TCP1': -5, + 'Stores - TCP1': 5 } for sle in sl_entries: self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty) - set_perpetual_inventory(0) - def test_stock_transfer_from_purchase_receipt_with_valuation(self): - set_perpetual_inventory(1) - warehouse = frappe.get_doc('Warehouse', '_Test Warehouse 2 - _TC') - warehouse.account = '_Test Account Stock In Hand - _TC' + warehouse = frappe.get_doc('Warehouse', 'Work In Progress - TCP1') + warehouse.account = '_Test Account Stock In Hand - TCP1' warehouse.save() - pr = make_purchase_receipt(do_not_save=1) - pr.items[0].from_warehouse = '_Test Warehouse 2 - _TC' + pr1 = make_purchase_receipt(warehouse = 'Work In Progress - TCP1', + company="_Test Company with perpetual inventory") + + pr = make_purchase_receipt(company="_Test Company with perpetual inventory", + warehouse = "Stores - TCP1", do_not_save=1) + + pr.items[0].from_warehouse = 'Work In Progress - TCP1' pr.supplier_warehouse = '' pr.append('taxes', { 'charge_type': 'On Net Total', - 'account_head': '_Test Account Shipping Charges - _TC', + 'account_head': '_Test Account Shipping Charges - TCP1', 'category': 'Valuation and Total', - 'cost_center': 'Main - _TC', + 'cost_center': 'Main - TCP1', 'description': 'Test', 'rate': 9 }) @@ -553,14 +558,14 @@ class TestPurchaseReceipt(unittest.TestCase): sl_entries = get_sl_entries('Purchase Receipt', pr.name) expected_gle = [ - ['Stock In Hand - _TC', 272.5, 0.0], - ['_Test Account Stock In Hand - _TC', 0.0, 250.0], - ['_Test Account Shipping Charges - _TC', 0.0, 22.5] + ['Stock In Hand - TCP1', 272.5, 0.0], + ['_Test Account Stock In Hand - TCP1', 0.0, 250.0], + ['_Test Account Shipping Charges - TCP1', 0.0, 22.5] ] expected_sle = { - '_Test Warehouse 2 - _TC': -5, - '_Test Warehouse - _TC': 5 + 'Work In Progress - TCP1': -5, + 'Stores - TCP1': 5 } for sle in sl_entries: @@ -573,8 +578,6 @@ class TestPurchaseReceipt(unittest.TestCase): warehouse.account = '' warehouse.save() - set_perpetual_inventory(0) - def get_sl_entries(voucher_type, voucher_no): return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference @@ -582,7 +585,7 @@ def get_sl_entries(voucher_type, voucher_no): order by posting_time desc""", (voucher_type, voucher_no), as_dict=1) def get_gl_entries(voucher_type, voucher_no): - return frappe.db.sql("""select account, debit, credit, cost_center + return frappe.db.sql("""select account, debit, credit, cost_center, is_cancelled from `tabGL Entry` where voucher_type=%s and voucher_no=%s order by account desc""", (voucher_type, voucher_no), as_dict=1) diff --git a/erpnext/stock/doctype/purchase_receipt/test_records.json b/erpnext/stock/doctype/purchase_receipt/test_records.json index e7ea9af6b9d..724e3d729a2 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_records.json +++ b/erpnext/stock/doctype/purchase_receipt/test_records.json @@ -83,5 +83,37 @@ } ], "supplier": "_Test Supplier" + }, + + { + "buying_price_list": "_Test Price List", + "company": "_Test Company", + "conversion_rate": 1.0, + "currency": "INR", + "doctype": "Purchase Receipt", + "base_grand_total": 5000.0, + "is_subcontracted": "Yes", + "base_net_total": 5000.0, + "items": [ + { + "base_amount": 5000.0, + "conversion_factor": 1.0, + "description": "_Test FG Item", + "doctype": "Purchase Receipt Item", + "item_code": "_Test FG Item", + "item_name": "_Test FG Item", + "parentfield": "items", + "qty": 10.0, + "rate": 500.0, + "received_qty": 10.0, + "rejected_qty": 0.0, + "stock_uom": "_Test UOM", + "uom": "_Test UOM", + "warehouse": "_Test Warehouse - _TC", + "cost_center": "Main - _TC" + } + ], + "supplier": "_Test Supplier", + "supplier_warehouse": "_Test Warehouse - _TC" } ] \ No newline at end of file diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index b32c709be36..914eea379a8 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -130,13 +130,17 @@ class SerialNo(StockController): sle_dict = self.get_stock_ledger_entries(serial_no) if sle_dict: if sle_dict.get("incoming", []): - entries["purchase_sle"] = sle_dict["incoming"][0] + sle_list = [sle for sle in sle_dict["incoming"] if sle.is_cancelled == 0] + if sle_list: + entries["purchase_sle"] = sle_list[0] if len(sle_dict.get("incoming", [])) - len(sle_dict.get("outgoing", [])) > 0: entries["last_sle"] = sle_dict["incoming"][0] else: entries["last_sle"] = sle_dict["outgoing"][0] - entries["delivery_sle"] = sle_dict["outgoing"][0] + sle_list = [sle for sle in sle_dict["outgoing"] if sle.is_cancelled == 0] + if sle_list: + entries["delivery_sle"] = sle_list[0] return entries @@ -147,11 +151,11 @@ class SerialNo(StockController): for sle in frappe.db.sql(""" SELECT voucher_type, voucher_no, - posting_date, posting_time, incoming_rate, actual_qty, serial_no + posting_date, posting_time, incoming_rate, actual_qty, serial_no, is_cancelled FROM `tabStock Ledger Entry` WHERE - item_code=%s AND company = %s AND ifnull(is_cancelled, 'No')='No' + item_code=%s AND company = %s AND (serial_no = %s OR serial_no like %s OR serial_no like %s @@ -171,7 +175,7 @@ class SerialNo(StockController): def on_trash(self): sl_entries = frappe.db.sql("""select serial_no from `tabStock Ledger Entry` - where serial_no like %s and item_code=%s and ifnull(is_cancelled, 'No')='No'""", + where serial_no like %s and item_code=%s""", ("%%%s%%" % self.name, self.item_code), as_dict=True) # Find the exact match @@ -221,7 +225,7 @@ def validate_serial_no(sle, item_det): if serial_nos: frappe.throw(_("Item {0} is not setup for Serial Nos. Column must be blank").format(sle.item_code), SerialNoNotRequiredError) - elif sle.is_cancelled == "No": + else: if serial_nos: if cint(sle.actual_qty) != flt(sle.actual_qty): frappe.throw(_("Serial No {0} quantity {1} cannot be a fraction").format(sle.item_code, sle.actual_qty)) @@ -239,6 +243,10 @@ def validate_serial_no(sle, item_det): "delivery_document_no", "delivery_document_type", "warehouse", "purchase_document_no", "company"], as_dict=1) + if sr and cint(sle.actual_qty) < 0 and sr.warehouse != sle.warehouse: + frappe.throw(_("Cannot cancel {0} {1} because Serial No {2} does not belong to the warehouse {3}") + .format(sle.voucher_type, sle.voucher_no, serial_no, sle.warehouse), SerialNoWarehouseError) + if sr.item_code!=sle.item_code: if not allow_serial_nos_with_different_item(serial_no, sle): frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no, @@ -265,7 +273,7 @@ def validate_serial_no(sle, item_det): frappe.throw(_("Serial No {0} does not belong to Batch {1}").format(serial_no, sle.batch_no), SerialNoBatchError) - if sle.is_cancelled=="No" and not sr.warehouse: + if not sr.warehouse: frappe.throw(_("Serial No {0} does not belong to any Warehouse") .format(serial_no), SerialNoWarehouseError) @@ -311,12 +319,6 @@ def validate_serial_no(sle, item_det): elif cint(sle.actual_qty) < 0 or not item_det.serial_no_series: frappe.throw(_("Serial Nos Required for Serialized Item {0}").format(sle.item_code), SerialNoRequiredError) - elif serial_nos: - for serial_no in serial_nos: - sr = frappe.db.get_value("Serial No", serial_no, ["name", "warehouse"], as_dict=1) - if sr and cint(sle.actual_qty) < 0 and sr.warehouse != sle.warehouse: - frappe.throw(_("Cannot cancel {0} {1} because Serial No {2} does not belong to the warehouse {3}") - .format(sle.voucher_type, sle.voucher_no, serial_no, sle.warehouse)) def validate_material_transfer_entry(sle_doc): sle_doc.update({ @@ -324,7 +326,7 @@ def validate_material_transfer_entry(sle_doc): "skip_serial_no_validaiton": False }) - if (sle_doc.voucher_type == "Stock Entry" and sle_doc.is_cancelled == "No" and + if (sle_doc.voucher_type == "Stock Entry" and frappe.get_cached_value("Stock Entry", sle_doc.voucher_no, "purpose") == "Material Transfer"): if sle_doc.actual_qty < 0: sle_doc.skip_update_serial_no = True @@ -367,7 +369,7 @@ def allow_serial_nos_with_different_item(sle_serial_no, sle): stock_entry = frappe.get_cached_doc("Stock Entry", sle.voucher_no) if stock_entry.purpose in ("Repack", "Manufacture"): for d in stock_entry.get("items"): - if d.serial_no and (d.s_warehouse if sle.is_cancelled=="No" else d.t_warehouse): + if d.serial_no and (d.s_warehouse or d.t_warehouse): serial_nos = get_serial_nos(d.serial_no) if sle_serial_no in serial_nos: allow_serial_nos = True @@ -376,7 +378,7 @@ def allow_serial_nos_with_different_item(sle_serial_no, sle): def update_serial_nos(sle, item_det): if sle.skip_update_serial_no: return - if sle.is_cancelled == "No" and not sle.serial_no and cint(sle.actual_qty) > 0 \ + if not sle.serial_no and cint(sle.actual_qty) > 0 \ and item_det.has_serial_no == 1 and item_det.serial_no_series: serial_nos = get_auto_serial_nos(item_det.serial_no_series, sle.actual_qty) frappe.db.set(sle, "serial_no", serial_nos) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 95f9d4633b5..f9aae7baa88 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -107,6 +107,9 @@ class StockEntry(StockController): self.update_work_order() self.update_stock_ledger() + + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + self.make_gl_entries_on_cancel() self.update_cost_in_project() self.update_transferred_qty() @@ -651,7 +654,7 @@ class StockEntry(StockController): if self.docstatus == 2: sl_entries.reverse() - self.make_sl_entries(sl_entries, self.amended_from and 'Yes' or 'No') + self.make_sl_entries(sl_entries) def get_gl_entries(self, warehouse_account): gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account) @@ -674,7 +677,7 @@ class StockEntry(StockController): multiply_based_on = d.basic_amount if total_basic_amount else d.qty item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account] += \ - (t.amount * multiply_based_on) / divide_based_on + flt(t.amount * multiply_based_on) / divide_based_on if item_account_wise_additional_cost: for d in self.get("items"): diff --git a/erpnext/stock/doctype/stock_entry/test_records.json b/erpnext/stock/doctype/stock_entry/test_records.json index cfbdce4d772..dc212874139 100644 --- a/erpnext/stock/doctype/stock_entry/test_records.json +++ b/erpnext/stock/doctype/stock_entry/test_records.json @@ -24,7 +24,6 @@ { "company": "_Test Company", "doctype": "Stock Entry", - "posting_date": "2013-01-25", "purpose": "Material Issue", "stock_entry_type": "Material Issue", "items": [ @@ -47,7 +46,6 @@ { "company": "_Test Company", "doctype": "Stock Entry", - "posting_date": "2013-01-25", "purpose": "Material Transfer", "stock_entry_type": "Material Transfer", "items": [ diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 2afabe1480d..0fbc63101e6 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -149,10 +149,10 @@ class TestStockEntry(unittest.TestCase): mr.cancel() - self.assertFalse(frappe.db.sql("""select * from `tabStock Ledger Entry` + self.assertTrue(frappe.db.sql("""select * from `tabStock Ledger Entry` where voucher_type='Stock Entry' and voucher_no=%s""", mr.name)) - self.assertFalse(frappe.db.sql("""select * from `tabGL Entry` + self.assertTrue(frappe.db.sql("""select * from `tabGL Entry` where voucher_type='Stock Entry' and voucher_no=%s""", mr.name)) def test_material_issue_gl_entry(self): @@ -178,12 +178,6 @@ class TestStockEntry(unittest.TestCase): ) mi.cancel() - self.assertFalse(frappe.db.sql("""select name from `tabStock Ledger Entry` - where voucher_type='Stock Entry' and voucher_no=%s""", mi.name)) - - self.assertFalse(frappe.db.sql("""select name from `tabGL Entry` - where voucher_type='Stock Entry' and voucher_no=%s""", mi.name)) - def test_material_transfer_gl_entry(self): company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') @@ -216,11 +210,6 @@ class TestStockEntry(unittest.TestCase): ) mtn.cancel() - self.assertFalse(frappe.db.sql("""select * from `tabStock Ledger Entry` - where voucher_type='Stock Entry' and voucher_no=%s""", mtn.name)) - - self.assertFalse(frappe.db.sql("""select * from `tabGL Entry` - where voucher_type='Stock Entry' and voucher_no=%s""", mtn.name)) def test_repack_no_change_in_valuation(self): company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company') @@ -544,10 +533,10 @@ class TestStockEntry(unittest.TestCase): frappe.db.set_value("Stock Settings", None, "stock_frozen_upto", '') # test freeze_stocks_upto_days - frappe.db.set_value("Stock Settings", None, "stock_frozen_upto_days", 7) + frappe.db.set_value("Stock Settings", None, "stock_frozen_upto_days", -1) se = frappe.copy_doc(test_records[0]) se.set_posting_time = 1 - se.posting_date = add_days(nowdate(), -15) + se.posting_date = nowdate() se.set_stock_entry_type() se.insert() self.assertRaises(StockFreezeError, se.submit) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json index c03eb79eeca..fda17e08ab3 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_copy": 1, "autoname": "MAT-SLE-.YYYY.-.#####", "creation": "2013-01-29 19:25:42", @@ -255,11 +256,10 @@ "width": "150px" }, { + "default": "0", "fieldname": "is_cancelled", - "fieldtype": "Select", - "hidden": 1, + "fieldtype": "Check", "label": "Is Cancelled", - "options": "\nNo\nYes", "report_hide": 1 }, { @@ -275,7 +275,8 @@ "icon": "fa fa-list", "idx": 1, "in_create": 1, - "modified": "2020-02-25 22:53:33.504681", + "links": [], + "modified": "2020-04-23 05:57:03.985520", "modified_by": "Administrator", "module": "Stock", "name": "Stock Ledger Entry", diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index dab5a7beb87..101c6e099ec 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -46,7 +46,7 @@ class StockLedgerEntry(Document): def calculate_batch_qty(self): if self.batch_no: batch_qty = frappe.db.get_value("Stock Ledger Entry", - {"docstatus": 1, "batch_no": self.batch_no, "is_cancelled": "No"}, + {"docstatus": 1, "batch_no": self.batch_no}, "sum(actual_qty)") or 0 frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty) @@ -93,7 +93,7 @@ class StockLedgerEntry(Document): elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}): frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item)) - elif item_det.has_batch_no ==0 and self.batch_no and self.is_cancelled == "No": + elif item_det.has_batch_no ==0 and self.batch_no: frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code)) if item_det.has_variants: diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 1791978a068..dd284e4a966 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -227,7 +227,7 @@ erpnext.stock.StockReconciliation = erpnext.stock.StockController.extend({ }, refresh: function() { - if(this.frm.doc.docstatus==1) { + if(this.frm.doc.docstatus > 0) { this.show_stock_ledger(); if (erpnext.is_perpetual_inventory_enabled(this.frm.doc.company)) { this.show_general_ledger(); diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 0a49c26b629..5e469c24d72 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -6,7 +6,6 @@ import frappe, erpnext import frappe.defaults from frappe import msgprint, _ from frappe.utils import cstr, flt, cint -from erpnext.stock.stock_ledger import update_entries_after from erpnext.controllers.stock_controller import StockController from erpnext.accounts.utils import get_company_default from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -43,7 +42,8 @@ class StockReconciliation(StockController): update_serial_nos_after_submit(self, "items") def on_cancel(self): - self.delete_and_repost_sle() + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + self.make_sle_on_cancel() self.make_gl_entries_on_cancel() def remove_items_with_no_change(self): @@ -193,6 +193,7 @@ class StockReconciliation(StockController): if row.serial_no or row.batch_no: frappe.throw(_("Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.") \ .format(row.idx, frappe.bold(row.item_code))) + previous_sle = get_previous_sle({ "item_code": row.item_code, "warehouse": row.warehouse, @@ -319,7 +320,7 @@ class StockReconciliation(StockController): "voucher_detail_no": row.name, "company": self.company, "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"), - "is_cancelled": "No" if self.docstatus != 2 else "Yes", + "is_cancelled": 1 if self.docstatus == 2 else 0, "serial_no": '\n'.join(serial_nos) if serial_nos else '', "batch_no": row.batch_no, "valuation_rate": flt(row.valuation_rate, row.precision("valuation_rate")) @@ -328,27 +329,35 @@ class StockReconciliation(StockController): if not row.batch_no: data.qty_after_transaction = flt(row.qty, row.precision("qty")) + if self.docstatus == 2 and not row.batch_no: + if row.current_qty: + data.actual_qty = -1 * row.current_qty + data.qty_after_transaction = flt(row.current_qty) + data.valuation_rate = flt(row.current_valuation_rate) + data.stock_value = data.qty_after_transaction * data.valuation_rate + data.stock_value_difference = -1 * flt(row.amount_difference) + else: + data.actual_qty = row.qty + data.qty_after_transaction = 0.0 + data.valuation_rate = flt(row.valuation_rate) + data.stock_value_difference = -1 * flt(row.amount_difference) + return data - def delete_and_repost_sle(self): - """ Delete Stock Ledger Entries related to this voucher - and repost future Stock Ledger Entries""" - - existing_entries = frappe.db.sql("""select distinct item_code, warehouse - from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s""", - (self.doctype, self.name), as_dict=1) - - # delete entries - frappe.db.sql("""delete from `tabStock Ledger Entry` - where voucher_type=%s and voucher_no=%s""", (self.doctype, self.name)) - + def make_sle_on_cancel(self): sl_entries = [] has_serial_no = False for row in self.items: if row.serial_no or row.batch_no or row.current_serial_no: has_serial_no = True - self.get_sle_for_serialized_items(row, sl_entries) + serial_nos = '' + if row.current_serial_no: + serial_nos = get_serial_nos(row.current_serial_no) + + sl_entries.append(self.get_sle_for_items(row, serial_nos)) + else: + sl_entries.append(self.get_sle_for_items(row)) if sl_entries: if has_serial_no: @@ -358,14 +367,6 @@ class StockReconciliation(StockController): allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock) - # repost future entries for selected item_code, warehouse - for entries in existing_entries: - update_entries_after({ - "item_code": entries.item_code, - "warehouse": entries.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time - }) def merge_similar_item_serial_nos(self, sl_entries): # If user has put the same item in multiple row with different serial no @@ -434,12 +435,6 @@ class StockReconciliation(StockController): else: self._submit() - def cancel(self): - if len(self.items) > 100: - self.queue_action('cancel') - else: - self._cancel() - @frappe.whitelist() def get_items(warehouse, posting_date, posting_time, company): lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 51d027f22ef..15714161c66 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -34,11 +34,11 @@ class TestStockReconciliation(unittest.TestCase): # [[qty, valuation_rate, posting_date, # posting_time, expected_stock_value, bin_qty, bin_valuation]] input_data = [ - [50, 1000, "2012-12-26", "12:00"], - [25, 900, "2012-12-26", "12:00"], - ["", 1000, "2012-12-20", "12:05"], - [20, "", "2012-12-26", "12:05"], - [0, "", "2012-12-31", "12:10"] + [50, 1000], + [25, 900], + ["", 1000], + [20, ""], + [0, ""] ] for d in input_data: @@ -47,13 +47,13 @@ class TestStockReconciliation(unittest.TestCase): last_sle = get_previous_sle({ "item_code": "_Test Item", "warehouse": "Stores - TCP1", - "posting_date": d[2], - "posting_time": d[3] + "posting_date": nowdate(), + "posting_time": nowtime() }) # submit stock reconciliation stock_reco = create_stock_reconciliation(qty=d[0], rate=d[1], - posting_date=d[2], posting_time=d[3], warehouse="Stores - TCP1", + posting_date=nowdate(), posting_time=nowtime(), warehouse="Stores - TCP1", company=company, expense_account = "Stock Adjustment - TCP1") # check stock value @@ -68,8 +68,8 @@ class TestStockReconciliation(unittest.TestCase): and valuation_rate == last_sle.get("valuation_rate"): self.assertFalse(sle) else: - self.assertEqual(sle[0].qty_after_transaction, qty_after_transaction) - self.assertEqual(sle[0].stock_value, qty_after_transaction * valuation_rate) + self.assertEqual(flt(sle[0].qty_after_transaction, 1), flt(qty_after_transaction, 1)) + self.assertEqual(flt(sle[0].stock_value, 1), flt(qty_after_transaction * valuation_rate, 1)) # no gl entries self.assertTrue(frappe.db.get_value("Stock Ledger Entry", @@ -77,16 +77,10 @@ class TestStockReconciliation(unittest.TestCase): acc_bal, stock_bal, wh_list = get_stock_and_account_balance("Stock In Hand - TCP1", stock_reco.posting_date, stock_reco.company) - self.assertEqual(acc_bal, stock_bal) + self.assertEqual(flt(acc_bal, 1), flt(stock_bal, 1)) stock_reco.cancel() - self.assertFalse(frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Stock Reconciliation", "voucher_no": stock_reco.name})) - - self.assertFalse(frappe.db.get_value("GL Entry", - {"voucher_type": "Stock Reconciliation", "voucher_no": stock_reco.name})) - def test_get_items(self): create_warehouse("_Test Warehouse Group 1", {"is_group": 1}) create_warehouse("_Test Warehouse Ledger 1", @@ -113,7 +107,6 @@ class TestStockReconciliation(unittest.TestCase): sr = create_stock_reconciliation(item_code=serial_item_code, warehouse = serial_warehouse, qty=5, rate=200) - # print(sr.name) serial_nos = get_serial_nos(sr.items[0].serial_no) self.assertEqual(len(serial_nos), 5) @@ -133,7 +126,6 @@ class TestStockReconciliation(unittest.TestCase): sr = create_stock_reconciliation(item_code=serial_item_code, warehouse = serial_warehouse, qty=5, rate=300, serial_no = '\n'.join(serial_nos)) - # print(sr.name) serial_nos1 = get_serial_nos(sr.items[0].serial_no) self.assertEqual(len(serial_nos1), 5) @@ -155,10 +147,6 @@ class TestStockReconciliation(unittest.TestCase): stock_doc = frappe.get_doc("Stock Reconciliation", d) stock_doc.cancel() - for d in serial_nos + serial_nos1: - if frappe.db.exists("Serial No", d): - frappe.delete_doc("Serial No", d) - def test_stock_reco_for_batch_item(self): set_perpetual_inventory() @@ -208,13 +196,13 @@ class TestStockReconciliation(unittest.TestCase): def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - make_stock_entry(posting_date="2012-12-15", posting_time="02:00", item_code="_Test Item", + make_stock_entry(posting_date=nowdate(), posting_time=nowtime(), item_code="_Test Item", target=warehouse, qty=10, basic_rate=700) - make_stock_entry(posting_date="2012-12-25", posting_time="03:00", item_code="_Test Item", + make_stock_entry(posting_date=nowdate(), posting_time=nowtime(), item_code="_Test Item", source=warehouse, qty=15) - make_stock_entry(posting_date="2013-01-05", posting_time="07:00", item_code="_Test Item", + make_stock_entry(posting_date=nowdate(), posting_time=nowtime(), item_code="_Test Item", target=warehouse, qty=15, basic_rate=1200) def create_batch_or_serial_no_items(): diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js index 9adfbf7cd03..6f12c2731bb 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.js +++ b/erpnext/stock/report/stock_ledger/stock_ledger.js @@ -32,7 +32,7 @@ frappe.query_reports["Stock Ledger"] = { "options": "Warehouse", "get_query": function() { const company = frappe.query_report.get_filter_value('company'); - return { + return { filters: { 'company': company } } } @@ -82,6 +82,11 @@ frappe.query_reports["Stock Ledger"] = { "label": __("Include UOM"), "fieldtype": "Link", "options": "UOM" + }, + { + "fieldname": "show_cancelled_entries", + "label": __("Show Cancelled Entries"), + "fieldtype": "Check" } ], "formatter": function (value, row, column, data, default_formatter) { @@ -96,9 +101,3 @@ frappe.query_reports["Stock Ledger"] = { return value; }, } - -// $(function() { -// $(wrapper).bind("show", function() { -// frappe.query_report.load(); -// }); -// }); \ No newline at end of file diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 0190f09f3d4..abf959eb0bf 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -181,6 +181,9 @@ def get_sle_conditions(filters): if filters.get("project"): conditions.append("project=%(project)s") + if not filters.get("show_cancelled_entries"): + conditions.append("is_cancelled = 0") + return "and {}".format(" and ".join(conditions)) if conditions else "" diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index 56973153609..b5ae1b78eb4 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -6,7 +6,6 @@ import frappe from frappe.utils import flt, cstr, nowdate, nowtime from erpnext.stock.utils import update_bin from erpnext.stock.stock_ledger import update_entries_after -from erpnext.controllers.stock_controller import update_gl_entries_after def repost(only_actual=False, allow_negative_stock=False, allow_zero_rate=False, only_bin=False): """ @@ -56,12 +55,13 @@ def repost_stock(item_code, warehouse, allow_zero_rate=False, update_bin_qty(item_code, warehouse, qty_dict) -def repost_actual_qty(item_code, warehouse, allow_zero_rate=False, allow_negative_stock=False): update_entries_after({ "item_code": item_code, "warehouse": warehouse }, +def repost_actual_qty(item_code, warehouse, allow_zero_rate=False, allow_negative_stock=False): + update_entries_after({ "item_code": item_code, "warehouse": warehouse }, allow_zero_rate=allow_zero_rate, allow_negative_stock=allow_negative_stock) def get_balance_qty_from_sle(item_code, warehouse): balance_qty = frappe.db.sql("""select qty_after_transaction from `tabStock Ledger Entry` - where item_code=%s and warehouse=%s and is_cancelled='No' + where item_code=%s and warehouse=%s order by posting_date desc, posting_time desc, creation desc limit 1""", (item_code, warehouse)) @@ -191,7 +191,7 @@ def set_stock_balance_as_per_serial_no(item_code=None, posting_date=None, postin print(d[0], d[1], d[2], serial_nos[0][0]) sle = frappe.db.sql("""select valuation_rate, company from `tabStock Ledger Entry` - where item_code = %s and warehouse = %s and ifnull(is_cancelled, 'No') = 'No' + where item_code = %s and warehouse = %s order by posting_date desc limit 1""", (d[0], d[1])) sle_dict = { @@ -208,7 +208,6 @@ def set_stock_balance_as_per_serial_no(item_code=None, posting_date=None, postin 'stock_uom' : d[3], 'incoming_rate' : sle and flt(serial_nos[0][0]) > flt(d[2]) and flt(sle[0][0]) or 0, 'company' : sle and cstr(sle[0][1]) or 0, - 'is_cancelled' : 'No', 'batch_no' : '', 'serial_no' : '' } @@ -220,8 +219,7 @@ def set_stock_balance_as_per_serial_no(item_code=None, posting_date=None, postin args = sle_dict.copy() args.update({ - "sle_id": sle_doc.name, - "is_amended": 'No' + "sle_id": sle_doc.name }) update_bin(args) @@ -246,15 +244,3 @@ def reset_serial_no_status_and_warehouse(serial_nos=None): sr.save() except: pass - -def repost_gle_for_stock_transactions(posting_date=None, posting_time=None, for_warehouses=None): - frappe.db.auto_commit_on_many_writes = 1 - - if not posting_date: - posting_date = "1900-01-01" - if not posting_time: - posting_time = "00:00" - - update_gl_entries_after(posting_date, posting_time, for_warehouses=for_warehouses) - - frappe.db.auto_commit_on_many_writes = 0 diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 7567a1ae758..b4cb8cadb45 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -4,8 +4,8 @@ from __future__ import unicode_literals import frappe, erpnext from frappe import _ -from frappe.utils import cint, flt, cstr, now -from erpnext.stock.utils import get_valuation_method +from frappe.utils import cint, flt, cstr, now, now_datetime +from erpnext.stock.utils import get_valuation_method, get_incoming_outgoing_rate_for_cancel import json from six import iteritems @@ -16,36 +16,48 @@ class NegativeStockError(frappe.ValidationError): pass _exceptions = frappe.local('stockledger_exceptions') # _exceptions = [] -def make_sl_entries(sl_entries, is_amended=None, allow_negative_stock=False, via_landed_cost_voucher=False): +def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): if sl_entries: from erpnext.stock.utils import update_bin - cancel = True if sl_entries[0].get("is_cancelled") == "Yes" else False + cancel = sl_entries[0].get("is_cancelled") if cancel: - set_as_cancel(sl_entries[0].get('voucher_no'), sl_entries[0].get('voucher_type')) + set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no')) for sle in sl_entries: sle_id = None - if sle.get('is_cancelled') == 'Yes': - sle['actual_qty'] = -flt(sle['actual_qty']) + if via_landed_cost_voucher or cancel: + sle['posting_date'] = now_datetime().strftime('%Y-%m-%d') + sle['posting_time'] = now_datetime().strftime('%H:%M:%S.%f') + + if cancel: + sle['actual_qty'] = -flt(sle.get('actual_qty'), 0) + + if sle['actual_qty'] < 0 and not sle.get('outgoing_rate'): + sle['outgoing_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code, + sle.voucher_type, sle.voucher_no, sle.voucher_detail_no) + sle['incoming_rate'] = 0.0 + + if sle['actual_qty'] > 0 and not sle.get('incoming_rate'): + sle['incoming_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code, + sle.voucher_type, sle.voucher_no, sle.voucher_detail_no) + sle['outgoing_rate'] = 0.0 + if sle.get("actual_qty") or sle.get("voucher_type")=="Stock Reconciliation": sle_id = make_entry(sle, allow_negative_stock, via_landed_cost_voucher) args = sle.copy() args.update({ - "sle_id": sle_id, - "is_amended": is_amended + "sle_id": sle_id }) update_bin(args, allow_negative_stock, via_landed_cost_voucher) - if cancel: - delete_cancelled_entry(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no')) def set_as_cancel(voucher_type, voucher_no): - frappe.db.sql("""update `tabStock Ledger Entry` set is_cancelled='Yes', + frappe.db.sql("""update `tabStock Ledger Entry` set is_cancelled=1, modified=%s, modified_by=%s - where voucher_no=%s and voucher_type=%s""", + where voucher_type=%s and voucher_no=%s and is_cancelled = 0""", (now(), frappe.session.user, voucher_type, voucher_no)) def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False): @@ -58,9 +70,6 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False): sle.submit() return sle.name -def delete_cancelled_entry(voucher_type, voucher_no): - frappe.db.sql("""delete from `tabStock Ledger Entry` - where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no)) class update_entries_after(object): """ @@ -106,14 +115,17 @@ class update_entries_after(object): self.stock_queue = json.loads(self.previous_sle.stock_queue or "[]") self.valuation_method = get_valuation_method(self.item_code) self.stock_value_difference = 0.0 - self.build() + self.build(args.get('sle_id')) - def build(self): - # includes current entry! - entries_to_fix = self.get_sle_after_datetime() - - for sle in entries_to_fix: + def build(self, sle_id): + if sle_id: + sle = get_sle_by_id(sle_id) self.process_sle(sle) + else: + # includes current entry! + entries_to_fix = self.get_sle_after_datetime() + for sle in entries_to_fix: + self.process_sle(sle) if self.exceptions: self.raise_exceptions() @@ -403,7 +415,10 @@ class update_entries_after(object): def get_sle_before_datetime(self): """get previous stock ledger entry before current time-bucket""" - return get_stock_ledger_entries(self.args, "<", "desc", "limit 1", for_update=False) + if self.args.get('sle_id'): + self.args['name'] = self.args.get('sle_id') + + return get_stock_ledger_entries(self.args, "<=", "desc", "limit 1", for_update=False) def get_sle_after_datetime(self): """get Stock Ledger Entries after a particular datetime, for reposting""" @@ -470,9 +485,10 @@ def get_stock_ledger_entries(previous_sle, operator=None, if operator in (">", "<=") and previous_sle.get("name"): conditions += " and name!=%(name)s" - return frappe.db.sql("""select *, timestamp(posting_date, posting_time) as "timestamp" from `tabStock Ledger Entry` + return frappe.db.sql(""" + select *, timestamp(posting_date, posting_time) as "timestamp" + from `tabStock Ledger Entry` where item_code = %%(item_code)s - and ifnull(is_cancelled, 'No')='No' %(conditions)s order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s %(limit)s %(for_update)s""" % { @@ -482,6 +498,11 @@ def get_stock_ledger_entries(previous_sle, operator=None, "order": order }, previous_sle, as_dict=1, debug=debug) +def get_sle_by_id(sle_id): + return frappe.db.get_all('Stock Ledger Entry', + fields=['*', 'timestamp(posting_date, posting_time) as timestamp'], + filters={'name': sle_id})[0] + def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True): # Get valuation rate from last sle for the same item and warehouse diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 7f32b8d8bb8..f21dc3f8b03 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -364,4 +364,16 @@ def add_additional_uom_columns(columns, result, include_uom, conversion_factors) else: row[data.converted_col] = flt(value_before_conversion) / conversion_factor - result[row_idx] = row \ No newline at end of file + result[row_idx] = row + +def get_incoming_outgoing_rate_for_cancel(item_code, voucher_type, voucher_no, voucher_detail_no): + outgoing_rate = frappe.db.sql("""SELECT abs(stock_value_difference / actual_qty) + FROM `tabStock Ledger Entry` + WHERE voucher_type = %s and voucher_no = %s + and item_code = %s and voucher_detail_no = %s + ORDER BY CREATION DESC limit 1""", + (voucher_type, voucher_no, item_code, voucher_detail_no)) + + outgoing_rate = outgoing_rate[0][0] if outgoing_rate else 0.0 + + return outgoing_rate \ No newline at end of file diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 14674c067cd..ea96503dff9 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -5,8 +5,9 @@ from __future__ import unicode_literals import frappe import frappe.share from frappe import _ -from frappe.utils import cstr, now_datetime, cint, flt, get_time, get_link_to_form +from frappe.utils import cstr, now_datetime, cint, flt, get_time, get_datetime, get_link_to_form from erpnext.controllers.status_updater import StatusUpdater +from erpnext.accounts.utils import get_fiscal_year from six import string_types @@ -28,6 +29,8 @@ class TransactionBase(StatusUpdater): except ValueError: frappe.throw(_('Invalid Posting Time')) + self.validate_with_last_transaction_posting_time() + def add_calendar_event(self, opts, force=False): if cstr(self.contact_by) != cstr(self._prev.contact_by) or \ cstr(self.contact_date) != cstr(self._prev.contact_date) or force or \ @@ -148,6 +151,30 @@ class TransactionBase(StatusUpdater): return ret + def validate_with_last_transaction_posting_time(self): + + if self.doctype not in ["Sales Invoice", "Purchase Invoice", "Stock Entry", "Stock Reconciliation", + "Delivery Note", "Purchase Receipt", "Fees"]: + return + + if self.doctype in ["Sales Invoice", "Purchase Invoice"]: + if not (self.get("update_stock") or self.get("is_pos")): + return + + fiscal_year = get_fiscal_year(self.get('posting_date'), as_dict=True).name + + last_transaction_time = frappe.db.sql(""" + select MAX(timestamp(posting_date, posting_time)) as posting_time + from `tabStock Ledger Entry` + where docstatus = 1 and fiscal_year = %s""", (fiscal_year))[0][0] + + cur_doc_posting_datetime = "%s %s" % (self.posting_date, self.get("posting_time") or "00:00:00") + + if last_transaction_time and get_datetime(cur_doc_posting_datetime) < get_datetime(last_transaction_time): + frappe.throw(_("""Posting timestamp of current transaction + must be after last Stock transaction's timestamp which is {0}""").format(frappe.bold(last_transaction_time)), + title=_("Backdated Stock Entry")) + def delete_events(ref_type, ref_name): events = frappe.db.sql_list(""" SELECT distinct `tabEvent`.name From 078c95c4385e7b293c45b70d6474b967b87afad6 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 30 Apr 2020 10:57:38 +0530 Subject: [PATCH 53/58] fix: print heading field shown in gst section for india region (#21499) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 918fa140b2d..db205891444 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -149,9 +149,9 @@ "edit_printing_settings", "letter_head", "group_same_items", - "language", - "column_break_84", "select_print_heading", + "column_break_84", + "language", "more_information", "inter_company_invoice_reference", "is_internal_customer", @@ -1579,7 +1579,7 @@ "idx": 181, "is_submittable": 1, "links": [], - "modified": "2020-04-17 12:38:41.435728", + "modified": "2020-04-29 13:37:09.355300", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", From 691f69c7316165c6137e887e20a35f590b62e417 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 30 Apr 2020 11:02:28 +0530 Subject: [PATCH 54/58] fix: Consider any kind of exemptions only if tax exemptions are allowed on tax slab (#21474) --- .../income_tax_slab/income_tax_slab.json | 21 ++----- .../salary_component/salary_component.json | 4 +- erpnext/hr/doctype/salary_slip/salary_slip.py | 62 ++++++++++--------- 3 files changed, 41 insertions(+), 46 deletions(-) diff --git a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json index 6d89b197d27..bc6bc5ed370 100644 --- a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json +++ b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json @@ -93,13 +93,15 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-04-24 12:28:36.805904", + "modified": "2020-04-27 20:10:42.755762", "modified_by": "Administrator", "module": "HR", "name": "Income Tax Slab", "owner": "Administrator", "permissions": [ { + "amend": 1, + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -109,9 +111,11 @@ "report": 1, "role": "System Manager", "share": 1, + "submit": 1, "write": 1 }, { + "amend": 1, "cancel": 1, "create": 1, "delete": 1, @@ -126,6 +130,7 @@ "write": 1 }, { + "amend": 1, "cancel": 1, "create": 1, "delete": 1, @@ -138,20 +143,6 @@ "share": 1, "submit": 1, "write": 1 - }, - { - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Administrator", - "share": 1, - "submit": 1, - "write": 1 } ], "sort_field": "modified", diff --git a/erpnext/hr/doctype/salary_component/salary_component.json b/erpnext/hr/doctype/salary_component/salary_component.json index 5487e1dee85..97c46c829e7 100644 --- a/erpnext/hr/doctype/salary_component/salary_component.json +++ b/erpnext/hr/doctype/salary_component/salary_component.json @@ -227,7 +227,7 @@ { "default": "0", "depends_on": "eval:doc.type == \"Deduction\" && !doc.variable_based_on_taxable_salary", - "description": "If checked, the full amount will be deducted from taxable income before calculating income tax. Otherwise, it can be exempted via Employee Tax Exemption Declaration.", + "description": "If checked, the full amount will be deducted from taxable income before calculating income tax without any declaration or proof submission.", "fieldname": "exempted_from_income_tax", "fieldtype": "Check", "label": "Exempted from Income Tax" @@ -235,7 +235,7 @@ ], "icon": "fa fa-flag", "links": [], - "modified": "2020-04-24 14:50:28.994054", + "modified": "2020-04-28 15:46:45.252945", "modified_by": "Administrator", "module": "HR", "name": "Salary Component", diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index 8a4da7e7d38..fc65fb58aeb 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -549,15 +549,16 @@ class SalarySlip(TransactionBase): remaining_sub_periods = get_period_factor(self.employee, self.start_date, self.end_date, self.payroll_frequency, payroll_period)[1] # get taxable_earnings, paid_taxes for previous period - previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date, self.start_date) + previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date, + self.start_date, tax_slab.allow_tax_exemption) previous_total_paid_taxes = self.get_tax_paid_in_period(payroll_period.start_date, self.start_date, tax_component) # get taxable_earnings for current period (all days) - current_taxable_earnings = self.get_taxable_earnings() + current_taxable_earnings = self.get_taxable_earnings(tax_slab.allow_tax_exemption) future_structured_taxable_earnings = current_taxable_earnings.taxable_earnings * (math.ceil(remaining_sub_periods) - 1) # get taxable_earnings, addition_earnings for current actual payment days - current_taxable_earnings_for_payment_days = self.get_taxable_earnings(based_on_payment_days=1) + current_taxable_earnings_for_payment_days = self.get_taxable_earnings(tax_slab.allow_tax_exemption, based_on_payment_days=1) current_structured_taxable_earnings = current_taxable_earnings_for_payment_days.taxable_earnings current_additional_earnings = current_taxable_earnings_for_payment_days.additional_income current_additional_earnings_with_full_tax = current_taxable_earnings_for_payment_days.additional_income_with_full_tax @@ -616,7 +617,7 @@ class SalarySlip(TransactionBase): return income_tax_slab_doc - def get_taxable_earnings_for_prev_period(self, start_date, end_date): + def get_taxable_earnings_for_prev_period(self, start_date, end_date, allow_tax_exemption=False): taxable_earnings = frappe.db.sql(""" select sum(sd.amount) from @@ -636,24 +637,26 @@ class SalarySlip(TransactionBase): }) taxable_earnings = flt(taxable_earnings[0][0]) if taxable_earnings else 0 - exempted_amount = frappe.db.sql(""" - select sum(sd.amount) - from - `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name - where - sd.parentfield='deductions' - and sd.exempted_from_income_tax=1 - and is_flexible_benefit=0 - and ss.docstatus=1 - and ss.employee=%(employee)s - and ss.start_date between %(from_date)s and %(to_date)s - and ss.end_date between %(from_date)s and %(to_date)s - """, { - "employee": self.employee, - "from_date": start_date, - "to_date": end_date - }) - exempted_amount = flt(exempted_amount[0][0]) if exempted_amount else 0 + exempted_amount = 0 + if allow_tax_exemption: + exempted_amount = frappe.db.sql(""" + select sum(sd.amount) + from + `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name + where + sd.parentfield='deductions' + and sd.exempted_from_income_tax=1 + and is_flexible_benefit=0 + and ss.docstatus=1 + and ss.employee=%(employee)s + and ss.start_date between %(from_date)s and %(to_date)s + and ss.end_date between %(from_date)s and %(to_date)s + """, { + "employee": self.employee, + "from_date": start_date, + "to_date": end_date + }) + exempted_amount = flt(exempted_amount[0][0]) if exempted_amount else 0 return taxable_earnings - exempted_amount @@ -681,7 +684,7 @@ class SalarySlip(TransactionBase): return total_tax_paid - def get_taxable_earnings(self, based_on_payment_days=0): + def get_taxable_earnings(self, allow_tax_exemption=False, based_on_payment_days=0): joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, ["date_of_joining", "relieving_date"]) @@ -715,12 +718,13 @@ class SalarySlip(TransactionBase): else: taxable_earnings += amount - for ded in self.deductions: - if ded.exempted_from_income_tax: - amount = ded.amount - if based_on_payment_days: - amount = self.get_amount_based_on_payment_days(ded, joining_date, relieving_date)[0] - taxable_earnings -= flt(amount) + if allow_tax_exemption: + for ded in self.deductions: + if ded.exempted_from_income_tax: + amount = ded.amount + if based_on_payment_days: + amount = self.get_amount_based_on_payment_days(ded, joining_date, relieving_date)[0] + taxable_earnings -= flt(amount) return frappe._dict({ "taxable_earnings": taxable_earnings, From 62394d7762be735cd72d5b8dd5d3957db1d4982b Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 30 Apr 2020 11:02:53 +0530 Subject: [PATCH 55/58] fix: Tax calcualtion based on slab (#21496) Co-authored-by: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> --- erpnext/hr/doctype/salary_slip/salary_slip.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index fc65fb58aeb..db93f31f4a1 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -826,13 +826,13 @@ class SalarySlip(TransactionBase): for slab in tax_slab.slabs: if slab.condition and not self.eval_tax_slab_condition(slab.condition, data): continue - if not slab.to_amount and annual_taxable_earning > slab.from_amount: - tax_amount += (annual_taxable_earning - slab.from_amount) * slab.percent_deduction *.01 + if not slab.to_amount and annual_taxable_earning >= slab.from_amount: + tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction *.01 continue - if annual_taxable_earning > slab.from_amount and annual_taxable_earning < slab.to_amount: - tax_amount += (annual_taxable_earning - slab.from_amount) * slab.percent_deduction *.01 - elif annual_taxable_earning > slab.from_amount and annual_taxable_earning > slab.to_amount: - tax_amount += (slab.to_amount - slab.from_amount) * slab.percent_deduction * .01 + if annual_taxable_earning >= slab.from_amount and annual_taxable_earning < slab.to_amount: + tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction *.01 + elif annual_taxable_earning >= slab.from_amount and annual_taxable_earning >= slab.to_amount: + tax_amount += (slab.to_amount - slab.from_amount + 1) * slab.percent_deduction * .01 # other taxes and charges on income tax for d in tax_slab.other_taxes_and_charges: From b8d633cb4a8b1101d7a4e7c6fa64c89521c8fd33 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 30 Apr 2020 11:03:49 +0530 Subject: [PATCH 56/58] fix: Desk links for Income Tax Slab and Employee Other Income (#21510) --- erpnext/config/hr.py | 8 ++++++++ erpnext/hr/desk_page/hr/hr.json | 6 +++--- erpnext/hr/doctype/income_tax_slab/income_tax_slab.json | 3 ++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/erpnext/config/hr.py b/erpnext/config/hr.py index 7b3b4660f57..9855a115a60 100644 --- a/erpnext/config/hr.py +++ b/erpnext/config/hr.py @@ -170,6 +170,10 @@ def get_data(): "type": "doctype", "name": "Payroll Period", }, + { + "type": "doctype", + "name": "Income Tax Slab", + }, { "type": "doctype", "name": "Salary Component", @@ -209,6 +213,10 @@ def get_data(): "name": "Employee Tax Exemption Proof Submission", "dependencies": ["Employee"] }, + { + "type": "doctype", + "name": "Employee Other Income", + }, { "type": "doctype", "name": "Employee Benefit Application", diff --git a/erpnext/hr/desk_page/hr/hr.json b/erpnext/hr/desk_page/hr/hr.json index 743aa232391..22aa170744c 100644 --- a/erpnext/hr/desk_page/hr/hr.json +++ b/erpnext/hr/desk_page/hr/hr.json @@ -23,7 +23,7 @@ { "hidden": 0, "label": "Payroll", - "links": "[\n {\n \"label\": \"Salary Structure\",\n \"name\": \"Salary Structure\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Salary Structure\",\n \"Employee\"\n ],\n \"label\": \"Salary Structure Assignment\",\n \"name\": \"Salary Structure Assignment\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Payroll Entry\",\n \"name\": \"Payroll Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Salary Slip\",\n \"name\": \"Salary Slip\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Payroll Period\",\n \"name\": \"Payroll Period\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Salary Component\",\n \"name\": \"Salary Component\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Additional Salary\",\n \"name\": \"Additional Salary\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Retention Bonus\",\n \"name\": \"Retention Bonus\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Incentive\",\n \"name\": \"Employee Incentive\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Salary Slip\"\n ],\n \"doctype\": \"Salary Slip\",\n \"is_query_report\": true,\n \"label\": \"Salary Register\",\n \"name\": \"Salary Register\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"label\": \"Salary Structure\",\n \"name\": \"Salary Structure\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Salary Structure\",\n \"Employee\"\n ],\n \"label\": \"Salary Structure Assignment\",\n \"name\": \"Salary Structure Assignment\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Payroll Entry\",\n \"name\": \"Payroll Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Salary Slip\",\n \"name\": \"Salary Slip\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Payroll Period\",\n \"name\": \"Payroll Period\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Income Tax Slab\",\n \"name\": \"Income Tax Slab\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Salary Component\",\n \"name\": \"Salary Component\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Additional Salary\",\n \"name\": \"Additional Salary\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Retention Bonus\",\n \"name\": \"Retention Bonus\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Incentive\",\n \"name\": \"Employee Incentive\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Salary Slip\"\n ],\n \"doctype\": \"Salary Slip\",\n \"is_query_report\": true,\n \"label\": \"Salary Register\",\n \"name\": \"Salary Register\",\n \"type\": \"report\"\n }\n]" }, { "hidden": 0, @@ -73,7 +73,7 @@ { "hidden": 0, "label": "Employee Tax and Benefits", - "links": "[\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Tax Exemption Declaration\",\n \"name\": \"Employee Tax Exemption Declaration\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Tax Exemption Proof Submission\",\n \"name\": \"Employee Tax Exemption Proof Submission\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Benefit Application\",\n \"name\": \"Employee Benefit Application\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Benefit Claim\",\n \"name\": \"Employee Benefit Claim\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Tax Exemption Category\",\n \"name\": \"Employee Tax Exemption Category\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Tax Exemption Sub Category\",\n \"name\": \"Employee Tax Exemption Sub Category\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Tax Exemption Declaration\",\n \"name\": \"Employee Tax Exemption Declaration\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Tax Exemption Proof Submission\",\n \"name\": \"Employee Tax Exemption Proof Submission\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\",\n \"Payroll Period\"\n ],\n \"label\": \"Employee Other Income\",\n \"name\": \"Employee Other Income\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Benefit Application\",\n \"name\": \"Employee Benefit Application\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Benefit Claim\",\n \"name\": \"Employee Benefit Claim\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Tax Exemption Category\",\n \"name\": \"Employee Tax Exemption Category\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Tax Exemption Sub Category\",\n \"name\": \"Employee Tax Exemption Sub Category\",\n \"type\": \"doctype\"\n }\n]" } ], "category": "Modules", @@ -88,7 +88,7 @@ "idx": 0, "is_standard": 1, "label": "HR", - "modified": "2020-04-01 11:28:50.860012", + "modified": "2020-04-29 20:29:22.114309", "modified_by": "Administrator", "module": "HR", "name": "HR", diff --git a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json index bc6bc5ed370..f74315f32e9 100644 --- a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json +++ b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json @@ -80,6 +80,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "other_taxes_and_charges", "fieldname": "taxes_and_charges_on_income_tax_section", "fieldtype": "Section Break", "label": "Taxes and Charges on Income Tax" @@ -93,7 +94,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-04-27 20:10:42.755762", + "modified": "2020-04-29 15:08:21.436120", "modified_by": "Administrator", "module": "HR", "name": "Income Tax Slab", From 090e6093f2106d863ddb6d0bd19edf59819add16 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 30 Apr 2020 11:04:44 +0530 Subject: [PATCH 57/58] fix: list index out of range error (#21467) * fix: list index out of range error * fix: condititon --- .../gross_and_net_profit_report.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py index 260f35f270d..1c458107d40 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py @@ -35,6 +35,12 @@ def execute(filters=None): }) return columns, data + # to avoid error eg: gross_income[0] : list index out of range + if not gross_income: + gross_income = [{}] + if not gross_expense: + gross_expense = [{}] + data.append({ "account_name": "'" + _("Included in Gross Profit") + "'", "account": "'" + _("Included in Gross Profit") + "'" From a8779872eed2590a486054eda99a01014ff25a35 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 30 Apr 2020 11:28:43 +0530 Subject: [PATCH 58/58] feat: force cost center renaming from cost center form (#21503) --- .../doctype/cost_center/cost_center.js | 30 ++++++++---- .../doctype/cost_center/cost_center.json | 3 +- erpnext/accounts/utils.py | 47 +++++++------------ 3 files changed, 39 insertions(+), 41 deletions(-) diff --git a/erpnext/accounts/doctype/cost_center/cost_center.js b/erpnext/accounts/doctype/cost_center/cost_center.js index 96ec57dcb0b..9e2f6eed3b6 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.js +++ b/erpnext/accounts/doctype/cost_center/cost_center.js @@ -18,7 +18,7 @@ frappe.ui.form.on('Cost Center', { }, refresh: function(frm) { if (!frm.is_new()) { - frm.add_custom_button(__('Update Cost Center Number'), function () { + frm.add_custom_button(__('Update Cost Center Name / Number'), function () { frm.trigger("update_cost_center_number"); }); } @@ -47,35 +47,45 @@ frappe.ui.form.on('Cost Center', { }, update_cost_center_number: function(frm) { var d = new frappe.ui.Dialog({ - title: __('Update Cost Center Number'), + title: __('Update Cost Center Name / Number'), fields: [ { - "label": 'Cost Center Number', + "label": "Cost Center Name", + "fieldname": "cost_center_name", + "fieldtype": "Data", + "reqd": 1, + "default": frm.doc.cost_center_name + }, + { + "label": "Cost Center Number", "fieldname": "cost_center_number", "fieldtype": "Data", - "reqd": 1 + "reqd": 1, + "default": frm.doc.cost_center_number } ], primary_action: function() { var data = d.get_values(); - if(data.cost_center_number === frm.doc.cost_center_number) { + if(data.cost_center_name === frm.doc.cost_center_name && data.cost_center_number === frm.doc.cost_center_number) { d.hide(); return; } + frappe.dom.freeze(); frappe.call({ - method: "erpnext.accounts.utils.update_number_field", + method: "erpnext.accounts.utils.update_cost_center", args: { - doctype_name: frm.doc.doctype, - name: frm.doc.name, - field_name: d.fields[0].fieldname, - number_value: data.cost_center_number, + docname: frm.doc.name, + cost_center_name: data.cost_center_name, + cost_center_number: data.cost_center_number, company: frm.doc.company }, callback: function(r) { + frappe.dom.unfreeze(); if(!r.exc) { if(r.message) { frappe.set_route("Form", "Cost Center", r.message); } else { + me.frm.set_value("cost_center_name", data.cost_center_name); me.frm.set_value("cost_center_number", data.cost_center_number); } d.hide(); diff --git a/erpnext/accounts/doctype/cost_center/cost_center.json b/erpnext/accounts/doctype/cost_center/cost_center.json index 99b89d15161..5013c92a327 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.json +++ b/erpnext/accounts/doctype/cost_center/cost_center.json @@ -2,7 +2,6 @@ "actions": [], "allow_copy": 1, "allow_import": 1, - "allow_rename": 1, "creation": "2013-01-23 19:57:17", "description": "Track separate Income and Expense for product verticals or divisions.", "doctype": "DocType", @@ -126,7 +125,7 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2020-03-18 17:59:04.321637", + "modified": "2020-04-29 16:09:30.025214", "modified_by": "Administrator", "module": "Accounts", "name": "Cost Center", diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index b5d6ca9bbce..5165495786d 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -817,48 +817,37 @@ def create_payment_gateway_account(gateway): pass @frappe.whitelist() -def update_number_field(doctype_name, name, field_name, number_value, company): +def update_cost_center(docname, cost_center_name, cost_center_number, company): ''' - doctype_name = Name of the DocType - name = Docname being referred - field_name = Name of the field thats holding the 'number' attribute - number_value = Numeric value entered in field_name - - Stores the number entered in the dialog to the DocType's field. - Renames the document by adding the number as a prefix to the current name and updates all transaction where it was present. ''' - doc_title = frappe.db.get_value(doctype_name, name, frappe.scrub(doctype_name)+"_name") + validate_field_number("Cost Center", docname, cost_center_number, company, "cost_center_number") - validate_field_number(doctype_name, name, number_value, company, field_name) + if cost_center_number: + frappe.db.set_value("Cost Center", docname, "cost_center_number", cost_center_number.strip()) + else: + frappe.db.set_value("Cost Center", docname, "cost_center_number", "") - frappe.db.set_value(doctype_name, name, field_name, number_value) + frappe.db.set_value("Cost Center", docname, "cost_center_name", cost_center_name.strip()) - if doc_title[0].isdigit(): - separator = " - " if " - " in doc_title else " " - doc_title = doc_title.split(separator, 1)[1] - - frappe.db.set_value(doctype_name, name, frappe.scrub(doctype_name)+"_name", doc_title) - - new_name = get_autoname_with_number(number_value, doc_title, name, company) - - if name != new_name: - frappe.rename_doc(doctype_name, name, new_name) + new_name = get_autoname_with_number(cost_center_number, cost_center_name, docname, company) + if docname != new_name: + frappe.rename_doc("Cost Center", docname, new_name, force=1) return new_name -def validate_field_number(doctype_name, name, number_value, company, field_name): +def validate_field_number(doctype_name, docname, number_value, company, field_name): ''' Validate if the number entered isn't already assigned to some other document. ''' if number_value: + filters = {field_name: number_value, "name": ["!=", docname]} if company: - doctype_with_same_number = frappe.db.get_value(doctype_name, - {field_name: number_value, "company": company, "name": ["!=", name]}) - else: - doctype_with_same_number = frappe.db.get_value(doctype_name, - {field_name: number_value, "name": ["!=", name]}) + filters["company"] = company + + doctype_with_same_number = frappe.db.get_value(doctype_name, filters) + if doctype_with_same_number: - frappe.throw(_("{0} Number {1} already used in account {2}") - .format(doctype_name, number_value, doctype_with_same_number)) + frappe.throw(_("{0} Number {1} is already used in {2} {3}") + .format(doctype_name, number_value, doctype_name.lower(), doctype_with_same_number)) def get_autoname_with_number(number_value, doc_title, name, company): ''' append title with prefix as number and suffix as company's abbreviation separated by '-' '''