Merge branch 'develop' of github.com:frappe/erpnext into feature-pick-list
diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js
index 6fdd797..efac1af 100644
--- a/erpnext/accounts/doctype/account/account_tree.js
+++ b/erpnext/accounts/doctype/account/account_tree.js
@@ -123,7 +123,8 @@
if(frappe.boot.user.can_read.indexOf("GL Entry") !== -1){
// show Dr if positive since balance is calculated as debit - credit else show Cr
- let dr_or_cr = node.data.balance_in_account_currency > 0 ? "Dr": "Cr";
+ let balance = node.data.balance_in_account_currency || node.data.balance;
+ let dr_or_cr = balance > 0 ? "Dr": "Cr";
if (node.data && node.data.balance!==undefined) {
$('<span class="balance-area pull-right text-muted small">'
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js
index 88b11dd..5cea0d1 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js
@@ -40,16 +40,9 @@
},
document_type: function(frm) {
-
frm.set_value('label', frm.doc.document_type);
frm.set_value('fieldname', frappe.model.scrub(frm.doc.document_type));
- if (frm.is_new()){
- let row = frappe.model.add_child(frm.doc, "Accounting Dimension Detail", "dimension_defaults");
- row.reference_document = frm.doc.document_type;
- frm.refresh_fields("dimension_defaults");
- }
-
frappe.db.get_value('Accounting Dimension', {'document_type': frm.doc.document_type}, 'document_type', (r) => {
if (r && r.document_type) {
frm.set_df_property('document_type', 'description', "Document type is already set as dimension");
diff --git a/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json b/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json
index 1ccef6c..e9e1f43 100644
--- a/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json
+++ b/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json
@@ -17,8 +17,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
- "options": "Company",
- "reqd": 1
+ "options": "Company"
},
{
"fieldname": "reference_document",
@@ -34,8 +33,7 @@
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Default Dimension",
- "options": "reference_document",
- "reqd": 1
+ "options": "reference_document"
},
{
"columns": 3,
@@ -55,7 +53,7 @@
}
],
"istable": 1,
- "modified": "2019-07-17 23:34:33.026883",
+ "modified": "2019-08-15 11:59:09.389891",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting Dimension Detail",
diff --git a/erpnext/accounts/doctype/payment_order/payment_order.js b/erpnext/accounts/doctype/payment_order/payment_order.js
index f2f00ce..ce9cfe5 100644
--- a/erpnext/accounts/doctype/payment_order/payment_order.js
+++ b/erpnext/accounts/doctype/payment_order/payment_order.js
@@ -66,6 +66,7 @@
get_query_filters: {
bank: frm.doc.bank,
docstatus: 1,
+ payment_type: ("!=", "Receive"),
bank_account: frm.doc.company_bank_account,
paid_from: frm.doc.account,
payment_order_status: ["=", "Initiated"],
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index cb94722..e42f4af 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -469,7 +469,9 @@
# fetch and append data from Activity Log
data += frappe.db.sql("""select {fields}
from `tabActivity Log`
- where reference_doctype={doctype} and reference_name={name}
+ 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}")
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,
diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py
index 7b9c939..3c8de60 100644
--- a/erpnext/accounts/report/financial_statements.py
+++ b/erpnext/accounts/report/financial_statements.py
@@ -425,9 +425,12 @@
all_cost_centers = []
for d in cost_centers:
- lft, rgt = frappe.db.get_value("Cost Center", d, ["lft", "rgt"])
- children = frappe.get_all("Cost Center", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
- all_cost_centers += [c.name for c in children]
+ if frappe.db.exists("Cost Center", d):
+ lft, rgt = frappe.db.get_value("Cost Center", d, ["lft", "rgt"])
+ children = frappe.get_all("Cost Center", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
+ all_cost_centers += [c.name for c in children]
+ else:
+ frappe.throw(_("Cost Center: {0} does not exist".format(d)))
return list(set(all_cost_centers))
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 2d78d26..c5cad73 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -303,14 +303,17 @@
},
set_depreciation_rate: function(frm, row) {
- if (row.total_number_of_depreciations && row.frequency_of_depreciation) {
+ if (row.total_number_of_depreciations && row.frequency_of_depreciation
+ && row.expected_value_after_useful_life) {
frappe.call({
method: "get_depreciation_rate",
doc: frm.doc,
args: row,
callback: function(r) {
if (r.message) {
- frappe.model.set_value(row.doctype, row.name, "rate_of_depreciation", r.message);
+ frappe.flags.dont_change_rate = true;
+ frappe.model.set_value(row.doctype, row.name,
+ "rate_of_depreciation", flt(r.message, precision("rate_of_depreciation", row)));
}
}
});
@@ -338,6 +341,14 @@
total_number_of_depreciations: function(frm, cdt, cdn) {
const row = locals[cdt][cdn];
frm.events.set_depreciation_rate(frm, row);
+ },
+
+ rate_of_depreciation: function(frm, cdt, cdn) {
+ if(!frappe.flags.dont_change_rate) {
+ frappe.model.set_value(cdt, cdn, "expected_value_after_useful_life", 0);
+ }
+
+ frappe.flags.dont_change_rate = false;
}
});
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index c398a73..45d2ec2 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -6,7 +6,7 @@
import frappe, erpnext, math, json
from frappe import _
from six import string_types
-from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff
+from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, add_days
from frappe.model.document import Document
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.assets.doctype.asset.depreciation \
@@ -101,97 +101,88 @@
def set_depreciation_rate(self):
for d in self.get("finance_books"):
- d.rate_of_depreciation = self.get_depreciation_rate(d, on_validate=True)
+ d.rate_of_depreciation = flt(self.get_depreciation_rate(d, on_validate=True),
+ d.precision("rate_of_depreciation"))
def make_depreciation_schedule(self):
- depreciation_method = [d.depreciation_method for d in self.finance_books]
-
- if 'Manual' not in depreciation_method:
+ if 'Manual' not in [d.depreciation_method for d in self.finance_books]:
self.schedules = []
- if not self.get("schedules") and self.available_for_use_date:
- total_depreciations = sum([d.total_number_of_depreciations for d in self.get('finance_books')])
+ if self.get("schedules") or not self.available_for_use_date:
+ return
- for d in self.get('finance_books'):
- self.validate_asset_finance_books(d)
+ for d in self.get('finance_books'):
+ self.validate_asset_finance_books(d)
- value_after_depreciation = (flt(self.gross_purchase_amount) -
- flt(self.opening_accumulated_depreciation))
+ value_after_depreciation = (flt(self.gross_purchase_amount) -
+ flt(self.opening_accumulated_depreciation))
- d.value_after_depreciation = value_after_depreciation
+ d.value_after_depreciation = value_after_depreciation
- no_of_depreciations = cint(d.total_number_of_depreciations - 1) - cint(self.number_of_depreciations_booked)
- end_date = add_months(d.depreciation_start_date,
- no_of_depreciations * cint(d.frequency_of_depreciation))
+ number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \
+ cint(self.number_of_depreciations_booked)
- total_days = date_diff(end_date, self.available_for_use_date)
- rate_per_day = (value_after_depreciation - d.get("expected_value_after_useful_life")) / total_days
+ has_pro_rata = self.check_is_pro_rata(d)
- number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \
- cint(self.number_of_depreciations_booked)
+ if has_pro_rata:
+ number_of_pending_depreciations += 1
- from_date = self.available_for_use_date
- if number_of_pending_depreciations:
- next_depr_date = getdate(add_months(self.available_for_use_date,
- number_of_pending_depreciations * 12))
- if (cint(frappe.db.get_value("Asset Settings", None, "schedule_based_on_fiscal_year")) == 1
- and getdate(d.depreciation_start_date) < next_depr_date):
+ skip_row = False
+ for n in range(number_of_pending_depreciations):
+ # If depreciation is already completed (for double declining balance)
+ if skip_row: continue
- number_of_pending_depreciations += 1
- for n in range(number_of_pending_depreciations):
- if n == list(range(number_of_pending_depreciations))[-1]:
- schedule_date = add_months(self.available_for_use_date, n * 12)
- previous_scheduled_date = add_months(d.depreciation_start_date, (n-1) * 12)
- depreciation_amount = \
- self.get_depreciation_amount_prorata_temporis(value_after_depreciation,
- d, previous_scheduled_date, schedule_date)
+ depreciation_amount = self.get_depreciation_amount(value_after_depreciation,
+ d.total_number_of_depreciations, d)
- elif n == list(range(number_of_pending_depreciations))[0]:
- schedule_date = d.depreciation_start_date
- depreciation_amount = \
- self.get_depreciation_amount_prorata_temporis(value_after_depreciation,
- d, self.available_for_use_date, schedule_date)
+ if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
+ schedule_date = add_months(d.depreciation_start_date,
+ n * cint(d.frequency_of_depreciation))
- else:
- schedule_date = add_months(d.depreciation_start_date, n * 12)
- depreciation_amount = \
- self.get_depreciation_amount_prorata_temporis(value_after_depreciation, d)
+ # For first row
+ if has_pro_rata and n==0:
+ depreciation_amount, days = get_pro_rata_amt(d, depreciation_amount,
+ self.available_for_use_date, d.depreciation_start_date)
+ # For last row
+ elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
+ to_date = add_months(self.available_for_use_date,
+ n * cint(d.frequency_of_depreciation))
- if value_after_depreciation != 0:
- value_after_depreciation -= flt(depreciation_amount)
+ depreciation_amount, days = get_pro_rata_amt(d,
+ depreciation_amount, schedule_date, to_date)
- self.append("schedules", {
- "schedule_date": schedule_date,
- "depreciation_amount": depreciation_amount,
- "depreciation_method": d.depreciation_method,
- "finance_book": d.finance_book,
- "finance_book_id": d.idx
- })
- else:
- for n in range(number_of_pending_depreciations):
- schedule_date = add_months(d.depreciation_start_date,
- n * cint(d.frequency_of_depreciation))
+ schedule_date = add_days(schedule_date, days)
- if d.depreciation_method in ("Straight Line", "Manual"):
- days = date_diff(schedule_date, from_date)
- if n == 0: days += 1
+ if not depreciation_amount: continue
+ value_after_depreciation -= flt(depreciation_amount,
+ self.precision("gross_purchase_amount"))
- depreciation_amount = days * rate_per_day
- from_date = schedule_date
- else:
- depreciation_amount = self.get_depreciation_amount(value_after_depreciation,
- d.total_number_of_depreciations, d)
+ # Adjust depreciation amount in the last period based on the expected value after useful life
+ if d.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
+ and value_after_depreciation != d.expected_value_after_useful_life)
+ or value_after_depreciation < d.expected_value_after_useful_life):
+ depreciation_amount += (value_after_depreciation - d.expected_value_after_useful_life)
+ skip_row = True
- if depreciation_amount:
- value_after_depreciation -= flt(depreciation_amount)
+ if depreciation_amount > 0:
+ self.append("schedules", {
+ "schedule_date": schedule_date,
+ "depreciation_amount": depreciation_amount,
+ "depreciation_method": d.depreciation_method,
+ "finance_book": d.finance_book,
+ "finance_book_id": d.idx
+ })
- self.append("schedules", {
- "schedule_date": schedule_date,
- "depreciation_amount": depreciation_amount,
- "depreciation_method": d.depreciation_method,
- "finance_book": d.finance_book,
- "finance_book_id": d.idx
- })
+ def check_is_pro_rata(self, row):
+ has_pro_rata = False
+
+ days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1
+ total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
+
+ if days < total_days:
+ has_pro_rata = True
+
+ return has_pro_rata
def validate_asset_finance_books(self, row):
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
@@ -261,31 +252,14 @@
return flt(self.get('finance_books')[cint(idx)-1].value_after_depreciation)
def get_depreciation_amount(self, depreciable_value, total_number_of_depreciations, row):
- if row.depreciation_method in ["Straight Line", "Manual"]:
- amt = (flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life) -
- flt(self.opening_accumulated_depreciation))
-
- depreciation_amount = amt * row.rate_of_depreciation
- else:
- depreciation_amount = flt(depreciable_value) * (flt(row.rate_of_depreciation) / 100)
- value_after_depreciation = flt(depreciable_value) - depreciation_amount
- if value_after_depreciation < flt(row.expected_value_after_useful_life):
- depreciation_amount = flt(depreciable_value) - flt(row.expected_value_after_useful_life)
-
- return depreciation_amount
-
- def get_depreciation_amount_prorata_temporis(self, depreciable_value, row, start_date=None, end_date=None):
- if start_date and end_date:
- prorata_temporis = min(abs(flt(date_diff(str(end_date), str(start_date)))) / flt(frappe.db.get_value("Asset Settings", None, "number_of_days_in_fiscal_year")), 1)
- else:
- prorata_temporis = 1
+ precision = self.precision("gross_purchase_amount")
if row.depreciation_method in ("Straight Line", "Manual"):
depreciation_amount = (flt(row.value_after_depreciation) -
flt(row.expected_value_after_useful_life)) / (cint(row.total_number_of_depreciations) -
- cint(self.number_of_depreciations_booked)) * prorata_temporis
+ cint(self.number_of_depreciations_booked))
else:
- depreciation_amount = self.get_depreciation_amount(depreciable_value, row.total_number_of_depreciations, row)
+ depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100), precision)
return depreciation_amount
@@ -301,9 +275,12 @@
flt(accumulated_depreciation_after_full_schedule),
self.precision('gross_purchase_amount'))
- if row.expected_value_after_useful_life < asset_value_after_full_schedule:
+ if (row.expected_value_after_useful_life and
+ row.expected_value_after_useful_life < asset_value_after_full_schedule):
frappe.throw(_("Depreciation Row {0}: Expected value after useful life must be greater than or equal to {1}")
.format(row.idx, asset_value_after_full_schedule))
+ elif not row.expected_value_after_useful_life:
+ row.expected_value_after_useful_life = asset_value_after_full_schedule
def validate_cancellation(self):
if self.status not in ("Submitted", "Partially Depreciated", "Fully Depreciated"):
@@ -412,15 +389,7 @@
if isinstance(args, string_types):
args = json.loads(args)
- number_of_depreciations_booked = 0
- if self.is_existing_asset:
- number_of_depreciations_booked = self.number_of_depreciations_booked
-
float_precision = cint(frappe.db.get_default("float_precision")) or 2
- tot_no_of_depreciation = flt(args.get("total_number_of_depreciations")) - flt(number_of_depreciations_booked)
-
- if args.get("depreciation_method") in ["Straight Line", "Manual"]:
- return 1.0 / tot_no_of_depreciation
if args.get("depreciation_method") == 'Double Declining Balance':
return 200.0 / args.get("total_number_of_depreciations")
@@ -600,3 +569,15 @@
def is_cwip_accounting_disabled():
return cint(frappe.db.get_single_value("Asset Settings", "disable_cwip_accounting"))
+
+def get_pro_rata_amt(row, depreciation_amount, from_date, to_date):
+ days = date_diff(to_date, from_date)
+ total_days = get_total_days(to_date, row.frequency_of_depreciation)
+
+ return (depreciation_amount * flt(days)) / flt(total_days), days
+
+def get_total_days(date, frequency):
+ period_start_date = add_months(date,
+ cint(frequency) * -1)
+
+ return date_diff(date, period_start_date)
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index fceccfb..481ee7d 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -88,23 +88,23 @@
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
asset = frappe.get_doc('Asset', asset_name)
asset.calculate_depreciation = 1
- asset.available_for_use_date = '2020-06-06'
- asset.purchase_date = '2020-06-06'
+ asset.available_for_use_date = '2030-01-01'
+ asset.purchase_date = '2030-01-01'
asset.append("finance_books", {
"expected_value_after_useful_life": 10000,
- "next_depreciation_date": "2020-12-31",
"depreciation_method": "Straight Line",
"total_number_of_depreciations": 3,
- "frequency_of_depreciation": 10,
- "depreciation_start_date": "2020-06-06"
+ "frequency_of_depreciation": 12,
+ "depreciation_start_date": "2030-12-31"
})
asset.save()
+
self.assertEqual(asset.status, "Draft")
expected_schedules = [
- ["2020-06-06", 147.54, 147.54],
- ["2021-04-06", 44852.46, 45000.0],
- ["2022-02-06", 45000.0, 90000.00]
+ ["2030-12-31", 30000.00, 30000.00],
+ ["2031-12-31", 30000.00, 60000.00],
+ ["2032-12-31", 30000.00, 90000.00]
]
schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
@@ -118,20 +118,21 @@
asset.calculate_depreciation = 1
asset.number_of_depreciations_booked = 1
asset.opening_accumulated_depreciation = 40000
+ asset.available_for_use_date = "2030-06-06"
asset.append("finance_books", {
"expected_value_after_useful_life": 10000,
- "next_depreciation_date": "2020-12-31",
"depreciation_method": "Straight Line",
"total_number_of_depreciations": 3,
- "frequency_of_depreciation": 10,
- "depreciation_start_date": "2020-06-06"
+ "frequency_of_depreciation": 12,
+ "depreciation_start_date": "2030-12-31"
})
asset.insert()
self.assertEqual(asset.status, "Draft")
asset.save()
expected_schedules = [
- ["2020-06-06", 164.47, 40164.47],
- ["2021-04-06", 49835.53, 90000.00]
+ ["2030-12-31", 14246.58, 54246.58],
+ ["2031-12-31", 25000.00, 79246.58],
+ ["2032-06-06", 10753.42, 90000.00]
]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
for d in asset.get("schedules")]
@@ -145,24 +146,23 @@
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
asset = frappe.get_doc('Asset', asset_name)
asset.calculate_depreciation = 1
- asset.available_for_use_date = '2020-06-06'
- asset.purchase_date = '2020-06-06'
+ asset.available_for_use_date = '2030-01-01'
+ asset.purchase_date = '2030-01-01'
asset.append("finance_books", {
"expected_value_after_useful_life": 10000,
- "next_depreciation_date": "2020-12-31",
"depreciation_method": "Double Declining Balance",
"total_number_of_depreciations": 3,
- "frequency_of_depreciation": 10,
- "depreciation_start_date": "2020-06-06"
+ "frequency_of_depreciation": 12,
+ "depreciation_start_date": '2030-12-31'
})
asset.insert()
self.assertEqual(asset.status, "Draft")
asset.save()
expected_schedules = [
- ["2020-06-06", 66666.67, 66666.67],
- ["2021-04-06", 22222.22, 88888.89],
- ["2022-02-06", 1111.11, 90000.0]
+ ['2030-12-31', 66667.00, 66667.00],
+ ['2031-12-31', 22222.11, 88889.11],
+ ['2032-12-31', 1110.89, 90000.0]
]
schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
@@ -177,23 +177,21 @@
asset.is_existing_asset = 1
asset.number_of_depreciations_booked = 1
asset.opening_accumulated_depreciation = 50000
+ asset.available_for_use_date = '2030-01-01'
+ asset.purchase_date = '2029-11-30'
asset.append("finance_books", {
"expected_value_after_useful_life": 10000,
- "next_depreciation_date": "2020-12-31",
"depreciation_method": "Double Declining Balance",
"total_number_of_depreciations": 3,
- "frequency_of_depreciation": 10,
- "depreciation_start_date": "2020-06-06"
+ "frequency_of_depreciation": 12,
+ "depreciation_start_date": "2030-12-31"
})
asset.insert()
self.assertEqual(asset.status, "Draft")
- asset.save()
-
- asset.save()
expected_schedules = [
- ["2020-06-06", 33333.33, 83333.33],
- ["2021-04-06", 6666.67, 90000.0]
+ ["2030-12-31", 33333.50, 83333.50],
+ ["2031-12-31", 6666.50, 90000.0]
]
schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
@@ -209,25 +207,25 @@
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
asset = frappe.get_doc('Asset', asset_name)
asset.calculate_depreciation = 1
- asset.purchase_date = '2020-01-30'
+ asset.purchase_date = '2030-01-30'
asset.is_existing_asset = 0
- asset.available_for_use_date = "2020-01-30"
+ asset.available_for_use_date = "2030-01-30"
asset.append("finance_books", {
"expected_value_after_useful_life": 10000,
"depreciation_method": "Straight Line",
"total_number_of_depreciations": 3,
- "frequency_of_depreciation": 10,
- "depreciation_start_date": "2020-12-31"
+ "frequency_of_depreciation": 12,
+ "depreciation_start_date": "2030-12-31"
})
asset.insert()
asset.save()
expected_schedules = [
- ["2020-12-31", 28000.0, 28000.0],
- ["2021-12-31", 30000.0, 58000.0],
- ["2022-12-31", 30000.0, 88000.0],
- ["2023-01-30", 2000.0, 90000.0]
+ ["2030-12-31", 27534.25, 27534.25],
+ ["2031-12-31", 30000.0, 57534.25],
+ ["2032-12-31", 30000.0, 87534.25],
+ ["2033-01-30", 2465.75, 90000.0]
]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@@ -266,8 +264,8 @@
self.assertEqual(asset.get("schedules")[0].journal_entry[:4], "DEPR")
expected_gle = (
- ("_Test Accumulated Depreciations - _TC", 0.0, 32129.24),
- ("_Test Depreciations - _TC", 32129.24, 0.0)
+ ("_Test Accumulated Depreciations - _TC", 0.0, 30000.0),
+ ("_Test Depreciations - _TC", 30000.0, 0.0)
)
gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
@@ -277,15 +275,15 @@
self.assertEqual(gle, expected_gle)
self.assertEqual(asset.get("value_after_depreciation"), 0)
- def test_depreciation_entry_for_wdv(self):
+ def test_depreciation_entry_for_wdv_without_pro_rata(self):
pr = make_purchase_receipt(item_code="Macbook Pro",
qty=1, rate=8000.0, location="Test Location")
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
asset = frappe.get_doc('Asset', asset_name)
asset.calculate_depreciation = 1
- asset.available_for_use_date = '2030-06-06'
- asset.purchase_date = '2030-06-06'
+ asset.available_for_use_date = '2030-01-01'
+ asset.purchase_date = '2030-01-01'
asset.append("finance_books", {
"expected_value_after_useful_life": 1000,
"depreciation_method": "Written Down Value",
@@ -298,9 +296,41 @@
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
expected_schedules = [
- ["2030-12-31", 4000.0, 4000.0],
- ["2031-12-31", 2000.0, 6000.0],
- ["2032-12-31", 1000.0, 7000.0],
+ ["2030-12-31", 4000.00, 4000.00],
+ ["2031-12-31", 2000.00, 6000.00],
+ ["2032-12-31", 1000.00, 7000.0],
+ ]
+
+ schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
+ for d in asset.get("schedules")]
+
+ self.assertEqual(schedules, expected_schedules)
+
+ def test_pro_rata_depreciation_entry_for_wdv(self):
+ pr = make_purchase_receipt(item_code="Macbook Pro",
+ qty=1, rate=8000.0, location="Test Location")
+
+ asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
+ asset = frappe.get_doc('Asset', asset_name)
+ asset.calculate_depreciation = 1
+ asset.available_for_use_date = '2030-06-06'
+ asset.purchase_date = '2030-01-01'
+ asset.append("finance_books", {
+ "expected_value_after_useful_life": 1000,
+ "depreciation_method": "Written Down Value",
+ "total_number_of_depreciations": 3,
+ "frequency_of_depreciation": 12,
+ "depreciation_start_date": "2030-12-31"
+ })
+ asset.save(ignore_permissions=True)
+
+ self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
+
+ expected_schedules = [
+ ["2030-12-31", 2279.45, 2279.45],
+ ["2031-12-31", 2860.28, 5139.73],
+ ["2032-12-31", 1430.14, 6569.87],
+ ["2033-06-06", 430.13, 7000.0],
]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@@ -346,18 +376,19 @@
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
asset = frappe.get_doc('Asset', asset_name)
asset.calculate_depreciation = 1
- asset.available_for_use_date = '2020-06-06'
- asset.purchase_date = '2020-06-06'
+ asset.available_for_use_date = nowdate()
+ asset.purchase_date = nowdate()
asset.append("finance_books", {
"expected_value_after_useful_life": 10000,
"depreciation_method": "Straight Line",
"total_number_of_depreciations": 3,
"frequency_of_depreciation": 10,
- "depreciation_start_date": "2020-06-06"
+ "depreciation_start_date": nowdate()
})
asset.insert()
asset.submit()
- post_depreciation_entries(date="2021-01-01")
+
+ post_depreciation_entries(date=add_months(nowdate(), 10))
scrap_asset(asset.name)
@@ -366,9 +397,9 @@
self.assertTrue(asset.journal_entry_for_scrap)
expected_gle = (
- ("_Test Accumulated Depreciations - _TC", 147.54, 0.0),
+ ("_Test Accumulated Depreciations - _TC", 30000.0, 0.0),
("_Test Fixed Asset - _TC", 0.0, 100000.0),
- ("_Test Gain/Loss on Asset Disposal - _TC", 99852.46, 0.0)
+ ("_Test Gain/Loss on Asset Disposal - _TC", 70000.0, 0.0)
)
gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
@@ -412,9 +443,9 @@
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
expected_gle = (
- ("_Test Accumulated Depreciations - _TC", 23051.47, 0.0),
+ ("_Test Accumulated Depreciations - _TC", 20392.16, 0.0),
("_Test Fixed Asset - _TC", 0.0, 100000.0),
- ("_Test Gain/Loss on Asset Disposal - _TC", 51948.53, 0.0),
+ ("_Test Gain/Loss on Asset Disposal - _TC", 54607.84, 0.0),
("Debtors - _TC", 25000.0, 0.0)
)
diff --git a/erpnext/assets/doctype/asset_settings/asset_settings.json b/erpnext/assets/doctype/asset_settings/asset_settings.json
index a3fee96..edc5ce1 100644
--- a/erpnext/assets/doctype/asset_settings/asset_settings.json
+++ b/erpnext/assets/doctype/asset_settings/asset_settings.json
@@ -54,75 +54,6 @@
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
- "fieldname": "schedule_based_on_fiscal_year",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Calculate Prorated Depreciation Schedule Based on Fiscal Year",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "360",
- "depends_on": "eval:doc.schedule_based_on_fiscal_year",
- "description": "This value is used for pro-rata temporis calculation",
- "fetch_if_empty": 0,
- "fieldname": "number_of_days_in_fiscal_year",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Number of Days in Fiscal Year",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "disable_cwip_accounting",
"fieldtype": "Check",
"hidden": 0,
@@ -159,7 +90,7 @@
"issingle": 1,
"istable": 0,
"max_attachments": 0,
- "modified": "2019-03-08 10:44:41.924547",
+ "modified": "2019-05-26 18:31:19.930563",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Settings",
diff --git a/erpnext/config/hr.py b/erpnext/config/hr.py
index 0d05cb1..eae937c 100644
--- a/erpnext/config/hr.py
+++ b/erpnext/config/hr.py
@@ -134,6 +134,12 @@
"name": "Employee Leave Balance",
"doctype": "Leave Application"
},
+ {
+ "type": "report",
+ "is_query_report": True,
+ "name": "Leave Ledger Entry",
+ "doctype": "Leave Ledger Entry"
+ },
]
},
{
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 2fddcdf..b713958 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -18,34 +18,31 @@
validate_returned_items(doc)
def validate_return_against(doc):
- filters = {"doctype": doc.doctype, "docstatus": 1, "company": doc.company}
- if doc.meta.get_field("customer") and doc.customer:
- filters["customer"] = doc.customer
- elif doc.meta.get_field("supplier") and doc.supplier:
- filters["supplier"] = doc.supplier
-
- if not frappe.db.exists(filters):
+ if not frappe.db.exists(doc.doctype, doc.return_against):
frappe.throw(_("Invalid {0}: {1}")
.format(doc.meta.get_label("return_against"), doc.return_against))
else:
ref_doc = frappe.get_doc(doc.doctype, doc.return_against)
- # validate posting date time
- return_posting_datetime = "%s %s" % (doc.posting_date, doc.get("posting_time") or "00:00:00")
- ref_posting_datetime = "%s %s" % (ref_doc.posting_date, ref_doc.get("posting_time") or "00:00:00")
+ party_type = "customer" if doc.doctype in ("Sales Invoice", "Delivery Note") else "supplier"
- if get_datetime(return_posting_datetime) < get_datetime(ref_posting_datetime):
- frappe.throw(_("Posting timestamp must be after {0}").format(format_datetime(ref_posting_datetime)))
+ if ref_doc.company == doc.company and ref_doc.get(party_type) == doc.get(party_type) and ref_doc.docstatus == 1:
+ # validate posting date time
+ return_posting_datetime = "%s %s" % (doc.posting_date, doc.get("posting_time") or "00:00:00")
+ ref_posting_datetime = "%s %s" % (ref_doc.posting_date, ref_doc.get("posting_time") or "00:00:00")
- # validate same exchange rate
- if doc.conversion_rate != ref_doc.conversion_rate:
- frappe.throw(_("Exchange Rate must be same as {0} {1} ({2})")
- .format(doc.doctype, doc.return_against, ref_doc.conversion_rate))
+ if get_datetime(return_posting_datetime) < get_datetime(ref_posting_datetime):
+ frappe.throw(_("Posting timestamp must be after {0}").format(format_datetime(ref_posting_datetime)))
- # validate update stock
- if doc.doctype == "Sales Invoice" and doc.update_stock and not ref_doc.update_stock:
- frappe.throw(_("'Update Stock' can not be checked because items are not delivered via {0}")
- .format(doc.return_against))
+ # validate same exchange rate
+ if doc.conversion_rate != ref_doc.conversion_rate:
+ frappe.throw(_("Exchange Rate must be same as {0} {1} ({2})")
+ .format(doc.doctype, doc.return_against, ref_doc.conversion_rate))
+
+ # validate update stock
+ if doc.doctype == "Sales Invoice" and doc.update_stock and not ref_doc.update_stock:
+ frappe.throw(_("'Update Stock' can not be checked because items are not delivered via {0}")
+ .format(doc.return_against))
def validate_returned_items(doc):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index be9a4fb..7e33a14 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -283,7 +283,9 @@
"erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status"
],
"daily_long": [
- "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms"
+ "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms",
+ "erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation",
+ "erpnext.hr.utils.generate_leave_encashment"
],
"monthly_long": [
"erpnext.accounts.deferred_revenue.convert_deferred_revenue_to_income",
diff --git a/erpnext/hr/doctype/attendance/attendance.json b/erpnext/hr/doctype/attendance/attendance.json
index eb38147..bc89b36 100644
--- a/erpnext/hr/doctype/attendance/attendance.json
+++ b/erpnext/hr/doctype/attendance/attendance.json
@@ -4,6 +4,7 @@
"creation": "2013-01-10 16:34:13",
"doctype": "DocType",
"document_type": "Setup",
+ "engine": "InnoDB",
"field_order": [
"attendance_details",
"naming_series",
@@ -19,7 +20,9 @@
"department",
"shift",
"attendance_request",
- "amended_from"
+ "amended_from",
+ "late_entry",
+ "early_exit"
],
"fields": [
{
@@ -153,12 +156,24 @@
"fieldtype": "Link",
"label": "Shift",
"options": "Shift Type"
+ },
+ {
+ "default": "0",
+ "fieldname": "late_entry",
+ "fieldtype": "Check",
+ "label": "Late Entry"
+ },
+ {
+ "default": "0",
+ "fieldname": "early_exit",
+ "fieldtype": "Check",
+ "label": "Early Exit"
}
],
"icon": "fa fa-ok",
"idx": 1,
"is_submittable": 1,
- "modified": "2019-06-05 19:37:30.410071",
+ "modified": "2019-07-29 20:35:40.845422",
"modified_by": "Administrator",
"module": "HR",
"name": "Attendance",
diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.json b/erpnext/hr/doctype/employee_checkin/employee_checkin.json
index 15ec7c0..08fa4af 100644
--- a/erpnext/hr/doctype/employee_checkin/employee_checkin.json
+++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.json
@@ -14,8 +14,6 @@
"device_id",
"skip_auto_attendance",
"attendance",
- "entry_grace_period_consequence",
- "exit_grace_period_consequence",
"shift_start",
"shift_end",
"shift_actual_start",
@@ -81,20 +79,6 @@
"read_only": 1
},
{
- "default": "0",
- "fieldname": "entry_grace_period_consequence",
- "fieldtype": "Check",
- "hidden": 1,
- "label": "Entry Grace Period Consequence"
- },
- {
- "default": "0",
- "fieldname": "exit_grace_period_consequence",
- "fieldtype": "Check",
- "hidden": 1,
- "label": "Exit Grace Period Consequence"
- },
- {
"fieldname": "shift_start",
"fieldtype": "Datetime",
"hidden": 1,
@@ -119,7 +103,7 @@
"label": "Shift Actual End"
}
],
- "modified": "2019-06-10 15:33:22.731697",
+ "modified": "2019-07-23 23:47:33.975263",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Checkin",
diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.py b/erpnext/hr/doctype/employee_checkin/employee_checkin.py
index b0e15d9..d7d6706 100644
--- a/erpnext/hr/doctype/employee_checkin/employee_checkin.py
+++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.py
@@ -72,7 +72,7 @@
return doc
-def mark_attendance_and_link_log(logs, attendance_status, attendance_date, working_hours=None, shift=None):
+def mark_attendance_and_link_log(logs, attendance_status, attendance_date, working_hours=None, late_entry=False, early_exit=False, shift=None):
"""Creates an attendance and links the attendance to the Employee Checkin.
Note: If attendance is already present for the given date, the logs are marked as skipped and no exception is thrown.
@@ -98,7 +98,9 @@
'status': attendance_status,
'working_hours': working_hours,
'company': employee_doc.company,
- 'shift': shift
+ 'shift': shift,
+ 'late_entry': late_entry,
+ 'early_exit': early_exit
}
attendance = frappe.get_doc(doc_dict).insert()
attendance.submit()
@@ -124,11 +126,16 @@
:param working_hours_calc_type: One of: 'First Check-in and Last Check-out', 'Every Valid Check-in and Check-out'
"""
total_hours = 0
+ in_time = out_time = None
if check_in_out_type == 'Alternating entries as IN and OUT during the same shift':
+ in_time = logs[0].time
+ if len(logs) >= 2:
+ out_time = logs[-1].time
if working_hours_calc_type == 'First Check-in and Last Check-out':
# assumption in this case: First log always taken as IN, Last log always taken as OUT
- total_hours = time_diff_in_hours(logs[0].time, logs[-1].time)
+ total_hours = time_diff_in_hours(in_time, logs[-1].time)
elif working_hours_calc_type == 'Every Valid Check-in and Check-out':
+ logs = logs[:]
while len(logs) >= 2:
total_hours += time_diff_in_hours(logs[0].time, logs[1].time)
del logs[:2]
@@ -138,11 +145,15 @@
first_in_log = logs[find_index_in_dict(logs, 'log_type', 'IN')]
last_out_log = logs[len(logs)-1-find_index_in_dict(reversed(logs), 'log_type', 'OUT')]
if first_in_log and last_out_log:
- total_hours = time_diff_in_hours(first_in_log.time, last_out_log.time)
+ in_time, out_time = first_in_log.time, last_out_log.time
+ total_hours = time_diff_in_hours(in_time, out_time)
elif working_hours_calc_type == 'Every Valid Check-in and Check-out':
in_log = out_log = None
for log in logs:
if in_log and out_log:
+ if not in_time:
+ in_time = in_log.time
+ out_time = out_log.time
total_hours += time_diff_in_hours(in_log.time, out_log.time)
in_log = out_log = None
if not in_log:
@@ -150,8 +161,9 @@
elif not out_log:
out_log = log if log.log_type == 'OUT' else None
if in_log and out_log:
+ out_time = out_log.time
total_hours += time_diff_in_hours(in_log.time, out_log.time)
- return total_hours
+ return total_hours, in_time, out_time
def time_diff_in_hours(start, end):
return round((end-start).total_seconds() / 3600, 1)
diff --git a/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py b/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py
index 424d1a3..9f12ef2 100644
--- a/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py
+++ b/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py
@@ -70,16 +70,16 @@
logs_type_2 = [frappe._dict(x) for x in logs_type_2]
working_hours = calculate_working_hours(logs_type_1,check_in_out_type[0],working_hours_calc_type[0])
- self.assertEqual(working_hours, 6.5)
+ self.assertEqual(working_hours, (6.5, logs_type_1[0].time, logs_type_1[-1].time))
working_hours = calculate_working_hours(logs_type_1,check_in_out_type[0],working_hours_calc_type[1])
- self.assertEqual(working_hours, 4.5)
+ self.assertEqual(working_hours, (4.5, logs_type_1[0].time, logs_type_1[-1].time))
working_hours = calculate_working_hours(logs_type_2,check_in_out_type[1],working_hours_calc_type[0])
- self.assertEqual(working_hours, 5)
+ self.assertEqual(working_hours, (5, logs_type_2[1].time, logs_type_2[-1].time))
working_hours = calculate_working_hours(logs_type_2,check_in_out_type[1],working_hours_calc_type[1])
- self.assertEqual(working_hours, 4.5)
+ self.assertEqual(working_hours, (4.5, logs_type_2[1].time, logs_type_2[-1].time))
def make_n_checkins(employee, n, hours_to_reverse=1):
logs = [make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n+1))]
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json
index 8dd0acf..a41c887 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.json
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.json
@@ -24,6 +24,7 @@
"column_break_18",
"leave_approver_mandatory_in_leave_application",
"show_leaves_of_all_department_members_in_calendar",
+ "auto_leave_encashment",
"hiring_settings",
"check_vacancies"
],
@@ -153,12 +154,18 @@
"fieldname": "check_vacancies",
"fieldtype": "Check",
"label": "Check Vacancies On Job Offer Creation"
+ },
+ {
+ "default": "0",
+ "fieldname": "auto_leave_encashment",
+ "fieldtype": "Check",
+ "label": "Auto Leave Encashment"
}
],
"icon": "fa fa-cog",
"idx": 1,
"issingle": 1,
- "modified": "2019-07-01 18:59:55.256878",
+ "modified": "2019-08-05 13:07:17.993968",
"modified_by": "Administrator",
"module": "HR",
"name": "HR Settings",
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.js b/erpnext/hr/doctype/leave_allocation/leave_allocation.js
index 4b4bfaf..210a73c 100755
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.js
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.js
@@ -21,11 +21,41 @@
})
},
+ refresh: function(frm) {
+ if(frm.doc.docstatus === 1 && frm.doc.expired) {
+ var valid_expiry = moment(frappe.datetime.get_today()).isBetween(frm.doc.from_date, frm.doc.to_date);
+ if(valid_expiry) {
+ // expire current allocation
+ frm.add_custom_button(__('Expire Allocation'), function() {
+ frm.trigger("expire_allocation");
+ });
+ }
+ }
+ },
+
+ expire_allocation: function(frm) {
+ frappe.call({
+ method: 'erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.expire_allocation',
+ args: {
+ 'allocation': frm.doc,
+ 'expiry_date': frappe.datetime.get_today()
+ },
+ freeze: true,
+ callback: function(r){
+ if(!r.exc){
+ frappe.msgprint(__("Allocation Expired!"));
+ }
+ frm.refresh();
+ }
+ });
+ },
+
employee: function(frm) {
frm.trigger("calculate_total_leaves_allocated");
},
leave_type: function(frm) {
+ frm.trigger("leave_policy");
frm.trigger("calculate_total_leaves_allocated");
},
@@ -33,37 +63,38 @@
frm.trigger("calculate_total_leaves_allocated");
},
- carry_forwarded_leaves: function(frm) {
+ unused_leaves: function(frm) {
frm.set_value("total_leaves_allocated",
- flt(frm.doc.carry_forwarded_leaves) + flt(frm.doc.new_leaves_allocated));
+ flt(frm.doc.unused_leaves) + flt(frm.doc.new_leaves_allocated));
},
new_leaves_allocated: function(frm) {
frm.set_value("total_leaves_allocated",
- flt(frm.doc.carry_forwarded_leaves) + flt(frm.doc.new_leaves_allocated));
+ flt(frm.doc.unused_leaves) + flt(frm.doc.new_leaves_allocated));
},
+ leave_policy: function(frm) {
+ if(frm.doc.leave_policy && frm.doc.leave_type) {
+ frappe.db.get_value("Leave Policy Detail",{
+ 'parent': frm.doc.leave_policy,
+ 'leave_type': frm.doc.leave_type
+ }, 'annual_allocation', (r) => {
+ if (r && !r.exc) frm.set_value("new_leaves_allocated", flt(r.annual_allocation));
+ }, "Leave Policy");
+ }
+ },
calculate_total_leaves_allocated: function(frm) {
if (cint(frm.doc.carry_forward) == 1 && frm.doc.leave_type && frm.doc.employee) {
return frappe.call({
- method: "erpnext.hr.doctype.leave_allocation.leave_allocation.get_carry_forwarded_leaves",
- args: {
- "employee": frm.doc.employee,
- "date": frm.doc.from_date,
- "leave_type": frm.doc.leave_type,
- "carry_forward": frm.doc.carry_forward
- },
+ method: "set_total_leaves_allocated",
+ doc: frm.doc,
callback: function(r) {
- if (!r.exc && r.message) {
- frm.set_value('carry_forwarded_leaves', r.message);
- frm.set_value("total_leaves_allocated",
- flt(r.message) + flt(frm.doc.new_leaves_allocated));
- }
+ frm.refresh_fields();
}
})
} else if (cint(frm.doc.carry_forward) == 0) {
- frm.set_value("carry_forwarded_leaves", 0);
+ frm.set_value("unused_leaves", 0);
frm.set_value("total_leaves_allocated", flt(frm.doc.new_leaves_allocated));
}
}
-})
+});
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
index 6d61fe3..007497e 100644
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
@@ -1,683 +1,220 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
"allow_import": 1,
- "allow_rename": 0,
"autoname": "naming_series:",
- "beta": 0,
"creation": "2013-02-20 19:10:38",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
- "editable_grid": 0,
"engine": "InnoDB",
+ "field_order": [
+ "naming_series",
+ "employee",
+ "employee_name",
+ "department",
+ "column_break1",
+ "leave_type",
+ "from_date",
+ "to_date",
+ "section_break_6",
+ "new_leaves_allocated",
+ "carry_forward",
+ "unused_leaves",
+ "total_leaves_allocated",
+ "total_leaves_encashed",
+ "column_break_10",
+ "compensatory_request",
+ "leave_period",
+ "leave_policy",
+ "carry_forwarded_leaves_count",
+ "expired",
+ "amended_from",
+ "notes",
+ "description"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
"fieldname": "naming_series",
"fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Series",
- "length": 0,
"no_copy": 1,
"options": "HR-LAL-.YYYY.-",
- "permlevel": 0,
- "precision": "",
"print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
"reqd": 1,
- "search_index": 0,
- "set_only_once": 1,
- "translatable": 0,
- "unique": 0
+ "set_only_once": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "employee",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Employee",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "employee",
"oldfieldtype": "Link",
"options": "Employee",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
"reqd": 1,
- "search_index": 1,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "search_index": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fetch_from": "employee.employee_name",
"fieldname": "employee_name",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
"in_global_search": 1,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Employee Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 1,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "search_index": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fetch_from": "employee.department",
"fieldname": "department",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Department",
- "length": 0,
- "no_copy": 0,
"options": "Department",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "column_break1",
"fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "50%"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "leave_type",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Leave Type",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "leave_type",
"oldfieldtype": "Link",
"options": "Leave Type",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
"reqd": 1,
- "search_index": 1,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "search_index": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "from_date",
"fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "From Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "to_date",
"fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "To Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "section_break_6",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Allocation",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Allocation"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 1,
"bold": 1,
- "collapsible": 0,
- "columns": 0,
"fieldname": "new_leaves_allocated",
"fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "New Leaves Allocated",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "New Leaves Allocated"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "",
+ "default": "0",
"fieldname": "carry_forward",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Add unused leaves from previous allocations",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Add unused leaves from previous allocations"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "carry_forward",
- "fieldname": "carry_forwarded_leaves",
+ "fieldname": "unused_leaves",
"fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Unused leaves",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
"allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "total_leaves_allocated",
"fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Total Leaves Allocated",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:doc.total_leaves_encashed>0",
"fieldname": "total_leaves_encashed",
"fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Total Leaves Encashed",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "column_break_10",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "compensatory_request",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Compensatory Leave Request",
- "length": 0,
- "no_copy": 0,
"options": "Compensatory Leave Request",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "leave_period",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
"in_standard_filter": 1,
"label": "Leave Period",
- "length": 0,
- "no_copy": 0,
"options": "Leave Period",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "fetch_from": "employee.leave_policy",
+ "fieldname": "leave_policy",
+ "fieldtype": "Link",
+ "in_standard_filter": 1,
+ "label": "Leave Policy",
+ "options": "Leave Policy",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "expired",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "in_standard_filter": 1,
+ "label": "Expired",
+ "read_only": 1
+ },
+ {
"fieldname": "amended_from",
"fieldtype": "Link",
- "hidden": 0,
"ignore_user_permissions": 1,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Amended From",
- "length": 0,
"no_copy": 1,
"oldfieldname": "amended_from",
"oldfieldtype": "Data",
"options": "Leave Allocation",
- "permlevel": 0,
"print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
"collapsible": 1,
- "columns": 0,
"fieldname": "notes",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Notes",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Notes"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "description",
"fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Description",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "reason",
"oldfieldtype": "Small Text",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "300px"
+ },
+ {
+ "depends_on": "carry_forwarded_leaves_count",
+ "fieldname": "carry_forwarded_leaves_count",
+ "fieldtype": "Float",
+ "label": "Carry Forwarded Leaves",
+ "read_only": 1
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
"icon": "fa fa-ok",
"idx": 1,
- "image_view": 0,
- "in_create": 0,
"is_submittable": 1,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-01-30 11:28:09.360525",
+ "modified": "2019-08-08 15:08:42.440909",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Allocation",
@@ -689,15 +226,10 @@
"create": 1,
"delete": 1,
"email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
- "set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
@@ -709,28 +241,19 @@
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
"import": 1,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
- "set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
}
],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
"search_fields": "employee,employee_name,leave_type,total_leaves_allocated",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
- "timeline_field": "employee",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ "timeline_field": "employee"
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
index dc270db..296a52c 100755
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
@@ -3,11 +3,11 @@
from __future__ import unicode_literals
import frappe
-from frappe.utils import flt, date_diff, formatdate
+from frappe.utils import flt, date_diff, formatdate, add_days, today, getdate
from frappe import _
from frappe.model.document import Document
from erpnext.hr.utils import set_employee_name, get_leave_period
-from erpnext.hr.doctype.leave_application.leave_application import get_approved_leaves_for_period
+from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation, create_leave_ledger_entry
class OverlapError(frappe.ValidationError): pass
class BackDatedAllocationError(frappe.ValidationError): pass
@@ -40,14 +40,18 @@
frappe.throw(_("Total allocated leaves are more days than maximum allocation of {0} leave type for employee {1} in the period")\
.format(self.leave_type, self.employee))
- def on_update_after_submit(self):
- self.validate_new_leaves_allocated_value()
- self.set_total_leaves_allocated()
+ def on_submit(self):
+ self.create_leave_ledger_entry()
- frappe.db.set(self,'carry_forwarded_leaves', flt(self.carry_forwarded_leaves))
- frappe.db.set(self,'total_leaves_allocated',flt(self.total_leaves_allocated))
+ # expire all unused leaves in the ledger on creation of carry forward allocation
+ allocation = get_previous_allocation(self.from_date, self.leave_type, self.employee)
+ if self.carry_forward and allocation:
+ expire_allocation(allocation)
- self.validate_against_leave_applications()
+ def on_cancel(self):
+ self.create_leave_ledger_entry(submit=False)
+ if self.carry_forward:
+ self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True)
def validate_period(self):
if date_diff(self.to_date, self.from_date) <= 0:
@@ -87,13 +91,32 @@
BackDatedAllocationError)
def set_total_leaves_allocated(self):
- self.carry_forwarded_leaves = get_carry_forwarded_leaves(self.employee,
+ self.unused_leaves = get_carry_forwarded_leaves(self.employee,
self.leave_type, self.from_date, self.carry_forward)
- self.total_leaves_allocated = flt(self.carry_forwarded_leaves) + flt(self.new_leaves_allocated)
+ self.total_leaves_allocated = flt(self.unused_leaves) + flt(self.new_leaves_allocated)
+
+ if self.carry_forward:
+ self.maintain_carry_forwarded_leaves()
+ self.set_carry_forwarded_leaves_in_previous_allocation()
if not self.total_leaves_allocated and not frappe.db.get_value("Leave Type", self.leave_type, "is_earned_leave") and not frappe.db.get_value("Leave Type", self.leave_type, "is_compensatory"):
- frappe.throw(_("Total leaves allocated is mandatory for Leave Type {0}".format(self.leave_type)))
+ frappe.throw(_("Total leaves allocated is mandatory for Leave Type {0}").format(self.leave_type))
+
+ def maintain_carry_forwarded_leaves(self):
+ ''' Reduce the carry forwarded leaves to be within the maximum allowed leaves '''
+
+ max_leaves_allowed = frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed")
+ if self.new_leaves_allocated <= max_leaves_allowed <= self.total_leaves_allocated:
+ self.unused_leaves = max_leaves_allowed - flt(self.new_leaves_allocated)
+ self.total_leaves_allocated = flt(max_leaves_allowed)
+
+ def set_carry_forwarded_leaves_in_previous_allocation(self, on_cancel=False):
+ ''' Set carry forwarded leaves in previous allocation '''
+ previous_allocation = get_previous_allocation(self.from_date, self.leave_type, self.employee)
+ if on_cancel:
+ self.unused_leaves = 0.0
+ frappe.db.set_value("Leave Allocation", previous_allocation.name, 'carry_forwarded_leaves_count', self.unused_leaves)
def validate_total_leaves_allocated(self):
# Adding a day to include To Date in the difference
@@ -101,15 +124,37 @@
if date_difference < self.total_leaves_allocated:
frappe.throw(_("Total allocated leaves are more than days in the period"), OverAllocationError)
- def validate_against_leave_applications(self):
- leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type,
- self.from_date, self.to_date)
+ def create_leave_ledger_entry(self, submit=True):
+ if self.unused_leaves:
+ expiry_days = frappe.db.get_value("Leave Type", self.leave_type, "expire_carry_forwarded_leaves_after_days")
+ end_date = add_days(self.from_date, expiry_days - 1) if expiry_days else self.to_date
+ args = dict(
+ leaves=self.unused_leaves,
+ from_date=self.from_date,
+ to_date= min(getdate(end_date), getdate(self.to_date)),
+ is_carry_forward=1
+ )
+ create_leave_ledger_entry(self, args, submit)
- if flt(leaves_taken) > flt(self.total_leaves_allocated):
- if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
- frappe.msgprint(_("Note: Total allocated leaves {0} shouldn't be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken))
- else:
- frappe.throw(_("Total allocated leaves {0} cannot be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken), LessAllocationError)
+ args = dict(
+ leaves=self.new_leaves_allocated,
+ from_date=self.from_date,
+ to_date=self.to_date,
+ is_carry_forward=0
+ )
+ create_leave_ledger_entry(self, args, submit)
+
+def get_previous_allocation(from_date, leave_type, employee):
+ ''' Returns document properties of previous allocation '''
+ return frappe.db.get_value("Leave Allocation",
+ filters={
+ 'to_date': ("<", from_date),
+ 'leave_type': leave_type,
+ 'employee': employee,
+ 'docstatus': 1
+ },
+ order_by='to_date DESC',
+ fieldname=['name', 'from_date', 'to_date', 'employee', 'leave_type'], as_dict=1)
def get_leave_allocation_for_period(employee, leave_type, from_date, to_date):
leave_allocated = 0
@@ -136,25 +181,28 @@
@frappe.whitelist()
def get_carry_forwarded_leaves(employee, leave_type, date, carry_forward=None):
- carry_forwarded_leaves = 0
-
- if carry_forward:
+ ''' Returns carry forwarded leaves for the given employee '''
+ unused_leaves = 0.0
+ previous_allocation = get_previous_allocation(date, leave_type, employee)
+ if carry_forward and previous_allocation:
validate_carry_forward(leave_type)
+ unused_leaves = get_unused_leaves(employee, leave_type, previous_allocation.from_date, previous_allocation.to_date)
- previous_allocation = frappe.db.sql("""
- select name, from_date, to_date, total_leaves_allocated
- from `tabLeave Allocation`
- where employee=%s and leave_type=%s and docstatus=1 and to_date < %s
- order by to_date desc limit 1
- """, (employee, leave_type, date), as_dict=1)
- if previous_allocation:
- leaves_taken = get_approved_leaves_for_period(employee, leave_type,
- previous_allocation[0].from_date, previous_allocation[0].to_date)
+ return unused_leaves
- carry_forwarded_leaves = flt(previous_allocation[0].total_leaves_allocated) - flt(leaves_taken)
-
- return carry_forwarded_leaves
+def get_unused_leaves(employee, leave_type, from_date, to_date):
+ ''' Returns unused leaves between the given period while skipping leave allocation expiry '''
+ leaves = frappe.get_all("Leave Ledger Entry", filters={
+ 'employee': employee,
+ 'leave_type': leave_type,
+ 'from_date': ('>=', from_date),
+ 'to_date': ('<=', to_date)
+ }, or_filters={
+ 'is_expired': 0,
+ 'is_carry_forward': 1
+ }, fields=['sum(leaves) as leaves'])
+ return flt(leaves[0]['leaves'])
def validate_carry_forward(leave_type):
if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"):
- frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type))
+ frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type))
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py b/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py
index 72a1b7c..7456aeb 100644
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py
@@ -12,4 +12,9 @@
'items': ['Leave Encashment']
}
],
+ 'reports': [
+ {
+ 'items': ['Employee Leave Balance']
+ }
+ ]
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation_list.js b/erpnext/hr/doctype/leave_allocation/leave_allocation_list.js
new file mode 100644
index 0000000..93f7b83
--- /dev/null
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation_list.js
@@ -0,0 +1,11 @@
+// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
+// License: GNU General Public License v3. See license.txt
+
+// render
+frappe.listview_settings['Leave Allocation'] = {
+ get_indicator: function(doc) {
+ if(doc.status==="Expired") {
+ return [__("Expired"), "darkgrey", "expired, =, 1"];
+ }
+ },
+};
diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.js b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.js
index b8f4faf..0ef78f2 100644
--- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.js
+++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.js
@@ -34,7 +34,7 @@
() => assert.equal(today_date, cur_frm.doc.from_date,
"from date correctly set"),
// check for total leaves
- () => assert.equal(cur_frm.doc.carry_forwarded_leaves + 2, cur_frm.doc.total_leaves_allocated,
+ () => assert.equal(cur_frm.doc.unused_leaves + 2, cur_frm.doc.total_leaves_allocated,
"total leave calculation is correctly set"),
() => done()
]);
diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
index 3b22eb2..bdba8c9 100644
--- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
@@ -1,12 +1,14 @@
from __future__ import unicode_literals
import frappe
import unittest
-from frappe.utils import getdate
+from frappe.utils import nowdate, add_months, getdate, add_days
+from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
+from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation, expire_allocation
class TestLeaveAllocation(unittest.TestCase):
def test_overlapping_allocation(self):
frappe.db.sql("delete from `tabLeave Allocation`")
-
+
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
leaves = [
{
@@ -18,7 +20,7 @@
"from_date": getdate("2015-10-01"),
"to_date": getdate("2015-10-31"),
"new_leaves_allocated": 5,
- "docstatus": 1
+ "docstatus": 1
},
{
"doctype": "Leave Allocation",
@@ -28,17 +30,17 @@
"leave_type": "_Test Leave Type",
"from_date": getdate("2015-09-01"),
"to_date": getdate("2015-11-30"),
- "new_leaves_allocated": 5
+ "new_leaves_allocated": 5
}
]
frappe.get_doc(leaves[0]).save()
self.assertRaises(frappe.ValidationError, frappe.get_doc(leaves[1]).save)
-
- def test_invalid_period(self):
+
+ def test_invalid_period(self):
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
-
- d = frappe.get_doc({
+
+ doc = frappe.get_doc({
"doctype": "Leave Allocation",
"__islocal": 1,
"employee": employee.name,
@@ -46,15 +48,15 @@
"leave_type": "_Test Leave Type",
"from_date": getdate("2015-09-30"),
"to_date": getdate("2015-09-1"),
- "new_leaves_allocated": 5
+ "new_leaves_allocated": 5
})
-
+
#invalid period
- self.assertRaises(frappe.ValidationError, d.save)
-
+ self.assertRaises(frappe.ValidationError, doc.save)
+
def test_allocated_leave_days_over_period(self):
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
- d = frappe.get_doc({
+ doc = frappe.get_doc({
"doctype": "Leave Allocation",
"__islocal": 1,
"employee": employee.name,
@@ -62,10 +64,102 @@
"leave_type": "_Test Leave Type",
"from_date": getdate("2015-09-1"),
"to_date": getdate("2015-09-30"),
- "new_leaves_allocated": 35
+ "new_leaves_allocated": 35
})
-
- #allocated leave more than period
- self.assertRaises(frappe.ValidationError, d.save)
-
+ #allocated leave more than period
+ self.assertRaises(frappe.ValidationError, doc.save)
+
+ def test_carry_forward_calculation(self):
+ frappe.db.sql("delete from `tabLeave Allocation`")
+ frappe.db.sql("delete from `tabLeave Ledger Entry`")
+ leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
+ leave_type.submit()
+
+ # initial leave allocation
+ leave_allocation = create_leave_allocation(
+ leave_type="_Test_CF_leave",
+ from_date=add_months(nowdate(), -12),
+ to_date=add_months(nowdate(), -1),
+ carry_forward=0)
+ leave_allocation.submit()
+
+ # leave allocation with carry forward from previous allocation
+ leave_allocation_1 = create_leave_allocation(
+ leave_type="_Test_CF_leave",
+ carry_forward=1)
+ leave_allocation_1.submit()
+
+ self.assertEquals(leave_allocation.total_leaves_allocated, leave_allocation_1.unused_leaves)
+
+ def test_carry_forward_leaves_expiry(self):
+ frappe.db.sql("delete from `tabLeave Allocation`")
+ frappe.db.sql("delete from `tabLeave Ledger Entry`")
+ leave_type = create_leave_type(
+ leave_type_name="_Test_CF_leave_expiry",
+ is_carry_forward=1,
+ expire_carry_forwarded_leaves_after_days=90)
+ leave_type.submit()
+
+ # initial leave allocation
+ leave_allocation = create_leave_allocation(
+ leave_type="_Test_CF_leave_expiry",
+ from_date=add_months(nowdate(), -24),
+ to_date=add_months(nowdate(), -12),
+ carry_forward=0)
+ leave_allocation.submit()
+
+ leave_allocation = create_leave_allocation(
+ leave_type="_Test_CF_leave_expiry",
+ from_date=add_days(nowdate(), -90),
+ to_date=add_days(nowdate(), 100),
+ carry_forward=1)
+ leave_allocation.submit()
+
+ # expires all the carry forwarded leaves after 90 days
+ process_expired_allocation()
+
+ # leave allocation with carry forward of only new leaves allocated
+ leave_allocation_1 = create_leave_allocation(
+ leave_type="_Test_CF_leave_expiry",
+ carry_forward=1,
+ from_date=add_months(nowdate(), 6),
+ to_date=add_months(nowdate(), 12))
+ leave_allocation_1.submit()
+
+ self.assertEquals(leave_allocation_1.unused_leaves, leave_allocation.new_leaves_allocated)
+
+ def test_creation_of_leave_ledger_entry_on_submit(self):
+ frappe.db.sql("delete from `tabLeave Allocation`")
+
+ leave_allocation = create_leave_allocation()
+ leave_allocation.submit()
+
+ leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_allocation.name))
+
+ self.assertEquals(len(leave_ledger_entry), 1)
+ self.assertEquals(leave_ledger_entry[0].employee, leave_allocation.employee)
+ self.assertEquals(leave_ledger_entry[0].leave_type, leave_allocation.leave_type)
+ self.assertEquals(leave_ledger_entry[0].leaves, leave_allocation.new_leaves_allocated)
+
+ # check if leave ledger entry is deleted on cancellation
+ leave_allocation.cancel()
+ self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name}))
+
+def create_leave_allocation(**args):
+ args = frappe._dict(args)
+
+ employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
+ leave_allocation = frappe.get_doc({
+ "doctype": "Leave Allocation",
+ "__islocal": 1,
+ "employee": args.employee or employee.name,
+ "employee_name": args.employee_name or employee.employee_name,
+ "leave_type": args.leave_type or "_Test Leave Type",
+ "from_date": args.from_date or nowdate(),
+ "new_leaves_allocated": args.new_leaves_created or 15,
+ "carry_forward": args.carry_forward or 0,
+ "to_date": args.to_date or add_months(nowdate(), 12)
+ })
+ return leave_allocation
+
test_dependencies = ["Employee", "Leave Type"]
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js
index 5bce348..1746410 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.js
+++ b/erpnext/hr/doctype/leave_application/leave_application.js
@@ -49,7 +49,7 @@
async: false,
args: {
employee: frm.doc.employee,
- date: frm.doc.posting_date
+ date: frm.doc.from_date || frm.doc.posting_date
},
callback: function(r) {
if (!r.exc && r.message['leave_allocation']) {
@@ -60,9 +60,8 @@
}
}
});
-
$("div").remove(".form-dashboard-section");
- let section = frm.dashboard.add_section(
+ frm.dashboard.add_section(
frappe.render_template('leave_application_dashboard', {
data: leave_details
})
@@ -115,6 +114,7 @@
},
from_date: function(frm) {
+ frm.trigger("make_dashboard");
frm.trigger("half_day_datepicker");
frm.trigger("calculate_total_days");
},
@@ -138,12 +138,13 @@
},
get_leave_balance: function(frm) {
- if(frm.doc.docstatus==0 && frm.doc.employee && frm.doc.leave_type && frm.doc.from_date) {
+ if(frm.doc.docstatus==0 && frm.doc.employee && frm.doc.leave_type && frm.doc.from_date && frm.doc.to_date) {
return frappe.call({
method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_balance_on",
args: {
employee: frm.doc.employee,
date: frm.doc.from_date,
+ to_date: frm.doc.to_date,
leave_type: frm.doc.leave_type,
consider_all_leaves_in_the_allocation_period: true
},
diff --git a/erpnext/hr/doctype/leave_application/leave_application.json b/erpnext/hr/doctype/leave_application/leave_application.json
index b3800e0..cdb1add 100644
--- a/erpnext/hr/doctype/leave_application/leave_application.json
+++ b/erpnext/hr/doctype/leave_application/leave_application.json
@@ -1,332 +1,332 @@
{
- "allow_import": 1,
- "autoname": "naming_series:",
- "creation": "2013-02-20 11:18:11",
- "description": "Apply / Approve Leaves",
- "doctype": "DocType",
- "document_type": "Document",
- "engine": "InnoDB",
- "field_order": [
- "naming_series",
- "employee",
- "employee_name",
- "column_break_4",
- "leave_type",
- "department",
- "leave_balance",
- "section_break_5",
- "from_date",
- "to_date",
- "half_day",
- "half_day_date",
- "total_leave_days",
- "column_break1",
- "description",
- "section_break_7",
- "leave_approver",
- "leave_approver_name",
- "column_break_18",
- "status",
- "salary_slip",
- "sb10",
- "posting_date",
- "follow_via_email",
- "color",
- "column_break_17",
- "company",
- "letter_head",
- "amended_from"
- ],
- "fields": [
- {
- "fieldname": "naming_series",
- "fieldtype": "Select",
- "label": "Series",
- "no_copy": 1,
- "options": "HR-LAP-.YYYY.-",
- "print_hide": 1,
- "reqd": 1,
- "set_only_once": 1
- },
- {
- "fieldname": "employee",
- "fieldtype": "Link",
- "in_global_search": 1,
- "in_standard_filter": 1,
- "label": "Employee",
- "options": "Employee",
- "reqd": 1,
- "search_index": 1
- },
- {
- "fieldname": "employee_name",
- "fieldtype": "Data",
- "in_global_search": 1,
- "label": "Employee Name",
- "read_only": 1
- },
- {
- "fieldname": "column_break_4",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "leave_type",
- "fieldtype": "Link",
- "ignore_user_permissions": 1,
- "in_standard_filter": 1,
- "label": "Leave Type",
- "options": "Leave Type",
- "reqd": 1,
- "search_index": 1
- },
- {
- "fetch_from": "employee.department",
- "fieldname": "department",
- "fieldtype": "Link",
- "label": "Department",
- "options": "Department",
- "read_only": 1
- },
- {
- "fieldname": "leave_balance",
- "fieldtype": "Float",
- "label": "Leave Balance Before Application",
- "no_copy": 1,
- "read_only": 1
- },
- {
- "fieldname": "section_break_5",
- "fieldtype": "Section Break"
- },
- {
- "fieldname": "from_date",
- "fieldtype": "Date",
- "in_list_view": 1,
- "label": "From Date",
- "reqd": 1,
- "search_index": 1
- },
- {
- "fieldname": "to_date",
- "fieldtype": "Date",
- "label": "To Date",
- "reqd": 1,
- "search_index": 1
- },
- {
- "default": "0",
- "fieldname": "half_day",
- "fieldtype": "Check",
- "label": "Half Day"
- },
- {
- "depends_on": "eval:doc.half_day && (doc.from_date != doc.to_date)",
- "fieldname": "half_day_date",
- "fieldtype": "Date",
- "label": "Half Day Date"
- },
- {
- "fieldname": "total_leave_days",
- "fieldtype": "Float",
- "in_list_view": 1,
- "label": "Total Leave Days",
- "no_copy": 1,
- "precision": "1",
- "read_only": 1
- },
- {
- "fieldname": "column_break1",
- "fieldtype": "Column Break",
- "print_width": "50%",
- "width": "50%"
- },
- {
- "fieldname": "description",
- "fieldtype": "Small Text",
- "label": "Reason"
- },
- {
- "fieldname": "section_break_7",
- "fieldtype": "Section Break"
- },
- {
- "fieldname": "leave_approver",
- "fieldtype": "Link",
- "label": "Leave Approver",
- "options": "User"
- },
- {
- "fieldname": "leave_approver_name",
- "fieldtype": "Data",
- "label": "Leave Approver Name",
- "read_only": 1
- },
- {
- "fieldname": "column_break_18",
- "fieldtype": "Column Break"
- },
- {
- "default": "Open",
- "fieldname": "status",
- "fieldtype": "Select",
- "in_standard_filter": 1,
- "label": "Status",
- "no_copy": 1,
- "options": "Open\nApproved\nRejected\nCancelled"
- },
- {
- "fieldname": "salary_slip",
- "fieldtype": "Link",
- "label": "Salary Slip",
- "options": "Salary Slip",
- "print_hide": 1
- },
- {
- "fieldname": "sb10",
- "fieldtype": "Section Break"
- },
- {
- "default": "Today",
- "fieldname": "posting_date",
- "fieldtype": "Date",
- "label": "Posting Date",
- "no_copy": 1,
- "reqd": 1
- },
- {
- "allow_on_submit": 1,
- "default": "1",
- "fieldname": "follow_via_email",
- "fieldtype": "Check",
- "label": "Follow via Email",
- "print_hide": 1
- },
- {
- "allow_on_submit": 1,
- "fieldname": "color",
- "fieldtype": "Color",
- "label": "Color",
- "print_hide": 1
- },
- {
- "fieldname": "column_break_17",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "company",
- "fieldtype": "Link",
- "label": "Company",
- "options": "Company",
- "remember_last_selected_value": 1,
- "reqd": 1
- },
- {
- "allow_on_submit": 1,
- "fieldname": "letter_head",
- "fieldtype": "Link",
- "ignore_user_permissions": 1,
- "label": "Letter Head",
- "options": "Letter Head",
- "print_hide": 1
- },
- {
- "fieldname": "amended_from",
- "fieldtype": "Link",
- "ignore_user_permissions": 1,
- "label": "Amended From",
- "no_copy": 1,
- "options": "Leave Application",
- "print_hide": 1,
- "read_only": 1
- }
- ],
- "icon": "fa fa-calendar",
- "idx": 1,
- "is_submittable": 1,
- "max_attachments": 3,
- "modified": "2019-08-11 19:13:53.603011",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "Leave Application",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Employee",
- "share": 1,
- "write": 1
- },
- {
- "amend": 1,
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "HR Manager",
- "set_user_permissions": 1,
- "share": 1,
- "submit": 1,
- "write": 1
- },
- {
- "permlevel": 1,
- "read": 1,
- "role": "All"
- },
- {
- "amend": 1,
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "HR User",
- "set_user_permissions": 1,
- "share": 1,
- "submit": 1,
- "write": 1
- },
- {
- "amend": 1,
- "cancel": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Leave Approver",
- "share": 1,
- "submit": 1,
- "write": 1
- },
- {
- "permlevel": 1,
- "read": 1,
- "report": 1,
- "role": "HR User",
- "write": 1
- },
- {
- "permlevel": 1,
- "read": 1,
- "report": 1,
- "role": "Leave Approver",
- "write": 1
- }
- ],
- "search_fields": "employee,employee_name,leave_type,from_date,to_date,total_leave_days",
- "sort_field": "modified",
- "sort_order": "DESC",
- "timeline_field": "employee",
- "title_field": "employee_name"
-}
\ No newline at end of file
+ "allow_import": 1,
+ "autoname": "naming_series:",
+ "creation": "2013-02-20 11:18:11",
+ "description": "Apply / Approve Leaves",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "engine": "InnoDB",
+ "field_order": [
+ "naming_series",
+ "employee",
+ "employee_name",
+ "column_break_4",
+ "leave_type",
+ "department",
+ "leave_balance",
+ "section_break_5",
+ "from_date",
+ "to_date",
+ "half_day",
+ "half_day_date",
+ "total_leave_days",
+ "column_break1",
+ "description",
+ "section_break_7",
+ "leave_approver",
+ "leave_approver_name",
+ "column_break_18",
+ "status",
+ "salary_slip",
+ "sb10",
+ "posting_date",
+ "follow_via_email",
+ "color",
+ "column_break_17",
+ "company",
+ "letter_head",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Series",
+ "no_copy": 1,
+ "options": "HR-LAP-.YYYY.-",
+ "print_hide": 1,
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "employee",
+ "fieldtype": "Link",
+ "in_global_search": 1,
+ "in_standard_filter": 1,
+ "label": "Employee",
+ "options": "Employee",
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "fieldname": "employee_name",
+ "fieldtype": "Data",
+ "in_global_search": 1,
+ "label": "Employee Name",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "leave_type",
+ "fieldtype": "Link",
+ "ignore_user_permissions": 1,
+ "in_standard_filter": 1,
+ "label": "Leave Type",
+ "options": "Leave Type",
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "fetch_from": "employee.department",
+ "fieldname": "department",
+ "fieldtype": "Link",
+ "label": "Department",
+ "options": "Department",
+ "read_only": 1
+ },
+ {
+ "fieldname": "leave_balance",
+ "fieldtype": "Float",
+ "label": "Leave Balance Before Application",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "from_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "From Date",
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "fieldname": "to_date",
+ "fieldtype": "Date",
+ "label": "To Date",
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "half_day",
+ "fieldtype": "Check",
+ "label": "Half Day"
+ },
+ {
+ "depends_on": "eval:doc.half_day && (doc.from_date != doc.to_date)",
+ "fieldname": "half_day_date",
+ "fieldtype": "Date",
+ "label": "Half Day Date"
+ },
+ {
+ "fieldname": "total_leave_days",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Total Leave Days",
+ "no_copy": 1,
+ "precision": "1",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break1",
+ "fieldtype": "Column Break",
+ "print_width": "50%",
+ "width": "50%"
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "label": "Reason"
+ },
+ {
+ "fieldname": "section_break_7",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "leave_approver",
+ "fieldtype": "Link",
+ "label": "Leave Approver",
+ "options": "User"
+ },
+ {
+ "fieldname": "leave_approver_name",
+ "fieldtype": "Data",
+ "label": "Leave Approver Name",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_18",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "Open",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_standard_filter": 1,
+ "label": "Status",
+ "no_copy": 1,
+ "options": "Open\nApproved\nRejected\nCancelled"
+ },
+ {
+ "fieldname": "sb10",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "Today",
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "label": "Posting Date",
+ "no_copy": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "remember_last_selected_value": 1,
+ "reqd": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "default": "1",
+ "fieldname": "follow_via_email",
+ "fieldtype": "Check",
+ "label": "Follow via Email",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "column_break_17",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "salary_slip",
+ "fieldtype": "Link",
+ "label": "Salary Slip",
+ "options": "Salary Slip",
+ "print_hide": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "letter_head",
+ "fieldtype": "Link",
+ "ignore_user_permissions": 1,
+ "label": "Letter Head",
+ "options": "Letter Head",
+ "print_hide": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "color",
+ "fieldtype": "Color",
+ "label": "Color",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "ignore_user_permissions": 1,
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Leave Application",
+ "print_hide": 1,
+ "read_only": 1
+ }
+ ],
+ "icon": "fa fa-calendar",
+ "idx": 1,
+ "is_submittable": 1,
+ "max_attachments": 3,
+ "modified": "2019-08-13 13:32:04.860848",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Leave Application",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Employee",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "set_user_permissions": 1,
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "permlevel": 1,
+ "read": 1,
+ "role": "All"
+ },
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "set_user_permissions": 1,
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "amend": 1,
+ "cancel": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Leave Approver",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "permlevel": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "write": 1
+ },
+ {
+ "permlevel": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Leave Approver",
+ "write": 1
+ }
+ ],
+ "search_fields": "employee,employee_name,leave_type,from_date,to_date,total_leave_days",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "timeline_field": "employee",
+ "title_field": "employee_name"
+ }
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 1ef8ee0..0aa8849 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -5,11 +5,12 @@
import frappe
from frappe import _
from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, \
- comma_or, get_fullname, add_days, nowdate
+ comma_or, get_fullname, add_days, nowdate, get_datetime_str
from erpnext.hr.utils import set_employee_name, get_leave_period
from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import daterange
+from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry
class LeaveDayBlockedError(frappe.ValidationError): pass
class OverlapError(frappe.ValidationError): pass
@@ -50,6 +51,7 @@
# notify leave applier about approval
self.notify_employee()
+ self.create_leave_ledger_entry()
self.reload()
def on_cancel(self):
@@ -57,6 +59,7 @@
# notify leave applier about cancellation
self.notify_employee()
self.cancel_attendance()
+ self.create_leave_ledger_entry(submit=False)
def validate_applicable_after(self):
if self.leave_type:
@@ -193,9 +196,9 @@
frappe.throw(_("The day(s) on which you are applying for leave are holidays. You need not apply for leave."))
if not is_lwp(self.leave_type):
- self.leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date, docname=self.name,
+ self.leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date, self.to_date,
consider_all_leaves_in_the_allocation_period=True)
- if self.status != "Rejected" and self.leave_balance < self.total_leave_days:
+ if self.status != "Rejected" and (self.leave_balance < self.total_leave_days or not self.leave_balance):
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
frappe.msgprint(_("Note: There is not enough leave balance for Leave Type {0}")
.format(self.leave_type))
@@ -347,6 +350,54 @@
except frappe.OutgoingEmailError:
pass
+ def create_leave_ledger_entry(self, submit=True):
+ expiry_date = get_allocation_expiry(self.employee, self.leave_type,
+ self.to_date, self.from_date)
+
+ lwp = frappe.db.get_value("Leave Type", self.leave_type, "is_lwp")
+
+ if expiry_date:
+ self.create_ledger_entry_for_intermediate_allocation_expiry(expiry_date, submit, lwp)
+ else:
+ args = dict(
+ leaves=self.total_leave_days * -1,
+ from_date=self.from_date,
+ to_date=self.to_date,
+ is_lwp=lwp
+ )
+ create_leave_ledger_entry(self, args, submit)
+
+ def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, submit, lwp):
+ ''' splits leave application into two ledger entries to consider expiry of allocation '''
+ args = dict(
+ from_date=self.from_date,
+ to_date=expiry_date,
+ leaves=(date_diff(expiry_date, self.from_date) + 1) * -1,
+ is_lwp=lwp
+ )
+ create_leave_ledger_entry(self, args, submit)
+
+ if getdate(expiry_date) != getdate(self.to_date):
+ start_date = add_days(expiry_date, 1)
+ args.update(dict(
+ from_date=start_date,
+ to_date=self.to_date,
+ leaves=date_diff(self.to_date, expiry_date) * -1
+ ))
+ create_leave_ledger_entry(self, args, submit)
+
+def get_allocation_expiry(employee, leave_type, to_date, from_date):
+ ''' Returns expiry of carry forward allocation in leave ledger entry '''
+ expiry = frappe.get_all("Leave Ledger Entry",
+ filters={
+ 'employee': employee,
+ 'leave_type': leave_type,
+ 'is_carry_forward': 1,
+ 'transaction_type': 'Leave Allocation',
+ 'to_date': ['between', (from_date, to_date)]
+ },fields=['to_date'])
+ return expiry[0]['to_date'] if expiry else None
+
@frappe.whitelist()
def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day = None, half_day_date = None):
number_of_days = 0
@@ -364,14 +415,16 @@
@frappe.whitelist()
def get_leave_details(employee, date):
- allocation_records = get_leave_allocation_records(date, employee).get(employee, frappe._dict())
+ allocation_records = get_leave_allocation_records(employee, date)
leave_allocation = {}
for d in allocation_records:
allocation = allocation_records.get(d, frappe._dict())
- date = allocation.to_date
- leaves_taken = get_leaves_for_period(employee, d, allocation.from_date, date, status="Approved")
- leaves_pending = get_leaves_for_period(employee, d, allocation.from_date, date, status="Open")
- remaining_leaves = allocation.total_leaves_allocated - leaves_taken - leaves_pending
+ remaining_leaves = get_leave_balance_on(employee, d, date, to_date = allocation.to_date,
+ consider_all_leaves_in_the_allocation_period=True)
+ end_date = allocation.to_date
+ leaves_taken = get_leaves_for_period(employee, d, allocation.from_date, end_date) * -1
+ leaves_pending = get_pending_leaves_for_period(employee, d, allocation.from_date, end_date)
+
leave_allocation[d] = {
"total_leaves": allocation.total_leaves_allocated,
"leaves_taken": leaves_taken,
@@ -386,27 +439,131 @@
return ret
@frappe.whitelist()
-def get_leave_balance_on(employee, leave_type, date, allocation_records=None, docname=None,
- consider_all_leaves_in_the_allocation_period=False, consider_encashed_leaves=True):
+def get_leave_balance_on(employee, leave_type, date, to_date=nowdate(), consider_all_leaves_in_the_allocation_period=False):
+ '''
+ Returns leave balance till date
+ :param employee: employee name
+ :param leave_type: leave type
+ :param date: date to check balance on
+ :param to_date: future date to check for allocation expiry
+ :param consider_all_leaves_in_the_allocation_period: consider all leaves taken till the allocation end date
+ '''
- if allocation_records == None:
- allocation_records = get_leave_allocation_records(date, employee).get(employee, frappe._dict())
+ allocation_records = get_leave_allocation_records(employee, date, leave_type)
allocation = allocation_records.get(leave_type, frappe._dict())
- if consider_all_leaves_in_the_allocation_period:
- date = allocation.to_date
- leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, date, status="Approved", docname=docname)
- leaves_encashed = 0
- if frappe.db.get_value("Leave Type", leave_type, 'allow_encashment') and consider_encashed_leaves:
- leaves_encashed = flt(allocation.total_leaves_encashed)
- return flt(allocation.total_leaves_allocated) - (flt(leaves_taken) + flt(leaves_encashed))
+ end_date = allocation.to_date if consider_all_leaves_in_the_allocation_period else date
+ expiry = get_allocation_expiry(employee, leave_type, to_date, date)
-def get_leaves_for_period(employee, leave_type, from_date, to_date, status, docname=None):
- leave_applications = frappe.db.sql("""
- select name, employee, leave_type, from_date, to_date, total_leave_days
- from `tabLeave Application`
+ leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date)
+
+ return get_remaining_leaves(allocation, leaves_taken, date, expiry)
+
+def get_leave_allocation_records(employee, date, leave_type=None):
+ ''' returns the total allocated leaves and carry forwarded leaves based on ledger entries '''
+
+ conditions = ("and leave_type='%s'" % leave_type) if leave_type else ""
+ allocation_details = frappe.db.sql("""
+ SELECT
+ SUM(CASE WHEN is_carry_forward = 1 THEN leaves ELSE 0 END) as cf_leaves,
+ SUM(CASE WHEN is_carry_forward = 0 THEN leaves ELSE 0 END) as new_leaves,
+ MIN(from_date) as from_date,
+ MAX(to_date) as to_date,
+ leave_type
+ FROM `tabLeave Ledger Entry`
+ WHERE
+ from_date <= %(date)s
+ AND to_date >= %(date)s
+ AND docstatus=1
+ AND transaction_type="Leave Allocation"
+ AND employee=%(employee)s
+ AND is_expired=0
+ AND is_lwp=0
+ {0}
+ GROUP BY employee, leave_type
+ """.format(conditions), dict(date=date, employee=employee), as_dict=1) #nosec
+
+ allocated_leaves = frappe._dict()
+ for d in allocation_details:
+ allocated_leaves.setdefault(d.leave_type, frappe._dict({
+ "from_date": d.from_date,
+ "to_date": d.to_date,
+ "total_leaves_allocated": flt(d.cf_leaves) + flt(d.new_leaves),
+ "unused_leaves": d.cf_leaves,
+ "new_leaves_allocated": d.new_leaves,
+ "leave_type": d.leave_type
+ }))
+ return allocated_leaves
+
+def get_pending_leaves_for_period(employee, leave_type, from_date, to_date):
+ ''' Returns leaves that are pending approval '''
+ return frappe.db.get_value("Leave Application",
+ filters={
+ "employee": employee,
+ "leave_type": leave_type,
+ "from_date": ("<=", from_date),
+ "to_date": (">=", to_date),
+ "status": "Open"
+ }, fieldname=['SUM(total_leave_days)']) or flt(0)
+
+def get_remaining_leaves(allocation, leaves_taken, date, expiry):
+ ''' Returns minimum leaves remaining after comparing with remaining days for allocation expiry '''
+ def _get_remaining_leaves(allocated_leaves, end_date):
+ remaining_leaves = flt(allocated_leaves) + flt(leaves_taken)
+
+ if remaining_leaves > 0:
+ remaining_days = date_diff(end_date, date) + 1
+ remaining_leaves = min(remaining_days, remaining_leaves)
+
+ return remaining_leaves
+
+ total_leaves = allocation.total_leaves_allocated
+
+ if expiry and allocation.unused_leaves:
+ remaining_leaves = _get_remaining_leaves(allocation.unused_leaves, expiry)
+
+ total_leaves = flt(allocation.new_leaves_allocated) + flt(remaining_leaves)
+
+ return _get_remaining_leaves(total_leaves, allocation.to_date)
+
+def get_leaves_for_period(employee, leave_type, from_date, to_date):
+ leave_entries = get_leave_entries(employee, leave_type, from_date, to_date)
+ leave_days = 0
+
+ for leave_entry in leave_entries:
+ inclusive_period = leave_entry.from_date >= getdate(from_date) and leave_entry.to_date <= getdate(to_date)
+
+ if inclusive_period and leave_entry.transaction_type == 'Leave Encashment':
+ leave_days += leave_entry.leaves
+
+ elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' \
+ and not skip_expiry_leaves(leave_entry, to_date):
+ leave_days += leave_entry.leaves
+
+ else:
+ if leave_entry.from_date < getdate(from_date):
+ leave_entry.from_date = from_date
+ if leave_entry.to_date > getdate(to_date):
+ leave_entry.to_date = to_date
+
+ leave_days += get_number_of_leave_days(employee, leave_type,
+ leave_entry.from_date, leave_entry.to_date) * -1
+
+ return leave_days
+
+def skip_expiry_leaves(leave_entry, date):
+ ''' Checks whether the expired leaves coincide with the to_date of leave balance check '''
+ end_date = frappe.db.get_value("Leave Allocation", {'name': leave_entry.transaction_name}, ['to_date'])
+ return True if end_date == date and not leave_entry.is_carry_forward else False
+
+def get_leave_entries(employee, leave_type, from_date, to_date):
+ ''' Returns leave entries between from_date and to_date '''
+ return frappe.db.sql("""
+ select employee, leave_type, from_date, to_date, leaves, transaction_type, is_carry_forward
+ from `tabLeave Ledger Entry`
where employee=%(employee)s and leave_type=%(leave_type)s
- and status = %(status)s and docstatus != 2
+ and docstatus=1
+ and leaves<0
and (from_date between %(from_date)s and %(to_date)s
or to_date between %(from_date)s and %(to_date)s
or (from_date < %(from_date)s and to_date > %(to_date)s))
@@ -414,43 +571,8 @@
"from_date": from_date,
"to_date": to_date,
"employee": employee,
- "status": status,
"leave_type": leave_type
}, as_dict=1)
- leave_days = 0
- for leave_app in leave_applications:
- if docname and leave_app.name == docname:
- continue
- if leave_app.from_date >= getdate(from_date) and leave_app.to_date <= getdate(to_date):
- leave_days += leave_app.total_leave_days
- else:
- if leave_app.from_date < getdate(from_date):
- leave_app.from_date = from_date
- if leave_app.to_date > getdate(to_date):
- leave_app.to_date = to_date
-
- leave_days += get_number_of_leave_days(employee, leave_type,
- leave_app.from_date, leave_app.to_date)
-
- return leave_days
-
-def get_leave_allocation_records(date, employee=None):
- conditions = (" and employee='%s'" % employee) if employee else ""
-
- leave_allocation_records = frappe.db.sql("""
- select employee, leave_type, total_leaves_allocated, total_leaves_encashed, from_date, to_date
- from `tabLeave Allocation`
- where %s between from_date and to_date and docstatus=1 {0}""".format(conditions), (date), as_dict=1)
-
- allocated_leaves = frappe._dict()
- for d in leave_allocation_records:
- allocated_leaves.setdefault(d.employee, frappe._dict()).setdefault(d.leave_type, frappe._dict({
- "from_date": d.from_date,
- "to_date": d.to_date,
- "total_leaves_allocated": d.total_leaves_allocated,
- "total_leaves_encashed":d.total_leaves_encashed
- }))
- return allocated_leaves
@frappe.whitelist()
def get_holidays(employee, from_date, to_date):
@@ -629,4 +751,4 @@
if department:
return frappe.db.get_value('Department Approver', {'parent': department,
- 'parentfield': 'leave_approvers', 'idx': 1}, 'approver')
+ 'parentfield': 'leave_approvers', 'idx': 1}, 'approver')
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_application/leave_application_dashboard.py b/erpnext/hr/doctype/leave_application/leave_application_dashboard.py
new file mode 100644
index 0000000..8075b7b
--- /dev/null
+++ b/erpnext/hr/doctype/leave_application/leave_application_dashboard.py
@@ -0,0 +1,14 @@
+from __future__ import unicode_literals
+
+from frappe import _
+
+
+def get_data():
+ return {
+ 'reports': [
+ {
+ 'label': _('Reports'),
+ 'items': ['Employee Leave Balance']
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index d3dcca1..ad141a5 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -7,7 +7,9 @@
from erpnext.hr.doctype.leave_application.leave_application import LeaveDayBlockedError, OverlapError, NotAnOptionalHoliday, get_leave_balance_on
from frappe.permissions import clear_user_permissions_for_doctype
-from frappe.utils import add_days, nowdate, now_datetime, getdate
+from frappe.utils import add_days, nowdate, now_datetime, getdate, add_months
+from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
+from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
test_dependencies = ["Leave Allocation", "Leave Block List"]
@@ -17,6 +19,7 @@
"doctype": "Leave Application",
"employee": "_T-Employee-00001",
"from_date": "2013-05-01",
+ "description": "_Test Reason",
"leave_type": "_Test Leave Type",
"posting_date": "2013-01-02",
"to_date": "2013-05-05"
@@ -26,6 +29,7 @@
"doctype": "Leave Application",
"employee": "_T-Employee-00002",
"from_date": "2013-05-01",
+ "description": "_Test Reason",
"leave_type": "_Test Leave Type",
"posting_date": "2013-01-02",
"to_date": "2013-05-05"
@@ -35,6 +39,7 @@
"doctype": "Leave Application",
"employee": "_T-Employee-00001",
"from_date": "2013-01-15",
+ "description": "_Test Reason",
"leave_type": "_Test Leave Type LWP",
"posting_date": "2013-01-02",
"to_date": "2013-01-15"
@@ -44,8 +49,8 @@
class TestLeaveApplication(unittest.TestCase):
def setUp(self):
- for dt in ["Leave Application", "Leave Allocation", "Salary Slip"]:
- frappe.db.sql("delete from `tab%s`" % dt)
+ for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]:
+ frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec
@classmethod
def setUpClass(cls):
@@ -268,13 +273,14 @@
doctype = 'Leave Application',
employee = employee.name,
company = '_Test Company',
+ description = "_Test Reason",
leave_type = leave_type,
from_date = date,
to_date = date,
))
# can only apply on optional holidays
- self.assertTrue(NotAnOptionalHoliday, leave_application.insert)
+ self.assertRaises(NotAnOptionalHoliday, leave_application.insert)
leave_application.from_date = today
leave_application.to_date = today
@@ -285,7 +291,6 @@
# check leave balance is reduced
self.assertEqual(get_leave_balance_on(employee.name, leave_type, today), 9)
-
def test_leaves_allowed(self):
employee = get_employee()
leave_period = get_leave_period()
@@ -301,24 +306,25 @@
allocate_leaves(employee, leave_period, leave_type.name, 5)
leave_application = frappe.get_doc(dict(
- doctype = 'Leave Application',
+ doctype = 'Leave Application',
employee = employee.name,
leave_type = leave_type.name,
+ description = "_Test Reason",
from_date = date,
to_date = add_days(date, 2),
company = "_Test Company",
docstatus = 1,
status = "Approved"
))
-
- self.assertTrue(leave_application.insert())
+ leave_application.submit()
leave_application = frappe.get_doc(dict(
doctype = 'Leave Application',
employee = employee.name,
leave_type = leave_type.name,
+ description = "_Test Reason",
from_date = add_days(date, 4),
- to_date = add_days(date, 7),
+ to_date = add_days(date, 8),
company = "_Test Company",
docstatus = 1,
status = "Approved"
@@ -342,6 +348,7 @@
doctype = 'Leave Application',
employee = employee.name,
leave_type = leave_type.name,
+ description = "_Test Reason",
from_date = date,
to_date = add_days(date, 4),
company = "_Test Company",
@@ -363,6 +370,7 @@
doctype = 'Leave Application',
employee = employee.name,
leave_type = leave_type_1.name,
+ description = "_Test Reason",
from_date = date,
to_date = add_days(date, 4),
company = "_Test Company",
@@ -392,6 +400,7 @@
doctype = 'Leave Application',
employee = employee.name,
leave_type = leave_type.name,
+ description = "_Test Reason",
from_date = date,
to_date = add_days(date, 4),
company = "_Test Company",
@@ -401,6 +410,18 @@
self.assertRaises(frappe.ValidationError, leave_application.insert)
+ def test_leave_balance_near_allocaton_expiry(self):
+ employee = get_employee()
+ leave_type = create_leave_type(
+ leave_type_name="_Test_CF_leave_expiry",
+ is_carry_forward=1,
+ expire_carry_forwarded_leaves_after_days=90)
+ leave_type.submit()
+
+ create_carry_forwarded_allocation(employee, leave_type)
+
+ self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8)), 21)
+
def test_earned_leave(self):
leave_period = get_leave_period()
employee = get_employee()
@@ -444,9 +465,10 @@
allocation.insert(ignore_permissions=True)
allocation.submit()
leave_application = frappe.get_doc(dict(
- doctype = 'Leave Application',
+ doctype = 'Leave Application',
employee = employee.name,
leave_type = leave_type,
+ description = "_Test Reason",
from_date = '2018-10-02',
to_date = '2018-10-02',
company = '_Test Company',
@@ -457,9 +479,103 @@
leave_application.submit()
self.assertEqual(leave_application.docstatus, 1)
-def make_allocation_record(employee=None, leave_type=None):
- frappe.db.sql("delete from `tabLeave Allocation`")
+ def test_creation_of_leave_ledger_entry_on_submit(self):
+ employee = get_employee()
+ leave_type = create_leave_type(leave_type_name = 'Test Leave Type 1')
+ leave_type.save()
+
+ leave_allocation = create_leave_allocation(employee=employee.name, employee_name=employee.employee_name,
+ leave_type=leave_type.name)
+ leave_allocation.submit()
+
+ leave_application = frappe.get_doc(dict(
+ doctype = 'Leave Application',
+ employee = employee.name,
+ leave_type = leave_type.name,
+ from_date = add_days(nowdate(), 1),
+ to_date = add_days(nowdate(), 4),
+ description = "_Test Reason",
+ company = "_Test Company",
+ docstatus = 1,
+ status = "Approved"
+ ))
+ leave_application.submit()
+ leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_application.name))
+
+ self.assertEquals(leave_ledger_entry[0].employee, leave_application.employee)
+ self.assertEquals(leave_ledger_entry[0].leave_type, leave_application.leave_type)
+ self.assertEquals(leave_ledger_entry[0].leaves, leave_application.total_leave_days * -1)
+
+ # check if leave ledger entry is deleted on cancellation
+ leave_application.cancel()
+ self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_application.name}))
+
+ def test_ledger_entry_creation_on_intermediate_allocation_expiry(self):
+ employee = get_employee()
+ leave_type = create_leave_type(
+ leave_type_name="_Test_CF_leave_expiry",
+ is_carry_forward=1,
+ expire_carry_forwarded_leaves_after_days=90)
+ leave_type.submit()
+
+ create_carry_forwarded_allocation(employee, leave_type)
+
+ leave_application = frappe.get_doc(dict(
+ doctype = 'Leave Application',
+ employee = employee.name,
+ leave_type = leave_type.name,
+ from_date = add_days(nowdate(), -3),
+ to_date = add_days(nowdate(), 7),
+ description = "_Test Reason",
+ company = "_Test Company",
+ docstatus = 1,
+ status = "Approved"
+ ))
+ leave_application.submit()
+
+ leave_ledger_entry = frappe.get_all('Leave Ledger Entry', '*', filters=dict(transaction_name=leave_application.name))
+
+ self.assertEquals(len(leave_ledger_entry), 2)
+ self.assertEquals(leave_ledger_entry[0].employee, leave_application.employee)
+ self.assertEquals(leave_ledger_entry[0].leave_type, leave_application.leave_type)
+ self.assertEquals(leave_ledger_entry[0].leaves, -9)
+ self.assertEquals(leave_ledger_entry[1].leaves, -2)
+
+ def test_leave_application_creation_after_expiry(self):
+ # test leave balance for carry forwarded allocation
+ employee = get_employee()
+ leave_type = create_leave_type(
+ leave_type_name="_Test_CF_leave_expiry",
+ is_carry_forward=1,
+ expire_carry_forwarded_leaves_after_days=90)
+ leave_type.submit()
+
+ create_carry_forwarded_allocation(employee, leave_type)
+
+ self.assertEquals(get_leave_balance_on(employee.name, leave_type.name, add_days(nowdate(), -85), add_days(nowdate(), -84)), 0)
+
+def create_carry_forwarded_allocation(employee, leave_type):
+ # initial leave allocation
+ leave_allocation = create_leave_allocation(
+ leave_type="_Test_CF_leave_expiry",
+ employee=employee.name,
+ employee_name=employee.employee_name,
+ from_date=add_months(nowdate(), -24),
+ to_date=add_months(nowdate(), -12),
+ carry_forward=0)
+ leave_allocation.submit()
+
+ leave_allocation = create_leave_allocation(
+ leave_type="_Test_CF_leave_expiry",
+ employee=employee.name,
+ employee_name=employee.employee_name,
+ from_date=add_days(nowdate(), -84),
+ to_date=add_days(nowdate(), 100),
+ carry_forward=1)
+ leave_allocation.submit()
+
+def make_allocation_record(employee=None, leave_type=None):
allocation = frappe.get_doc({
"doctype": "Leave Allocation",
"employee": employee or "_T-Employee-00001",
@@ -513,4 +629,4 @@
"docstatus": 1
}).insert()
- allocate_leave.submit()
+ allocate_leave.submit()
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py
index 9944bc5..42f0179 100644
--- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py
+++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py
@@ -10,6 +10,8 @@
from erpnext.hr.utils import set_employee_name
from erpnext.hr.doctype.leave_application.leave_application import get_leave_balance_on
from erpnext.hr.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure
+from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry
+from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves
class LeaveEncashment(Document):
def validate(self):
@@ -25,7 +27,7 @@
def on_submit(self):
if not self.leave_allocation:
- self.leave_allocation = self.get_leave_allocation()
+ self.leave_allocation = self.get_leave_allocation().get('name')
additional_salary = frappe.new_doc("Additional Salary")
additional_salary.company = frappe.get_value("Employee", self.employee, "company")
additional_salary.employee = self.employee
@@ -40,6 +42,8 @@
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)
+ self.create_leave_ledger_entry()
+
def on_cancel(self):
if self.additional_salary:
frappe.get_doc("Additional Salary", self.additional_salary).cancel()
@@ -48,6 +52,7 @@
if self.leave_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)
+ self.create_leave_ledger_entry(submit=False)
def get_leave_details_for_encashment(self):
salary_structure = get_assigned_salary_structure(self.employee, self.encashment_date or getdate(nowdate()))
@@ -57,8 +62,10 @@
if not frappe.db.get_value("Leave Type", self.leave_type, 'allow_encashment'):
frappe.throw(_("Leave Type {0} is not encashable").format(self.leave_type))
- self.leave_balance = get_leave_balance_on(self.employee, self.leave_type,
- self.encashment_date or getdate(nowdate()), consider_all_leaves_in_the_allocation_period=True)
+ allocation = self.get_leave_allocation()
+
+ self.leave_balance = allocation.total_leaves_allocated - allocation.carry_forwarded_leaves_count\
+ - get_unused_leaves(self.employee, self.leave_type, allocation.from_date, self.encashment_date)
encashable_days = self.leave_balance - frappe.db.get_value('Leave Type', self.leave_type, 'encashment_threshold_days')
self.encashable_days = encashable_days if encashable_days > 0 else 0
@@ -66,12 +73,47 @@
per_day_encashment = frappe.db.get_value('Salary Structure', salary_structure , 'leave_encashment_amount_per_day')
self.encashment_amount = self.encashable_days * per_day_encashment if per_day_encashment > 0 else 0
- self.leave_allocation = self.get_leave_allocation()
+ self.leave_allocation = allocation.name
return True
def get_leave_allocation(self):
- leave_allocation = frappe.db.sql("""select name from `tabLeave Allocation` where '{0}'
+ leave_allocation = frappe.db.sql("""select name, to_date, total_leaves_allocated, carry_forwarded_leaves_count from `tabLeave Allocation` where '{0}'
between from_date and to_date and docstatus=1 and leave_type='{1}'
- and employee= '{2}'""".format(self.encashment_date or getdate(nowdate()), self.leave_type, self.employee))
+ and employee= '{2}'""".format(self.encashment_date or getdate(nowdate()), self.leave_type, self.employee), as_dict=1) #nosec
- return leave_allocation[0][0] if leave_allocation else None
+ return leave_allocation[0] if leave_allocation else None
+
+ def create_leave_ledger_entry(self, submit=True):
+ args = frappe._dict(
+ leaves=self.encashable_days * -1,
+ from_date=self.encashment_date,
+ to_date=self.encashment_date,
+ is_carry_forward=0
+ )
+ create_leave_ledger_entry(self, args, submit)
+
+ # create reverse entry for expired leaves
+ to_date = self.get_leave_allocation().get('to_date')
+ if to_date < getdate(nowdate()):
+ args = frappe._dict(
+ leaves=self.encashable_days,
+ from_date=to_date,
+ to_date=to_date,
+ is_carry_forward=0
+ )
+ create_leave_ledger_entry(self, args, submit)
+
+
+def create_leave_encashment(leave_allocation):
+ ''' Creates leave encashment for the given allocations '''
+ for allocation in leave_allocation:
+ if not get_assigned_salary_structure(allocation.employee, allocation.to_date):
+ continue
+ leave_encashment = frappe.get_doc(dict(
+ doctype="Leave Encashment",
+ leave_period=allocation.leave_period,
+ employee=allocation.employee,
+ leave_type=allocation.leave_type,
+ encashment_date=allocation.to_date
+ ))
+ 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 ef5c2aa..e5bd170 100644
--- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py
+++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py
@@ -9,42 +9,43 @@
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.salary_structure.test_salary_structure import make_salary_structure
from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period
+from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy\
test_dependencies = ["Leave Type"]
class TestLeaveEncashment(unittest.TestCase):
def setUp(self):
frappe.db.sql('''delete from `tabLeave Period`''')
- def test_leave_balance_value_and_amount(self):
- employee = "test_employee_encashment@salary.com"
- leave_type = "_Test Leave Type Encashment"
+ frappe.db.sql('''delete from `tabLeave Allocation`''')
+ frappe.db.sql('''delete from `tabLeave Ledger Entry`''')
+ frappe.db.sql('''delete from `tabAdditional Salary`''')
# create the leave policy
- leave_policy = frappe.get_doc({
- "doctype": "Leave Policy",
- "leave_policy_details": [{
- "leave_type": leave_type,
- "annual_allocation": 10
- }]
- }).insert()
+ leave_policy = create_leave_policy(
+ leave_type="_Test Leave Type Encashment",
+ annual_allocation=10)
leave_policy.submit()
# create employee, salary structure and assignment
- employee = make_employee(employee)
- frappe.db.set_value("Employee", employee, "leave_policy", leave_policy.name)
- salary_structure = make_salary_structure("Salary Structure for Encashment", "Monthly", employee,
+ self.employee = make_employee("test_employee_encashment@example.com")
+
+ frappe.db.set_value("Employee", self.employee, "leave_policy", leave_policy.name)
+
+ salary_structure = make_salary_structure("Salary Structure for Encashment", "Monthly", self.employee,
other_details={"leave_encashment_amount_per_day": 50})
# create the leave period and assign the leaves
- leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
- leave_period.grant_leave_allocation(employee=employee)
+ self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
+ self.leave_period.grant_leave_allocation(employee=self.employee)
+ def test_leave_balance_value_and_amount(self):
+ frappe.db.sql('''delete from `tabLeave Encashment`''')
leave_encashment = frappe.get_doc(dict(
- doctype = 'Leave Encashment',
- employee = employee,
- leave_type = leave_type,
- leave_period = leave_period.name,
- payroll_date = today()
+ doctype='Leave Encashment',
+ employee=self.employee,
+ leave_type="_Test Leave Type Encashment",
+ leave_period=self.leave_period.name,
+ payroll_date=today()
)).insert()
self.assertEqual(leave_encashment.leave_balance, 10)
@@ -53,3 +54,26 @@
leave_encashment.submit()
self.assertTrue(frappe.db.get_value("Leave Encashment", leave_encashment.name, "additional_salary"))
+
+ def test_creation_of_leave_ledger_entry_on_submit(self):
+ frappe.db.sql('''delete from `tabLeave Encashment`''')
+ leave_encashment = frappe.get_doc(dict(
+ doctype='Leave Encashment',
+ employee=self.employee,
+ leave_type="_Test Leave Type Encashment",
+ leave_period=self.leave_period.name,
+ payroll_date=today()
+ )).insert()
+
+ leave_encashment.submit()
+
+ leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_encashment.name))
+
+ self.assertEquals(len(leave_ledger_entry), 1)
+ self.assertEquals(leave_ledger_entry[0].employee, leave_encashment.employee)
+ self.assertEquals(leave_ledger_entry[0].leave_type, leave_encashment.leave_type)
+ self.assertEquals(leave_ledger_entry[0].leaves, leave_encashment.encashable_days * -1)
+
+ # check if leave ledger entry is deleted on cancellation
+ leave_encashment.cancel()
+ self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_encashment.name}))
diff --git a/erpnext/hr/doctype/leave_ledger_entry/__init__.py b/erpnext/hr/doctype/leave_ledger_entry/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/leave_ledger_entry/__init__.py
diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.js b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.js
new file mode 100644
index 0000000..c68d518
--- /dev/null
+++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Leave Ledger Entry', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json
new file mode 100644
index 0000000..c114222
--- /dev/null
+++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json
@@ -0,0 +1,168 @@
+{
+ "creation": "2019-05-09 15:47:39.760406",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "employee",
+ "employee_name",
+ "leave_type",
+ "transaction_type",
+ "transaction_name",
+ "leaves",
+ "column_break_7",
+ "from_date",
+ "to_date",
+ "is_carry_forward",
+ "is_expired",
+ "is_lwp",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "employee",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Employee",
+ "options": "Employee"
+ },
+ {
+ "fieldname": "employee_name",
+ "fieldtype": "Data",
+ "label": "Employee Name"
+ },
+ {
+ "fieldname": "leave_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Leave Type",
+ "options": "Leave Type"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Leave Ledger Entry",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "transaction_type",
+ "fieldtype": "Link",
+ "label": "Transaction Type",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "transaction_name",
+ "fieldtype": "Dynamic Link",
+ "label": "Transaction Name",
+ "options": "transaction_type"
+ },
+ {
+ "fieldname": "leaves",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Leaves"
+ },
+ {
+ "fieldname": "from_date",
+ "fieldtype": "Date",
+ "label": "From Date"
+ },
+ {
+ "fieldname": "to_date",
+ "fieldtype": "Date",
+ "label": "To Date"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_carry_forward",
+ "fieldtype": "Check",
+ "label": "Is Carry Forward"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_expired",
+ "fieldtype": "Check",
+ "label": "Is Expired"
+ },
+ {
+ "fieldname": "column_break_7",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_lwp",
+ "fieldtype": "Check",
+ "label": "Is Leave Without Pay"
+ }
+ ],
+ "in_create": 1,
+ "is_submittable": 1,
+ "modified": "2019-06-21 00:37:07.782810",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Leave Ledger Entry",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "if_owner": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "All",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "ASC",
+ "title_field": "employee"
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py
new file mode 100644
index 0000000..c82114e
--- /dev/null
+++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.model.document import Document
+from frappe import _
+from frappe.utils import add_days, today, flt, DATE_FORMAT, getdate
+
+class LeaveLedgerEntry(Document):
+ def validate(self):
+ if getdate(self.from_date) > getdate(self.to_date):
+ frappe.throw(_("To date needs to be before from date"))
+
+ def on_cancel(self):
+ # allow cancellation of expiry leaves
+ if self.is_expired:
+ frappe.db.set_value("Leave Allocation", self.transaction_name, "expired", 0)
+ else:
+ frappe.throw(_("Only expired allocation can be cancelled"))
+
+def validate_leave_allocation_against_leave_application(ledger):
+ ''' Checks that leave allocation has no leave application against it '''
+ leave_application_records = frappe.db.sql_list("""
+ SELECT transaction_name
+ FROM `tabLeave Ledger Entry`
+ WHERE
+ employee=%s
+ AND leave_type=%s
+ AND transaction_type='Leave Application'
+ AND from_date>=%s
+ AND to_date<=%s
+ """, (ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date))
+
+ if leave_application_records:
+ frappe.throw(_("Leave allocation %s is linked with leave application %s"
+ % (ledger.transaction_name, ', '.join(leave_application_records))))
+
+def create_leave_ledger_entry(ref_doc, args, submit=True):
+ ledger = frappe._dict(
+ doctype='Leave Ledger Entry',
+ employee=ref_doc.employee,
+ employee_name=ref_doc.employee_name,
+ leave_type=ref_doc.leave_type,
+ transaction_type=ref_doc.doctype,
+ transaction_name=ref_doc.name,
+ is_carry_forward=0,
+ is_expired=0,
+ is_lwp=0
+ )
+ ledger.update(args)
+
+ if submit:
+ frappe.get_doc(ledger).submit()
+ else:
+ delete_ledger_entry(ledger)
+
+def delete_ledger_entry(ledger):
+ ''' Delete ledger entry on cancel of leave application/allocation/encashment '''
+ if ledger.transaction_type == "Leave Allocation":
+ validate_leave_allocation_against_leave_application(ledger)
+
+ expired_entry = get_previous_expiry_ledger_entry(ledger)
+ frappe.db.sql("""DELETE
+ FROM `tabLeave Ledger Entry`
+ WHERE
+ `transaction_name`=%s
+ OR `name`=%s""", (ledger.transaction_name, expired_entry))
+
+def get_previous_expiry_ledger_entry(ledger):
+ ''' Returns the expiry ledger entry having same creation date as the ledger entry to be cancelled '''
+ creation_date = frappe.db.get_value("Leave Ledger Entry", filters={
+ 'transaction_name': ledger.transaction_name,
+ 'is_expired': 0,
+ 'transaction_type': 'Leave Allocation'
+ }, fieldname=['creation'])
+
+ creation_date = creation_date.strftime(DATE_FORMAT) if creation_date else ''
+
+ return frappe.db.get_value("Leave Ledger Entry", filters={
+ 'creation': ('like', creation_date+"%"),
+ 'employee': ledger.employee,
+ 'leave_type': ledger.leave_type,
+ 'is_expired': 1,
+ 'docstatus': 1,
+ 'is_carry_forward': 0
+ }, fieldname=['name'])
+
+def process_expired_allocation():
+ ''' Check if a carry forwarded allocation has expired and create a expiry ledger entry '''
+
+ # fetch leave type records that has carry forwarded leaves expiry
+ leave_type_records = frappe.db.get_values("Leave Type", filters={
+ 'expire_carry_forwarded_leaves_after_days': (">", 0)
+ }, fieldname=['name'])
+
+ if leave_type_records:
+ leave_type = [record[0] for record in leave_type_records]
+
+ expired_allocation = frappe.db.sql_list("""SELECT name
+ FROM `tabLeave Ledger Entry`
+ WHERE
+ `transaction_type`='Leave Allocation'
+ AND `is_expired`=1""")
+
+ expire_allocation = frappe.get_all("Leave Ledger Entry",
+ fields=['leaves', 'to_date', 'employee', 'leave_type', 'is_carry_forward', 'transaction_name as name', 'transaction_type'],
+ filters={
+ 'to_date': ("<", today()),
+ 'transaction_type': 'Leave Allocation',
+ 'transaction_name': ('not in', expired_allocation)
+ },
+ or_filters={
+ 'is_carry_forward': 0,
+ 'leave_type': ('in', leave_type)
+ })
+
+ if expire_allocation:
+ create_expiry_ledger_entry(expire_allocation)
+
+def create_expiry_ledger_entry(allocations):
+ ''' Create ledger entry for expired allocation '''
+ for allocation in allocations:
+ if allocation.is_carry_forward:
+ expire_carried_forward_allocation(allocation)
+ else:
+ expire_allocation(allocation)
+
+def get_remaining_leaves(allocation):
+ ''' Returns remaining leaves from the given allocation '''
+ return frappe.db.get_value("Leave Ledger Entry",
+ filters={
+ 'employee': allocation.employee,
+ 'leave_type': allocation.leave_type,
+ 'to_date': ('<=', allocation.to_date),
+ }, fieldname=['SUM(leaves)'])
+
+@frappe.whitelist()
+def expire_allocation(allocation, expiry_date=None):
+ ''' expires non-carry forwarded allocation '''
+ leaves = get_remaining_leaves(allocation)
+ expiry_date = expiry_date if expiry_date else allocation.to_date
+
+ if leaves:
+ args = dict(
+ leaves=flt(leaves) * -1,
+ transaction_name=allocation.name,
+ transaction_type='Leave Allocation',
+ from_date=expiry_date,
+ to_date=expiry_date,
+ is_carry_forward=0,
+ is_expired=1
+ )
+ create_leave_ledger_entry(allocation, args)
+
+ frappe.db.set_value("Leave Allocation", allocation.name, "expired", 1)
+
+def expire_carried_forward_allocation(allocation):
+ ''' Expires remaining leaves in the on carried forward allocation '''
+ from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period
+ leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type, allocation.from_date, allocation.to_date)
+ leaves = flt(allocation.leaves) + flt(leaves_taken)
+ if leaves > 0:
+ args = frappe._dict(
+ transaction_name=allocation.name,
+ transaction_type="Leave Allocation",
+ leaves=allocation.leaves * -1,
+ is_carry_forward=allocation.is_carry_forward,
+ is_expired=1,
+ from_date=allocation.to_date,
+ to_date=allocation.to_date
+ )
+ create_leave_ledger_entry(allocation, args)
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_ledger_entry/test_leave_ledger_entry.py b/erpnext/hr/doctype/leave_ledger_entry/test_leave_ledger_entry.py
new file mode 100644
index 0000000..6f7725c
--- /dev/null
+++ b/erpnext/hr/doctype/leave_ledger_entry/test_leave_ledger_entry.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestLeaveLedgerEntry(unittest.TestCase):
+ pass
diff --git a/erpnext/hr/doctype/leave_period/leave_period.js b/erpnext/hr/doctype/leave_period/leave_period.js
index b8c5f71..bad2b87 100644
--- a/erpnext/hr/doctype/leave_period/leave_period.js
+++ b/erpnext/hr/doctype/leave_period/leave_period.js
@@ -68,7 +68,7 @@
},
{
"label": "Add unused leaves from previous allocations",
- "fieldname": "carry_forward_leaves",
+ "fieldname": "carry_forward",
"fieldtype": "Check"
}
],
diff --git a/erpnext/hr/doctype/leave_period/leave_period.json b/erpnext/hr/doctype/leave_period/leave_period.json
index df28763..9e895c3 100644
--- a/erpnext/hr/doctype/leave_period/leave_period.json
+++ b/erpnext/hr/doctype/leave_period/leave_period.json
@@ -1,294 +1,294 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "HR-LPR-.YYYY.-.#####",
- "beta": 0,
- "creation": "2018-04-13 15:20:52.864288",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "allow_copy": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 1,
+ "allow_rename": 1,
+ "autoname": "HR-LPR-.YYYY.-.#####",
+ "beta": 0,
+ "creation": "2018-04-13 15:20:52.864288",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "",
+ "editable_grid": 1,
+ "engine": "InnoDB",
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "from_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "From Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "from_date",
+ "fieldtype": "Date",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "From Date",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
"unique": 0
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "to_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "To Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "to_date",
+ "fieldtype": "Date",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "To Date",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
"unique": 0
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "is_active",
+ "fieldtype": "Check",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Is Active",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
"unique": 0
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "company",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Company",
- "length": 0,
- "no_copy": 0,
- "options": "Company",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
"unique": 0
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "is_active",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Is Active",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Company",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Company",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
"unique": 0
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "optional_holiday_list",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Holiday List for Optional Leave",
- "length": 0,
- "no_copy": 0,
- "options": "Holiday List",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "optional_holiday_list",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Holiday List for Optional Leave",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Holiday List",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
"unique": 0
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-08-21 16:15:43.305502",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "Leave Period",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "has_web_view": 0,
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "idx": 0,
+ "image_view": 0,
+ "in_create": 0,
+ "is_submittable": 0,
+ "issingle": 0,
+ "istable": 0,
+ "max_attachments": 0,
+ "modified": "2019-05-30 16:15:43.305502",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Leave Period",
+ "name_case": "",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "amend": 0,
+ "cancel": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 0,
"write": 1
- },
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "HR Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "amend": 0,
+ "cancel": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 0,
"write": 1
- },
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "HR User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "amend": 0,
+ "cancel": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 0,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
+ ],
+ "quick_entry": 0,
+ "read_only": 0,
+ "read_only_onload": 0,
+ "show_name_in_global_search": 0,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1,
+ "track_seen": 0,
"track_views": 0
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_period/leave_period.py b/erpnext/hr/doctype/leave_period/leave_period.py
index 15fa8d6..a8566c4 100644
--- a/erpnext/hr/doctype/leave_period/leave_period.py
+++ b/erpnext/hr/doctype/leave_period/leave_period.py
@@ -5,9 +5,10 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-from frappe.utils import getdate, cstr
+from frappe.utils import getdate, cstr, add_days, date_diff, getdate, ceil
from frappe.model.document import Document
from erpnext.hr.utils import validate_overlap, get_employee_leave_policy
+from erpnext.hr.doctype.leave_allocation.leave_allocation import get_carry_forwarded_leaves
from frappe.utils.background_jobs import enqueue
from six import iteritems
@@ -21,8 +22,8 @@
condition_str = " and " + " and ".join(conditions) if len(conditions) else ""
- employees = frappe.db.sql_list("select name from tabEmployee where status='Active' {condition}"
- .format(condition=condition_str), tuple(values))
+ employees = frappe._dict(frappe.db.sql("select name, date_of_joining from tabEmployee where status='Active' {condition}" #nosec
+ .format(condition=condition_str), tuple(values)))
return employees
@@ -36,29 +37,29 @@
def grant_leave_allocation(self, grade=None, department=None, designation=None,
- employee=None, carry_forward_leaves=0):
- employees = self.get_employees({
+ employee=None, carry_forward=0):
+ employee_records = self.get_employees({
"grade": grade,
- "department": department,
- "designation": designation,
+ "department": department,
+ "designation": designation,
"name": employee
})
- if employees:
- if len(employees) > 20:
+ if employee_records:
+ if len(employee_records) > 20:
frappe.enqueue(grant_leave_alloc_for_employees, timeout=600,
- employees=employees, leave_period=self, carry_forward_leaves=carry_forward_leaves)
+ employee_records=employee_records, leave_period=self, carry_forward=carry_forward)
else:
- grant_leave_alloc_for_employees(employees, self, carry_forward_leaves)
+ grant_leave_alloc_for_employees(employee_records, self, carry_forward)
else:
frappe.msgprint(_("No Employee Found"))
-def grant_leave_alloc_for_employees(employees, leave_period, carry_forward_leaves=0):
+def grant_leave_alloc_for_employees(employee_records, leave_period, carry_forward=0):
leave_allocations = []
- existing_allocations_for = get_existing_allocations(employees, leave_period.name)
+ existing_allocations_for = get_existing_allocations(list(employee_records.keys()), leave_period.name)
leave_type_details = get_leave_type_details()
- count=0
- for employee in employees:
+ count = 0
+ for employee in employee_records.keys():
if employee in existing_allocations_for:
continue
count +=1
@@ -67,18 +68,24 @@
for leave_policy_detail in leave_policy.leave_policy_details:
if not leave_type_details.get(leave_policy_detail.leave_type).is_lwp:
leave_allocation = create_leave_allocation(employee, leave_policy_detail.leave_type,
- leave_policy_detail.annual_allocation, leave_type_details, leave_period, carry_forward_leaves)
+ leave_policy_detail.annual_allocation, leave_type_details, leave_period, carry_forward, employee_records.get(employee))
leave_allocations.append(leave_allocation)
frappe.db.commit()
- frappe.publish_progress(count*100/len(set(employees) - set(existing_allocations_for)), title = _("Allocating leaves..."))
+ frappe.publish_progress(count*100/len(set(employee_records.keys()) - set(existing_allocations_for)), title = _("Allocating leaves..."))
if leave_allocations:
frappe.msgprint(_("Leaves has been granted sucessfully"))
def get_existing_allocations(employees, leave_period):
leave_allocations = frappe.db.sql_list("""
- select distinct employee from `tabLeave Allocation`
- where leave_period=%s and employee in (%s) and docstatus=1
+ SELECT DISTINCT
+ employee
+ FROM `tabLeave Allocation`
+ WHERE
+ leave_period=%s
+ AND employee in (%s)
+ AND carry_forward=0
+ AND docstatus=1
""" % ('%s', ', '.join(['%s']*len(employees))), [leave_period] + employees)
if leave_allocations:
frappe.msgprint(_("Skipping Leave Allocation for the following employees, as Leave Allocation records already exists against them. {0}")
@@ -87,28 +94,36 @@
def get_leave_type_details():
leave_type_details = frappe._dict()
- leave_types = frappe.get_all("Leave Type", fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "is_carry_forward"])
+ leave_types = frappe.get_all("Leave Type",
+ fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "is_carry_forward", "expire_carry_forwarded_leaves_after_days"])
for d in leave_types:
leave_type_details.setdefault(d.name, d)
return leave_type_details
-def create_leave_allocation(employee, leave_type, new_leaves_allocated, leave_type_details, leave_period, carry_forward_leaves):
- allocation = frappe.new_doc("Leave Allocation")
- allocation.employee = employee
- allocation.leave_type = leave_type
- allocation.from_date = leave_period.from_date
- allocation.to_date = leave_period.to_date
+def create_leave_allocation(employee, leave_type, new_leaves_allocated, leave_type_details, leave_period, carry_forward, date_of_joining):
+ ''' Creates leave allocation for the given employee in the provided leave period '''
+ if carry_forward and not leave_type_details.get(leave_type).is_carry_forward:
+ carry_forward = 0
+
+ # Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period
+ if getdate(date_of_joining) > getdate(leave_period.from_date):
+ remaining_period = ((date_diff(leave_period.to_date, date_of_joining) + 1) / (date_diff(leave_period.to_date, leave_period.from_date) + 1))
+ new_leaves_allocated = ceil(new_leaves_allocated * remaining_period)
+
# Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0
if leave_type_details.get(leave_type).is_earned_leave == 1 or leave_type_details.get(leave_type).is_compensatory == 1:
new_leaves_allocated = 0
- allocation.new_leaves_allocated = new_leaves_allocated
- allocation.leave_period = leave_period.name
- if carry_forward_leaves:
- if leave_type_details.get(leave_type).is_carry_forward:
- allocation.carry_forward = carry_forward_leaves
+ allocation = frappe.get_doc(dict(
+ doctype="Leave Allocation",
+ employee=employee,
+ leave_type=leave_type,
+ from_date=leave_period.from_date,
+ to_date=leave_period.to_date,
+ new_leaves_allocated=new_leaves_allocated,
+ leave_period=leave_period.name,
+ carry_forward=carry_forward
+ ))
allocation.save(ignore_permissions = True)
allocation.submit()
- return allocation.name
-
-
+ return allocation.name
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py
index f97d285..48a2045 100644
--- a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py
+++ b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py
@@ -12,6 +12,9 @@
},
{
'items': ['Employee Grade']
- }
+ },
+ {
+ 'items': ['Leave Allocation']
+ },
]
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_policy/test_leave_policy.py b/erpnext/hr/doctype/leave_policy/test_leave_policy.py
index 2c6f1d0..fc868ea 100644
--- a/erpnext/hr/doctype/leave_policy/test_leave_policy.py
+++ b/erpnext/hr/doctype/leave_policy/test_leave_policy.py
@@ -12,16 +12,20 @@
if random_leave_type:
random_leave_type = random_leave_type[0]
leave_type = frappe.get_doc("Leave Type", random_leave_type.name)
- old_max_leaves_allowed = leave_type.max_leaves_allowed
leave_type.max_leaves_allowed = 2
leave_type.save()
- leave_policy_details = {
- "doctype": "Leave Policy",
- "leave_policy_details": [{
- "leave_type": leave_type.name,
- "annual_allocation": leave_type.max_leaves_allowed + 1
- }]
- }
+ leave_policy = create_leave_policy(leave_type=leave_type.name, annual_allocation=leave_type.max_leaves_allowed + 1)
- self.assertRaises(frappe.ValidationError, frappe.get_doc(leave_policy_details).insert)
+ self.assertRaises(frappe.ValidationError, leave_policy.insert)
+
+def create_leave_policy(**args):
+ ''' Returns an object of leave policy '''
+ args = frappe._dict(args)
+ return frappe.get_doc({
+ "doctype": "Leave Policy",
+ "leave_policy_details": [{
+ "leave_type": args.leave_type or "_Test Leave Type",
+ "annual_allocation": args.annual_allocation or 10
+ }]
+ })
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_type/leave_type.json b/erpnext/hr/doctype/leave_type/leave_type.json
index 6a7a80a..2f15e3b 100644
--- a/erpnext/hr/doctype/leave_type/leave_type.json
+++ b/erpnext/hr/doctype/leave_type/leave_type.json
@@ -1,5 +1,6 @@
{
"allow_copy": 0,
+ "allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 1,
@@ -14,10 +15,12 @@
"fields": [
{
"allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
+ "fetch_if_empty": 0,
"fieldname": "leave_type_name",
"fieldtype": "Data",
"hidden": 0,
@@ -42,15 +45,17 @@
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
- "unique": 0
+ "unique": 1
},
{
"allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
+ "fetch_if_empty": 0,
"fieldname": "max_leaves_allowed",
"fieldtype": "Int",
"hidden": 0,
@@ -78,10 +83,12 @@
},
{
"allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
+ "fetch_if_empty": 0,
"fieldname": "applicable_after",
"fieldtype": "Int",
"hidden": 0,
@@ -109,10 +116,12 @@
},
{
"allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
+ "fetch_if_empty": 0,
"fieldname": "max_continuous_days_allowed",
"fieldtype": "Int",
"hidden": 0,
@@ -141,10 +150,12 @@
},
{
"allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
+ "fetch_if_empty": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
@@ -171,10 +182,12 @@
},
{
"allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
+ "fetch_if_empty": 0,
"fieldname": "is_carry_forward",
"fieldtype": "Check",
"hidden": 0,
@@ -203,10 +216,13 @@
},
{
"allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
+ "depends_on": "",
+ "fetch_if_empty": 0,
"fieldname": "is_lwp",
"fieldtype": "Check",
"hidden": 0,
@@ -233,10 +249,12 @@
},
{
"allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
+ "fetch_if_empty": 0,
"fieldname": "is_optional_leave",
"fieldtype": "Check",
"hidden": 0,
@@ -264,10 +282,12 @@
},
{
"allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
+ "fetch_if_empty": 0,
"fieldname": "allow_negative",
"fieldtype": "Check",
"hidden": 0,
@@ -294,10 +314,12 @@
},
{
"allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
+ "fetch_if_empty": 0,
"fieldname": "include_holiday",
"fieldtype": "Check",
"hidden": 0,
@@ -324,10 +346,12 @@
},
{
"allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
+ "fetch_if_empty": 0,
"fieldname": "is_compensatory",
"fieldtype": "Check",
"hidden": 0,
@@ -355,10 +379,81 @@
},
{
"allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
+ "depends_on": "eval: doc.is_carry_forward == 1",
+ "fetch_if_empty": 0,
+ "fieldname": "carry_forward_section",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Carry Forward",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "depends_on": "",
+ "description": "Calculated in days",
+ "fetch_if_empty": 0,
+ "fieldname": "expire_carry_forwarded_leaves_after_days",
+ "fieldtype": "Int",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Expire Carry Forwarded Leaves (Days)",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 1,
+ "columns": 0,
+ "fetch_if_empty": 0,
"fieldname": "encashment",
"fieldtype": "Section Break",
"hidden": 0,
@@ -386,10 +481,12 @@
},
{
"allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
+ "fetch_if_empty": 0,
"fieldname": "allow_encashment",
"fieldtype": "Check",
"hidden": 0,
@@ -417,11 +514,13 @@
},
{
"allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "allow_encashment",
+ "fetch_if_empty": 0,
"fieldname": "encashment_threshold_days",
"fieldtype": "Int",
"hidden": 0,
@@ -449,11 +548,13 @@
},
{
"allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "allow_encashment",
+ "fetch_if_empty": 0,
"fieldname": "earning_component",
"fieldtype": "Link",
"hidden": 0,
@@ -482,10 +583,12 @@
},
{
"allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
+ "fetch_if_empty": 0,
"fieldname": "earned_leave",
"fieldtype": "Section Break",
"hidden": 0,
@@ -513,10 +616,12 @@
},
{
"allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
+ "fetch_if_empty": 0,
"fieldname": "is_earned_leave",
"fieldtype": "Check",
"hidden": 0,
@@ -544,11 +649,13 @@
},
{
"allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "is_earned_leave",
+ "fetch_if_empty": 0,
"fieldname": "earned_leave_frequency",
"fieldtype": "Select",
"hidden": 0,
@@ -577,12 +684,14 @@
},
{
"allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.5",
"depends_on": "is_earned_leave",
+ "fetch_if_empty": 0,
"fieldname": "rounding",
"fieldtype": "Select",
"hidden": 0,
@@ -611,17 +720,15 @@
}
],
"has_web_view": 0,
- "hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-flag",
"idx": 1,
- "image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2018-06-03 18:32:51.803472",
+ "modified": "2019-08-02 15:38:39.334283",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Type",
@@ -687,8 +794,8 @@
],
"quick_entry": 0,
"read_only": 0,
- "read_only_onload": 0,
"show_name_in_global_search": 0,
"track_changes": 0,
- "track_seen": 0
-}
+ "track_seen": 0,
+ "track_views": 0
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_type/leave_type.py b/erpnext/hr/doctype/leave_type/leave_type.py
index e0127e5..c0d1296 100644
--- a/erpnext/hr/doctype/leave_type/leave_type.py
+++ b/erpnext/hr/doctype/leave_type/leave_type.py
@@ -2,9 +2,22 @@
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
+import calendar
import frappe
+from datetime import datetime
+from frappe.utils import today
+from frappe import _
from frappe.model.document import Document
class LeaveType(Document):
- pass
\ No newline at end of file
+ def validate(self):
+ if self.is_lwp:
+ leave_allocation = frappe.get_all("Leave Allocation", filters={
+ 'leave_type': self.name,
+ 'from_date': ("<=", today()),
+ 'to_date': (">=", today())
+ }, fields=['name'])
+ leave_allocation = [l['name'] for l in leave_allocation]
+ if leave_allocation:
+ frappe.throw(_('Leave application is linked with leave allocations {0}. Leave application cannot be set as leave without pay').format(", ".join(leave_allocation))) #nosec
diff --git a/erpnext/hr/doctype/leave_type/test_leave_type.py b/erpnext/hr/doctype/leave_type/test_leave_type.py
index b844e49..0c4f435 100644
--- a/erpnext/hr/doctype/leave_type/test_leave_type.py
+++ b/erpnext/hr/doctype/leave_type/test_leave_type.py
@@ -2,6 +2,25 @@
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
-
import frappe
-test_records = frappe.get_test_records('Leave Type')
\ No newline at end of file
+from frappe import _
+
+test_records = frappe.get_test_records('Leave Type')
+
+def create_leave_type(**args):
+ args = frappe._dict(args)
+ if frappe.db.exists("Leave Type", args.leave_type_name):
+ return frappe.get_doc("Leave Type", args.leave_type_name)
+ leave_type = frappe.get_doc({
+ "doctype": "Leave Type",
+ "leave_type_name": args.leave_type_name or "_Test Leave Type",
+ "include_holiday": args.include_holidays or 1,
+ "allow_encashment": args.allow_encashment or 0,
+ "is_earned_leave": args.is_earned_leave or 0,
+ "is_lwp": args.is_lwp or 0,
+ "is_carry_forward": args.is_carry_forward or 0,
+ "expire_carry_forwarded_leaves_after_days": args.expire_carry_forwarded_leaves_after_days or 0,
+ "encashment_threshold_days": args.encashment_threshold_days or 5,
+ "earning_component": "Leave Encashment"
+ })
+ return leave_type
\ No newline at end of file
diff --git a/erpnext/hr/doctype/shift_type/shift_type.json b/erpnext/hr/doctype/shift_type/shift_type.json
index 86039de..61f3d2c 100644
--- a/erpnext/hr/doctype/shift_type/shift_type.json
+++ b/erpnext/hr/doctype/shift_type/shift_type.json
@@ -23,14 +23,9 @@
"grace_period_settings_auto_attendance_section",
"enable_entry_grace_period",
"late_entry_grace_period",
- "consequence_after",
- "consequence",
"column_break_18",
"enable_exit_grace_period",
- "enable_different_consequence_for_early_exit",
- "early_exit_grace_period",
- "early_exit_consequence_after",
- "early_exit_consequence"
+ "early_exit_grace_period"
],
"fields": [
{
@@ -108,21 +103,6 @@
"label": "Late Entry Grace Period"
},
{
- "depends_on": "enable_entry_grace_period",
- "description": "The number of occurrence after which the consequence is executed.",
- "fieldname": "consequence_after",
- "fieldtype": "Int",
- "label": "Consequence after"
- },
- {
- "default": "Half Day",
- "depends_on": "enable_entry_grace_period",
- "fieldname": "consequence",
- "fieldtype": "Select",
- "label": "Consequence",
- "options": "Half Day\nAbsent"
- },
- {
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
@@ -133,13 +113,6 @@
"label": "Enable Exit Grace Period"
},
{
- "default": "0",
- "depends_on": "enable_exit_grace_period",
- "fieldname": "enable_different_consequence_for_early_exit",
- "fieldtype": "Check",
- "label": "Enable Different Consequence for Early Exit"
- },
- {
"depends_on": "eval:doc.enable_exit_grace_period",
"description": "The time before the shift end time when check-out is considered as early (in minutes).",
"fieldname": "early_exit_grace_period",
@@ -147,21 +120,6 @@
"label": "Early Exit Grace Period"
},
{
- "depends_on": "eval:doc.enable_exit_grace_period && doc.enable_different_consequence_for_early_exit",
- "description": "The number of occurrence after which the consequence is executed.",
- "fieldname": "early_exit_consequence_after",
- "fieldtype": "Int",
- "label": "Early Exit Consequence after"
- },
- {
- "default": "Half Day",
- "depends_on": "eval:doc.enable_exit_grace_period && doc.enable_different_consequence_for_early_exit",
- "fieldname": "early_exit_consequence",
- "fieldtype": "Select",
- "label": "Early Exit Consequence",
- "options": "Half Day\nAbsent"
- },
- {
"default": "60",
"description": "Time after the end of shift during which check-out is considered for attendance.",
"fieldname": "allow_check_out_after_shift_end_time",
@@ -178,7 +136,6 @@
"depends_on": "enable_auto_attendance",
"fieldname": "grace_period_settings_auto_attendance_section",
"fieldtype": "Section Break",
- "hidden": 1,
"label": "Grace Period Settings For Auto Attendance"
},
{
@@ -201,7 +158,7 @@
"label": "Last Sync of Checkin"
}
],
- "modified": "2019-06-10 06:02:44.272036",
+ "modified": "2019-07-30 01:05:24.660666",
"modified_by": "Administrator",
"module": "HR",
"name": "Shift Type",
diff --git a/erpnext/hr/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py
index b98f445..8de92b2 100644
--- a/erpnext/hr/doctype/shift_type/shift_type.py
+++ b/erpnext/hr/doctype/shift_type/shift_type.py
@@ -28,8 +28,8 @@
logs = frappe.db.get_list('Employee Checkin', fields="*", filters=filters, order_by="employee,time")
for key, group in itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_actual_start'])):
single_shift_logs = list(group)
- attendance_status, working_hours = self.get_attendance(single_shift_logs)
- mark_attendance_and_link_log(single_shift_logs, attendance_status, key[1].date(), working_hours, self.name)
+ attendance_status, working_hours, late_entry, early_exit = self.get_attendance(single_shift_logs)
+ mark_attendance_and_link_log(single_shift_logs, attendance_status, key[1].date(), working_hours, late_entry, early_exit, self.name)
for employee in self.get_assigned_employee(self.process_attendance_after, True):
self.mark_absent_for_dates_with_no_attendance(employee)
@@ -39,12 +39,19 @@
1. These logs belongs to an single shift, single employee and is not in a holiday date.
2. Logs are in chronological order
"""
- total_working_hours = calculate_working_hours(logs, self.determine_check_in_and_check_out, self.working_hours_calculation_based_on)
+ late_entry = early_exit = False
+ total_working_hours, in_time, out_time = calculate_working_hours(logs, self.determine_check_in_and_check_out, self.working_hours_calculation_based_on)
+ if cint(self.enable_entry_grace_period) and in_time and in_time > logs[0].shift_start + timedelta(minutes=cint(self.late_entry_grace_period)):
+ late_entry = True
+
+ if cint(self.enable_exit_grace_period) and out_time and out_time < logs[0].shift_end - timedelta(minutes=cint(self.early_exit_grace_period)):
+ early_exit = True
+
if self.working_hours_threshold_for_absent and total_working_hours < self.working_hours_threshold_for_absent:
- return 'Absent', total_working_hours
+ return 'Absent', total_working_hours, late_entry, early_exit
if self.working_hours_threshold_for_half_day and total_working_hours < self.working_hours_threshold_for_half_day:
- return 'Half Day', total_working_hours
- return 'Present', total_working_hours
+ return 'Half Day', total_working_hours, late_entry, early_exit
+ return 'Present', total_working_hours, late_entry, early_exit
def mark_absent_for_dates_with_no_attendance(self, employee):
"""Marks Absents for the given employee on working days in this shift which have no attendance marked.
diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.js b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.js
index 59c2560..05728a2 100644
--- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.js
+++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.js
@@ -24,6 +24,18 @@
"options": "Company",
"reqd": 1,
"default": frappe.defaults.get_user_default("Company")
+ },
+ {
+ "fieldname":"department",
+ "label": __("Department"),
+ "fieldtype": "Link",
+ "options": "Department",
+ },
+ {
+ "fieldname":"employee",
+ "label": __("Employee"),
+ "fieldtype": "Link",
+ "options": "Employee",
}
]
}
diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
index 1843176..66e3614 100644
--- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
@@ -4,8 +4,9 @@
from __future__ import unicode_literals
import frappe
from frappe import _
+from frappe.utils import flt
from erpnext.hr.doctype.leave_application.leave_application \
- import get_leave_allocation_records, get_leave_balance_on, get_approved_leaves_for_period
+ import get_leave_balance_on, get_leaves_for_period
def execute(filters=None):
@@ -30,17 +31,28 @@
return columns
+def get_conditions(filters):
+ conditions = {
+ "status": "Active",
+ "company": filters.company,
+ }
+ if filters.get("department"):
+ conditions.update({"department": filters.get("department")})
+ if filters.get("employee"):
+ conditions.update({"employee": filters.get("employee")})
+
+ return conditions
+
def get_data(filters, leave_types):
user = frappe.session.user
- allocation_records_based_on_to_date = get_leave_allocation_records(filters.to_date)
- allocation_records_based_on_from_date = get_leave_allocation_records(filters.from_date)
+ conditions = get_conditions(filters)
if filters.to_date <= filters.from_date:
frappe.throw(_("From date can not be greater than than To date"))
active_employees = frappe.get_all("Employee",
- filters = { "status": "Active", "company": filters.company},
- fields = ["name", "employee_name", "department", "user_id"])
+ filters=conditions,
+ fields=["name", "employee_name", "department", "user_id"])
data = []
for employee in active_employees:
@@ -50,16 +62,14 @@
for leave_type in leave_types:
# leaves taken
- leaves_taken = get_approved_leaves_for_period(employee.name, leave_type,
- filters.from_date, filters.to_date)
+ leaves_taken = get_leaves_for_period(employee.name, leave_type,
+ filters.from_date, filters.to_date) * -1
# opening balance
- opening = get_leave_balance_on(employee.name, leave_type, filters.from_date,
- allocation_records_based_on_to_date.get(employee.name, frappe._dict()))
+ opening = get_total_allocated_leaves(employee.name, leave_type, filters.from_date, filters.to_date)
# closing balance
- closing = get_leave_balance_on(employee.name, leave_type, filters.to_date,
- allocation_records_based_on_to_date.get(employee.name, frappe._dict()))
+ closing = flt(opening) - flt(leaves_taken)
row += [opening, leaves_taken, closing]
@@ -84,3 +94,19 @@
where parent = %s and parentfield = 'leave_approvers'""", (d), as_dict=True)])
return approvers
+
+def get_total_allocated_leaves(employee, leave_type, from_date, to_date):
+ ''' Returns leave allocation between from date and to date '''
+ filters= {
+ 'from_date': ['between', (from_date, to_date)],
+ 'to_date': ['between', (from_date, to_date)],
+ 'docstatus': 1,
+ 'is_expired': 0,
+ 'leave_type': leave_type,
+ 'employee': employee,
+ 'transaction_type': 'Leave Allocation'
+ }
+
+ leave_allocation_records = frappe.db.get_all('Leave Ledger Entry', filters=filters, fields=['SUM(leaves) as leaves'])
+
+ return flt(leave_allocation_records[0].get('leaves')) if leave_allocation_records else flt(0)
\ No newline at end of file
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 e9c7029..1e9c83b 100644
--- a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py
+++ b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py
@@ -25,6 +25,7 @@
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)
@@ -65,6 +66,10 @@
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)
leaves = {}
for d in leave_details:
@@ -80,7 +85,8 @@
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
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index de2b090..1464a77 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe, erpnext
from frappe import _
-from frappe.utils import formatdate, format_datetime, getdate, get_datetime, nowdate, flt, cstr
+from frappe.utils import formatdate, format_datetime, getdate, get_datetime, nowdate, flt, cstr, add_days, today
from frappe.model.document import Document
from frappe.desk.form import assign_to
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
@@ -270,6 +270,21 @@
if leave_period:
return leave_period
+def generate_leave_encashment():
+ ''' Generates a draft leave encashment on allocation expiry '''
+ from erpnext.hr.doctype.leave_encashment.leave_encashment import create_leave_encashment
+
+ if frappe.db.get_single_value('HR Settings', 'auto_leave_encashment'):
+ leave_type = frappe.get_all('Leave Type', filters={'allow_encashment': 1}, fields=['name'])
+ leave_type=[l['name'] for l in leave_type]
+
+ leave_allocation = frappe.get_all("Leave Allocation", filters={
+ 'to_date': add_days(today(), -1),
+ 'leave_type': ('in', leave_type)
+ }, fields=['employee', 'leave_period', 'leave_type', 'to_date', 'total_leaves_allocated', 'new_leaves_allocated'])
+
+ create_leave_encashment(leave_allocation=leave_allocation)
+
def allocate_earned_leaves():
'''Allocate earned leaves to Employees'''
e_leave_types = frappe.get_all("Leave Type",
@@ -277,31 +292,43 @@
filters={'is_earned_leave' : 1})
today = getdate()
divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12}
- if e_leave_types:
- for e_leave_type in e_leave_types:
- leave_allocations = frappe.db.sql("""select name, employee, from_date, to_date from `tabLeave Allocation` where '{0}'
- between from_date and to_date and docstatus=1 and leave_type='{1}'"""
- .format(today, e_leave_type.name), as_dict=1)
- for allocation in leave_allocations:
- leave_policy = get_employee_leave_policy(allocation.employee)
- if not leave_policy:
- continue
- if not e_leave_type.earned_leave_frequency == "Monthly":
- if not check_frequency_hit(allocation.from_date, today, e_leave_type.earned_leave_frequency):
- continue
- annual_allocation = frappe.db.sql("""select annual_allocation from `tabLeave Policy Detail`
- where parent=%s and leave_type=%s""", (leave_policy.name, e_leave_type.name))
- if annual_allocation and annual_allocation[0]:
- earned_leaves = flt(annual_allocation[0][0]) / divide_by_frequency[e_leave_type.earned_leave_frequency]
- if e_leave_type.rounding == "0.5":
- earned_leaves = round(earned_leaves * 2) / 2
- else:
- earned_leaves = round(earned_leaves)
- allocated_leaves = frappe.db.get_value('Leave Allocation', allocation.name, 'total_leaves_allocated')
- new_allocation = flt(allocated_leaves) + flt(earned_leaves)
- new_allocation = new_allocation if new_allocation <= e_leave_type.max_leaves_allowed else e_leave_type.max_leaves_allowed
- frappe.db.set_value('Leave Allocation', allocation.name, 'total_leaves_allocated', new_allocation)
+ for e_leave_type in e_leave_types:
+ leave_allocations = frappe.db.sql("""select name, employee, from_date, to_date from `tabLeave Allocation` where %s
+ between from_date and to_date and docstatus=1 and leave_type=%s""", (today, e_leave_type.name), as_dict=1)
+ for allocation in leave_allocations:
+ leave_policy = get_employee_leave_policy(allocation.employee)
+ if not leave_policy:
+ continue
+ if not e_leave_type.earned_leave_frequency == "Monthly":
+ if not check_frequency_hit(allocation.from_date, today, e_leave_type.earned_leave_frequency):
+ continue
+ annual_allocation = frappe.db.get_value("Leave Policy Detail", filters={
+ 'parent': leave_policy.name,
+ 'leave_type': e_leave_type.name
+ }, fieldname=['annual_allocation'])
+ if annual_allocation:
+ earned_leaves = flt(annual_allocation) / divide_by_frequency[e_leave_type.earned_leave_frequency]
+ if e_leave_type.rounding == "0.5":
+ earned_leaves = round(earned_leaves * 2) / 2
+ else:
+ earned_leaves = round(earned_leaves)
+
+ allocation = frappe.get_doc('Leave Allocation', allocation.name)
+ new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves)
+ new_allocation = new_allocation if new_allocation <= e_leave_type.max_leaves_allowed else e_leave_type.max_leaves_allowed
+
+ if new_allocation == allocation.total_leaves_allocated:
+ continue
+ allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
+ create_earned_leave_ledger_entry(allocation, earned_leaves, today)
+
+def create_earned_leave_ledger_entry(allocation, earned_leaves, date):
+ ''' Create leave ledger entry based on the earned leave frequency '''
+ allocation.new_leaves_allocated = earned_leaves
+ allocation.from_date = date
+ allocation.unused_leaves = 0
+ allocation.create_leave_ledger_entry()
def check_frequency_hit(from_date, to_date, frequency):
'''Return True if current date matches frequency'''
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index b6ea542..7b82809 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -626,3 +626,4 @@
erpnext.patches.v12_0.update_ewaybill_field_position
erpnext.patches.v12_0.create_accounting_dimensions_in_missing_doctypes
erpnext.patches.v11_1.set_status_for_material_request_type_manufacture
+erpnext.patches.v12_0.generate_leave_ledger_entries
\ No newline at end of file
diff --git a/erpnext/patches/v12_0/generate_leave_ledger_entries.py b/erpnext/patches/v12_0/generate_leave_ledger_entries.py
new file mode 100644
index 0000000..44b59bf
--- /dev/null
+++ b/erpnext/patches/v12_0/generate_leave_ledger_entries.py
@@ -0,0 +1,86 @@
+# Copyright (c) 2018, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.utils import getdate
+
+def execute():
+ """ Generates leave ledger entries for leave allocation/application/encashment
+ for last allocation """
+ frappe.reload_doc("HR", "doctype", "Leave Ledger Entry")
+ frappe.reload_doc("HR", "doctype", "Leave Encashment")
+ if frappe.db.a_row_exists("Leave Ledger Entry"):
+ return
+
+ if not frappe.get_meta("Leave Allocation").has_field("unused_leaves"):
+ frappe.reload_doc("HR", "doctype", "Leave Allocation")
+ update_leave_allocation_fieldname()
+
+ generate_allocation_ledger_entries()
+ generate_application_leave_ledger_entries()
+ generate_encashment_leave_ledger_entries()
+ generate_expiry_allocation_ledger_entries()
+
+def update_leave_allocation_fieldname():
+ ''' maps data from old field to the new field '''
+ frappe.db.sql("""
+ UPDATE `tabLeave Allocation`
+ SET `unused_leaves` = `carry_forwarded_leaves`
+ """)
+
+def generate_allocation_ledger_entries():
+ ''' fix ledger entries for missing leave allocation transaction '''
+ allocation_list = get_allocation_records()
+
+ for allocation in allocation_list:
+ if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name}):
+ allocation.update(dict(doctype="Leave Allocation"))
+ allocation_obj = frappe.get_doc(allocation)
+ allocation_obj.create_leave_ledger_entry()
+
+def generate_application_leave_ledger_entries():
+ ''' fix ledger entries for missing leave application transaction '''
+ leave_applications = get_leaves_application_records()
+
+ for application in leave_applications:
+ if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Application', 'transaction_name': application.name}):
+ application.update(dict(doctype="Leave Application"))
+ frappe.get_doc(application).create_leave_ledger_entry()
+
+def generate_encashment_leave_ledger_entries():
+ ''' fix ledger entries for missing leave encashment transaction '''
+ leave_encashments = get_leave_encashment_records()
+
+ for encashment in leave_encashments:
+ if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Encashment', 'transaction_name': encashment.name}):
+ encashment.update(dict(doctype="Leave Encashment"))
+ frappe.get_doc(encashment).create_leave_ledger_entry()
+
+def generate_expiry_allocation_ledger_entries():
+ ''' fix ledger entries for missing leave allocation transaction '''
+ from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation
+ allocation_list = get_allocation_records()
+
+ for allocation in allocation_list:
+ if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name, 'is_expired': 1}):
+ allocation.update(dict(doctype="Leave Allocation"))
+ allocation_obj = frappe.get_doc(allocation)
+ expire_allocation(allocation_obj)
+
+def get_allocation_records():
+ return frappe.get_all("Leave Allocation", filters={
+ "docstatus": 1
+ }, fields=['name', 'employee', 'leave_type', 'new_leaves_allocated',
+ 'unused_leaves', 'from_date', 'to_date', 'carry_forward'
+ ], order_by='to_date ASC')
+
+def get_leaves_application_records():
+ return frappe.get_all("Leave Application", filters={
+ "docstatus": 1
+ }, fields=['name', 'employee', 'leave_type', 'total_leave_days', 'from_date', 'to_date'])
+
+def get_leave_encashment_records():
+ return frappe.get_all("Leave Encashment", filters={
+ "docstatus": 1
+ }, fields=['name', 'employee', 'leave_type', 'encashable_days', 'encashment_date'])
\ No newline at end of file
diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js
index f1c9209..e6c18a1 100644
--- a/erpnext/public/js/utils/dimension_tree_filter.js
+++ b/erpnext/public/js/utils/dimension_tree_filter.js
@@ -1,12 +1,13 @@
frappe.provide('frappe.ui.form');
erpnext.doctypes_with_dimensions = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset",
- "Expense Claim", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note", "Sales Invoice Item", "Purchase Invoice Item",
- "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item", "Purchase Receipt Item",
- "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule",
- "Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation",
- "Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription",
- "Subscription Plan"];
+ "Expense Claim", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note", "Shipping Rule", "Loyalty Program",
+ "Fee Schedule", "Fee Structure", "Stock Reconciliation", "Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool",
+ "Subscription", "Purchase Order", "Journal Entry", "Material Request", "Purchase Receipt", "Landed Cost Item", "Asset"];
+
+erpnext.child_docs = ["Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account",
+ "Material Request Item", "Delivery Note Item", "Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction",
+ "Landed Cost Item", "Asset Value Adjustment", "Opening Invoice Creation Tool Item", "Subscription Plan"];
frappe.call({
method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimension_filters",
@@ -26,21 +27,40 @@
"is_group": 0
});
}
- if (frm.is_new() && frappe.meta.has_field(doctype, 'company') && frm.doc.company) {
- frm.set_value(dimension['fieldname'], erpnext.default_dimensions[frm.doc.company][dimension['document_type']]);
+
+ if (Object.keys(erpnext.default_dimensions).length > 0) {
+ if (frappe.meta.has_field(doctype, dimension['fieldname'])) {
+ if (frm.is_new() && frappe.meta.has_field(doctype, 'company') && frm.doc.company) {
+ frm.set_value(dimension['fieldname'], erpnext.default_dimensions[frm.doc.company][dimension['document_type']]);
+ }
+ }
+
+ if (frm.doc.items && frm.doc.items.length) {
+ frm.doc.items[0][dimension['fieldname']] = erpnext.default_dimensions[frm.doc.company][dimension['document_type']];
+ }
+
+ if (frm.doc.accounts && frm.doc.accounts.length) {
+ frm.doc.accounts[0][dimension['fieldname']] = erpnext.default_dimensions[frm.doc.company][dimension['document_type']];
+ }
}
});
});
},
company: function(frm) {
- if(frm.doc.company) {
+ if(frm.doc.company && (Object.keys(erpnext.default_dimensions).length > 0)) {
erpnext.dimension_filters.forEach((dimension) => {
- frm.set_value(dimension['fieldname'], erpnext.default_dimensions[frm.doc.company][dimension['document_type']]);
+ if (frappe.meta.has_field(doctype, dimension['fieldname'])) {
+ frm.set_value(dimension['fieldname'], erpnext.default_dimensions[frm.doc.company][dimension['document_type']]);
+ }
});
}
},
+ });
+});
+erpnext.child_docs.forEach((doctype) => {
+ frappe.ui.form.on(doctype, {
items_add: function(frm, cdt, cdn) {
erpnext.dimension_filters.forEach((dimension) => {
var row = frappe.get_doc(cdt, cdn);
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index bc95c96..91b6f4c 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -701,6 +701,7 @@
"qty": args.qty or 1,
"rate": args.rate or 100,
"conversion_factor": 1.0,
+ "allow_zero_valuation_rate": args.allow_zero_valuation_rate or 1,
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no,
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 5fda2a4..920fc27 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -464,16 +464,22 @@
last_valuation_rate = frappe.db.sql("""select valuation_rate
from `tabStock Ledger Entry`
- where item_code = %s and warehouse = %s
- and valuation_rate >= 0
- order by posting_date desc, posting_time desc, creation desc limit 1""", (item_code, warehouse))
+ where
+ item_code = %s
+ AND warehouse = %s
+ AND valuation_rate >= 0
+ AND NOT (voucher_no = %s AND voucher_type = %s)
+ order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type))
if not last_valuation_rate:
# Get valuation rate from last sle for the item against any warehouse
last_valuation_rate = frappe.db.sql("""select valuation_rate
from `tabStock Ledger Entry`
- where item_code = %s and valuation_rate > 0
- order by posting_date desc, posting_time desc, creation desc limit 1""", item_code)
+ where
+ item_code = %s
+ AND valuation_rate > 0
+ AND NOT(voucher_no = %s AND voucher_type = %s)
+ order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, voucher_no, voucher_type))
if last_valuation_rate:
return flt(last_valuation_rate[0][0]) # as there is previous records, it might come with zero rate