Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 32 additions & 14 deletions hrms/hr/report/employee_leave_balance/employee_leave_balance.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def get_columns() -> list[dict]:
"width": 150,
},
{
"label": _("New Leave(s) Allocated"),
"label": _("Leave(s) Allocated"),
"fieldtype": "float",
"fieldname": "leaves_allocated",
"width": 200,
Expand Down Expand Up @@ -116,14 +116,22 @@ def get_data(filters: Filters) -> list:
new_allocation, expired_leaves, carry_forwarded_leaves = get_allocated_and_expired_leaves(
filters.from_date, filters.to_date, employee.name, leave_type
)
opening = get_opening_balance(employee.name, leave_type, filters, carry_forwarded_leaves)
on_allocation_boundary = is_opening_balance_on_allocation_boundary(
employee.name, leave_type, filters
)
opening = get_opening_balance(
employee.name, leave_type, filters, carry_forwarded_leaves, on_allocation_boundary
)
allocated_leaves = new_allocation + carry_forwarded_leaves
if on_allocation_boundary:
allocated_leaves -= carry_forwarded_leaves

row.leaves_allocated = flt(new_allocation, precision)
row.leaves_allocated = flt(allocated_leaves, precision)
row.leaves_expired = flt(expired_leaves, precision)
row.opening_balance = flt(opening, precision)
row.leaves_taken = flt(leaves_taken, precision)

closing = new_allocation + opening - (row.leaves_expired + leaves_taken)
closing = allocated_leaves + opening - (row.leaves_expired + leaves_taken)
row.closing_balance = flt(closing, precision)
row.indent = 1
data.append(row)
Expand Down Expand Up @@ -158,19 +166,17 @@ def get_employees(filters: Filters) -> list[dict]:


def get_opening_balance(
employee: str, leave_type: str, filters: Filters, carry_forwarded_leaves: float
employee: str,
leave_type: str,
filters: Filters,
carry_forwarded_leaves: float,
on_allocation_boundary: bool,
) -> float:
# allocation boundary condition
# opening balance is the closing leave balance 1 day before the filter start date
opening_balance_date = add_days(filters.from_date, -1)
allocation = get_previous_allocation(filters.from_date, leave_type, employee)

if (
allocation
and allocation.get("to_date")
and opening_balance_date
and getdate(allocation.get("to_date")) == getdate(opening_balance_date)
):
if on_allocation_boundary:
# if opening balance date is same as the previous allocation's expiry
# then opening balance should only consider carry forwarded leaves
opening_balance = carry_forwarded_leaves
Expand All @@ -181,6 +187,18 @@ def get_opening_balance(
return opening_balance


def is_opening_balance_on_allocation_boundary(employee: str, leave_type: str, filters: Filters) -> bool:
opening_balance_date = add_days(filters.from_date, -1)
allocation = get_previous_allocation(filters.from_date, leave_type, employee)

return bool(
allocation
and allocation.get("to_date")
and opening_balance_date
and getdate(allocation.get("to_date")) == getdate(opening_balance_date)
)


def get_allocated_and_expired_leaves(
from_date: str, to_date: str, employee: str, leave_type: str
) -> tuple[float, float, float]:
Expand All @@ -205,7 +223,7 @@ def get_allocated_leaves(from_date, to_date, employee, leave_type):
& (ledger.transaction_type.isin(["Leave Allocation", "Leave Adjustment"]))
& (ledger.employee == employee)
& (ledger.leave_type == leave_type)
& ((ledger.from_date[from_date:to_date]) | (ledger.to_date[from_date:to_date]))
& (ledger.from_date[from_date:to_date])
& ((ledger.is_expired == 0) & (ledger.is_carry_forward == 0))
)
).run()[0][0]
Expand Down Expand Up @@ -239,7 +257,7 @@ def get_cf_leaves(from_date, to_date, employee, leave_type):
& (ledger.transaction_type == "Leave Allocation")
& (ledger.employee == employee)
& (ledger.leave_type == leave_type)
& ((ledger.from_date[from_date:to_date]) | (ledger.to_date[from_date:to_date]))
& (ledger.from_date[from_date:to_date])
& ((ledger.is_expired == 0) & (ledger.is_carry_forward == 1))
)
).run()[0][0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,58 @@ def test_opening_balance_considers_carry_forwarded_leaves(self):
)
self.assertEqual(report[1][0].opening_balance, opening_balance)

