Merge pull request #34441 from frappe/version-13-hotfix
chore: release v13
This commit is contained in:
@@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
|
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
|
||||||
<div>
|
<div>
|
||||||
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party[0] }}</b></h5>
|
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party_name[0] }}</b></h5>
|
||||||
<h5 style="float: right;">
|
<h5 style="float: right;">
|
||||||
{{ _("Date: ") }}
|
{{ _("Date: ") }}
|
||||||
<b>{{ frappe.format(filters.from_date, 'Date')}}
|
<b>{{ frappe.format(filters.from_date, 'Date')}}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from erpnext.accounts.report.general_ledger.general_ledger import execute as get
|
|||||||
class ProcessStatementOfAccounts(Document):
|
class ProcessStatementOfAccounts(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
if not self.subject:
|
if not self.subject:
|
||||||
self.subject = "Statement Of Accounts for {{ customer.name }}"
|
self.subject = "Statement Of Accounts for {{ customer.customer_name }}"
|
||||||
if not self.body:
|
if not self.body:
|
||||||
self.body = "Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}."
|
self.body = "Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}."
|
||||||
|
|
||||||
@@ -87,6 +87,7 @@ def get_report_pdf(doc, consolidated=True):
|
|||||||
"account": [doc.account] if doc.account else None,
|
"account": [doc.account] if doc.account else None,
|
||||||
"party_type": "Customer",
|
"party_type": "Customer",
|
||||||
"party": [entry.customer],
|
"party": [entry.customer],
|
||||||
|
"party_name": [entry.customer_name] if entry.customer_name else None,
|
||||||
"presentation_currency": presentation_currency,
|
"presentation_currency": presentation_currency,
|
||||||
"group_by": doc.group_by,
|
"group_by": doc.group_by,
|
||||||
"currency": doc.currency,
|
"currency": doc.currency,
|
||||||
@@ -155,7 +156,7 @@ def get_customers_based_on_territory_or_customer_group(customer_collection, coll
|
|||||||
]
|
]
|
||||||
return frappe.get_list(
|
return frappe.get_list(
|
||||||
"Customer",
|
"Customer",
|
||||||
fields=["name", "email_id"],
|
fields=["name", "customer_name", "email_id"],
|
||||||
filters=[[fields_dict[customer_collection], "IN", selected]],
|
filters=[[fields_dict[customer_collection], "IN", selected]],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -178,7 +179,7 @@ def get_customers_based_on_sales_person(sales_person):
|
|||||||
if sales_person_records.get("Customer"):
|
if sales_person_records.get("Customer"):
|
||||||
return frappe.get_list(
|
return frappe.get_list(
|
||||||
"Customer",
|
"Customer",
|
||||||
fields=["name", "email_id"],
|
fields=["name", "customer_name", "email_id"],
|
||||||
filters=[["name", "in", list(sales_person_records["Customer"])]],
|
filters=[["name", "in", list(sales_person_records["Customer"])]],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -227,7 +228,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
|
|||||||
if customer_collection == "Sales Partner":
|
if customer_collection == "Sales Partner":
|
||||||
customers = frappe.get_list(
|
customers = frappe.get_list(
|
||||||
"Customer",
|
"Customer",
|
||||||
fields=["name", "email_id"],
|
fields=["name", "customer_name", "email_id"],
|
||||||
filters=[["default_sales_partner", "=", collection_name]],
|
filters=[["default_sales_partner", "=", collection_name]],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -244,7 +245,12 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
customer_list.append(
|
customer_list.append(
|
||||||
{"name": customer.name, "primary_email": primary_email, "billing_email": billing_email}
|
{
|
||||||
|
"name": customer.name,
|
||||||
|
"customer_name": customer.customer_name,
|
||||||
|
"primary_email": primary_email,
|
||||||
|
"billing_email": billing_email,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
return customer_list
|
return customer_list
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_workflow": 1,
|
|
||||||
"creation": "2020-08-03 16:35:21.852178",
|
"creation": "2020-08-03 16:35:21.852178",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"customer",
|
"customer",
|
||||||
|
"customer_name",
|
||||||
"billing_email",
|
"billing_email",
|
||||||
"primary_email"
|
"primary_email"
|
||||||
],
|
],
|
||||||
@@ -30,11 +30,18 @@
|
|||||||
"fieldtype": "Read Only",
|
"fieldtype": "Read Only",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Billing Email"
|
"label": "Billing Email"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "customer.customer_name",
|
||||||
|
"fieldname": "customer_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Customer Name",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-08-03 22:55:38.875601",
|
"modified": "2023-03-13 00:12:34.508086",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Process Statement Of Accounts Customer",
|
"name": "Process Statement Of Accounts Customer",
|
||||||
@@ -43,5 +50,6 @@
|
|||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,8 @@ def prepare_companywise_opening_balance(asset_data, liability_data, equity_data,
|
|||||||
for data in [asset_data, liability_data, equity_data]:
|
for data in [asset_data, liability_data, equity_data]:
|
||||||
if data:
|
if data:
|
||||||
account_name = get_root_account_name(data[0].root_type, company)
|
account_name = get_root_account_name(data[0].root_type, company)
|
||||||
opening_value += get_opening_balance(account_name, data, company) or 0.0
|
if account_name:
|
||||||
|
opening_value += get_opening_balance(account_name, data, company) or 0.0
|
||||||
|
|
||||||
opening_balance[company] = opening_value
|
opening_balance[company] = opening_value
|
||||||
|
|
||||||
@@ -155,7 +156,7 @@ def get_opening_balance(account_name, data, company):
|
|||||||
|
|
||||||
|
|
||||||
def get_root_account_name(root_type, company):
|
def get_root_account_name(root_type, company):
|
||||||
return frappe.get_all(
|
root_account = frappe.get_all(
|
||||||
"Account",
|
"Account",
|
||||||
fields=["account_name"],
|
fields=["account_name"],
|
||||||
filters={
|
filters={
|
||||||
@@ -165,7 +166,10 @@ def get_root_account_name(root_type, company):
|
|||||||
"parent_account": ("is", "not set"),
|
"parent_account": ("is", "not set"),
|
||||||
},
|
},
|
||||||
as_list=1,
|
as_list=1,
|
||||||
)[0][0]
|
)
|
||||||
|
|
||||||
|
if root_account:
|
||||||
|
return root_account[0][0]
|
||||||
|
|
||||||
|
|
||||||
def get_profit_loss_data(fiscal_year, companies, columns, filters):
|
def get_profit_loss_data(fiscal_year, companies, columns, filters):
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ def validate_filters(filters):
|
|||||||
|
|
||||||
|
|
||||||
def get_data(filters):
|
def get_data(filters):
|
||||||
|
|
||||||
accounts = frappe.db.sql(
|
accounts = frappe.db.sql(
|
||||||
"""select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt
|
"""select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt
|
||||||
|
|
||||||
@@ -118,12 +117,10 @@ def get_data(filters):
|
|||||||
ignore_closing_entries=not flt(filters.with_period_closing_entry),
|
ignore_closing_entries=not flt(filters.with_period_closing_entry),
|
||||||
)
|
)
|
||||||
|
|
||||||
total_row = calculate_values(
|
calculate_values(accounts, gl_entries_by_account, opening_balances)
|
||||||
accounts, gl_entries_by_account, opening_balances, filters, company_currency
|
|
||||||
)
|
|
||||||
accumulate_values_into_parents(accounts, accounts_by_name)
|
accumulate_values_into_parents(accounts, accounts_by_name)
|
||||||
|
|
||||||
data = prepare_data(accounts, filters, total_row, parent_children_map, company_currency)
|
data = prepare_data(accounts, filters, parent_children_map, company_currency)
|
||||||
data = filter_out_zero_value_rows(
|
data = filter_out_zero_value_rows(
|
||||||
data, parent_children_map, show_zero_values=filters.get("show_zero_values")
|
data, parent_children_map, show_zero_values=filters.get("show_zero_values")
|
||||||
)
|
)
|
||||||
@@ -218,7 +215,7 @@ def get_rootwise_opening_balances(filters, report_type):
|
|||||||
return opening
|
return opening
|
||||||
|
|
||||||
|
|
||||||
def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, company_currency):
|
def calculate_values(accounts, gl_entries_by_account, opening_balances):
|
||||||
init = {
|
init = {
|
||||||
"opening_debit": 0.0,
|
"opening_debit": 0.0,
|
||||||
"opening_credit": 0.0,
|
"opening_credit": 0.0,
|
||||||
@@ -228,22 +225,6 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters,
|
|||||||
"closing_credit": 0.0,
|
"closing_credit": 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
total_row = {
|
|
||||||
"account": "'" + _("Total") + "'",
|
|
||||||
"account_name": "'" + _("Total") + "'",
|
|
||||||
"warn_if_negative": True,
|
|
||||||
"opening_debit": 0.0,
|
|
||||||
"opening_credit": 0.0,
|
|
||||||
"debit": 0.0,
|
|
||||||
"credit": 0.0,
|
|
||||||
"closing_debit": 0.0,
|
|
||||||
"closing_credit": 0.0,
|
|
||||||
"parent_account": None,
|
|
||||||
"indent": 0,
|
|
||||||
"has_value": True,
|
|
||||||
"currency": company_currency,
|
|
||||||
}
|
|
||||||
|
|
||||||
for d in accounts:
|
for d in accounts:
|
||||||
d.update(init.copy())
|
d.update(init.copy())
|
||||||
|
|
||||||
@@ -261,8 +242,28 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters,
|
|||||||
|
|
||||||
prepare_opening_closing(d)
|
prepare_opening_closing(d)
|
||||||
|
|
||||||
for field in value_fields:
|
|
||||||
total_row[field] += d[field]
|
def calculate_total_row(accounts, company_currency):
|
||||||
|
total_row = {
|
||||||
|
"account": "'" + _("Total") + "'",
|
||||||
|
"account_name": "'" + _("Total") + "'",
|
||||||
|
"warn_if_negative": True,
|
||||||
|
"opening_debit": 0.0,
|
||||||
|
"opening_credit": 0.0,
|
||||||
|
"debit": 0.0,
|
||||||
|
"credit": 0.0,
|
||||||
|
"closing_debit": 0.0,
|
||||||
|
"closing_credit": 0.0,
|
||||||
|
"parent_account": None,
|
||||||
|
"indent": 0,
|
||||||
|
"has_value": True,
|
||||||
|
"currency": company_currency,
|
||||||
|
}
|
||||||
|
|
||||||
|
for d in accounts:
|
||||||
|
if not d.parent_account:
|
||||||
|
for field in value_fields:
|
||||||
|
total_row[field] += d[field]
|
||||||
|
|
||||||
return total_row
|
return total_row
|
||||||
|
|
||||||
@@ -274,7 +275,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name):
|
|||||||
accounts_by_name[d.parent_account][key] += d[key]
|
accounts_by_name[d.parent_account][key] += d[key]
|
||||||
|
|
||||||
|
|
||||||
def prepare_data(accounts, filters, total_row, parent_children_map, company_currency):
|
def prepare_data(accounts, filters, parent_children_map, company_currency):
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
for d in accounts:
|
for d in accounts:
|
||||||
@@ -305,6 +306,7 @@ def prepare_data(accounts, filters, total_row, parent_children_map, company_curr
|
|||||||
row["has_value"] = has_value
|
row["has_value"] = has_value
|
||||||
data.append(row)
|
data.append(row)
|
||||||
|
|
||||||
|
total_row = calculate_total_row(accounts, company_currency)
|
||||||
data.extend([{}, total_row])
|
data.extend([{}, total_row])
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype):
|
|||||||
fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)]
|
fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)]
|
||||||
|
|
||||||
# Used retrun against and supplier and is_retrun because there is an index added for it
|
# Used retrun against and supplier and is_retrun because there is an index added for it
|
||||||
data = frappe.db.get_list(
|
data = frappe.get_all(
|
||||||
doctype,
|
doctype,
|
||||||
fields=fields,
|
fields=fields,
|
||||||
filters=[
|
filters=[
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ class LeaveAllocation(Document):
|
|||||||
if self.carry_forward:
|
if self.carry_forward:
|
||||||
self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True)
|
self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True)
|
||||||
|
|
||||||
|
# nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
|
||||||
def on_update_after_submit(self):
|
def on_update_after_submit(self):
|
||||||
if self.has_value_changed("new_leaves_allocated"):
|
if self.has_value_changed("new_leaves_allocated"):
|
||||||
self.validate_against_leave_applications()
|
self.validate_against_leave_applications()
|
||||||
@@ -99,7 +100,11 @@ class LeaveAllocation(Document):
|
|||||||
# run required validations again since total leaves are being updated
|
# run required validations again since total leaves are being updated
|
||||||
self.validate_leave_days_and_dates()
|
self.validate_leave_days_and_dates()
|
||||||
|
|
||||||
leaves_to_be_added = self.new_leaves_allocated - self.get_existing_leave_count()
|
leaves_to_be_added = flt(
|
||||||
|
(self.new_leaves_allocated - self.get_existing_leave_count()),
|
||||||
|
self.precision("new_leaves_allocated"),
|
||||||
|
)
|
||||||
|
|
||||||
args = {
|
args = {
|
||||||
"leaves": leaves_to_be_added,
|
"leaves": leaves_to_be_added,
|
||||||
"from_date": self.from_date,
|
"from_date": self.from_date,
|
||||||
@@ -118,14 +123,13 @@ class LeaveAllocation(Document):
|
|||||||
"employee": self.employee,
|
"employee": self.employee,
|
||||||
"company": self.company,
|
"company": self.company,
|
||||||
"leave_type": self.leave_type,
|
"leave_type": self.leave_type,
|
||||||
|
"is_carry_forward": 0,
|
||||||
|
"docstatus": 1,
|
||||||
},
|
},
|
||||||
pluck="leaves",
|
fields=["SUM(leaves) as total_leaves"],
|
||||||
)
|
)
|
||||||
total_existing_leaves = 0
|
|
||||||
for entry in ledger_entries:
|
|
||||||
total_existing_leaves += entry
|
|
||||||
|
|
||||||
return total_existing_leaves
|
return ledger_entries[0].total_leaves if ledger_entries else 0
|
||||||
|
|
||||||
def validate_against_leave_applications(self):
|
def validate_against_leave_applications(self):
|
||||||
leaves_taken = get_approved_leaves_for_period(
|
leaves_taken = get_approved_leaves_for_period(
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class TestLeaveAllocation(FrappeTestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
frappe.db.delete("Leave Period")
|
frappe.db.delete("Leave Period")
|
||||||
frappe.db.delete("Leave Allocation")
|
frappe.db.delete("Leave Allocation")
|
||||||
|
frappe.db.delete("Leave Ledger Entry")
|
||||||
|
|
||||||
emp_id = make_employee("test_emp_leave_allocation@salary.com", company="_Test Company")
|
emp_id = make_employee("test_emp_leave_allocation@salary.com", company="_Test Company")
|
||||||
self.employee = frappe.get_doc("Employee", emp_id)
|
self.employee = frappe.get_doc("Employee", emp_id)
|
||||||
@@ -69,7 +70,6 @@ class TestLeaveAllocation(FrappeTestCase):
|
|||||||
|
|
||||||
def test_validation_for_over_allocation(self):
|
def test_validation_for_over_allocation(self):
|
||||||
leave_type = create_leave_type(leave_type_name="Test Over Allocation", is_carry_forward=1)
|
leave_type = create_leave_type(leave_type_name="Test Over Allocation", is_carry_forward=1)
|
||||||
leave_type.save()
|
|
||||||
|
|
||||||
doc = frappe.get_doc(
|
doc = frappe.get_doc(
|
||||||
{
|
{
|
||||||
@@ -137,9 +137,9 @@ class TestLeaveAllocation(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
).insert()
|
).insert()
|
||||||
|
|
||||||
leave_type = create_leave_type(leave_type_name="_Test Allocation Validation", is_carry_forward=1)
|
leave_type = create_leave_type(
|
||||||
leave_type.max_leaves_allowed = 25
|
leave_type_name="_Test Allocation Validation", is_carry_forward=1, max_leaves_allowed=25
|
||||||
leave_type.save()
|
)
|
||||||
|
|
||||||
# 15 leaves allocated in this period
|
# 15 leaves allocated in this period
|
||||||
allocation = create_leave_allocation(
|
allocation = create_leave_allocation(
|
||||||
@@ -174,9 +174,9 @@ class TestLeaveAllocation(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
).insert()
|
).insert()
|
||||||
|
|
||||||
leave_type = create_leave_type(leave_type_name="_Test Allocation Validation", is_carry_forward=1)
|
leave_type = create_leave_type(
|
||||||
leave_type.max_leaves_allowed = 30
|
leave_type_name="_Test Allocation Validation", is_carry_forward=1, max_leaves_allowed=30
|
||||||
leave_type.save()
|
)
|
||||||
|
|
||||||
# 15 leaves allocated
|
# 15 leaves allocated
|
||||||
allocation = create_leave_allocation(
|
allocation = create_leave_allocation(
|
||||||
@@ -207,7 +207,6 @@ class TestLeaveAllocation(FrappeTestCase):
|
|||||||
|
|
||||||
def test_validate_back_dated_allocation_update(self):
|
def test_validate_back_dated_allocation_update(self):
|
||||||
leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
|
leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
|
||||||
leave_type.save()
|
|
||||||
|
|
||||||
# initial leave allocation = 15
|
# initial leave allocation = 15
|
||||||
leave_allocation = create_leave_allocation(
|
leave_allocation = create_leave_allocation(
|
||||||
@@ -235,10 +234,12 @@ class TestLeaveAllocation(FrappeTestCase):
|
|||||||
self.assertRaises(BackDatedAllocationError, leave_allocation.save)
|
self.assertRaises(BackDatedAllocationError, leave_allocation.save)
|
||||||
|
|
||||||
def test_carry_forward_calculation(self):
|
def test_carry_forward_calculation(self):
|
||||||
leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
|
leave_type = create_leave_type(
|
||||||
leave_type.maximum_carry_forwarded_leaves = 10
|
leave_type_name="_Test_CF_leave",
|
||||||
leave_type.max_leaves_allowed = 30
|
is_carry_forward=1,
|
||||||
leave_type.save()
|
maximum_carry_forwarded_leaves=10,
|
||||||
|
max_leaves_allowed=30,
|
||||||
|
)
|
||||||
|
|
||||||
# initial leave allocation = 15
|
# initial leave allocation = 15
|
||||||
leave_allocation = create_leave_allocation(
|
leave_allocation = create_leave_allocation(
|
||||||
@@ -286,7 +287,6 @@ class TestLeaveAllocation(FrappeTestCase):
|
|||||||
is_carry_forward=1,
|
is_carry_forward=1,
|
||||||
expire_carry_forwarded_leaves_after_days=90,
|
expire_carry_forwarded_leaves_after_days=90,
|
||||||
)
|
)
|
||||||
leave_type.save()
|
|
||||||
|
|
||||||
# initial leave allocation
|
# initial leave allocation
|
||||||
leave_allocation = create_leave_allocation(
|
leave_allocation = create_leave_allocation(
|
||||||
@@ -352,12 +352,51 @@ class TestLeaveAllocation(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
leave_allocation.submit()
|
leave_allocation.submit()
|
||||||
leave_allocation.reload()
|
leave_allocation.reload()
|
||||||
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
self.assertEqual(leave_allocation.total_leaves_allocated, 15)
|
||||||
|
|
||||||
leave_allocation.new_leaves_allocated = 40
|
leave_allocation.new_leaves_allocated = 40
|
||||||
leave_allocation.submit()
|
leave_allocation.save()
|
||||||
leave_allocation.reload()
|
leave_allocation.reload()
|
||||||
self.assertTrue(leave_allocation.total_leaves_allocated, 40)
|
|
||||||
|
updated_entry = frappe.db.get_all(
|
||||||
|
"Leave Ledger Entry",
|
||||||
|
{"transaction_name": leave_allocation.name},
|
||||||
|
pluck="leaves",
|
||||||
|
order_by="creation desc",
|
||||||
|
limit=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(updated_entry[0], 25)
|
||||||
|
self.assertEqual(leave_allocation.total_leaves_allocated, 40)
|
||||||
|
|
||||||
|
def test_leave_addition_after_submit_with_carry_forward(self):
|
||||||
|
from erpnext.hr.doctype.leave_application.test_leave_application import (
|
||||||
|
create_carry_forwarded_allocation,
|
||||||
|
)
|
||||||
|
|
||||||
|
leave_type = create_leave_type(
|
||||||
|
leave_type_name="_Test_CF_leave_expiry",
|
||||||
|
is_carry_forward=1,
|
||||||
|
include_holiday=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
leave_allocation = create_carry_forwarded_allocation(self.employee, leave_type)
|
||||||
|
# 15 new leaves, 15 carry forwarded leaves
|
||||||
|
self.assertEqual(leave_allocation.total_leaves_allocated, 30)
|
||||||
|
|
||||||
|
leave_allocation.new_leaves_allocated = 32
|
||||||
|
leave_allocation.save()
|
||||||
|
leave_allocation.reload()
|
||||||
|
|
||||||
|
updated_entry = frappe.db.get_all(
|
||||||
|
"Leave Ledger Entry",
|
||||||
|
{"transaction_name": leave_allocation.name},
|
||||||
|
pluck="leaves",
|
||||||
|
order_by="creation desc",
|
||||||
|
limit=1,
|
||||||
|
)
|
||||||
|
self.assertEqual(updated_entry[0], 17)
|
||||||
|
self.assertEqual(leave_allocation.total_leaves_allocated, 47)
|
||||||
|
|
||||||
def test_leave_subtraction_after_submit(self):
|
def test_leave_subtraction_after_submit(self):
|
||||||
leave_allocation = create_leave_allocation(
|
leave_allocation = create_leave_allocation(
|
||||||
@@ -365,12 +404,49 @@ class TestLeaveAllocation(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
leave_allocation.submit()
|
leave_allocation.submit()
|
||||||
leave_allocation.reload()
|
leave_allocation.reload()
|
||||||
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
self.assertEqual(leave_allocation.total_leaves_allocated, 15)
|
||||||
|
|
||||||
leave_allocation.new_leaves_allocated = 10
|
leave_allocation.new_leaves_allocated = 10
|
||||||
leave_allocation.submit()
|
leave_allocation.submit()
|
||||||
leave_allocation.reload()
|
leave_allocation.reload()
|
||||||
self.assertTrue(leave_allocation.total_leaves_allocated, 10)
|
|
||||||
|
updated_entry = frappe.db.get_all(
|
||||||
|
"Leave Ledger Entry",
|
||||||
|
{"transaction_name": leave_allocation.name},
|
||||||
|
pluck="leaves",
|
||||||
|
order_by="creation desc",
|
||||||
|
limit=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(updated_entry[0], -5)
|
||||||
|
self.assertEqual(leave_allocation.total_leaves_allocated, 10)
|
||||||
|
|
||||||
|
def test_leave_subtraction_after_submit_with_carry_forward(self):
|
||||||
|
from erpnext.hr.doctype.leave_application.test_leave_application import (
|
||||||
|
create_carry_forwarded_allocation,
|
||||||
|
)
|
||||||
|
|
||||||
|
leave_type = create_leave_type(
|
||||||
|
leave_type_name="_Test_CF_leave_expiry",
|
||||||
|
is_carry_forward=1,
|
||||||
|
include_holiday=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
leave_allocation = create_carry_forwarded_allocation(self.employee, leave_type)
|
||||||
|
self.assertEqual(leave_allocation.total_leaves_allocated, 30)
|
||||||
|
|
||||||
|
leave_allocation.new_leaves_allocated = 8
|
||||||
|
leave_allocation.save()
|
||||||
|
|
||||||
|
updated_entry = frappe.db.get_all(
|
||||||
|
"Leave Ledger Entry",
|
||||||
|
{"transaction_name": leave_allocation.name},
|
||||||
|
pluck="leaves",
|
||||||
|
order_by="creation desc",
|
||||||
|
limit=1,
|
||||||
|
)
|
||||||
|
self.assertEqual(updated_entry[0], -7)
|
||||||
|
self.assertEqual(leave_allocation.total_leaves_allocated, 23)
|
||||||
|
|
||||||
def test_validation_against_leave_application_after_submit(self):
|
def test_validation_against_leave_application_after_submit(self):
|
||||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
|
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
|
||||||
|
|||||||
@@ -856,6 +856,7 @@ def get_leave_allocation_records(employee, date, leave_type=None):
|
|||||||
Min(Ledger.from_date).as_("from_date"),
|
Min(Ledger.from_date).as_("from_date"),
|
||||||
Max(Ledger.to_date).as_("to_date"),
|
Max(Ledger.to_date).as_("to_date"),
|
||||||
Ledger.leave_type,
|
Ledger.leave_type,
|
||||||
|
Ledger.employee,
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(Ledger.from_date <= date)
|
(Ledger.from_date <= date)
|
||||||
@@ -895,6 +896,7 @@ def get_leave_allocation_records(employee, date, leave_type=None):
|
|||||||
"unused_leaves": d.cf_leaves,
|
"unused_leaves": d.cf_leaves,
|
||||||
"new_leaves_allocated": d.new_leaves,
|
"new_leaves_allocated": d.new_leaves,
|
||||||
"leave_type": d.leave_type,
|
"leave_type": d.leave_type,
|
||||||
|
"employee": d.employee,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -933,26 +935,51 @@ def get_remaining_leaves(
|
|||||||
|
|
||||||
return remaining_leaves
|
return remaining_leaves
|
||||||
|
|
||||||
leave_balance = leave_balance_for_consumption = flt(allocation.total_leaves_allocated) + flt(
|
|
||||||
leaves_taken
|
|
||||||
)
|
|
||||||
|
|
||||||
# balance for carry forwarded leaves
|
|
||||||
if cf_expiry and allocation.unused_leaves:
|
if cf_expiry and allocation.unused_leaves:
|
||||||
|
# allocation contains both carry forwarded and new leaves
|
||||||
|
new_leaves_taken, cf_leaves_taken = get_new_and_cf_leaves_taken(allocation, cf_expiry)
|
||||||
|
|
||||||
if getdate(date) > getdate(cf_expiry):
|
if getdate(date) > getdate(cf_expiry):
|
||||||
# carry forwarded leave expiry date passed
|
# carry forwarded leaves have expired
|
||||||
cf_leaves = remaining_cf_leaves = 0
|
cf_leaves = remaining_cf_leaves = 0
|
||||||
else:
|
else:
|
||||||
cf_leaves = flt(allocation.unused_leaves) + flt(leaves_taken)
|
cf_leaves = flt(allocation.unused_leaves) + flt(cf_leaves_taken)
|
||||||
remaining_cf_leaves = _get_remaining_leaves(cf_leaves, cf_expiry)
|
remaining_cf_leaves = _get_remaining_leaves(cf_leaves, cf_expiry)
|
||||||
|
|
||||||
leave_balance = flt(allocation.new_leaves_allocated) + flt(cf_leaves)
|
# new leaves allocated - new leaves taken + cf leave balance
|
||||||
leave_balance_for_consumption = flt(allocation.new_leaves_allocated) + flt(remaining_cf_leaves)
|
# Note: `new_leaves_taken` is added here because its already a -ve number in the ledger
|
||||||
|
leave_balance = (flt(allocation.new_leaves_allocated) + flt(new_leaves_taken)) + flt(cf_leaves)
|
||||||
|
leave_balance_for_consumption = (
|
||||||
|
flt(allocation.new_leaves_allocated) + flt(new_leaves_taken)
|
||||||
|
) + flt(remaining_cf_leaves)
|
||||||
|
else:
|
||||||
|
# allocation only contains newly allocated leaves
|
||||||
|
leave_balance = leave_balance_for_consumption = flt(allocation.total_leaves_allocated) + flt(
|
||||||
|
leaves_taken
|
||||||
|
)
|
||||||
|
|
||||||
remaining_leaves = _get_remaining_leaves(leave_balance_for_consumption, allocation.to_date)
|
remaining_leaves = _get_remaining_leaves(leave_balance_for_consumption, allocation.to_date)
|
||||||
return frappe._dict(leave_balance=leave_balance, leave_balance_for_consumption=remaining_leaves)
|
return frappe._dict(leave_balance=leave_balance, leave_balance_for_consumption=remaining_leaves)
|
||||||
|
|
||||||
|
|
||||||
|
def get_new_and_cf_leaves_taken(allocation: Dict, cf_expiry: str) -> Tuple[float, float]:
|
||||||
|
"""returns new leaves taken and carry forwarded leaves taken within an allocation period based on cf leave expiry"""
|
||||||
|
cf_leaves_taken = get_leaves_for_period(
|
||||||
|
allocation.employee, allocation.leave_type, allocation.from_date, cf_expiry
|
||||||
|
)
|
||||||
|
new_leaves_taken = get_leaves_for_period(
|
||||||
|
allocation.employee, allocation.leave_type, add_days(cf_expiry, 1), allocation.to_date
|
||||||
|
)
|
||||||
|
|
||||||
|
# using abs because leaves taken is a -ve number in the ledger
|
||||||
|
if abs(cf_leaves_taken) > allocation.unused_leaves:
|
||||||
|
# adjust the excess leaves in new_leaves_taken
|
||||||
|
new_leaves_taken += -(abs(cf_leaves_taken) - allocation.unused_leaves)
|
||||||
|
cf_leaves_taken = -allocation.unused_leaves
|
||||||
|
|
||||||
|
return new_leaves_taken, cf_leaves_taken
|
||||||
|
|
||||||
|
|
||||||
def get_leaves_for_period(
|
def get_leaves_for_period(
|
||||||
employee: str, leave_type: str, from_date: str, to_date: str, skip_expired_leaves: bool = True
|
employee: str, leave_type: str, from_date: str, to_date: str, skip_expired_leaves: bool = True
|
||||||
) -> float:
|
) -> float:
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from erpnext.hr.doctype.leave_application.leave_application import (
|
|||||||
get_leave_allocation_records,
|
get_leave_allocation_records,
|
||||||
get_leave_balance_on,
|
get_leave_balance_on,
|
||||||
get_leave_details,
|
get_leave_details,
|
||||||
|
get_new_and_cf_leaves_taken,
|
||||||
)
|
)
|
||||||
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
|
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
|
||||||
create_assignment_for_multiple_employees,
|
create_assignment_for_multiple_employees,
|
||||||
@@ -96,6 +97,9 @@ class TestLeaveApplication(unittest.TestCase):
|
|||||||
from_date = get_year_start(getdate())
|
from_date = get_year_start(getdate())
|
||||||
to_date = get_year_ending(getdate())
|
to_date = get_year_ending(getdate())
|
||||||
self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
|
self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
|
||||||
|
list_without_weekly_offs = make_holiday_list(
|
||||||
|
"Holiday List w/o Weekly Offs", from_date=from_date, to_date=to_date, add_weekly_offs=False
|
||||||
|
)
|
||||||
|
|
||||||
if not frappe.db.exists("Leave Type", "_Test Leave Type"):
|
if not frappe.db.exists("Leave Type", "_Test Leave Type"):
|
||||||
frappe.get_doc(
|
frappe.get_doc(
|
||||||
@@ -698,7 +702,7 @@ class TestLeaveApplication(unittest.TestCase):
|
|||||||
leave_type_name="_Test_CF_leave_expiry",
|
leave_type_name="_Test_CF_leave_expiry",
|
||||||
is_carry_forward=1,
|
is_carry_forward=1,
|
||||||
expire_carry_forwarded_leaves_after_days=90,
|
expire_carry_forwarded_leaves_after_days=90,
|
||||||
).insert()
|
)
|
||||||
|
|
||||||
create_carry_forwarded_allocation(employee, leave_type)
|
create_carry_forwarded_allocation(employee, leave_type)
|
||||||
details = get_leave_balance_on(
|
details = get_leave_balance_on(
|
||||||
@@ -770,7 +774,6 @@ class TestLeaveApplication(unittest.TestCase):
|
|||||||
employee = get_employee()
|
employee = get_employee()
|
||||||
|
|
||||||
leave_type = create_leave_type(leave_type_name="Test Leave Type 1")
|
leave_type = create_leave_type(leave_type_name="Test Leave Type 1")
|
||||||
leave_type.save()
|
|
||||||
|
|
||||||
leave_allocation = create_leave_allocation(
|
leave_allocation = create_leave_allocation(
|
||||||
employee=employee.name, employee_name=employee.employee_name, leave_type=leave_type.name
|
employee=employee.name, employee_name=employee.employee_name, leave_type=leave_type.name
|
||||||
@@ -813,7 +816,6 @@ class TestLeaveApplication(unittest.TestCase):
|
|||||||
expire_carry_forwarded_leaves_after_days=90,
|
expire_carry_forwarded_leaves_after_days=90,
|
||||||
include_holiday=True,
|
include_holiday=True,
|
||||||
)
|
)
|
||||||
leave_type.submit()
|
|
||||||
|
|
||||||
create_carry_forwarded_allocation(employee, leave_type)
|
create_carry_forwarded_allocation(employee, leave_type)
|
||||||
|
|
||||||
@@ -852,7 +854,6 @@ class TestLeaveApplication(unittest.TestCase):
|
|||||||
is_carry_forward=1,
|
is_carry_forward=1,
|
||||||
expire_carry_forwarded_leaves_after_days=90,
|
expire_carry_forwarded_leaves_after_days=90,
|
||||||
)
|
)
|
||||||
leave_type.submit()
|
|
||||||
|
|
||||||
create_carry_forwarded_allocation(employee, leave_type)
|
create_carry_forwarded_allocation(employee, leave_type)
|
||||||
|
|
||||||
@@ -990,25 +991,29 @@ class TestLeaveApplication(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
self.assertEqual(leave_allocation, expected)
|
self.assertEqual(leave_allocation, expected)
|
||||||
|
|
||||||
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
@set_holiday_list("Holiday List w/o Weekly Offs", "_Test Company")
|
||||||
def test_leave_details_with_expired_cf_leaves(self):
|
def test_leave_details_with_expired_cf_leaves(self):
|
||||||
|
"""Tests leave details:
|
||||||
|
Case 1: All leaves available before cf leave expiry
|
||||||
|
Case 2: Remaining Leaves after cf leave expiry
|
||||||
|
"""
|
||||||
employee = get_employee()
|
employee = get_employee()
|
||||||
leave_type = create_leave_type(
|
leave_type = create_leave_type(
|
||||||
leave_type_name="_Test_CF_leave_expiry",
|
leave_type_name="_Test_CF_leave_expiry",
|
||||||
is_carry_forward=1,
|
is_carry_forward=1,
|
||||||
expire_carry_forwarded_leaves_after_days=90,
|
expire_carry_forwarded_leaves_after_days=90,
|
||||||
).insert()
|
)
|
||||||
|
|
||||||
leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
|
leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
|
||||||
cf_expiry = frappe.db.get_value(
|
cf_expiry = frappe.db.get_value(
|
||||||
"Leave Ledger Entry", {"transaction_name": leave_alloc.name, "is_carry_forward": 1}, "to_date"
|
"Leave Ledger Entry", {"transaction_name": leave_alloc.name, "is_carry_forward": 1}, "to_date"
|
||||||
)
|
)
|
||||||
|
|
||||||
# all leaves available before cf leave expiry
|
# case 1: all leaves available before cf leave expiry
|
||||||
leave_details = get_leave_details(employee.name, add_days(cf_expiry, -1))
|
leave_details = get_leave_details(employee.name, add_days(cf_expiry, -1))
|
||||||
self.assertEqual(leave_details["leave_allocation"][leave_type.name]["remaining_leaves"], 30.0)
|
self.assertEqual(leave_details["leave_allocation"][leave_type.name]["remaining_leaves"], 30.0)
|
||||||
|
|
||||||
# cf leaves expired
|
# case 2: cf leaves expired
|
||||||
leave_details = get_leave_details(employee.name, add_days(cf_expiry, 1))
|
leave_details = get_leave_details(employee.name, add_days(cf_expiry, 1))
|
||||||
expected_data = {
|
expected_data = {
|
||||||
"total_leaves": 30.0,
|
"total_leaves": 30.0,
|
||||||
@@ -1017,6 +1022,119 @@ class TestLeaveApplication(unittest.TestCase):
|
|||||||
"leaves_pending_approval": 0.0,
|
"leaves_pending_approval": 0.0,
|
||||||
"remaining_leaves": 15.0,
|
"remaining_leaves": 15.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.assertEqual(leave_details["leave_allocation"][leave_type.name], expected_data)
|
||||||
|
|
||||||
|
@set_holiday_list("Holiday List w/o Weekly Offs", "_Test Company")
|
||||||
|
def test_leave_details_with_application_across_cf_expiry(self):
|
||||||
|
"""Tests leave details with leave application across cf expiry, such that:
|
||||||
|
cf leaves are partially expired and partially consumed
|
||||||
|
"""
|
||||||
|
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_alloc = create_carry_forwarded_allocation(employee, leave_type)
|
||||||
|
cf_expiry = frappe.db.get_value(
|
||||||
|
"Leave Ledger Entry", {"transaction_name": leave_alloc.name, "is_carry_forward": 1}, "to_date"
|
||||||
|
)
|
||||||
|
|
||||||
|
# leave application across cf expiry
|
||||||
|
application = make_leave_application(
|
||||||
|
employee.name,
|
||||||
|
cf_expiry,
|
||||||
|
add_days(cf_expiry, 3),
|
||||||
|
leave_type.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
leave_details = get_leave_details(employee.name, add_days(cf_expiry, 4))
|
||||||
|
expected_data = {
|
||||||
|
"total_leaves": 30.0,
|
||||||
|
"expired_leaves": 14.0,
|
||||||
|
"leaves_taken": 4.0,
|
||||||
|
"leaves_pending_approval": 0.0,
|
||||||
|
"remaining_leaves": 12.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertEqual(leave_details["leave_allocation"][leave_type.name], expected_data)
|
||||||
|
|
||||||
|
@set_holiday_list("Holiday List w/o Weekly Offs", "_Test Company")
|
||||||
|
def test_leave_details_with_application_across_cf_expiry_2(self):
|
||||||
|
"""Tests the same case as above but with leave days greater than cf leaves allocated"""
|
||||||
|
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_alloc = create_carry_forwarded_allocation(employee, leave_type)
|
||||||
|
cf_expiry = frappe.db.get_value(
|
||||||
|
"Leave Ledger Entry", {"transaction_name": leave_alloc.name, "is_carry_forward": 1}, "to_date"
|
||||||
|
)
|
||||||
|
|
||||||
|
# leave application across cf expiry, 20 days leave
|
||||||
|
application = make_leave_application(
|
||||||
|
employee.name,
|
||||||
|
add_days(cf_expiry, -16),
|
||||||
|
add_days(cf_expiry, 3),
|
||||||
|
leave_type.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 15 cf leaves and 5 new leaves should be consumed
|
||||||
|
# after adjustment of the actual days breakup (17 and 3) because only 15 cf leaves have been allocated
|
||||||
|
new_leaves_taken, cf_leaves_taken = get_new_and_cf_leaves_taken(leave_alloc, cf_expiry)
|
||||||
|
self.assertEqual(new_leaves_taken, -5.0)
|
||||||
|
self.assertEqual(cf_leaves_taken, -15.0)
|
||||||
|
|
||||||
|
leave_details = get_leave_details(employee.name, add_days(cf_expiry, 4))
|
||||||
|
expected_data = {
|
||||||
|
"total_leaves": 30.0,
|
||||||
|
"expired_leaves": 0,
|
||||||
|
"leaves_taken": 20.0,
|
||||||
|
"leaves_pending_approval": 0.0,
|
||||||
|
"remaining_leaves": 10.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertEqual(leave_details["leave_allocation"][leave_type.name], expected_data)
|
||||||
|
|
||||||
|
@set_holiday_list("Holiday List w/o Weekly Offs", "_Test Company")
|
||||||
|
def test_leave_details_with_application_after_cf_expiry(self):
|
||||||
|
"""Tests leave details with leave application after cf expiry, such that:
|
||||||
|
cf leaves are completely expired and only newly allocated leaves are consumed
|
||||||
|
"""
|
||||||
|
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_alloc = create_carry_forwarded_allocation(employee, leave_type)
|
||||||
|
cf_expiry = frappe.db.get_value(
|
||||||
|
"Leave Ledger Entry", {"transaction_name": leave_alloc.name, "is_carry_forward": 1}, "to_date"
|
||||||
|
)
|
||||||
|
|
||||||
|
# leave application after cf expiry
|
||||||
|
application = make_leave_application(
|
||||||
|
employee.name,
|
||||||
|
add_days(cf_expiry, 1),
|
||||||
|
add_days(cf_expiry, 4),
|
||||||
|
leave_type.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
leave_details = get_leave_details(employee.name, add_days(cf_expiry, 4))
|
||||||
|
expected_data = {
|
||||||
|
"total_leaves": 30.0,
|
||||||
|
"expired_leaves": 15.0,
|
||||||
|
"leaves_taken": 4.0,
|
||||||
|
"leaves_pending_approval": 0.0,
|
||||||
|
"remaining_leaves": 11.0,
|
||||||
|
}
|
||||||
|
|
||||||
self.assertEqual(leave_details["leave_allocation"][leave_type.name], expected_data)
|
self.assertEqual(leave_details["leave_allocation"][leave_type.name], expected_data)
|
||||||
|
|
||||||
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||||
@@ -1027,7 +1145,7 @@ class TestLeaveApplication(unittest.TestCase):
|
|||||||
leave_type_name="_Test_CF_leave_expiry",
|
leave_type_name="_Test_CF_leave_expiry",
|
||||||
is_carry_forward=1,
|
is_carry_forward=1,
|
||||||
expire_carry_forwarded_leaves_after_days=90,
|
expire_carry_forwarded_leaves_after_days=90,
|
||||||
).insert()
|
)
|
||||||
|
|
||||||
leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
|
leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
|
||||||
cf_expiry = frappe.db.get_value(
|
cf_expiry = frappe.db.get_value(
|
||||||
@@ -1043,6 +1161,7 @@ class TestLeaveApplication(unittest.TestCase):
|
|||||||
"unused_leaves": 15.0,
|
"unused_leaves": 15.0,
|
||||||
"new_leaves_allocated": 15.0,
|
"new_leaves_allocated": 15.0,
|
||||||
"leave_type": leave_type.name,
|
"leave_type": leave_type.name,
|
||||||
|
"employee": employee.name,
|
||||||
}
|
}
|
||||||
self.assertEqual(details.get(leave_type.name), expected_data)
|
self.assertEqual(details.get(leave_type.name), expected_data)
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ test_records = frappe.get_test_records("Leave Type")
|
|||||||
def create_leave_type(**args):
|
def create_leave_type(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
if frappe.db.exists("Leave Type", args.leave_type_name):
|
if frappe.db.exists("Leave Type", args.leave_type_name):
|
||||||
return frappe.get_doc("Leave Type", args.leave_type_name)
|
frappe.delete_doc("Leave Type", args.leave_type_name, force=True)
|
||||||
|
|
||||||
leave_type = frappe.get_doc(
|
leave_type = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Leave Type",
|
"doctype": "Leave Type",
|
||||||
@@ -23,10 +24,14 @@ def create_leave_type(**args):
|
|||||||
"expire_carry_forwarded_leaves_after_days": args.expire_carry_forwarded_leaves_after_days 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,
|
"encashment_threshold_days": args.encashment_threshold_days or 5,
|
||||||
"earning_component": "Leave Encashment",
|
"earning_component": "Leave Encashment",
|
||||||
|
"max_leaves_allowed": args.max_leaves_allowed,
|
||||||
|
"maximum_carry_forwarded_leaves": args.maximum_carry_forwarded_leaves,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if leave_type.is_ppl:
|
if leave_type.is_ppl:
|
||||||
leave_type.fraction_of_daily_salary_per_leave = args.fraction_of_daily_salary_per_leave or 0.5
|
leave_type.fraction_of_daily_salary_per_leave = args.fraction_of_daily_salary_per_leave or 0.5
|
||||||
|
|
||||||
|
leave_type.insert()
|
||||||
|
|
||||||
return leave_type
|
return leave_type
|
||||||
|
|||||||
@@ -154,7 +154,6 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
|
|||||||
@set_holiday_list("_Test Emp Balance Holiday List", "_Test Company")
|
@set_holiday_list("_Test Emp Balance Holiday List", "_Test Company")
|
||||||
def test_opening_balance_considers_carry_forwarded_leaves(self):
|
def test_opening_balance_considers_carry_forwarded_leaves(self):
|
||||||
leave_type = create_leave_type(leave_type_name="_Test_CF_leave_expiry", is_carry_forward=1)
|
leave_type = create_leave_type(leave_type_name="_Test_CF_leave_expiry", is_carry_forward=1)
|
||||||
leave_type.insert()
|
|
||||||
|
|
||||||
# 30 leaves allocated for first half of the year
|
# 30 leaves allocated for first half of the year
|
||||||
allocation1 = make_allocation_record(
|
allocation1 = make_allocation_record(
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class BOMTree:
|
|||||||
|
|
||||||
# specifying the attributes to save resources
|
# specifying the attributes to save resources
|
||||||
# ref: https://docs.python.org/3/reference/datamodel.html#slots
|
# ref: https://docs.python.org/3/reference/datamodel.html#slots
|
||||||
__slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"]
|
__slots__ = ["name", "child_items", "is_bom", "item_code", "qty", "exploded_qty", "bom_qty"]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1
|
self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1
|
||||||
@@ -50,9 +50,10 @@ class BOMTree:
|
|||||||
def __create_tree(self):
|
def __create_tree(self):
|
||||||
bom = frappe.get_cached_doc("BOM", self.name)
|
bom = frappe.get_cached_doc("BOM", self.name)
|
||||||
self.item_code = bom.item
|
self.item_code = bom.item
|
||||||
|
self.bom_qty = bom.quantity
|
||||||
|
|
||||||
for item in bom.get("items", []):
|
for item in bom.get("items", []):
|
||||||
qty = item.qty / bom.quantity # quantity per unit
|
qty = item.stock_qty / bom.quantity # quantity per unit
|
||||||
exploded_qty = self.exploded_qty * qty
|
exploded_qty = self.exploded_qty * qty
|
||||||
if item.bom_no:
|
if item.bom_no:
|
||||||
child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty)
|
child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty)
|
||||||
|
|||||||
@@ -476,7 +476,7 @@ frappe.ui.form.on("Work Order Item", {
|
|||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if (r.message) {
|
if (r.message) {
|
||||||
frappe.model.set_value(cdt, cdn, {
|
frappe.model.set_value(cdt, cdn, {
|
||||||
"required_qty": 1,
|
"required_qty": row.required_qty || 1,
|
||||||
"item_name": r.message.item_name,
|
"item_name": r.message.item_name,
|
||||||
"description": r.message.description,
|
"description": r.message.description,
|
||||||
"source_warehouse": r.message.default_warehouse,
|
"source_warehouse": r.message.default_warehouse,
|
||||||
|
|||||||
@@ -690,7 +690,7 @@ class WorkOrder(Document):
|
|||||||
|
|
||||||
for node in bom_traversal:
|
for node in bom_traversal:
|
||||||
if node.is_bom:
|
if node.is_bom:
|
||||||
operations.extend(_get_operations(node.name, qty=node.exploded_qty))
|
operations.extend(_get_operations(node.name, qty=node.exploded_qty / node.bom_qty))
|
||||||
|
|
||||||
bom_qty = frappe.get_cached_value("BOM", self.bom_no, "quantity")
|
bom_qty = frappe.get_cached_value("BOM", self.bom_no, "quantity")
|
||||||
operations.extend(_get_operations(self.bom_no, qty=1.0 / bom_qty))
|
operations.extend(_get_operations(self.bom_no, qty=1.0 / bom_qty))
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import Floor, Sum
|
||||||
|
from frappe.utils import cint
|
||||||
from pypika.terms import ExistsCriterion
|
from pypika.terms import ExistsCriterion
|
||||||
|
|
||||||
|
|
||||||
@@ -34,57 +35,55 @@ def get_columns():
|
|||||||
|
|
||||||
|
|
||||||
def get_bom_stock(filters):
|
def get_bom_stock(filters):
|
||||||
qty_to_produce = filters.get("qty_to_produce") or 1
|
qty_to_produce = filters.get("qty_to_produce")
|
||||||
if int(qty_to_produce) < 0:
|
if cint(qty_to_produce) <= 0:
|
||||||
frappe.throw(_("Quantity to Produce can not be less than Zero"))
|
frappe.throw(_("Quantity to Produce should be greater than zero."))
|
||||||
|
|
||||||
if filters.get("show_exploded_view"):
|
if filters.get("show_exploded_view"):
|
||||||
bom_item_table = "BOM Explosion Item"
|
bom_item_table = "BOM Explosion Item"
|
||||||
else:
|
else:
|
||||||
bom_item_table = "BOM Item"
|
bom_item_table = "BOM Item"
|
||||||
|
|
||||||
bin = frappe.qb.DocType("Bin")
|
warehouse_details = frappe.db.get_value(
|
||||||
bom = frappe.qb.DocType("BOM")
|
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
|
||||||
bom_item = frappe.qb.DocType(bom_item_table)
|
|
||||||
|
|
||||||
query = (
|
|
||||||
frappe.qb.from_(bom)
|
|
||||||
.inner_join(bom_item)
|
|
||||||
.on(bom.name == bom_item.parent)
|
|
||||||
.left_join(bin)
|
|
||||||
.on(bom_item.item_code == bin.item_code)
|
|
||||||
.select(
|
|
||||||
bom_item.item_code,
|
|
||||||
bom_item.description,
|
|
||||||
bom_item.stock_qty,
|
|
||||||
bom_item.stock_uom,
|
|
||||||
(bom_item.stock_qty / bom.quantity) * qty_to_produce,
|
|
||||||
Sum(bin.actual_qty),
|
|
||||||
Sum(bin.actual_qty) / (bom_item.stock_qty / bom.quantity),
|
|
||||||
)
|
|
||||||
.where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM"))
|
|
||||||
.groupby(bom_item.item_code)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if filters.get("warehouse"):
|
BOM = frappe.qb.DocType("BOM")
|
||||||
warehouse_details = frappe.db.get_value(
|
BOM_ITEM = frappe.qb.DocType(bom_item_table)
|
||||||
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
|
BIN = frappe.qb.DocType("Bin")
|
||||||
)
|
WH = frappe.qb.DocType("Warehouse")
|
||||||
|
CONDITIONS = ()
|
||||||
|
|
||||||
if warehouse_details:
|
if warehouse_details:
|
||||||
wh = frappe.qb.DocType("Warehouse")
|
CONDITIONS = ExistsCriterion(
|
||||||
query = query.where(
|
frappe.qb.from_(WH)
|
||||||
ExistsCriterion(
|
.select(WH.name)
|
||||||
frappe.qb.from_(wh)
|
.where(
|
||||||
.select(wh.name)
|
(WH.lft >= warehouse_details.lft)
|
||||||
.where(
|
& (WH.rgt <= warehouse_details.rgt)
|
||||||
(wh.lft >= warehouse_details.lft)
|
& (BIN.warehouse == WH.name)
|
||||||
& (wh.rgt <= warehouse_details.rgt)
|
|
||||||
& (bin.warehouse == wh.name)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
else:
|
)
|
||||||
query = query.where(bin.warehouse == filters.get("warehouse"))
|
else:
|
||||||
|
CONDITIONS = BIN.warehouse == filters.get("warehouse")
|
||||||
|
|
||||||
return query.run()
|
QUERY = (
|
||||||
|
frappe.qb.from_(BOM)
|
||||||
|
.inner_join(BOM_ITEM)
|
||||||
|
.on(BOM.name == BOM_ITEM.parent)
|
||||||
|
.left_join(BIN)
|
||||||
|
.on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS))
|
||||||
|
.select(
|
||||||
|
BOM_ITEM.item_code,
|
||||||
|
BOM_ITEM.description,
|
||||||
|
BOM_ITEM.stock_qty,
|
||||||
|
BOM_ITEM.stock_uom,
|
||||||
|
BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity,
|
||||||
|
Sum(BIN.actual_qty).as_("actual_qty"),
|
||||||
|
Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))),
|
||||||
|
)
|
||||||
|
.where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM"))
|
||||||
|
.groupby(BOM_ITEM.item_code)
|
||||||
|
)
|
||||||
|
|
||||||
|
return QUERY.run()
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.exceptions import ValidationError
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
from frappe.utils import floor
|
||||||
|
|
||||||
|
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||||
|
from erpnext.manufacturing.report.bom_stock_report.bom_stock_report import (
|
||||||
|
get_bom_stock as bom_stock_report,
|
||||||
|
)
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
|
|
||||||
|
|
||||||
|
class TestBomStockReport(FrappeTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.warehouse = "_Test Warehouse - _TC"
|
||||||
|
self.fg_item, self.rm_items = create_items()
|
||||||
|
make_stock_entry(target=self.warehouse, item_code=self.rm_items[0], qty=20, basic_rate=100)
|
||||||
|
make_stock_entry(target=self.warehouse, item_code=self.rm_items[1], qty=40, basic_rate=200)
|
||||||
|
self.bom = make_bom(item=self.fg_item, quantity=1, raw_materials=self.rm_items, rm_qty=10)
|
||||||
|
|
||||||
|
def test_bom_stock_report(self):
|
||||||
|
# Test 1: When `qty_to_produce` is 0.
|
||||||
|
filters = frappe._dict(
|
||||||
|
{
|
||||||
|
"bom": self.bom.name,
|
||||||
|
"warehouse": "Stores - _TC",
|
||||||
|
"qty_to_produce": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertRaises(ValidationError, bom_stock_report, filters)
|
||||||
|
|
||||||
|
# Test 2: When stock is not available.
|
||||||
|
data = bom_stock_report(
|
||||||
|
frappe._dict(
|
||||||
|
{
|
||||||
|
"bom": self.bom.name,
|
||||||
|
"warehouse": "Stores - _TC",
|
||||||
|
"qty_to_produce": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
expected_data = get_expected_data(self.bom, "Stores - _TC", 1)
|
||||||
|
self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
|
||||||
|
|
||||||
|
# Test 3: When stock is available.
|
||||||
|
data = bom_stock_report(
|
||||||
|
frappe._dict(
|
||||||
|
{
|
||||||
|
"bom": self.bom.name,
|
||||||
|
"warehouse": self.warehouse,
|
||||||
|
"qty_to_produce": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
expected_data = get_expected_data(self.bom, self.warehouse, 1)
|
||||||
|
self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
|
||||||
|
|
||||||
|
|
||||||
|
def create_items():
|
||||||
|
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||||
|
rm_item1 = make_item(
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"standard_rate": 100,
|
||||||
|
"opening_stock": 100,
|
||||||
|
"last_purchase_rate": 100,
|
||||||
|
}
|
||||||
|
).name
|
||||||
|
rm_item2 = make_item(
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"standard_rate": 200,
|
||||||
|
"opening_stock": 200,
|
||||||
|
"last_purchase_rate": 200,
|
||||||
|
}
|
||||||
|
).name
|
||||||
|
|
||||||
|
return fg_item, [rm_item1, rm_item2]
|
||||||
|
|
||||||
|
|
||||||
|
def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False):
|
||||||
|
expected_data = []
|
||||||
|
|
||||||
|
for item in bom.get("exploded_items") if show_exploded_view else bom.get("items"):
|
||||||
|
in_stock_qty = None
|
||||||
|
if frappe.db.exists("Bin", {"item_code": item.item_code, "warehouse": warehouse}, "actual_qty"):
|
||||||
|
in_stock_qty = frappe.get_cached_value(
|
||||||
|
"Bin", {"item_code": item.item_code, "warehouse": warehouse}, "actual_qty"
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_data.append(
|
||||||
|
[
|
||||||
|
item.item_code,
|
||||||
|
item.description,
|
||||||
|
item.stock_qty,
|
||||||
|
item.stock_uom,
|
||||||
|
item.stock_qty * qty_to_produce / bom.quantity,
|
||||||
|
in_stock_qty,
|
||||||
|
floor(in_stock_qty / (item.stock_qty * qty_to_produce / bom.quantity))
|
||||||
|
if in_stock_qty
|
||||||
|
else None,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return expected_data
|
||||||
@@ -324,6 +324,8 @@ class SalarySlip(TransactionBase):
|
|||||||
|
|
||||||
holidays = self.get_holidays_for_employee(self.start_date, self.end_date)
|
holidays = self.get_holidays_for_employee(self.start_date, self.end_date)
|
||||||
|
|
||||||
|
joining_date, relieving_date = self.get_joining_and_relieving_dates()
|
||||||
|
|
||||||
if not cint(include_holidays_in_total_working_days):
|
if not cint(include_holidays_in_total_working_days):
|
||||||
working_days -= len(holidays)
|
working_days -= len(holidays)
|
||||||
working_days_list = [cstr(day) for day in working_days_list if cstr(day) not in holidays]
|
working_days_list = [cstr(day) for day in working_days_list if cstr(day) not in holidays]
|
||||||
@@ -335,10 +337,14 @@ class SalarySlip(TransactionBase):
|
|||||||
frappe.throw(_("Please set Payroll based on in Payroll settings"))
|
frappe.throw(_("Please set Payroll based on in Payroll settings"))
|
||||||
|
|
||||||
if payroll_based_on == "Attendance":
|
if payroll_based_on == "Attendance":
|
||||||
actual_lwp, absent = self.calculate_lwp_ppl_and_absent_days_based_on_attendance(holidays)
|
actual_lwp, absent = self.calculate_lwp_ppl_and_absent_days_based_on_attendance(
|
||||||
|
holidays, relieving_date
|
||||||
|
)
|
||||||
self.absent_days = absent
|
self.absent_days = absent
|
||||||
else:
|
else:
|
||||||
actual_lwp = self.calculate_lwp_or_ppl_based_on_leave_application(holidays, working_days_list)
|
actual_lwp = self.calculate_lwp_or_ppl_based_on_leave_application(
|
||||||
|
holidays, working_days_list, relieving_date
|
||||||
|
)
|
||||||
|
|
||||||
if not lwp:
|
if not lwp:
|
||||||
lwp = actual_lwp
|
lwp = actual_lwp
|
||||||
@@ -461,7 +467,10 @@ class SalarySlip(TransactionBase):
|
|||||||
def get_holidays_for_employee(self, start_date, end_date):
|
def get_holidays_for_employee(self, start_date, end_date):
|
||||||
return get_holiday_dates_for_employee(self.employee, start_date, end_date)
|
return get_holiday_dates_for_employee(self.employee, start_date, end_date)
|
||||||
|
|
||||||
def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days_list):
|
def calculate_lwp_or_ppl_based_on_leave_application(
|
||||||
|
self, holidays, working_days_list, relieving_date=None
|
||||||
|
):
|
||||||
|
|
||||||
lwp = 0
|
lwp = 0
|
||||||
|
|
||||||
daily_wages_fraction_for_half_day = (
|
daily_wages_fraction_for_half_day = (
|
||||||
@@ -469,6 +478,9 @@ class SalarySlip(TransactionBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for d in working_days_list:
|
for d in working_days_list:
|
||||||
|
if relieving_date and getdate(d) > getdate(relieving_date):
|
||||||
|
break
|
||||||
|
|
||||||
leave = get_lwp_or_ppl_for_date(d, self.employee, holidays)
|
leave = get_lwp_or_ppl_for_date(d, self.employee, holidays)
|
||||||
|
|
||||||
if leave:
|
if leave:
|
||||||
@@ -488,10 +500,15 @@ class SalarySlip(TransactionBase):
|
|||||||
|
|
||||||
return lwp
|
return lwp
|
||||||
|
|
||||||
def calculate_lwp_ppl_and_absent_days_based_on_attendance(self, holidays):
|
def calculate_lwp_ppl_and_absent_days_based_on_attendance(self, holidays, relieving_date=None):
|
||||||
lwp = 0
|
lwp = 0
|
||||||
absent = 0
|
absent = 0
|
||||||
|
|
||||||
|
end_date = self.end_date
|
||||||
|
|
||||||
|
if relieving_date:
|
||||||
|
end_date = relieving_date
|
||||||
|
|
||||||
daily_wages_fraction_for_half_day = (
|
daily_wages_fraction_for_half_day = (
|
||||||
flt(frappe.db.get_value("Payroll Settings", None, "daily_wages_fraction_for_half_day")) or 0.5
|
flt(frappe.db.get_value("Payroll Settings", None, "daily_wages_fraction_for_half_day")) or 0.5
|
||||||
)
|
)
|
||||||
@@ -506,7 +523,7 @@ class SalarySlip(TransactionBase):
|
|||||||
for leave_type in leave_types:
|
for leave_type in leave_types:
|
||||||
leave_type_map[leave_type.name] = leave_type
|
leave_type_map[leave_type.name] = leave_type
|
||||||
|
|
||||||
attendances = frappe.db.sql(
|
attendances = frappe.db.sql( # nosemgrep
|
||||||
"""
|
"""
|
||||||
SELECT attendance_date, status, leave_type
|
SELECT attendance_date, status, leave_type
|
||||||
FROM `tabAttendance`
|
FROM `tabAttendance`
|
||||||
@@ -516,7 +533,7 @@ class SalarySlip(TransactionBase):
|
|||||||
AND docstatus = 1
|
AND docstatus = 1
|
||||||
AND attendance_date between %s and %s
|
AND attendance_date between %s and %s
|
||||||
""",
|
""",
|
||||||
values=(self.employee, self.start_date, self.end_date),
|
values=(self.employee, self.start_date, end_date),
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -267,7 +267,6 @@ class TestSalarySlip(FrappeTestCase):
|
|||||||
make_leave_application(emp_id, first_sunday, add_days(first_sunday, 3), "Leave Without Pay")
|
make_leave_application(emp_id, first_sunday, add_days(first_sunday, 3), "Leave Without Pay")
|
||||||
|
|
||||||
leave_type_ppl = create_leave_type(leave_type_name="Test Partially Paid Leave", is_ppl=1)
|
leave_type_ppl = create_leave_type(leave_type_name="Test Partially Paid Leave", is_ppl=1)
|
||||||
leave_type_ppl.save()
|
|
||||||
|
|
||||||
alloc = create_leave_allocation(
|
alloc = create_leave_allocation(
|
||||||
employee=emp_id,
|
employee=emp_id,
|
||||||
@@ -1128,6 +1127,35 @@ class TestSalarySlip(FrappeTestCase):
|
|||||||
if deduction.salary_component == "TDS":
|
if deduction.salary_component == "TDS":
|
||||||
self.assertEqual(deduction.amount, rounded(monthly_tax_amount))
|
self.assertEqual(deduction.amount, rounded(monthly_tax_amount))
|
||||||
|
|
||||||
|
@change_settings("Payroll Settings", {"payroll_based_on": "Leave"})
|
||||||
|
def test_lwp_calculation_based_on_relieving_date(self):
|
||||||
|
emp_id = make_employee("test_lwp_based_on_relieving_date@salary.com")
|
||||||
|
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
||||||
|
frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0)
|
||||||
|
|
||||||
|
month_start_date = get_first_day(nowdate())
|
||||||
|
first_sunday = get_first_sunday(for_date=month_start_date)
|
||||||
|
relieving_date = add_days(first_sunday, 10)
|
||||||
|
leave_start_date = add_days(first_sunday, 16)
|
||||||
|
leave_end_date = add_days(leave_start_date, 2)
|
||||||
|
|
||||||
|
make_leave_application(emp_id, leave_start_date, leave_end_date, "Leave Without Pay")
|
||||||
|
|
||||||
|
frappe.db.set_value("Employee", emp_id, {"relieving_date": relieving_date, "status": "Left"})
|
||||||
|
|
||||||
|
ss = make_employee_salary_slip(
|
||||||
|
"test_lwp_based_on_relieving_date@salary.com",
|
||||||
|
"Monthly",
|
||||||
|
"Test Payment Based On Leave Application",
|
||||||
|
)
|
||||||
|
|
||||||
|
holidays = ss.get_holidays_for_employee(month_start_date, relieving_date)
|
||||||
|
days_between_start_and_relieving = date_diff(relieving_date, month_start_date) + 1
|
||||||
|
|
||||||
|
self.assertEqual(ss.leave_without_pay, 0)
|
||||||
|
|
||||||
|
self.assertEqual(ss.payment_days, (days_between_start_and_relieving - len(holidays)))
|
||||||
|
|
||||||
|
|
||||||
def get_no_of_days():
|
def get_no_of_days():
|
||||||
no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month)
|
no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month)
|
||||||
@@ -1587,9 +1615,8 @@ def setup_test():
|
|||||||
frappe.db.set_value("HR Settings", None, "leave_approval_notification_template", None)
|
frappe.db.set_value("HR Settings", None, "leave_approval_notification_template", None)
|
||||||
|
|
||||||
|
|
||||||
def make_holiday_list(list_name=None, from_date=None, to_date=None):
|
def make_holiday_list(list_name=None, from_date=None, to_date=None, add_weekly_offs=True):
|
||||||
if not (from_date and to_date):
|
fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
|
||||||
fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
|
|
||||||
name = list_name or "Salary Slip Test Holiday List"
|
name = list_name or "Salary Slip Test Holiday List"
|
||||||
|
|
||||||
frappe.delete_doc_if_exists("Holiday List", name, force=True)
|
frappe.delete_doc_if_exists("Holiday List", name, force=True)
|
||||||
@@ -1600,10 +1627,13 @@ def make_holiday_list(list_name=None, from_date=None, to_date=None):
|
|||||||
"holiday_list_name": name,
|
"holiday_list_name": name,
|
||||||
"from_date": from_date or fiscal_year[1],
|
"from_date": from_date or fiscal_year[1],
|
||||||
"to_date": to_date or fiscal_year[2],
|
"to_date": to_date or fiscal_year[2],
|
||||||
"weekly_off": "Sunday",
|
|
||||||
}
|
}
|
||||||
).insert()
|
).insert()
|
||||||
holiday_list.get_weekly_off_dates()
|
|
||||||
|
if add_weekly_offs:
|
||||||
|
holiday_list.weekly_off = "Sunday"
|
||||||
|
holiday_list.get_weekly_off_dates()
|
||||||
|
|
||||||
holiday_list.save()
|
holiday_list.save()
|
||||||
holiday_list = holiday_list.name
|
holiday_list = holiday_list.name
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class ItemAlternative(Document):
|
|||||||
if not item_data.allow_alternative_item:
|
if not item_data.allow_alternative_item:
|
||||||
frappe.throw(alternate_item_check_msg.format(self.item_code))
|
frappe.throw(alternate_item_check_msg.format(self.item_code))
|
||||||
if self.two_way and not alternative_item_data.allow_alternative_item:
|
if self.two_way and not alternative_item_data.allow_alternative_item:
|
||||||
frappe.throw(alternate_item_check_msg.format(self.item_code))
|
frappe.throw(alternate_item_check_msg.format(self.alternative_item_code))
|
||||||
|
|
||||||
def validate_duplicate(self):
|
def validate_duplicate(self):
|
||||||
if frappe.db.get_value(
|
if frappe.db.get_value(
|
||||||
|
|||||||
@@ -831,7 +831,7 @@ def update_billing_percentage(pr_doc, update_modified=True):
|
|||||||
# Update Billing % based on pending accepted qty
|
# Update Billing % based on pending accepted qty
|
||||||
total_amount, total_billed_amount = 0, 0
|
total_amount, total_billed_amount = 0, 0
|
||||||
for item in pr_doc.items:
|
for item in pr_doc.items:
|
||||||
return_data = frappe.db.get_list(
|
return_data = frappe.get_all(
|
||||||
"Purchase Receipt",
|
"Purchase Receipt",
|
||||||
fields=["sum(abs(`tabPurchase Receipt Item`.qty)) as qty"],
|
fields=["sum(abs(`tabPurchase Receipt Item`.qty)) as qty"],
|
||||||
filters=[
|
filters=[
|
||||||
|
|||||||
@@ -2801,7 +2801,7 @@ Stock Ledger Entries and GL Entries are reposted for the selected Purchase Recei
|
|||||||
Stock Levels,Niveaux du Stocks,
|
Stock Levels,Niveaux du Stocks,
|
||||||
Stock Liabilities,Passif du Stock,
|
Stock Liabilities,Passif du Stock,
|
||||||
Stock Options,Options du Stock,
|
Stock Options,Options du Stock,
|
||||||
Stock Qty,Qté en Stock,
|
Stock Qty,Qté en unité de stock,
|
||||||
Stock Received But Not Billed,Stock Reçus Mais Non Facturés,
|
Stock Received But Not Billed,Stock Reçus Mais Non Facturés,
|
||||||
Stock Reports,Rapports de stock,
|
Stock Reports,Rapports de stock,
|
||||||
Stock Summary,Résumé du Stock,
|
Stock Summary,Résumé du Stock,
|
||||||
|
|||||||
|
Can't render this file because it is too large.
|
Reference in New Issue
Block a user