Merge pull request #21612 from anupamvs/social-media-auth
fix: fixing callback urls
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index bcb22f0..83c670e 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -228,6 +228,8 @@
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/doctype/payment_order/payment_order.py b/erpnext/accounts/doctype/payment_order/payment_order.py
index 3f3174a..7ecdc41 100644
--- a/erpnext/accounts/doctype/payment_order/payment_order.py
+++ b/erpnext/accounts/doctype/payment_order/payment_order.py
@@ -80,7 +80,7 @@
paid_amt += d.amount
je.append('accounts', {
- 'account': doc.references[0].account,
+ 'account': doc.account,
'credit_in_account_currency': paid_amt
})
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index fb1a4f4..bfe35ab 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -297,7 +297,8 @@
fields = ["*"],
filters = {
"voucher_type": voucher_type,
- "voucher_no": voucher_no
+ "voucher_no": voucher_no,
+ "is_cancelled": 0
})
if gl_entries:
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 7af5fa8..6afe208 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -296,6 +296,9 @@
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 06dfa19..a3200d5 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -32,7 +32,7 @@
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 @@
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 a0f8d15..aed78e7 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -560,6 +560,81 @@
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 3a08baa..3da355e 100644
--- a/erpnext/assets/doctype/asset_movement/asset_movement.py
+++ b/erpnext/assets/doctype/asset_movement/asset_movement.py
@@ -110,6 +110,7 @@
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/selling_controller.py b/erpnext/controllers/selling_controller.py
index 90ba8b3..1e0a48c 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -165,9 +165,9 @@
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 @@
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 @@
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 24c6d6f..5cf09b3 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 18f6b8b..fb42b6f 100644
--- a/erpnext/hr/doctype/additional_salary/additional_salary.js
+++ b/erpnext/hr/doctype/additional_salary/additional_salary.js
@@ -13,5 +13,5 @@
}
};
});
- }
+ },
});
diff --git a/erpnext/hr/doctype/additional_salary/additional_salary.json b/erpnext/hr/doctype/additional_salary/additional_salary.json
index 7d69f7e..bfb543f 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
},
{
@@ -106,13 +111,6 @@
"reqd": 1
},
{
- "fieldname": "salary_slip",
- "fieldtype": "Link",
- "label": "Salary Slip",
- "options": "Salary Slip",
- "read_only": 1
- },
- {
"fetch_from": "salary_component.type",
"fieldname": "type",
"fieldtype": "Data",
@@ -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 bc7dcee..bab6fb5 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.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 @@
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 @@
@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 @@
'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 949ba20..6f93fb5 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 3896603..6cc49cf 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance.js
+++ b/erpnext/hr/doctype/employee_advance/employee_advance.js
@@ -23,6 +23,14 @@
}
};
});
+
+ frm.set_query('salary_component', function(doc) {
+ return {
+ filters: {
+ "type": "Deduction"
+ }
+ };
+ });
},
refresh: function(frm) {
@@ -47,19 +55,37 @@
}
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 d233a2b..8c5ce42 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 f0663ae..23e4992 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance.py
+++ b/erpnext/hr/doctype/employee_advance/employee_advance.py
@@ -133,8 +133,20 @@
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 ce8e1ea..e2d8a11 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"
],
@@ -66,14 +65,6 @@
"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",
"label": "Salary Component",
@@ -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 2e138f8..44763fc 100644
--- a/erpnext/hr/doctype/employee_incentive/employee_incentive.py
+++ b/erpnext/hr/doctype/employee_incentive/employee_incentive.py
@@ -9,37 +9,13 @@
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 6498530..6bd0ab0 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 8c7b6f7..76dc942 100644
--- a/erpnext/hr/doctype/holiday_list/holiday_list.py
+++ b/erpnext/hr/doctype/holiday_list/holiday_list.py
@@ -23,6 +23,7 @@
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 7d6fd42..50a08b1 100644
--- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py
+++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py
@@ -30,13 +30,16 @@
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 @@
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 e5bd170..ac7755b 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 @@
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 @@
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 49671d5..e43f744 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 @@
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 545f56a..fe5f83b 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": [
@@ -193,6 +194,12 @@
"options": "<h3>Condition and Formula Help</h3>\n\n<p>Notes:</p>\n\n<ol>\n<li>Use field <code>base</code> for using base salary of the Employee</li>\n<li>Use Salary Component abbreviations in conditions and formulas. <code>BS = Basic Salary</code></li>\n<li>Use field name for employee details in conditions and formulas. <code>Employment Type = employment_type</code><code>Branch = branch</code></li>\n<li>Use field name from Salary Slip in conditions and formulas. <code>Payment Days = payment_days</code><code>Leave without pay = leave_without_pay</code></li>\n<li>Direct Amount can also be entered based on Condtion. See example 3</li></ol>\n\n<h4>Examples</h4>\n<ol>\n<li>Calculating Basic Salary based on <code>base</code>\n<pre><code>Condition: base < 10000</code></pre>\n<pre><code>Formula: base * .2</code></pre></li>\n<li>Calculating HRA based on Basic Salary<code>BS</code> \n<pre><code>Condition: BS > 2000</code></pre>\n<pre><code>Formula: BS * .1</code></pre></li>\n<li>Calculating TDS based on Employment Type<code>employment_type</code> \n<pre><code>Condition: employment_type==\"Intern\"</code></pre>\n<pre><code>Amount: 1000</code></pre></li>\n</ol>"
},
{
+ "fieldname": "additional_salary",
+ "fieldtype": "Link",
+ "label": "Additional Salary ",
+ "options": "Additional Salary"
+ },
+ {
"default": "0",
"depends_on": "eval:doc.parentfield=='deductions'",
"fetch_from": "salary_component.exempted_from_income_tax",
@@ -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 db93f31..4d5c843 100644
--- a/erpnext/hr/doctype/salary_slip/salary_slip.py
+++ b/erpnext/hr/doctype/salary_slip/salary_slip.py
@@ -66,7 +66,6 @@
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 @@
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 @@
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 @@
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 @@
'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 @@
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 @@
"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 fc687a3..a7dcb94 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 @@
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 @@
# 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 @@
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/doctype/upload_attendance/upload_attendance.py b/erpnext/hr/doctype/upload_attendance/upload_attendance.py
index f75bb41..61faea1 100644
--- a/erpnext/hr/doctype/upload_attendance/upload_attendance.py
+++ b/erpnext/hr/doctype/upload_attendance/upload_attendance.py
@@ -145,7 +145,7 @@
def remove_holidays(rows):
rows = [ row for row in rows if row[4] != "Holiday"]
- return
+ return rows
from frappe.modules import scrub
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 348c5e7..bd4ed3c 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 @@
"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 9a9e42e..82ed277 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 import msgprint, _
from calendar import monthrange
+status_map = {
+ "Absent": "A",
+ "Half Day": "HD",
+ "Holiday": "<b>H</b>",
+ "Weekly Off": "<b>WO</b>",
+ "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([ "<b>"+ parameter + "</b>"])
+ 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":"<b>H</b>",
- "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 @@
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 @@
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 c7a2fba..76e10e5 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -233,7 +233,7 @@
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
@@ -248,11 +248,13 @@
for loan_security in loan_security_pledge_details:
unpledge_request.append('securities', {
"loan_security": loan_security.loan_security,
- "qty": loan_security.qty,
- "against_pledge": loan_security.parent
+ "qty": loan_security.qty
})
- 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 90b8534..364e2ff 100644
--- a/erpnext/loan_management/doctype/loan/test_loan.py
+++ b/erpnext/loan_management/doctype/loan/test_loan.py
@@ -14,6 +14,8 @@
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
+from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
class TestLoan(unittest.TestCase):
def setUp(self):
@@ -151,7 +153,7 @@
repayment_entry.save()
repayment_entry.submit()
- penalty_amount = (accrued_interest_amount * 5 * 25) / (100 * days_in_year(get_datetime(first_date).year))
+ penalty_amount = (accrued_interest_amount * 4 * 25) / (100 * days_in_year(get_datetime(first_date).year))
self.assertEquals(flt(repayment_entry.penalty_amount, 2), flt(penalty_amount, 2))
amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
@@ -276,6 +278,55 @@
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, 6),
+ "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.load_from_db()
+
+ pledged_qty = get_pledged_security_qty(loan.name)
+
+ self.assertEqual(loan.status, 'Closed')
+ self.assertEquals(sum(pledged_qty.values()), 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 5979ee3..2ab668a 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 @@
(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,
@@ -261,6 +264,7 @@
penalty_amount = 0
payable_principal_amount = 0
final_due_date = ''
+ due_date = ''
for entry in accrued_interest_entries:
# Loan repayment due date is one day after the loan interest is accrued
@@ -269,7 +273,7 @@
due_date = add_days(entry.posting_date, 1)
no_of_late_days = date_diff(posting_date,
- add_days(due_date, loan_type_details.grace_period_in_days)) + 1
+ add_days(due_date, loan_type_details.grace_period_in_days))
if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary):
penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)/365
@@ -287,9 +291,9 @@
pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable
- if payment_type == "Loan Closure" and not payable_principal_amount:
- if final_due_date:
- pending_days = date_diff(posting_date, final_due_date)
+ if payment_type == "Loan Closure":
+ if due_date:
+ pending_days = date_diff(posting_date, due_date) + 1
else:
pending_days = date_diff(posting_date, against_loan_doc.disbursement_date) + 1
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 eb61358..f97e596 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 @@
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 @@
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 ab040f1..308c438 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 @@
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 = {}
@@ -69,7 +69,7 @@
loan_security_map[loan.name]['security_value'] += current_loan_security_amount - (current_loan_security_amount * loan.haircut/100)
for loan, value in iteritems(loan_security_map):
- if (value["security_value"]/value["loan_amount"]) < ltv_ratio:
+ if (value["loan_amount"]/value['security_value'] * 100) > ltv_ratio:
create_loan_security_shortfall(loan, value, process_loan_security_shortfall)
def create_loan_security_shortfall(loan, value, process_loan_security_shortfall):
diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js
index 72c5f38..8223206 100644
--- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js
+++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.js
@@ -4,10 +4,8 @@
frappe.ui.form.on('Loan Security Unpledge', {
refresh: function(frm) {
- frm.set_query("against_pledge", "securities", () => {
- return {
- filters : [["status", "in", ["Pledged", "Partially Pledged"]]]
- };
- });
+ if (frm.doc.docstatus == 1 && frm.doc.status == 'Approved') {
+ frm.set_df_property('status', 'read_only', 1);
+ }
}
});
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 ba94855..aece46f 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",
@@ -81,13 +82,6 @@
"reqd": 1
},
{
- "fieldname": "unpledge_type",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Unpledge Type",
- "read_only": 1
- },
- {
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
@@ -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 02b1ecb..5e9d82a 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
@@ -8,78 +8,114 @@
from frappe.model.document import Document
from frappe.utils import get_datetime, flt
import json
+from six import iteritems
from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price
class LoanSecurityUnpledge(Document):
def validate(self):
- self.validate_pledges()
+ self.validate_duplicate_securities()
+ self.validate_unpledge_qty()
- def validate_pledges(self):
- pledge_details = self.get_pledge_details()
+ def on_cancel(self):
+ self.update_loan_security_pledge(cancel=1)
+ self.update_loan_status(cancel=1)
+ self.db_set('status', 'Requested')
- loan = frappe.get_doc("Loan", self.loan)
+ def validate_duplicate_securities(self):
+ security_list = []
+ for d in self.securities:
+ if d.loan_security not in security_list:
+ security_list.append(d.loan_security)
+ else:
+ frappe.throw(_("Row {0}: Loan Security {1} added multiple times").format(
+ d.idx, frappe.bold(d.loan_security)))
- pledge_qty_map = {}
- remaining_qty = 0
- unpledge_value = 0
+ def validate_unpledge_qty(self):
+ pledge_qty_map = get_pledged_security_qty(self.loan)
- for pledge in pledge_details:
- pledge_qty_map.setdefault((pledge.parent, pledge.loan_security), pledge.qty)
+ ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type",
+ fields=["name", "loan_to_value_ratio"], as_list=1))
+
+ loan_security_price_map = frappe._dict(frappe.get_all("Loan Security Price",
+ fields=["loan_security", "loan_security_price"],
+ filters = {
+ "valid_from": ("<=", get_datetime()),
+ "valid_upto": (">=", get_datetime())
+ }, as_list=1))
+
+ loan_amount, principal_paid = frappe.get_value("Loan", self.loan, ['loan_amount', 'total_principal_paid'])
+ pending_principal_amount = loan_amount - principal_paid
+ security_value = 0
for security in self.securities:
- pledged_qty = pledge_qty_map.get((security.against_pledge, security.loan_security), 0)
- if not pledged_qty:
- frappe.throw(_("Zero qty of {0} pledged against loan {0}").format(frappe.bold(security.loan_security),
- frappe.bold(self.loan)))
+ pledged_qty = pledge_qty_map.get(security.loan_security)
- unpledge_qty = pledged_qty - security.qty
- security_price = security.qty * get_loan_security_price(security.loan_security)
+ if security.qty > pledged_qty:
+ frappe.throw(_("""Row {0}: {1} {2} of {3} is pledged against Loan {4}.
+ You are trying to unpledge more""").format(security.idx, pledged_qty, security.uom,
+ frappe.bold(security.loan_security), frappe.bold(self.loan)))
- if unpledge_qty < 0:
- frappe.throw(_("Cannot unpledge more than {0} qty of {0}").format(frappe.bold(pledged_qty),
- frappe.bold(security.loan_security)))
+ qty_after_unpledge = pledged_qty - security.qty
+ ltv_ratio = ltv_ratio_map.get(security.loan_security_type)
- remaining_qty += unpledge_qty
- unpledge_value += security_price - flt(security_price * security.haircut/100)
+ security_value += qty_after_unpledge * loan_security_price_map.get(security.loan_security)
- if unpledge_value > loan.total_principal_paid:
- frappe.throw(_("Cannot Unpledge, loan security value is greater than the repaid amount"))
+ if not security_value and pending_principal_amount > 0:
+ frappe.throw("Cannot Unpledge, loan to value ratio is breaching")
- if not remaining_qty:
- self.db_set('unpledge_type', 'Unpledged')
- else:
- self.db_set('unpledge_type', 'Partially Pledged')
-
-
- def get_pledge_details(self):
- pledge_details = frappe.db.sql("""
- SELECT p.parent, p.loan_security, p.qty as qty FROM
- `tabLoan Security Pledge` lsp,
- `tabPledge` p
- WHERE
- p.parent = lsp.name
- AND lsp.loan = %s
- AND lsp.docstatus = 1
- AND lsp.status = "Pledged"
- """,(self.loan), as_dict=1)
-
- return pledge_details
+ if security_value and (pending_principal_amount/security_value) * 100 > ltv_ratio:
+ frappe.throw("Cannot Unpledge, loan to value ratio is breaching")
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_status()
+ self.db_set('unpledge_time', get_datetime())
- frappe.db.sql("""UPDATE `tabLoan Security Pledge`
- SET status = %s WHERE loan = %s""", (self.unpledge_type, 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:
+ pledged_qty = 0
+ current_pledges = get_pledged_security_qty(self.loan)
- if self.unpledge_type == 'Unpledged':
- frappe.db.set_value("Loan", self.loan, 'status', 'Closed')
+ for security, qty in iteritems(current_pledges):
+ pledged_qty += qty
+
+ if not pledged_qty:
+ frappe.db.set_value('Loan', self.loan, 'status', 'Closed')
+
+@frappe.whitelist()
+def get_pledged_security_qty(loan):
+
+ current_pledges = {}
+
+ unpledges = frappe._dict(frappe.db.sql("""
+ SELECT u.loan_security, sum(u.qty) as qty
+ FROM `tabLoan Security Unpledge` up, `tabUnpledge` u
+ WHERE up.loan = %s
+ AND u.parent = up.name
+ AND up.status = 'Approved'
+ GROUP BY u.loan_security
+ """, (loan)))
+
+ pledges = frappe._dict(frappe.db.sql("""
+ SELECT p.loan_security, sum(p.qty) as qty
+ FROM `tabLoan Security Pledge` lp, `tabPledge`p
+ WHERE lp.loan = %s
+ AND p.parent = lp.name
+ AND lp.status = 'Pledged'
+ GROUP BY p.loan_security
+ """, (loan)))
+
+ for security, qty in iteritems(pledges):
+ current_pledges.setdefault(security, qty)
+ current_pledges[security] -= unpledges.get(security, 0.0)
+
+ return current_pledges
+
+
+
+
+
diff --git a/erpnext/loan_management/doctype/unpledge/unpledge.json b/erpnext/loan_management/doctype/unpledge/unpledge.json
index 9e6277d..ee192d7 100644
--- a/erpnext/loan_management/doctype/unpledge/unpledge.json
+++ b/erpnext/loan_management/doctype/unpledge/unpledge.json
@@ -1,11 +1,11 @@
{
+ "actions": [],
"creation": "2019-09-21 13:22:19.793797",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan_security",
- "against_pledge",
"loan_security_type",
"loan_security_code",
"haircut",
@@ -55,14 +55,6 @@
"reqd": 1
},
{
- "fieldname": "against_pledge",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Against Pledge",
- "options": "Loan Security Pledge",
- "reqd": 1
- },
- {
"fetch_from": "loan_security.haircut",
"fieldname": "haircut",
"fieldtype": "Percent",
@@ -71,7 +63,8 @@
}
],
"istable": 1,
- "modified": "2019-10-02 12:48:18.588236",
+ "links": [],
+ "modified": "2020-05-06 10:50:18.448552",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Unpledge",
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 8a31cf3..5255933 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -495,6 +495,7 @@
execute:frappe.delete_doc('DocType', 'Production Planning Tool', ignore_missing=True)
erpnext.patches.v10_0.migrate_daily_work_summary_settings_to_daily_work_summary_group # 24-12-2018
erpnext.patches.v10_0.add_default_cash_flow_mappers
+erpnext.patches.v11_0.rename_duplicate_item_code_values
erpnext.patches.v11_0.make_quality_inspection_template
erpnext.patches.v10_0.update_status_for_multiple_source_in_po
erpnext.patches.v10_0.set_auto_created_serial_no_in_stock_entry
@@ -630,7 +631,7 @@
execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_source')
execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart')
execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_field')
-erpnext.patches.v12_0.add_default_dashboards
+erpnext.patches.v12_0.add_default_dashboards # 2020-04-05
erpnext.patches.v12_0.remove_bank_remittance_custom_fields
erpnext.patches.v12_0.generate_leave_ledger_entries
execute:frappe.delete_doc_if_exists("Report", "Loan Repayment")
@@ -678,3 +679,5 @@
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
+execute:frappe.delete_doc_if_exists("Page", "appointment-analytic")
diff --git a/erpnext/patches/v11_0/rename_duplicate_item_code_values.py b/erpnext/patches/v11_0/rename_duplicate_item_code_values.py
new file mode 100644
index 0000000..00ab562
--- /dev/null
+++ b/erpnext/patches/v11_0/rename_duplicate_item_code_values.py
@@ -0,0 +1,8 @@
+import frappe
+
+def execute():
+ items = []
+ items = frappe.db.sql("""select item_code from `tabItem` group by item_code having count(*) > 1""", as_dict=True)
+ if items:
+ for item in items:
+ frappe.db.sql("""update `tabItem` set item_code=name where item_code = %s""", (item.item_code))
diff --git a/erpnext/patches/v12_0/add_default_dashboards.py b/erpnext/patches/v12_0/add_default_dashboards.py
index 0c3f2f8..2a91e1b 100644
--- a/erpnext/patches/v12_0/add_default_dashboards.py
+++ b/erpnext/patches/v12_0/add_default_dashboards.py
@@ -6,4 +6,5 @@
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
index f5bd8c3..6f843cd 100644
--- a/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py
+++ b/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py
@@ -3,6 +3,10 @@
from collections import defaultdict
def execute():
+
+ frappe.reload_doc('stock', 'doctype', 'delivery_note_item', force=True)
+ frappe.reload_doc('stock', 'doctype', 'purchase_receipt_item', force=True)
+
def map_rows(doc_row, return_doc_row, detail_field, doctype):
"""Map rows after identifying similar ones."""
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 0000000..ddcadcb
--- /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 bb8c131..b182dfc 100644
--- a/erpnext/setup/setup_wizard/data/dashboard_charts.py
+++ b/erpnext/setup/setup_wizard/data/dashboard_charts.py
@@ -29,7 +29,8 @@
{ "chart": "Incoming Bills (Purchase Invoice)" },
{ "chart": "Bank Balance" },
{ "chart": "Income" },
- { "chart": "Expenses" }
+ { "chart": "Expenses" },
+ { "chart": "Patient Appointments" }
]
}
],
@@ -107,6 +108,21 @@
"document_type": "Sales Invoice",
"type": "Bar",
"width": "Half"
+ },
+ {
+ "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/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index 9b7249e..a091ac7 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -7,7 +7,7 @@
from frappe import _
from frappe.model.document import Document
from frappe.model.naming import make_autoname, revert_series_if_last
-from frappe.utils import flt, cint
+from frappe.utils import flt, cint, get_link_to_form
from frappe.utils.jinja import render_template
from frappe.utils.data import add_days
from six import string_types
@@ -124,7 +124,7 @@
if has_expiry_date and not self.expiry_date:
frappe.throw(msg=_("Please set {0} for Batched Item {1}, which is used to set {2} on Submit.") \
.format(frappe.bold("Shelf Life in Days"),
- frappe.utils.get_link_to_form("Item", self.item),
+ get_link_to_form("Item", self.item),
frappe.bold("Batch Expiry Date")),
title=_("Expiry Date Mandatory"))
@@ -264,16 +264,20 @@
def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
cond = ''
- if serial_no:
+ if serial_no and frappe.get_cached_value('Item', item_code, 'has_batch_no'):
+ serial_nos = get_serial_nos(serial_no)
batch = frappe.get_all("Serial No",
fields = ["distinct batch_no"],
filters= {
"item_code": item_code,
"warehouse": warehouse,
- "name": ("in", get_serial_nos(serial_no))
+ "name": ("in", serial_nos)
}
)
+ if not batch:
+ validate_serial_no_with_batch(serial_nos, item_code)
+
if batch and len(batch) > 1:
return []
@@ -288,4 +292,15 @@
and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL) {0}
group by batch_id
order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC
- """.format(cond), (item_code, warehouse), as_dict=True)
\ No newline at end of file
+ """.format(cond), (item_code, warehouse), as_dict=True)
+
+def validate_serial_no_with_batch(serial_nos, item_code):
+ if frappe.get_cached_value("Serial No", serial_nos[0], "item_code") != item_code:
+ frappe.throw(_("The serial no {0} does not belong to item {1}")
+ .format(get_link_to_form("Serial No", serial_nos[0]), get_link_to_form("Item", item_code)))
+
+ serial_no_link = ','.join([get_link_to_form("Serial No", sn) for sn in serial_nos])
+
+ message = "Serial Nos" if len(serial_nos) > 1 else "Serial No"
+ frappe.throw(_("There is no batch found against the {0}: {1}")
+ .format(message, serial_no_link))
\ No newline at end of file