def test_carry_forwarded_leaves_are_included_when_they_expire(self):
leave_type = create_leave_type(
leave_type_name="_Test_CF_report_expiry",
is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=7,
)

previous_allocation = make_allocation_record(
employee=self.employee_id,
from_date=add_days(self.date, -45),
to_date=add_days(self.date, -31),
leave_type=leave_type.name,
leaves=7,
)
current_allocation = make_allocation_record(
employee=self.employee_id,
from_date=add_days(self.date, -20),
to_date=add_days(self.date, 30),
carry_forward=True,
leave_type=leave_type.name,
leaves=11,
)

leave_application = make_leave_application(
self.employee_id,
add_days(current_allocation.from_date, 8),
add_days(current_allocation.from_date, 16),
leave_type.name,
)
leave_application.reload()
process_expired_allocation()

filters = frappe._dict(
{
"from_date": current_allocation.from_date,
"to_date": current_allocation.to_date,
"employee": self.employee_id,
}
)
report = execute(filters)
row = next(row for row in report[1] if row.leave_type == leave_type.name)

expected_allocated = current_allocation.new_leaves_allocated + current_allocation.unused_leaves
expected_closing = row.opening_balance + row.leaves_allocated - row.leaves_expired - row.leaves_taken

self.assertEqual(current_allocation.unused_leaves, previous_allocation.new_leaves_allocated)
self.assertEqual(row.opening_balance, 0)
self.assertEqual(row.leaves_allocated, flt(expected_allocated))
self.assertEqual(row.leaves_expired, flt(current_allocation.unused_leaves))
self.assertEqual(row.leaves_taken, flt(leave_application.total_leave_days))
self.assertEqual(row.closing_balance, flt(expected_closing))

@assign_holiday_list("_Test Emp Balance Holiday List", "_Test Company")
def test_employee_status_filter(self):
frappe.get_doc(test_records[0]).insert()
Expand Down
39 changes: 29 additions & 10 deletions hrms/locale/de.po
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: contact@frappe.io\n"
"POT-Creation-Date: 2026-06-21 10:42+0000\n"
"PO-Revision-Date: 2026-06-22 20:40\n"
"PO-Revision-Date: 2026-06-23 20:42\n"
"Last-Translator: contact@frappe.io\n"
"Language-Team: German\n"
"MIME-Version: 1.0\n"
Expand Down Expand Up @@ -250,7 +250,26 @@ msgid "<h3>Help</h3>\n\n"
"<pre><code>Condition: employment_type==\"Intern\"</code></pre>\n"
"<pre><code>Amount: 1000</code></pre></li>\n"
"</ol>"
msgstr ""
msgstr "<h3>Hilfe</h3>\n\n"
"<p>Hinweise:</p>\n\n"
"<ol>\n"
"<li>Verwenden Sie das Feld <code>base</code>, um das Grundgehalt des Mitarbeiters zu nutzen</li>\n"
"<li>Verwenden Sie Abkürzungen der Gehaltskomponenten in Bedingungen und Formeln. <code>BS = Grundgehalt</code></li>\n"
"<li>Verwenden Sie den Feldnamen für Mitarbeiterdetails in Bedingungen und Formeln. <code>Employment Type = employment_type</code><code>Branch = branch</code></li>\n"
"<li>Verwenden Sie den Feldnamen aus dem Gehaltszettel in Bedingungen und Formeln. <code>Payment Days = payment_days</code><code>Leave without pay = leave_without_pay</code></li>\n"
"<li>Ein direkter Betrag kann ebenfalls basierend auf einer Bedingung eingegeben werden. Siehe Beispiel 3</li></ol>\n\n"
"<h4>Beispiele</h4>\n"
"<ol>\n"
"<li>Berechnung des Grundgehalts basierend auf <code>base</code>\n"
"<pre><code>Bedingung: base &lt; 10000</code></pre>\n"
"<pre><code>Formel: base * .2</code></pre></li>\n"
"<li>Berechnung der HRA basierend auf dem Grundgehalt <code>BS</code> \n"
"<pre><code>Bedingung: BS &gt; 2000</code></pre>\n"
"<pre><code>Formel: BS * .1</code></pre></li>\n"
"<li>Berechnung der TDS basierend auf Beschäftigungsart <code>employment_type</code> \n"
"<pre><code>Bedingung: employment_type==\"Intern\"</code></pre>\n"
"<pre><code>Betrag: 1000</code></pre></li>\n"
"</ol>"

