diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index bcb22f0e57c..83c670eace4 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -228,6 +228,8 @@ class PaymentEntry(AccountsController): valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry") elif self.party_type == "Employee": valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance") + elif self.party_type == "Shareholder": + valid_reference_doctypes = ("Journal Entry") for d in self.get("references"): if not d.allocated_amount: diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 6e5b33f07d2..528fb4e113a 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -162,7 +162,7 @@ def get_default_price_list(party): def set_price_list(party_details, party, party_type, given_price_list, pos=None): # price list price_list = get_permitted_documents('Price List') - + # if there is only one permitted document based on user permissions, set it if price_list and len(price_list) == 1: price_list = price_list[0] @@ -465,23 +465,25 @@ def get_timeline_data(doctype, name): from frappe.desk.form.load import get_communication_data out = {} - fields = 'date(creation), count(name)' + fields = 'creation, count(*)' after = add_years(None, -1).strftime('%Y-%m-%d') - group_by='group by date(creation)' + group_by='group by Date(creation)' - data = get_communication_data(doctype, name, after=after, group_by='group by date(creation)', - fields='date(C.creation) as creation, count(C.name)',as_dict=False) + data = get_communication_data(doctype, name, after=after, group_by='group by creation', + fields='C.creation as creation, count(C.name)',as_dict=False) # fetch and append data from Activity Log data += frappe.db.sql("""select {fields} from `tabActivity Log` - where (reference_doctype="{doctype}" and reference_name="{name}") - or (timeline_doctype in ("{doctype}") and timeline_name="{name}") - or (reference_doctype in ("Quotation", "Opportunity") and timeline_name="{name}") + where (reference_doctype=%(doctype)s and reference_name=%(name)s) + or (timeline_doctype in (%(doctype)s) and timeline_name=%(name)s) + or (reference_doctype in ("Quotation", "Opportunity") and timeline_name=%(name)s) and status!='Success' and creation > {after} {group_by} order by creation desc - """.format(doctype=frappe.db.escape(doctype), name=frappe.db.escape(name), fields=fields, - group_by=group_by, after=after), as_dict=False) + """.format(fields=fields, group_by=group_by, after=after), { + "doctype": doctype, + "name": name + }, as_dict=False) timeline_items = dict(data) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 7af5fa8eaa7..6afe208b5b1 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -296,6 +296,9 @@ def get_accountwise_gle(filters, gl_entries, gle_map): data[key].debit_in_account_currency += flt(gle.debit_in_account_currency) data[key].credit_in_account_currency += flt(gle.credit_in_account_currency) + if data[key].against_voucher: + data[key].against_voucher += ', ' + gle.against_voucher + from_date, to_date = getdate(filters.from_date), getdate(filters.to_date) for gle in gl_entries: if (gle.posting_date < from_date or diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 06dfa19bf2f..a3200d56443 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -32,7 +32,7 @@ class Asset(AccountsController): self.validate_in_use_date() self.set_status() self.make_asset_movement() - if not self.booked_fixed_asset and is_cwip_accounting_enabled(self.asset_category): + if not self.booked_fixed_asset and self.validate_make_gl_entry(): self.make_gl_entries() def before_cancel(self): @@ -455,18 +455,55 @@ class Asset(AccountsController): for d in self.get('finance_books'): if d.finance_book == self.default_finance_book: return cint(d.idx) - 1 + + def validate_make_gl_entry(self): + purchase_document = self.get_purchase_document() + asset_bought_with_invoice = purchase_document == self.purchase_invoice + fixed_asset_account, cwip_account = self.get_asset_accounts() + cwip_enabled = is_cwip_accounting_enabled(self.asset_category) + # check if expense already has been booked in case of cwip was enabled after purchasing asset + expense_booked = False + cwip_booked = False + + if asset_bought_with_invoice: + expense_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""", + (purchase_document, fixed_asset_account), as_dict=1) + else: + cwip_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""", + (purchase_document, cwip_account), as_dict=1) + + if cwip_enabled and (expense_booked or not cwip_booked): + # if expense has already booked from invoice or cwip is booked from receipt + return False + elif not cwip_enabled and (not expense_booked or cwip_booked): + # if cwip is disabled but expense hasn't been booked yet + return True + elif cwip_enabled: + # default condition + return True + + def get_purchase_document(self): + asset_bought_with_invoice = self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock') + purchase_document = self.purchase_invoice if asset_bought_with_invoice else self.purchase_receipt + + return purchase_document + + def get_asset_accounts(self): + fixed_asset_account = get_asset_category_account('fixed_asset_account', asset=self.name, + asset_category = self.asset_category, company = self.company) + + cwip_account = get_asset_account("capital_work_in_progress_account", + self.name, self.asset_category, self.company) + + return fixed_asset_account, cwip_account def make_gl_entries(self): gl_entries = [] - if ((self.purchase_receipt \ - or (self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock'))) - and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()): - fixed_asset_account = get_asset_category_account('fixed_asset_account', asset=self.name, - asset_category = self.asset_category, company = self.company) + purchase_document = self.get_purchase_document() + fixed_asset_account, cwip_account = self.get_asset_accounts() - cwip_account = get_asset_account("capital_work_in_progress_account", - self.name, self.asset_category, self.company) + if (purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()): gl_entries.append(self.get_gl_dict({ "account": cwip_account, diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index a0f8d156d72..aed78e7746d 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -560,6 +560,81 @@ class TestAsset(unittest.TestCase): self.assertEqual(gle, expected_gle) + def test_gle_with_cwip_toggling(self): + # TEST: purchase an asset with cwip enabled and then disable cwip and try submitting the asset + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1) + + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=5000, do_not_submit=True, location="Test Location") + pr.set('taxes', [{ + 'category': 'Total', + 'add_deduct_tax': 'Add', + 'charge_type': 'On Net Total', + 'account_head': '_Test Account Service Tax - _TC', + 'description': '_Test Account Service Tax', + 'cost_center': 'Main - _TC', + 'rate': 5.0 + }, { + 'category': 'Valuation and Total', + 'add_deduct_tax': 'Add', + 'charge_type': 'On Net Total', + 'account_head': '_Test Account Shipping Charges - _TC', + 'description': '_Test Account Shipping Charges', + 'cost_center': 'Main - _TC', + 'rate': 5.0 + }]) + pr.submit() + expected_gle = ( + ("Asset Received But Not Billed - _TC", 0.0, 5250.0), + ("CWIP Account - _TC", 5250.0, 0.0) + ) + pr_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + where voucher_type='Purchase Receipt' and voucher_no = %s + order by account""", pr.name) + self.assertEqual(pr_gle, expected_gle) + + pi = make_invoice(pr.name) + pi.submit() + expected_gle = ( + ("_Test Account Service Tax - _TC", 250.0, 0.0), + ("_Test Account Shipping Charges - _TC", 250.0, 0.0), + ("Asset Received But Not Billed - _TC", 5250.0, 0.0), + ("Creditors - _TC", 0.0, 5500.0), + ("Expenses Included In Asset Valuation - _TC", 0.0, 250.0), + ) + pi_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + where voucher_type='Purchase Invoice' and voucher_no = %s + order by account""", pi.name) + self.assertEqual(pi_gle, expected_gle) + + asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name') + asset_doc = frappe.get_doc('Asset', asset) + month_end_date = get_last_day(nowdate()) + asset_doc.available_for_use_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15) + self.assertEqual(asset_doc.gross_purchase_amount, 5250.0) + asset_doc.append("finance_books", { + "expected_value_after_useful_life": 200, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + "depreciation_start_date": month_end_date + }) + + # disable cwip and try submitting + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0) + asset_doc.submit() + # asset should have gl entries even if cwip is disabled + expected_gle = ( + ("_Test Fixed Asset - _TC", 5250.0, 0.0), + ("CWIP Account - _TC", 0.0, 5250.0) + ) + gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + where voucher_type='Asset' and voucher_no = %s + order by account""", asset_doc.name) + self.assertEqual(gle, expected_gle) + + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1) + def test_expense_head(self): pr = make_purchase_receipt(item_code="Macbook Pro", qty=2, rate=200000.0, location="Test Location") diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index 3a08baa714e..3da355e2b99 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -110,6 +110,7 @@ class AssetMovement(Document): ORDER BY asm.transaction_date asc """, (d.asset, self.company, 'Receipt'), as_dict=1) + if auto_gen_movement_entry and auto_gen_movement_entry[0].get('name') == self.name: frappe.throw(_('{0} will be cancelled automatically on asset cancellation as it was \ auto generated for Asset {1}').format(self.name, d.asset)) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 81fdbbefc35..90c67f1e521 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -74,7 +74,7 @@ def validate_returned_items(doc): for d in doc.get("items"): if d.item_code and (flt(d.qty) < 0 or flt(d.get('received_qty')) < 0): if d.item_code not in valid_items: - frappe.throw(_("Row # {0}: Returned Item {1} does not exists in {2} {3}") + frappe.throw(_("Row # {0}: Returned Item {1} does not exist in {2} {3}") .format(d.idx, d.item_code, doc.doctype, doc.return_against)) else: ref = valid_items.get(d.item_code, frappe._dict()) @@ -266,6 +266,8 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.purchase_order = source_doc.purchase_order target_doc.purchase_order_item = source_doc.purchase_order_item target_doc.rejected_warehouse = source_doc.rejected_warehouse + target_doc.purchase_receipt_item = source_doc.name + elif doctype == "Purchase Invoice": target_doc.received_qty = -1* source_doc.received_qty target_doc.rejected_qty = -1* source_doc.rejected_qty @@ -282,6 +284,7 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.so_detail = source_doc.so_detail target_doc.si_detail = source_doc.si_detail target_doc.expense_account = source_doc.expense_account + target_doc.dn_detail = source_doc.name if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return elif doctype == "Sales Invoice": diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 90ba8b3644b..1e0a48c134b 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -165,9 +165,9 @@ class SellingController(StockController): d.stock_qty = flt(d.qty) * flt(d.conversion_factor) def validate_selling_price(self): - def throw_message(item_name, rate, ref_rate_field): - frappe.throw(_("""Selling rate for item {0} is lower than its {1}. Selling rate should be atleast {2}""") - .format(item_name, ref_rate_field, rate)) + def throw_message(idx, item_name, rate, ref_rate_field): + frappe.throw(_("""Row #{}: Selling rate for item {} is lower than its {}. Selling rate should be atleast {}""") + .format(idx, item_name, ref_rate_field, rate)) if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"): return @@ -181,8 +181,8 @@ class SellingController(StockController): last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"]) last_purchase_rate_in_sales_uom = last_purchase_rate / (it.conversion_factor or 1) - if flt(it.base_rate) < flt(last_purchase_rate_in_sales_uom) and not self.get('is_internal_customer'): - throw_message(it.item_name, last_purchase_rate_in_sales_uom, "last purchase rate") + if flt(it.base_rate) < flt(last_purchase_rate_in_sales_uom): + throw_message(it.idx, frappe.bold(it.item_name), last_purchase_rate_in_sales_uom, "last purchase rate") last_valuation_rate = frappe.db.sql(""" SELECT valuation_rate FROM `tabStock Ledger Entry` WHERE item_code = %s @@ -193,7 +193,7 @@ class SellingController(StockController): last_valuation_rate_in_sales_uom = last_valuation_rate[0][0] / (it.conversion_factor or 1) if is_stock_item and flt(it.base_rate) < flt(last_valuation_rate_in_sales_uom) \ and not self.get('is_internal_customer'): - throw_message(it.name, last_valuation_rate_in_sales_uom, "valuation rate") + throw_message(it.idx, frappe.bold(it.item_name), last_valuation_rate_in_sales_uom, "valuation rate") def get_item_list(self): diff --git a/erpnext/healthcare/desk_page/healthcare/healthcare.json b/erpnext/healthcare/desk_page/healthcare/healthcare.json index 24c6d6fc37f..5cf09b34b24 100644 --- a/erpnext/healthcare/desk_page/healthcare/healthcare.json +++ b/erpnext/healthcare/desk_page/healthcare/healthcare.json @@ -47,7 +47,12 @@ } ], "category": "Domains", - "charts": [], + "charts": [ + { + "chart_name": "Patient Appointments", + "label": "Patient Appointments" + } + ], "charts_label": "", "creation": "2020-03-02 17:23:17.919682", "developer_mode_only": 0, @@ -58,7 +63,7 @@ "idx": 0, "is_standard": 1, "label": "Healthcare", - "modified": "2020-04-20 11:42:43.889576", + "modified": "2020-04-25 22:31:36.576444", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare", diff --git a/erpnext/hr/doctype/additional_salary/additional_salary.js b/erpnext/hr/doctype/additional_salary/additional_salary.js index 18f6b8b52d5..fb42b6f410e 100644 --- a/erpnext/hr/doctype/additional_salary/additional_salary.js +++ b/erpnext/hr/doctype/additional_salary/additional_salary.js @@ -13,5 +13,5 @@ frappe.ui.form.on('Additional Salary', { } }; }); - } + }, }); diff --git a/erpnext/hr/doctype/additional_salary/additional_salary.json b/erpnext/hr/doctype/additional_salary/additional_salary.json index 7d69f7e7fc2..bfb543f49ad 100644 --- a/erpnext/hr/doctype/additional_salary/additional_salary.json +++ b/erpnext/hr/doctype/additional_salary/additional_salary.json @@ -13,10 +13,14 @@ "salary_component", "overwrite_salary_structure_amount", "deduct_full_tax_on_selected_payroll_date", + "ref_doctype", + "ref_docname", "column_break_5", "company", + "is_recurring", + "from_date", + "to_date", "payroll_date", - "salary_slip", "type", "department", "amount", @@ -74,12 +78,13 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:(doc.is_recurring==0)", "description": "Date on which this component is applied", "fieldname": "payroll_date", "fieldtype": "Date", "in_list_view": 1, "label": "Payroll Date", - "reqd": 1, + "mandatory_depends_on": "eval:(doc.is_recurring==0)", "search_index": 1 }, { @@ -105,13 +110,6 @@ "options": "Company", "reqd": 1 }, - { - "fieldname": "salary_slip", - "fieldtype": "Link", - "label": "Salary Slip", - "options": "Salary Slip", - "read_only": 1 - }, { "fetch_from": "salary_component.type", "fieldname": "type", @@ -127,11 +125,45 @@ "options": "Additional Salary", "print_hide": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_recurring", + "fieldtype": "Check", + "label": "Is Recurring" + }, + { + "depends_on": "eval:(doc.is_recurring==1)", + "fieldname": "from_date", + "fieldtype": "Date", + "label": "From Date", + "mandatory_depends_on": "eval:(doc.is_recurring==1)" + }, + { + "depends_on": "eval:(doc.is_recurring==1)", + "fieldname": "to_date", + "fieldtype": "Date", + "label": "To Date", + "mandatory_depends_on": "eval:(doc.is_recurring==1)" + }, + { + "fieldname": "ref_doctype", + "fieldtype": "Link", + "label": "Reference Document Type", + "options": "DocType", + "read_only": 1 + }, + { + "fieldname": "ref_docname", + "fieldtype": "Dynamic Link", + "label": "Reference Document", + "options": "ref_doctype", + "read_only": 1 } ], "is_submittable": 1, "links": [], - "modified": "2019-12-12 19:07:23.635901", + "modified": "2020-04-04 18:06:29.170878", "modified_by": "Administrator", "module": "HR", "name": "Additional Salary", diff --git a/erpnext/hr/doctype/additional_salary/additional_salary.py b/erpnext/hr/doctype/additional_salary/additional_salary.py index bc7dcee55e4..bab6fb545f8 100644 --- a/erpnext/hr/doctype/additional_salary/additional_salary.py +++ b/erpnext/hr/doctype/additional_salary/additional_salary.py @@ -9,6 +9,11 @@ from frappe import _ from frappe.utils import getdate, date_diff class AdditionalSalary(Document): + + def on_submit(self): + if self.ref_doctype == "Employee Advance" and self.ref_docname: + frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", self.amount) + def before_insert(self): if frappe.db.exists("Additional Salary", {"employee": self.employee, "salary_component": self.salary_component, "amount": self.amount, "payroll_date": self.payroll_date, "company": self.company, "docstatus": 1}): @@ -21,10 +26,19 @@ class AdditionalSalary(Document): frappe.throw(_("Amount should not be less than zero.")) def validate_dates(self): - date_of_joining, relieving_date = frappe.db.get_value("Employee", self.employee, + date_of_joining, relieving_date = frappe.db.get_value("Employee", self.employee, ["date_of_joining", "relieving_date"]) - if date_of_joining and getdate(self.payroll_date) < getdate(date_of_joining): - frappe.throw(_("Payroll date can not be less than employee's joining date")) + + if getdate(self.from_date) > getdate(self.to_date): + frappe.throw(_("From Date can not be greater than To Date.")) + + if date_of_joining: + if getdate(self.payroll_date) < getdate(date_of_joining): + frappe.throw(_("Payroll date can not be less than employee's joining date.")) + elif getdate(self.from_date) < getdate(date_of_joining): + frappe.throw(_("From date can not be less than employee's joining date.")) + elif getdate(self.to_date) > getdate(relieving_date): + frappe.throw(_("To date can not be greater than employee's relieving date.")) def get_amount(self, sal_start_date, sal_end_date): start_date = getdate(sal_start_date) @@ -40,15 +54,18 @@ class AdditionalSalary(Document): @frappe.whitelist() def get_additional_salary_component(employee, start_date, end_date, component_type): - additional_components = frappe.db.sql(""" - select salary_component, sum(amount) as amount, overwrite_salary_structure_amount, deduct_full_tax_on_selected_payroll_date + additional_salaries = frappe.db.sql(""" + select name, salary_component, type, amount, overwrite_salary_structure_amount, deduct_full_tax_on_selected_payroll_date from `tabAdditional Salary` where employee=%(employee)s and docstatus = 1 - and payroll_date between %(from_date)s and %(to_date)s - and type = %(component_type)s - group by salary_component, overwrite_salary_structure_amount - order by salary_component, overwrite_salary_structure_amount + and ( + payroll_date between %(from_date)s and %(to_date)s + or + from_date <= %(to_date)s and to_date >= %(to_date)s + ) + and type = %(component_type)s + order by salary_component, overwrite_salary_structure_amount DESC """, { 'employee': employee, 'from_date': start_date, @@ -56,21 +73,38 @@ def get_additional_salary_component(employee, start_date, end_date, component_ty 'component_type': "Earning" if component_type == "earnings" else "Deduction" }, as_dict=1) - additional_components_list = [] + existing_salary_components= [] + salary_components_details = {} + additional_salary_details = [] + + overwrites_components = [ele.salary_component for ele in additional_salaries if ele.overwrite_salary_structure_amount == 1] + component_fields = ["depends_on_payment_days", "salary_component_abbr", "is_tax_applicable", "variable_based_on_taxable_salary", 'type'] - for d in additional_components: - struct_row = frappe._dict({'salary_component': d.salary_component}) - component = frappe.get_all("Salary Component", filters={'name': d.salary_component}, fields=component_fields) - if component: - struct_row.update(component[0]) + for d in additional_salaries: - struct_row['deduct_full_tax_on_selected_payroll_date'] = d.deduct_full_tax_on_selected_payroll_date - struct_row['is_additional_component'] = 1 + if d.salary_component not in existing_salary_components: + component = frappe.get_all("Salary Component", filters={'name': d.salary_component}, fields=component_fields) + struct_row = frappe._dict({'salary_component': d.salary_component}) + if component: + struct_row.update(component[0]) - additional_components_list.append(frappe._dict({ - 'amount': d.amount, - 'type': component[0].type, - 'struct_row': struct_row, - 'overwrite': d.overwrite_salary_structure_amount, - })) - return additional_components_list \ No newline at end of file + struct_row['deduct_full_tax_on_selected_payroll_date'] = d.deduct_full_tax_on_selected_payroll_date + struct_row['is_additional_component'] = 1 + + salary_components_details[d.salary_component] = struct_row + + + if overwrites_components.count(d.salary_component) > 1: + frappe.throw(_("Multiple Additional Salaries with overwrite property exist for Salary Component: {0} between {1} and {2}.".format(d.salary_component, start_date, end_date)), title=_("Error")) + else: + additional_salary_details.append({ + 'name': d.name, + 'component': d.salary_component, + 'amount': d.amount, + 'type': d.type, + 'overwrite': d.overwrite_salary_structure_amount, + }) + + existing_salary_components.append(d.salary_component) + + return salary_components_details, additional_salary_details \ No newline at end of file diff --git a/erpnext/hr/doctype/additional_salary/test_additional_salary.py b/erpnext/hr/doctype/additional_salary/test_additional_salary.py index 949ba20335b..6f93fb5df8d 100644 --- a/erpnext/hr/doctype/additional_salary/test_additional_salary.py +++ b/erpnext/hr/doctype/additional_salary/test_additional_salary.py @@ -3,6 +3,44 @@ # See license.txt from __future__ import unicode_literals import unittest +import frappe, erpnext +from frappe.utils import nowdate, add_days +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.salary_component.test_salary_component import create_salary_component +from erpnext.hr.doctype.salary_slip.test_salary_slip import make_employee_salary_slip, setup_test + class TestAdditionalSalary(unittest.TestCase): - pass + + def setUp(self): + setup_test() + + def test_recurring_additional_salary(self): + emp_id = make_employee("test_additional@salary.com") + frappe.db.set_value("Employee", emp_id, "relieving_date", add_days(nowdate(), 1800)) + add_sal = get_additional_salary(emp_id) + + ss = make_employee_salary_slip("test_additional@salary.com", "Monthly") + for earning in ss.earnings: + if earning.salary_component == "Recurring Salary Component": + amount = earning.amount + salary_component = earning.salary_component + + self.assertEqual(amount, add_sal.amount) + self.assertEqual(salary_component, add_sal.salary_component) + + + +def get_additional_salary(emp_id): + create_salary_component("Recurring Salary Component") + add_sal = frappe.new_doc("Additional Salary") + add_sal.employee = emp_id + add_sal.salary_component = "Recurring Salary Component" + add_sal.is_recurring = 1 + add_sal.from_date = add_days(nowdate(), -50) + add_sal.to_date = add_days(nowdate(), 180) + add_sal.amount = 5000 + add_sal.save() + add_sal.submit() + + return add_sal diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.js b/erpnext/hr/doctype/employee_advance/employee_advance.js index 389660387b7..6cc49cfff23 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.js +++ b/erpnext/hr/doctype/employee_advance/employee_advance.js @@ -23,6 +23,14 @@ frappe.ui.form.on('Employee Advance', { } }; }); + + frm.set_query('salary_component', function(doc) { + return { + filters: { + "type": "Deduction" + } + }; + }); }, refresh: function(frm) { @@ -47,19 +55,37 @@ frappe.ui.form.on('Employee Advance', { } if (frm.doc.docstatus === 1 - && (flt(frm.doc.claimed_amount) + flt(frm.doc.return_amount) < flt(frm.doc.paid_amount)) - && frappe.model.can_create("Journal Entry")) { + && (flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) && flt(frm.doc.paid_amount) != flt(frm.doc.return_amount))) { - frm.add_custom_button(__("Return"), function() { - frm.trigger('make_return_entry'); - }, __('Create')); + if (frm.doc.repay_unclaimed_amount_from_salary == 0 && frappe.model.can_create("Journal Entry")){ + frm.add_custom_button(__("Return"), function() { + frm.trigger('make_return_entry'); + }, __('Create')); + }else if (frm.doc.repay_unclaimed_amount_from_salary == 1 && frappe.model.can_create("Additional Salary")){ + frm.add_custom_button(__("Deduction from salary"), function() { + frm.events.make_deduction_via_additional_salary(frm) + }, __('Create')); + } } }, + make_deduction_via_additional_salary: function(frm){ + frappe.call({ + method: "erpnext.hr.doctype.employee_advance.employee_advance.create_return_through_additional_salary", + args: { + doc: frm.doc + }, + callback: function (r){ + var doclist = frappe.model.sync(r.message); + frappe.set_route("Form", doclist[0].doctype, doclist[0].name); + } + }); + }, + make_payment_entry: function(frm) { var method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry"; if(frm.doc.__onload && frm.doc.__onload.make_payment_via_journal_entry) { - method = "erpnext.hr.doctype.employee_advance.employee_advance.make_bank_entry" + method = "erpnext.hr.doctype.employee_advance.employee_advance.make_bank_entry"; } return frappe.call({ method: method, diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json index d233a2bb936..8c5ce42d870 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.json +++ b/erpnext/hr/doctype/employee_advance/employee_advance.json @@ -10,9 +10,10 @@ "naming_series", "employee", "employee_name", + "department", "column_break_4", "posting_date", - "department", + "repay_unclaimed_amount_from_salary", "section_break_8", "purpose", "column_break_11", @@ -164,16 +165,23 @@ "options": "Mode of Payment" }, { + "allow_on_submit": 1, "fieldname": "return_amount", "fieldtype": "Currency", "label": "Returned Amount", "options": "Company:company:default_currency", "read_only": 1 + }, + { + "default": "0", + "fieldname": "repay_unclaimed_amount_from_salary", + "fieldtype": "Check", + "label": "Repay unclaimed amount from salary" } ], "is_submittable": 1, "links": [], - "modified": "2019-12-15 19:04:07.044505", + "modified": "2020-03-06 15:11:33.747535", "modified_by": "Administrator", "module": "HR", "name": "Employee Advance", @@ -210,4 +218,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index f0663aefa83..23e4992066b 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -133,8 +133,20 @@ def make_bank_entry(dt, dn): return je.as_dict() @frappe.whitelist() -def make_return_entry(employee, company, employee_advance_name, - return_amount, advance_account, mode_of_payment=None): +def create_return_through_additional_salary(doc): + import json + doc = frappe._dict(json.loads(doc)) + additional_salary = frappe.new_doc('Additional Salary') + additional_salary.employee = doc.employee + additional_salary.amount = doc.paid_amount - doc.claimed_amount + additional_salary.company = doc.company + additional_salary.ref_doctype = doc.doctype + additional_salary.ref_docname = doc.name + + return additional_salary + +@frappe.whitelist() +def make_return_entry(employee_name, company, employee_advance_name, return_amount, mode_of_payment, advance_account): return_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment) mode_of_payment_type = '' diff --git a/erpnext/hr/doctype/employee_incentive/employee_incentive.json b/erpnext/hr/doctype/employee_incentive/employee_incentive.json index ce8e1ea230a..e2d8a11f47d 100644 --- a/erpnext/hr/doctype/employee_incentive/employee_incentive.json +++ b/erpnext/hr/doctype/employee_incentive/employee_incentive.json @@ -9,10 +9,9 @@ "employee", "incentive_amount", "employee_name", - "additional_salary", + "salary_component", "column_break_5", "payroll_date", - "salary_component", "department", "amended_from" ], @@ -65,14 +64,6 @@ "options": "Department", "read_only": 1 }, - { - "fieldname": "additional_salary", - "fieldtype": "Link", - "label": "Additional Salary", - "no_copy": 1, - "options": "Additional Salary", - "read_only": 1 - }, { "fieldname": "salary_component", "fieldtype": "Link", @@ -83,7 +74,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2019-12-12 13:24:44.761540", + "modified": "2020-03-05 18:59:40.526014", "modified_by": "Administrator", "module": "HR", "name": "Employee Incentive", diff --git a/erpnext/hr/doctype/employee_incentive/employee_incentive.py b/erpnext/hr/doctype/employee_incentive/employee_incentive.py index 2e138f8ef56..44763fc077e 100644 --- a/erpnext/hr/doctype/employee_incentive/employee_incentive.py +++ b/erpnext/hr/doctype/employee_incentive/employee_incentive.py @@ -9,37 +9,13 @@ from frappe.model.document import Document class EmployeeIncentive(Document): def on_submit(self): company = frappe.db.get_value('Employee', self.employee, 'company') - additional_salary = frappe.db.exists('Additional Salary', { - 'employee': self.employee, - 'salary_component': self.salary_component, - 'payroll_date': self.payroll_date, - 'company': company, - 'docstatus': 1 - }) - if not additional_salary: - additional_salary = frappe.new_doc('Additional Salary') - additional_salary.employee = self.employee - additional_salary.salary_component = self.salary_component - additional_salary.amount = self.incentive_amount - additional_salary.payroll_date = self.payroll_date - additional_salary.company = company - additional_salary.submit() - self.db_set('additional_salary', additional_salary.name) - - else: - incentive_added = frappe.db.get_value('Additional Salary', additional_salary, 'amount') + self.incentive_amount - frappe.db.set_value('Additional Salary', additional_salary, 'amount', incentive_added) - self.db_set('additional_salary', additional_salary) - - def on_cancel(self): - if self.additional_salary: - incentive_removed = frappe.db.get_value('Additional Salary', self.additional_salary, 'amount') - self.incentive_amount - if incentive_removed == 0: - frappe.get_doc('Additional Salary', self.additional_salary).cancel() - else: - frappe.db.set_value('Additional Salary', self.additional_salary, 'amount', incentive_removed) - - self.db_set('additional_salary', '') - - + additional_salary = frappe.new_doc('Additional Salary') + additional_salary.employee = self.employee + additional_salary.salary_component = self.salary_component + additional_salary.amount = self.incentive_amount + additional_salary.payroll_date = self.payroll_date + additional_salary.company = company + additional_salary.ref_doctype = self.doctype + additional_salary.ref_docname = self.name + additional_salary.submit() diff --git a/erpnext/hr/doctype/holiday/holiday.json b/erpnext/hr/doctype/holiday/holiday.json index 6498530eb16..6bd0ab0dcb6 100644 --- a/erpnext/hr/doctype/holiday/holiday.json +++ b/erpnext/hr/doctype/holiday/holiday.json @@ -1,87 +1,60 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-02-22 01:27:46", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, + "actions": [], + "creation": "2013-02-22 01:27:46", + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "holiday_date", + "column_break_2", + "weekly_off", + "section_break_4", + "description" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "holiday_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Date", - "length": 0, - "no_copy": 0, - "oldfieldname": "holiday_date", - "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "holiday_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Date", + "oldfieldname": "holiday_date", + "oldfieldtype": "Date", + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "description", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "300px", - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "description", + "fieldtype": "Text Editor", + "in_list_view": 1, + "label": "Description", + "print_width": "300px", + "reqd": 1, "width": "300px" + }, + { + "default": "0", + "fieldname": "weekly_off", + "fieldtype": "Check", + "label": "Weekly Off" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2016-07-11 03:28:00.660849", - "modified_by": "Administrator", - "module": "HR", - "name": "Holiday", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "sort_order": "ASC", - "track_seen": 0 + ], + "idx": 1, + "istable": 1, + "links": [], + "modified": "2020-04-18 19:03:23.507845", + "modified_by": "Administrator", + "module": "HR", + "name": "Holiday", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "ASC" } \ No newline at end of file diff --git a/erpnext/hr/doctype/holiday_list/holiday_list.py b/erpnext/hr/doctype/holiday_list/holiday_list.py index 8c7b6f723f9..76dc9429f19 100644 --- a/erpnext/hr/doctype/holiday_list/holiday_list.py +++ b/erpnext/hr/doctype/holiday_list/holiday_list.py @@ -23,6 +23,7 @@ class HolidayList(Document): ch = self.append('holidays', {}) ch.description = self.weekly_off ch.holiday_date = d + ch.weekly_off = 1 ch.idx = last_idx + i + 1 def validate_values(self): diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py index 7d6fd422c0f..50a08b12bc0 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py @@ -30,13 +30,16 @@ class LeaveEncashment(Document): additional_salary = frappe.new_doc("Additional Salary") additional_salary.company = frappe.get_value("Employee", self.employee, "company") additional_salary.employee = self.employee - additional_salary.salary_component = frappe.get_value("Leave Type", self.leave_type, "earning_component") + earning_component = frappe.get_value("Leave Type", self.leave_type, "earning_component") + if not earning_component: + frappe.throw(_("Please set Earning Component for Leave type: {0}.".format(self.leave_type))) + additional_salary.salary_component = earning_component additional_salary.payroll_date = self.encashment_date additional_salary.amount = self.encashment_amount + additional_salary.ref_doctype = self.doctype + additional_salary.ref_docname = self.name additional_salary.submit() - self.db_set("additional_salary", additional_salary.name) - # Set encashed leaves in Allocation frappe.db.set_value("Leave Allocation", self.leave_allocation, "total_leaves_encashed", frappe.db.get_value('Leave Allocation', self.leave_allocation, 'total_leaves_encashed') + self.encashable_days) @@ -118,4 +121,4 @@ def create_leave_encashment(leave_allocation): leave_type=allocation.leave_type, encashment_date=allocation.to_date )) - leave_encashment.insert(ignore_permissions=True) + leave_encashment.insert(ignore_permissions=True) \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py index e5bd170bc4a..ac7755b23a7 100644 --- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py @@ -53,7 +53,10 @@ class TestLeaveEncashment(unittest.TestCase): self.assertEqual(leave_encashment.encashment_amount, 250) leave_encashment.submit() - self.assertTrue(frappe.db.get_value("Leave Encashment", leave_encashment.name, "additional_salary")) + + # assert links + add_sal = frappe.get_all("Additional Salary", filters = {"ref_docname": leave_encashment.name})[0] + self.assertTrue(add_sal) def test_creation_of_leave_ledger_entry_on_submit(self): frappe.db.sql('''delete from `tabLeave Encashment`''') @@ -75,5 +78,8 @@ class TestLeaveEncashment(unittest.TestCase): self.assertEquals(leave_ledger_entry[0].leaves, leave_encashment.encashable_days * -1) # check if leave ledger entry is deleted on cancellation + + frappe.db.sql("Delete from `tabAdditional Salary` WHERE ref_docname = %s", (leave_encashment.name) ) + leave_encashment.cancel() self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_encashment.name})) diff --git a/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py b/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py index 49671d5e224..e43f744bd43 100644 --- a/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py @@ -17,7 +17,7 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_ class TestPayrollEntry(unittest.TestCase): def setUp(self): - for dt in ["Salary Slip", "Salary Component", "Salary Component Account", "Payroll Entry"]: + for dt in ["Salary Slip", "Salary Component", "Salary Component Account", "Payroll Entry", "Salary Structure"]: frappe.db.sql("delete from `tab%s`" % dt) make_earning_salary_component(setup=True, company_list=["_Test Company"]) diff --git a/erpnext/hr/doctype/salary_detail/salary_detail.json b/erpnext/hr/doctype/salary_detail/salary_detail.json index 545f56a0b60..fe5f83b5328 100644 --- a/erpnext/hr/doctype/salary_detail/salary_detail.json +++ b/erpnext/hr/doctype/salary_detail/salary_detail.json @@ -26,6 +26,7 @@ "tax_on_flexible_benefit", "tax_on_additional_salary", "section_break_11", + "additional_salary", "condition_and_formula_help" ], "fields": [ @@ -192,6 +193,12 @@ "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
" }, + { + "fieldname": "additional_salary", + "fieldtype": "Link", + "label": "Additional Salary ", + "options": "Additional Salary" + }, { "default": "0", "depends_on": "eval:doc.parentfield=='deductions'", @@ -204,7 +211,7 @@ ], "istable": 1, "links": [], - "modified": "2020-04-24 20:00:16.475295", + "modified": "2020-04-04 20:00:16.475295", "modified_by": "Administrator", "module": "HR", "name": "Salary Detail", @@ -213,4 +220,4 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC" -} \ No newline at end of file +} diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index db93f31f4a1..4d5c8437c67 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -66,7 +66,6 @@ class SalarySlip(TransactionBase): else: self.set_status() self.update_status(self.name) - self.update_salary_slip_in_additional_salary() self.make_loan_repayment_entry() if (frappe.db.get_single_value("HR Settings", "email_salary_slip_to_employee")) and not frappe.flags.via_payroll_entry: self.email_salary_slip() @@ -74,7 +73,6 @@ class SalarySlip(TransactionBase): def on_cancel(self): self.set_status() self.update_status() - self.update_salary_slip_in_additional_salary() self.cancel_loan_repayment_entry() def on_trash(self): @@ -464,14 +462,15 @@ class SalarySlip(TransactionBase): self.update_component_row(frappe._dict(last_benefit.struct_row), amount, "earnings") def add_additional_salary_components(self, component_type): - additional_components = get_additional_salary_component(self.employee, + salary_components_details, additional_salary_details = get_additional_salary_component(self.employee, self.start_date, self.end_date, component_type) - if additional_components: - for additional_component in additional_components: - amount = additional_component.amount - overwrite = additional_component.overwrite - self.update_component_row(frappe._dict(additional_component.struct_row), amount, - component_type, overwrite=overwrite) + if salary_components_details and additional_salary_details: + for additional_salary in additional_salary_details: + additional_salary =frappe._dict(additional_salary) + amount = additional_salary.amount + overwrite = additional_salary.overwrite + self.update_component_row(frappe._dict(salary_components_details[additional_salary.component]), amount, + component_type, overwrite=overwrite, additional_salary=additional_salary.name) def add_tax_components(self, payroll_period): # Calculate variable_based_on_taxable_salary after all components updated in salary slip @@ -491,13 +490,12 @@ class SalarySlip(TransactionBase): tax_row = self.get_salary_slip_row(d) self.update_component_row(tax_row, tax_amount, "deductions") - def update_component_row(self, struct_row, amount, key, overwrite=1): + def update_component_row(self, struct_row, amount, key, overwrite=1, additional_salary = ''): component_row = None for d in self.get(key): if d.salary_component == struct_row.salary_component: component_row = d - - if not component_row: + if not component_row or (struct_row.get("is_additional_component") and not overwrite): if amount: self.append(key, { 'amount': amount, @@ -505,6 +503,7 @@ class SalarySlip(TransactionBase): 'depends_on_payment_days' : struct_row.depends_on_payment_days, 'salary_component' : struct_row.salary_component, 'abbr' : struct_row.abbr, + 'additional_salary': additional_salary, 'do_not_include_in_total' : struct_row.do_not_include_in_total, 'is_tax_applicable': struct_row.is_tax_applicable, 'is_flexible_benefit': struct_row.is_flexible_benefit, @@ -517,6 +516,7 @@ class SalarySlip(TransactionBase): if struct_row.get("is_additional_component"): if overwrite: component_row.additional_amount = amount - component_row.get("default_amount", 0) + component_row.additional_salary = additional_salary else: component_row.additional_amount = amount @@ -936,14 +936,6 @@ class SalarySlip(TransactionBase): "repay_from_salary": 1, }) - - def update_salary_slip_in_additional_salary(self): - salary_slip = self.name if self.docstatus==1 else None - frappe.db.sql(""" - update `tabAdditional Salary` set salary_slip=%s - where employee=%s and payroll_date between %s and %s and docstatus=1 - """, (salary_slip, self.employee, self.start_date, self.end_date)) - def make_loan_repayment_entry(self): for loan in self.loans: repayment_entry = create_repayment_entry(loan.loan, self.employee, diff --git a/erpnext/hr/doctype/salary_slip/test_salary_slip.py b/erpnext/hr/doctype/salary_slip/test_salary_slip.py index fc687a355c0..a7dcb941677 100644 --- a/erpnext/hr/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/test_salary_slip.py @@ -18,19 +18,7 @@ from erpnext.hr.doctype.employee_tax_exemption_declaration.test_employee_tax_exe class TestSalarySlip(unittest.TestCase): def setUp(self): - make_earning_salary_component(setup=True, company_list=["_Test Company"]) - make_deduction_salary_component(setup=True, company_list=["_Test Company"]) - - 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) - + setup_test() def tearDown(self): frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 0) frappe.set_user("Administrator") @@ -374,19 +362,6 @@ class TestSalarySlip(unittest.TestCase): # undelete fixture data frappe.db.rollback() - def make_holiday_list(self): - fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company()) - if not frappe.db.get_value("Holiday List", "Salary Slip Test Holiday List"): - holiday_list = frappe.get_doc({ - "doctype": "Holiday List", - "holiday_list_name": "Salary Slip Test Holiday List", - "from_date": fiscal_year[1], - "to_date": fiscal_year[2], - "weekly_off": "Sunday" - }).insert() - holiday_list.get_weekly_off_dates() - holiday_list.save() - def make_activity_for_employee(self): activity_type = frappe.get_doc("Activity Type", "_Test Activity Type") activity_type.billing_rate = 50 @@ -702,4 +677,31 @@ def make_leave_application(employee, from_date, to_date, leave_type, company=Non status = "Approved", leave_approver = 'test@example.com' )) - leave_application.submit() \ No newline at end of file + leave_application.submit() + +def setup_test(): + 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", "Attendance"]: + frappe.db.sql("delete from `tab%s`" % dt) + + 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 make_holiday_list(): + fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company()) + if not frappe.db.get_value("Holiday List", "Salary Slip Test Holiday List"): + holiday_list = frappe.get_doc({ + "doctype": "Holiday List", + "holiday_list_name": "Salary Slip Test Holiday List", + "from_date": fiscal_year[1], + "to_date": fiscal_year[2], + "weekly_off": "Sunday" + }).insert() + holiday_list.get_weekly_off_dates() + holiday_list.save() diff --git a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.js b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.js index 348c5e7cb7b..bd4ed3c4caf 100644 --- a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.js +++ b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.js @@ -31,6 +31,18 @@ frappe.query_reports["Monthly Attendance Sheet"] = { "options": "Company", "default": frappe.defaults.get_user_default("Company"), "reqd": 1 + }, + { + "fieldname":"group_by", + "label": __("Group By"), + "fieldtype": "Select", + "options": ["","Branch","Grade","Department","Designation"] + }, + { + "fieldname":"summarized_view", + "label": __("Summarized View"), + "fieldtype": "Check", + "Default": 0, } ], diff --git a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py index 9a9e42e5e8e..d98ed1b414b 100644 --- a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py +++ b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py @@ -7,65 +7,127 @@ from frappe.utils import cstr, cint, getdate from frappe import msgprint, _ from calendar import monthrange +status_map = { + "Absent": "A", + "Half Day": "HD", + "Holiday": "H", + "Weekly Off": "WO", + "On Leave": "L", + "Present": "P", + "Work From Home": "WFH" + } + +day_abbr = [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun" +] + def execute(filters=None): if not filters: filters = {} conditions, filters = get_conditions(filters) columns = get_columns(filters) att_map = get_attendance_list(conditions, filters) - emp_map = get_employee_details(filters) - holiday_list = [emp_map[d]["holiday_list"] for d in emp_map if emp_map[d]["holiday_list"]] + if filters.group_by: + emp_map, group_by_parameters = get_employee_details(filters.group_by, filters.company) + holiday_list = [] + for parameter in group_by_parameters: + h_list = [emp_map[parameter][d]["holiday_list"] for d in emp_map[parameter] if emp_map[parameter][d]["holiday_list"]] + holiday_list += h_list + else: + emp_map = get_employee_details(filters.group_by, filters.company) + holiday_list = [emp_map[d]["holiday_list"] for d in emp_map if emp_map[d]["holiday_list"]] + + default_holiday_list = frappe.get_cached_value('Company', filters.get("company"), "default_holiday_list") holiday_list.append(default_holiday_list) holiday_list = list(set(holiday_list)) holiday_map = get_holiday(holiday_list, filters["month"]) data = [] - leave_types = frappe.db.sql("""select name from `tabLeave Type`""", as_list=True) - leave_list = [d[0] for d in leave_types] - columns.extend(leave_list) - columns.extend([_("Total Late Entries") + ":Float:120", _("Total Early Exits") + ":Float:120"]) - for emp in sorted(att_map): - emp_det = emp_map.get(emp) - if not emp_det: + leave_list = None + if filters.summarized_view: + leave_types = frappe.db.sql("""select name from `tabLeave Type`""", as_list=True) + leave_list = [d[0] + ":Float:120" for d in leave_types] + columns.extend(leave_list) + columns.extend([_("Total Late Entries") + ":Float:120", _("Total Early Exits") + ":Float:120"]) + + if filters.group_by: + for parameter in group_by_parameters: + data.append([ ""+ parameter + ""]) + record = add_data(emp_map[parameter], att_map, filters, holiday_map, conditions, leave_list=leave_list) + data += record + else: + record = add_data(emp_map, att_map, filters, holiday_map, conditions, leave_list=leave_list) + data += record + + return columns, data + + +def add_data(employee_map, att_map, filters, holiday_map, conditions, leave_list=None): + + record = [] + for emp in employee_map: + emp_det = employee_map.get(emp) + if not emp_det or emp not in att_map: continue - row = [emp, emp_det.employee_name, emp_det.branch, emp_det.department, emp_det.designation, - emp_det.company] + row = [] + if filters.group_by: + row += [" "] + row += [emp, emp_det.employee_name] - total_p = total_a = total_l = 0.0 + total_p = total_a = total_l = total_h = total_um= 0.0 for day in range(filters["total_days_in_month"]): + status = None status = att_map.get(emp).get(day + 1) - status_map = { - "Absent": "A", - "Half Day": "HD", - "Holiday":"H", - "On Leave": "L", - "Present": "P", - "Work From Home": "WFH" - } if status is None and holiday_map: emp_holiday_list = emp_det.holiday_list if emp_det.holiday_list else default_holiday_list - if emp_holiday_list in holiday_map and (day+1) in holiday_map[emp_holiday_list]: - status = "Holiday" - row.append(status_map.get(status, "")) + if emp_holiday_list in holiday_map: + for idx, ele in enumerate(holiday_map[emp_holiday_list]): + if day+1 == holiday_map[emp_holiday_list][idx][0]: + if holiday_map[emp_holiday_list][idx][1]: + status = "Weekly Off" + else: + status = "Holiday" + total_h += 1 - if status == "Present": - total_p += 1 - elif status == "Absent": - total_a += 1 - elif status == "On Leave": - total_l += 1 - elif status == "Half Day": - total_p += 0.5 - total_a += 0.5 - total_l += 0.5 - row += [total_p, total_l, total_a] + # if emp_holiday_list in holiday_map and (day+1) in holiday_map[emp_holiday_list][0]: + # if holiday_map[emp_holiday_list][1]: + # status= "Weekly Off" + # else: + # status = "Holiday" + + # += 1 + + if not filters.summarized_view: + row.append(status_map.get(status, "")) + else: + if status == "Present": + total_p += 1 + elif status == "Absent": + total_a += 1 + elif status == "On Leave": + total_l += 1 + elif status == "Half Day": + total_p += 0.5 + total_a += 0.5 + total_l += 0.5 + elif not status: + total_um += 1 + + if filters.summarized_view: + row += [total_p, total_l, total_a, total_h, total_um] if not filters.get("employee"): filters.update({"employee": emp}) @@ -73,43 +135,53 @@ def execute(filters=None): elif not filters.get("employee") == emp: filters.update({"employee": emp}) - leave_details = frappe.db.sql("""select leave_type, status, count(*) as count from `tabAttendance`\ - where leave_type is not NULL %s group by leave_type, status""" % conditions, filters, as_dict=1) + if filters.summarized_view: + leave_details = frappe.db.sql("""select leave_type, status, count(*) as count from `tabAttendance`\ + where leave_type is not NULL %s group by leave_type, status""" % conditions, filters, as_dict=1) - time_default_counts = frappe.db.sql("""select (select count(*) from `tabAttendance` where \ - late_entry = 1 %s) as late_entry_count, (select count(*) from tabAttendance where \ - early_exit = 1 %s) as early_exit_count""" % (conditions, conditions), filters) + time_default_counts = frappe.db.sql("""select (select count(*) from `tabAttendance` where \ + late_entry = 1 %s) as late_entry_count, (select count(*) from tabAttendance where \ + early_exit = 1 %s) as early_exit_count""" % (conditions, conditions), filters) - leaves = {} - for d in leave_details: - if d.status == "Half Day": - d.count = d.count * 0.5 - if d.leave_type in leaves: - leaves[d.leave_type] += d.count - else: - leaves[d.leave_type] = d.count + leaves = {} + for d in leave_details: + if d.status == "Half Day": + d.count = d.count * 0.5 + if d.leave_type in leaves: + leaves[d.leave_type] += d.count + else: + leaves[d.leave_type] = d.count - for d in leave_list: - if d in leaves: - row.append(leaves[d]) - else: - row.append("0.0") + for d in leave_list: + if d in leaves: + row.append(leaves[d]) + else: + row.append("0.0") - row.extend([time_default_counts[0][0],time_default_counts[0][1]]) - data.append(row) - return columns, data + row.extend([time_default_counts[0][0],time_default_counts[0][1]]) + record.append(row) + + + return record def get_columns(filters): - columns = [ - _("Employee") + ":Link/Employee:120", _("Employee Name") + "::140", _("Branch")+ ":Link/Branch:120", - _("Department") + ":Link/Department:120", _("Designation") + ":Link/Designation:120", - _("Company") + ":Link/Company:120" + + columns = [] + + if filters.group_by: + columns = [_(filters.group_by)+ ":Link/Branch:120"] + + columns += [ + _("Employee") + ":Link/Employee:120", _("Employee Name") + ":Link/Employee:120" ] - for day in range(filters["total_days_in_month"]): - columns.append(cstr(day+1) +"::20") - - columns += [_("Total Present") + ":Float:80", _("Total Leaves") + ":Float:80", _("Total Absent") + ":Float:80"] + if not filters.summarized_view: + for day in range(filters["total_days_in_month"]): + date = str(filters.year) + "-" + str(filters.month)+ "-" + str(day+1) + day_name = day_abbr[getdate(date).weekday()] + columns.append(cstr(day+1)+ " " +day_name +"::65") + else: + columns += [_("Total Present") + ":Float:120", _("Total Leaves") + ":Float:120", _("Total Absent") + ":Float:120", _("Total Holidays") + ":Float:120", _("Unmarked Days")+ ":Float:120"] return columns def get_attendance_list(conditions, filters): @@ -140,19 +212,43 @@ def get_conditions(filters): return conditions, filters -def get_employee_details(filters): - emp_map = frappe._dict() - for d in frappe.db.sql("""select name, employee_name, designation, department, branch, company, - holiday_list from tabEmployee where company = %s""", (filters.get("company")), as_dict=1): - emp_map.setdefault(d.name, d) +def get_employee_details(group_by, company): + emp_map = {} + query = """select name, employee_name, designation, department, branch, company, + holiday_list from `tabEmployee` where company = '%s' """ % frappe.db.escape(company) - return emp_map + if group_by: + group_by = group_by.lower() + query += " order by " + group_by + " ASC" + + employee_details = frappe.db.sql(query , as_dict=1) + + group_by_parameters = [] + if group_by: + + group_by_parameters = list(set(detail.get(group_by, "") for detail in employee_details if detail.get(group_by, ""))) + for parameter in group_by_parameters: + emp_map[parameter] = {} + + + for d in employee_details: + if group_by and len(group_by_parameters): + if d.get(group_by, None): + + emp_map[d.get(group_by)][d.name] = d + else: + emp_map[d.name] = d + + if not group_by: + return emp_map + else: + return emp_map, group_by_parameters def get_holiday(holiday_list, month): holiday_map = frappe._dict() for d in holiday_list: if d: - holiday_map.setdefault(d, frappe.db.sql_list('''select day(holiday_date) from `tabHoliday` + holiday_map.setdefault(d, frappe.db.sql('''select day(holiday_date), weekly_off from `tabHoliday` where parent=%s and month(holiday_date)=%s''', (d, month))) return holiday_map diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index c7a2fba878e..c550d4952d0 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -233,7 +233,7 @@ def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as return repayment_entry @frappe.whitelist() -def create_loan_security_unpledge(loan, applicant_type, applicant, company): +def create_loan_security_unpledge(loan, applicant_type, applicant, company, as_dict=1): loan_security_pledge_details = frappe.db.sql(""" SELECT p.parent, p.loan_security, p.qty as qty FROM `tabLoan Security Pledge` lsp , `tabPledge` p WHERE p.parent = lsp.name AND lsp.loan = %s AND lsp.docstatus = 1 @@ -252,7 +252,10 @@ def create_loan_security_unpledge(loan, applicant_type, applicant, company): "against_pledge": loan_security.parent }) - return unpledge_request.as_dict() + if as_dict: + return unpledge_request.as_dict() + else: + return unpledge_request diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 90b8534bc8a..77a1fcc574d 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -14,6 +14,7 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_ process_loan_interest_accrual_for_term_loans) from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall import create_process_loan_security_shortfall +from erpnext.loan_management.doctype.loan.loan import create_loan_security_unpledge class TestLoan(unittest.TestCase): def setUp(self): @@ -276,6 +277,56 @@ class TestLoan(unittest.TestCase): frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 250 where loan_security='Test Security 2'""") + def test_loan_security_unpledge(self): + pledges = [] + pledges.append({ + "loan_security": "Test Security 1", + "qty": 4000.00, + "haircut": 50 + }) + + loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_security_pledge.name, + posting_date=get_first_day(nowdate())) + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + + no_of_days += 6 + + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime(first_date).year) * 100) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), + "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry.submit() + + amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', + 'paid_principal_amount']) + + loan.load_from_db() + self.assertEquals(loan.status, "Loan Closure Requested") + + unpledge_request = create_loan_security_unpledge(loan.name, loan.applicant_type, loan.applicant, loan.company, as_dict=0) + unpledge_request.submit() + unpledge_request.status = 'Approved' + unpledge_request.save() + + loan_security_pledge.load_from_db() + loan.load_from_db() + + self.assertEqual(loan.status, 'Closed') + for security in loan_security_pledge.securities: + self.assertEquals(security.qty, 0) + def create_loan_accounts(): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 5979ee31022..452c8368199 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -78,7 +78,10 @@ class LoanRepayment(AccountsController): (flt(payment.paid_principal_amount), flt(payment.paid_interest_amount), payment.loan_interest_accrual)) if flt(loan.total_principal_paid + self.principal_amount_paid, 2) >= flt(loan.total_payment, 2): - frappe.db.set_value("Loan", self.against_loan, "status", "Loan Closure Requested") + if loan.is_secured_loan: + frappe.db.set_value("Loan", self.against_loan, "status", "Loan Closure Requested") + else: + frappe.db.set_value("Loan", self.against_loan, "status", "Closed") frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s WHERE name = %s """, (loan.total_amount_paid + self.amount_paid, diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py index eb6135868d0..f97e5965a5f 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py @@ -13,6 +13,7 @@ from erpnext.loan_management.doctype.loan_security_price.loan_security_price imp class LoanSecurityPledge(Document): def validate(self): self.set_pledge_amount() + self.validate_duplicate_securities() def on_submit(self): if self.loan: @@ -21,6 +22,15 @@ class LoanSecurityPledge(Document): update_shortfall_status(self.loan, self.total_security_value) update_loan(self.loan, self.maximum_loan_value) + def validate_duplicate_securities(self): + security_list = [] + for security in self.securities: + if security.loan_security not in security_list: + security_list.append(security.loan_security) + else: + frappe.throw(_('Loan Security {0} added multiple times').format(frappe.bold( + security.loan_security))) + def set_pledge_amount(self): total_security_value = 0 maximum_loan_value = 0 diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py index ab040f1d333..8ca6e3e908d 100644 --- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -53,7 +53,7 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): loans = frappe.db.sql(""" SELECT l.name, l.loan_amount, l.total_principal_paid, lp.loan_security, lp.haircut, lp.qty, lp.loan_security_type FROM `tabLoan` l, `tabPledge` lp , `tabLoan Security Pledge`p WHERE lp.parent = p.name and p.loan = l.name and l.docstatus = 1 - and l.is_secured_loan and l.status = 'Disbursed' and p.status in ('Pledged', 'Partially Unpledged')""", as_dict=1) + and l.is_secured_loan and l.status = 'Disbursed' and p.status = 'Pledged'""", as_dict=1) loan_security_map = {} diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json index ba948550316..aece46ffda6 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "LSU-.{applicant}.-.#####", "creation": "2019-09-21 13:23:16.117028", "doctype": "DocType", @@ -15,7 +16,6 @@ "status", "loan_security_details_section", "securities", - "unpledge_type", "amended_from" ], "fields": [ @@ -47,6 +47,7 @@ { "allow_on_submit": 1, "default": "Requested", + "depends_on": "eval:doc.docstatus == 1", "fieldname": "status", "fieldtype": "Select", "label": "Status", @@ -80,13 +81,6 @@ "options": "Unpledge", "reqd": 1 }, - { - "fieldname": "unpledge_type", - "fieldtype": "Data", - "hidden": 1, - "label": "Unpledge Type", - "read_only": 1 - }, { "fieldname": "company", "fieldtype": "Link", @@ -104,7 +98,8 @@ } ], "is_submittable": 1, - "modified": "2019-10-28 07:41:47.084882", + "links": [], + "modified": "2020-05-05 07:23:18.440058", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Unpledge", diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index 02b1ecb4ca5..b2bb22a3ce4 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -13,31 +13,43 @@ from erpnext.loan_management.doctype.loan_security_price.loan_security_price imp class LoanSecurityUnpledge(Document): def validate(self): self.validate_pledges() + self.validate_duplicate_securities() + + def on_cancel(self): + self.update_loan_security_pledge(cancel=1) + self.update_loan_status(cancel=1) + self.db_set('status', 'Requested') + + def validate_duplicate_securities(self): + security_list = [] + for d in self.securities: + security = [d.loan_security, d.against_pledge] + if security not in security_list: + security_list.append(security) + else: + frappe.throw(_("Row {0}: Loan Security {1} against Loan Security Pledge {2} added multiple times").format( + d.idx, frappe.bold(d.loan_security), frappe.bold(d.against_pledge))) def validate_pledges(self): - pledge_details = self.get_pledge_details() - + pledge_qty_map = self.get_pledge_details() loan = frappe.get_doc("Loan", self.loan) - pledge_qty_map = {} remaining_qty = 0 unpledge_value = 0 - for pledge in pledge_details: - pledge_qty_map.setdefault((pledge.parent, pledge.loan_security), pledge.qty) - for security in self.securities: pledged_qty = pledge_qty_map.get((security.against_pledge, security.loan_security), 0) if not pledged_qty: - frappe.throw(_("Zero qty of {0} pledged against loan {0}").format(frappe.bold(security.loan_security), + frappe.throw(_("Zero qty of {0} pledged against loan {1}").format(frappe.bold(security.loan_security), frappe.bold(self.loan))) unpledge_qty = pledged_qty - security.qty security_price = security.qty * get_loan_security_price(security.loan_security) if unpledge_qty < 0: - frappe.throw(_("Cannot unpledge more than {0} qty of {0}").format(frappe.bold(pledged_qty), - frappe.bold(security.loan_security))) + frappe.throw(_("""Row {0}: Cannot unpledge more than {1} qty of {2} against + Loan Security Pledge {3}""").format(security.idx, frappe.bold(pledged_qty), + frappe.bold(security.loan_security), frappe.bold(security.against_pledge))) remaining_qty += unpledge_qty unpledge_value += security_price - flt(security_price * security.haircut/100) @@ -45,41 +57,57 @@ class LoanSecurityUnpledge(Document): if unpledge_value > loan.total_principal_paid: frappe.throw(_("Cannot Unpledge, loan security value is greater than the repaid amount")) - if not remaining_qty: - self.db_set('unpledge_type', 'Unpledged') - else: - self.db_set('unpledge_type', 'Partially Pledged') - - def get_pledge_details(self): + pledge_qty_map = {} + pledge_details = frappe.db.sql(""" - SELECT p.parent, p.loan_security, p.qty as qty FROM + SELECT p.parent, p.loan_security, p.qty FROM `tabLoan Security Pledge` lsp, `tabPledge` p WHERE p.parent = lsp.name AND lsp.loan = %s AND lsp.docstatus = 1 - AND lsp.status = "Pledged" - """,(self.loan), as_dict=1) + AND lsp.status in ('Pledged', 'Partially Pledged') + """, (self.loan), as_dict=1) - return pledge_details + for pledge in pledge_details: + pledge_qty_map.setdefault((pledge.parent, pledge.loan_security), pledge.qty) + + return pledge_qty_map def on_update_after_submit(self): if self.status == "Approved": - frappe.db.sql(""" - UPDATE - `tabPledge` p, `tabUnpledge` u, `tabLoan Security Pledge` lsp, - `tabLoan Security Unpledge` lsu SET p.qty = (p.qty - u.qty) - WHERE - lsp.loan = %s - AND lsu.status = 'Requested' - AND u.parent = %s - AND p.parent = u.against_pledge - AND p.loan_security = u.loan_security""",(self.loan, self.name)) + self.update_loan_security_pledge() + self.update_loan_status() - frappe.db.sql("""UPDATE `tabLoan Security Pledge` - SET status = %s WHERE loan = %s""", (self.unpledge_type, self.loan)) + def update_loan_security_pledge(self, cancel=0): + if cancel: + new_qty = 'p.qty + u.qty' + else: + new_qty = 'p.qty - u.qty' + + frappe.db.sql(""" + UPDATE + `tabPledge` p, `tabUnpledge` u, `tabLoan Security Pledge` lsp, `tabLoan Security Unpledge` lsu + SET p.qty = {new_qty} + WHERE + lsp.loan = %s + AND p.parent = u.against_pledge + AND p.parent = lsp.name + AND lsp.docstatus = 1 + AND p.loan_security = u.loan_security""".format(new_qty=new_qty),(self.loan)) + + def update_loan_status(self, cancel=0): + if cancel: + loan_status = frappe.get_value('Loan', self.loan, 'status') + if loan_status == 'Closed': + frappe.db.set_value('Loan', self.loan, 'status', 'Loan Closure Requested') + else: + pledge_qty = frappe.db.sql("""SELECT SUM(c.qty) + FROM `tabLoan Security Pledge` p, `tabPledge` c + WHERE p.loan = %s AND c.parent = p.name""", (self.loan))[0][0] + + if not pledge_qty: + frappe.db.set_value('Loan', self.loan, 'status', 'Closed') - if self.unpledge_type == 'Unpledged': - frappe.db.set_value("Loan", self.loan, 'status', 'Closed') diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 1f8fabd2990..96aa1e4c79d 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -662,6 +662,7 @@ 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 erpnext.patches.v12_0.rename_bank_reconciliation_fields # 2020-01-22 +erpnext.patches.v12_0.set_purchase_receipt_delivery_note_detail 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 @@ -677,3 +678,4 @@ 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 erpnext.patches.v12_0.retain_permission_rules_for_video_doctype +erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive 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 d7936110edb..83b2a4cc09e 100644 --- a/erpnext/patches/v11_0/add_permissions_in_gst_settings.py +++ b/erpnext/patches/v11_0/add_permissions_in_gst_settings.py @@ -7,4 +7,5 @@ def execute(): return frappe.reload_doc("regional", "doctype", "lower_deduction_certificate") - add_permissions() \ No newline at end of file + frappe.reload_doc("regional", "doctype", "gstr_3b_report") + add_permissions() diff --git a/erpnext/patches/v12_0/add_default_dashboards.py b/erpnext/patches/v12_0/add_default_dashboards.py index 0c3f2f86aef..2a91e1b9327 100644 --- a/erpnext/patches/v12_0/add_default_dashboards.py +++ b/erpnext/patches/v12_0/add_default_dashboards.py @@ -6,4 +6,5 @@ from erpnext.setup.setup_wizard.operations.install_fixtures import add_dashboard def execute(): frappe.reload_doc("desk", "doctype", "number_card_link") + frappe.reload_doc("healthcare", "doctype", "patient_appointment") add_dashboards() diff --git a/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py b/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py new file mode 100644 index 00000000000..f5bd8c3aa29 --- /dev/null +++ b/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py @@ -0,0 +1,84 @@ +from __future__ import unicode_literals +import frappe +from collections import defaultdict + +def execute(): + def map_rows(doc_row, return_doc_row, detail_field, doctype): + """Map rows after identifying similar ones.""" + + frappe.db.sql(""" UPDATE `tab{doctype} Item` set {detail_field} = '{doc_row_name}' + where name = '{return_doc_row_name}'""" \ + .format(doctype=doctype, + detail_field=detail_field, + doc_row_name=doc_row.get('name'), + return_doc_row_name=return_doc_row.get('name'))) #nosec + + def row_is_mappable(doc_row, return_doc_row, detail_field): + """Checks if two rows are similar enough to be mapped.""" + + if doc_row.item_code == return_doc_row.item_code and not return_doc_row.get(detail_field): + if doc_row.get('batch_no') and return_doc_row.get('batch_no') and doc_row.batch_no == return_doc_row.batch_no: + return True + + elif doc_row.get('serial_no') and return_doc_row.get('serial_no'): + doc_sn = doc_row.serial_no.split('\n') + return_doc_sn = return_doc_row.serial_no.split('\n') + + if set(doc_sn) & set(return_doc_sn): + # if two rows have serial nos in common, map them + return True + + elif doc_row.rate == return_doc_row.rate: + return True + else: + return False + + def make_return_document_map(doctype, return_document_map): + """Returns a map of documents and it's return documents. + Format => { 'document' : ['return_document_1','return_document_2'] }""" + + return_against_documents = frappe.db.sql(""" + SELECT + return_against as document, name as return_document + FROM `tab{doctype}` + WHERE + is_return = 1 and docstatus = 1""".format(doctype=doctype),as_dict=1) #nosec + + for entry in return_against_documents: + return_document_map[entry.document].append(entry.return_document) + + return return_document_map + + def set_document_detail_in_return_document(doctype): + """Map each row of the original document in the return document.""" + mapped = [] + return_document_map = defaultdict(list) + detail_field = "purchase_receipt_item" if doctype=="Purchase Receipt" else "dn_detail" + + child_doc = frappe.scrub("{0} Item".format(doctype)) + frappe.reload_doc("stock", "doctype", child_doc) + + return_document_map = make_return_document_map(doctype, return_document_map) + + #iterate through original documents and its return documents + for docname in return_document_map: + doc_items = frappe.get_doc(doctype, docname).get("items") + for return_doc in return_document_map[docname]: + return_doc_items = frappe.get_doc(doctype, return_doc).get("items") + + #iterate through return document items and original document items for mapping + for return_item in return_doc_items: + for doc_item in doc_items: + if row_is_mappable(doc_item, return_item, detail_field) and doc_item.get('name') not in mapped: + map_rows(doc_item, return_item, detail_field, doctype) + mapped.append(doc_item.get('name')) + break + else: + continue + + set_document_detail_in_return_document("Purchase Receipt") + set_document_detail_in_return_document("Delivery Note") + frappe.db.commit() + + + diff --git a/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py b/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py new file mode 100644 index 00000000000..ddcadcb4de9 --- /dev/null +++ b/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py @@ -0,0 +1,52 @@ +from __future__ import unicode_literals + +import frappe + +def execute(): + if not frappe.db.table_exists("Additional Salary"): + return + + for doctype in ("Additional Salary", "Leave Encashment", "Employee Incentive", "Salary Detail"): + frappe.reload_doc("hr", "doctype", doctype) + + additional_salaries = frappe.get_all("Additional Salary", + fields = ['name', "salary_slip", "type", "salary_component"], + filters = {'salary_slip': ['!=', '']}, + group_by = 'salary_slip' + ) + leave_encashments = frappe.get_all("Leave Encashment", + fields = ["name","additional_salary"], + filters = {'additional_salary': ['!=', '']} + ) + employee_incentives = frappe.get_all("Employee Incentive", + fields= ["name", "additional_salary"], + filters = {'additional_salary': ['!=', '']} + ) + + for incentive in employee_incentives: + frappe.db.sql(""" UPDATE `tabAdditional Salary` + SET ref_doctype = 'Employee Incentive', ref_docname = %s + WHERE name = %s + """, (incentive['name'], incentive['additional_salary'])) + + + for leave_encashment in leave_encashments: + frappe.db.sql(""" UPDATE `tabAdditional Salary` + SET ref_doctype = 'Leave Encashment', ref_docname = %s + WHERE name = %s + """, (leave_encashment['name'], leave_encashment['additional_salary'])) + + salary_slips = [sal["salary_slip"] for sal in additional_salaries] + + for salary in additional_salaries: + comp_type = "earnings" if salary['type'] == 'Earning' else 'deductions' + if salary["salary_slip"] and salary_slips.count(salary["salary_slip"]) == 1: + frappe.db.sql(""" + UPDATE `tabSalary Detail` + SET additional_salary = %s + WHERE parenttype = 'Salary Slip' + and parentfield = %s + and parent = %s + and salary_component = %s + """, (salary["name"], comp_type, salary["salary_slip"], salary["salary_component"])) + diff --git a/erpnext/setup/setup_wizard/data/dashboard_charts.py b/erpnext/setup/setup_wizard/data/dashboard_charts.py index ccb23c07e85..d54462e1be4 100644 --- a/erpnext/setup/setup_wizard/data/dashboard_charts.py +++ b/erpnext/setup/setup_wizard/data/dashboard_charts.py @@ -29,7 +29,8 @@ def get_default_dashboards(): { "chart": "Incoming Bills (Purchase Invoice)" }, { "chart": "Bank Balance" }, { "chart": "Income" }, - { "chart": "Expenses" } + { "chart": "Expenses" }, + { "chart": "Patient Appointments" } ] }, { @@ -126,6 +127,21 @@ def get_default_dashboards(): 'type': 'Bar', 'custom_options': '{"type": "bar", "colors": ["#98d85b", "#fc4f51", "#7679fc"], "axisOptions": { "shortenYAxisNumbers": 1}, "barOptions": { "stacked": 1 }}', }, + { + "doctype": "Dashboard Chart", + "time_interval": "Daily", + "chart_name": "Patient Appointments", + "timespan": "Last Month", + "color": "#77ecca", + "filters_json": json.dumps({}), + "chart_type": "Count", + "timeseries": 1, + "based_on": "appointment_datetime", + "owner": "Administrator", + "document_type": "Patient Appointment", + "type": "Line", + "width": "Half" + } ] } diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 37f90979376..d04cf785ab1 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -388,13 +388,12 @@ def get_invoiced_qty_map(delivery_note): def get_returned_qty_map(delivery_note): """returns a map: {so_detail: returned_qty}""" - returned_qty_map = frappe._dict(frappe.db.sql("""select dn_item.item_code, sum(abs(dn_item.qty)) as qty + returned_qty_map = frappe._dict(frappe.db.sql("""select dn_item.dn_detail, abs(dn_item.qty) as qty from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn where dn.name = dn_item.parent and dn.docstatus = 1 and dn.is_return = 1 and dn.return_against = %s - group by dn_item.item_code """, delivery_note)) return returned_qty_map @@ -413,7 +412,7 @@ def make_sales_invoice(source_name, target_doc=None): target.run_method("set_po_nos") if len(target.get("items")) == 0: - frappe.throw(_("All these items have already been invoiced")) + frappe.throw(_("All these items have already been Invoiced/Returned")) target.run_method("calculate_taxes_and_totals") @@ -438,9 +437,9 @@ def make_sales_invoice(source_name, target_doc=None): pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0) returned_qty = 0 - if returned_qty_map.get(item_row.item_code, 0) > 0: - returned_qty = flt(returned_qty_map.get(item_row.item_code, 0)) - returned_qty_map[item_row.item_code] -= pending_qty + if returned_qty_map.get(item_row.name, 0) > 0: + returned_qty = flt(returned_qty_map.get(item_row.name, 0)) + returned_qty_map[item_row.name] -= pending_qty if returned_qty: if returned_qty >= pending_qty: diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index bf7007abeeb..a921a56f53f 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -612,6 +612,7 @@ class TestDeliveryNote(unittest.TestCase): dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-1, do_not_submit=True) dn1.items[0].against_sales_order = so.name dn1.items[0].so_detail = so.items[0].name + dn1.items[0].dn_detail = dn.items[0].name dn1.submit() si = make_sales_invoice(dn.name) @@ -638,7 +639,9 @@ class TestDeliveryNote(unittest.TestCase): si1.save() si1.submit() - create_delivery_note(is_return=1, return_against=dn.name, qty=-2) + dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-2, do_not_submit=True) + dn1.items[0].dn_detail = dn.items[0].name + dn1.submit() si2 = make_sales_invoice(dn.name) self.assertEquals(si2.items[0].qty, 2) diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 782ac84e57d..7ea2de27533 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -67,6 +67,7 @@ "so_detail", "against_sales_invoice", "si_detail", + "dn_detail", "section_break_40", "batch_no", "serial_no", @@ -699,6 +700,15 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "fieldname": "dn_detail", + "fieldtype": "Data", + "hidden": 1, + "label": "Against Delivery Note Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 8dfe1d10302..e6ab8d634d8 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -504,7 +504,7 @@ def make_purchase_invoice(source_name, target_doc=None): def set_missing_values(source, target): if len(target.get("items")) == 0: - frappe.throw(_("All items have already been invoiced")) + frappe.throw(_("All items have already been Invoiced/Returned")) doc = frappe.get_doc(target) doc.ignore_pricing_rule = 1 @@ -514,11 +514,11 @@ def make_purchase_invoice(source_name, target_doc=None): def update_item(source_doc, target_doc, source_parent): target_doc.qty, returned_qty = get_pending_qty(source_doc) - returned_qty_map[source_doc.item_code] = returned_qty + returned_qty_map[source_doc.name] = returned_qty def get_pending_qty(item_row): pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0) - returned_qty = flt(returned_qty_map.get(item_row.item_code, 0)) + returned_qty = flt(returned_qty_map.get(item_row.name, 0)) if returned_qty: if returned_qty >= pending_qty: pending_qty = 0 @@ -576,13 +576,12 @@ def get_invoiced_qty_map(purchase_receipt): def get_returned_qty_map(purchase_receipt): """returns a map: {so_detail: returned_qty}""" - returned_qty_map = frappe._dict(frappe.db.sql("""select pr_item.item_code, sum(abs(pr_item.qty)) as qty + returned_qty_map = frappe._dict(frappe.db.sql("""select pr_item.purchase_receipt_item, abs(pr_item.qty) as qty from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr where pr.name = pr_item.parent and pr.docstatus = 1 and pr.is_return = 1 and pr.return_against = %s - group by pr_item.item_code """, purchase_receipt)) return returned_qty_map diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 3d42590e4c6..649cfdcaace 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -475,6 +475,7 @@ class TestPurchaseReceipt(unittest.TestCase): pr1 = make_purchase_receipt(is_return=1, return_against=pr.name, qty=-1, do_not_submit=True) pr1.items[0].purchase_order = po.name pr1.items[0].purchase_order_item = po.items[0].name + pr1.items[0].purchase_receipt_item = pr.items[0].name pr1.submit() pi = make_purchase_invoice(pr.name) @@ -498,7 +499,9 @@ class TestPurchaseReceipt(unittest.TestCase): pi1.save() pi1.submit() - make_purchase_receipt(is_return=1, return_against=pr1.name, qty=-2) + pr2 = make_purchase_receipt(is_return=1, return_against=pr1.name, qty=-2, do_not_submit=True) + pr2.items[0].purchase_receipt_item = pr1.items[0].name + pr2.submit() pi2 = make_purchase_invoice(pr1.name) self.assertEquals(pi2.items[0].qty, 2) diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index bc6bce95d6e..c1e1f901ba6 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -71,6 +71,7 @@ "quality_inspection", "purchase_order_item", "material_request_item", + "purchase_receipt_item", "section_break_45", "allow_zero_valuation_rate", "bom", @@ -820,6 +821,15 @@ "label": "Supplier Warehouse", "options": "Warehouse" }, + { + "fieldname": "purchase_receipt_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Purchase Receipt Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, { "collapsible": 1, "fieldname": "image_column", @@ -829,7 +839,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-04-10 19:01:21.154963", + "modified": "2020-04-28 19:01:21.154963", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/tests/test_search.py b/erpnext/tests/test_search.py index b9665dbd765..566495f1ecc 100644 --- a/erpnext/tests/test_search.py +++ b/erpnext/tests/test_search.py @@ -4,11 +4,13 @@ import frappe from frappe.contacts.address_and_contact import filter_dynamic_link_doctypes class TestSearch(unittest.TestCase): - #Search for the word "cond", part of the word "conduire" (Lead) in french. + # Search for the word "cond", part of the word "conduire" (Lead) in french. def test_contact_search_in_foreign_language(self): frappe.local.lang = 'fr' - output = filter_dynamic_link_doctypes("DocType", "prospect", "name", 0, 20, {'fieldtype': 'HTML', 'fieldname': 'contact_html'}) - + output = filter_dynamic_link_doctypes("DocType", "cond", "name", 0, 20, { + 'fieldtype': 'HTML', + 'fieldname': 'contact_html' + }) result = [['found' for x in y if x=="Lead"] for y in output] self.assertTrue(['found'] in result)