#. Content of the 'html_6' (HTML) field in DocType 'Taxable Salary Slab'
#: hrms/payroll/doctype/taxable_salary_slab/taxable_salary_slab.json
Expand Down Expand Up @@ -3689,7 +3708,7 @@ msgstr "Fehler beim Aktualisieren von {0}"

#: hrms/payroll/utils.py:134
msgid "Error while evaluating the {doctype} {doclink} at row {row_id}. <br><br> <b>Error:</b> {error} <br><br> <b>Hint:</b> {description}"
msgstr ""
msgstr "Fehler beim Auswerten von {doctype} {doclink} in Zeile {row_id}. <br><br> <b>Fehler:</b> {error} <br><br> <b>Hinweis:</b> {description}"

#. Label of the estimated_cost_per_position (Currency) field in DocType
#. 'Staffing Plan Detail'
Expand Down Expand Up @@ -4642,7 +4661,7 @@ msgstr "Gratifikation"
#. Name of a DocType
#: hrms/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.json
msgid "Gratuity Applicable Component"
msgstr ""
msgstr "Abfindungsrelevante Komponente"

#. Label of the gratuity_rule (Link) field in DocType 'Gratuity'
#. Name of a DocType
Expand All @@ -4654,7 +4673,7 @@ msgstr "Gratifikationsregel"
#. Name of a DocType
#: hrms/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json
msgid "Gratuity Rule Slab"
msgstr ""
msgstr "Stufe der Abfindungsregel"

#. Label of a Card Break in the Tenure Workspace
#: hrms/hr/workspace/tenure/tenure.json
Expand Down Expand Up @@ -5109,7 +5128,7 @@ msgstr "Anreizbetrag"

#: hrms/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.js:122
msgid "Include Company Descendants"
msgstr ""
msgstr "Tochtergesellschaften einbeziehen"

#. Label of the include_holidays (Check) field in DocType 'Attendance Request'
#: hrms/hr/doctype/attendance_request/attendance_request.json
Expand Down Expand Up @@ -5873,7 +5892,7 @@ msgstr "Späte Einreise"
#. field in DocType 'Shift Type'
#: hrms/hr/doctype/shift_type/shift_type.json
msgid "Late Entry & Early Exit Settings for Auto Attendance"
msgstr ""
msgstr "Einstellungen für Spätantritt & Frühabgang (Auto-Anwesenheit)"

#: hrms/hr/report/shift_attendance/shift_attendance.py:88
msgid "Late Entry By"
Expand Down Expand Up @@ -6088,7 +6107,7 @@ msgstr "Ledger-Eintrag verlassen"

#: hrms/hr/doctype/leave_ledger_entry/leave_ledger_entry.py:43
msgid "Leave Ledger Entry's To date needs to be after From date. Currently, From Date is {0} and To Date is {1}"
msgstr ""
msgstr "Das „Bis-Datum“ des Urlaubskonto-Eintrags muss nach dem „Von-Datum“ liegen. Aktuell ist das Von-Datum {0} und das Bis-Datum {1}"

#. Label of the leave_period (Link) field in DocType 'Leave Allocation'
#. Option for the 'Dates Based On' (Select) field in DocType 'Leave Control
Expand Down Expand Up @@ -10351,7 +10370,7 @@ msgstr "Die Tage zwischen {0} und {1} sind keine gültigen Feiertage."

#: hrms/setup.py:132
msgid "The first Approver in the list will be set as the default Approver."
msgstr ""
msgstr "Der erste Genehmiger in der Liste wird automatisch als Standard festgelegt."

#: hrms/hr/doctype/leave_type/leave_type.py:84
msgid "The fraction of Daily Salary per Leave should be between 0 and 1"
Expand Down Expand Up @@ -11738,7 +11757,7 @@ msgstr "maxmustermann@email.de"
#. Label of the modify_half_day_status (Check) field in DocType 'Attendance'
#: hrms/hr/doctype/attendance/attendance.json
msgid "modify_half_day_status"
msgstr ""
msgstr "Halbtagsstatus ändern"

#: hrms/hr/doctype/department_approver/department_approver.py:103
msgid "or for the Employee's Department: {0}"
Expand Down
Loading