diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html index 3920d4cf096..b9680dfb3bf 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html @@ -15,7 +15,7 @@

{{ _("STATEMENTS OF ACCOUNTS") }}

-
{{ _("Customer: ") }} {{filters.party[0] }}
+
{{ _("Customer: ") }} {{filters.party_name[0] }}
{{ _("Date: ") }} {{ frappe.format(filters.from_date, 'Date')}} diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index c6b0c57ce5c..3bf92fca4a4 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -24,7 +24,7 @@ from erpnext.accounts.report.general_ledger.general_ledger import execute as get class ProcessStatementOfAccounts(Document): def validate(self): 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: self.body = "Hello {{ customer.name }},
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, "party_type": "Customer", "party": [entry.customer], + "party_name": [entry.customer_name] if entry.customer_name else None, "presentation_currency": presentation_currency, "group_by": doc.group_by, "currency": doc.currency, @@ -155,7 +156,7 @@ def get_customers_based_on_territory_or_customer_group(customer_collection, coll ] return frappe.get_list( "Customer", - fields=["name", "email_id"], + fields=["name", "customer_name", "email_id"], 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"): return frappe.get_list( "Customer", - fields=["name", "email_id"], + fields=["name", "customer_name", "email_id"], filters=[["name", "in", list(sales_person_records["Customer"])]], ) else: @@ -227,7 +228,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory): if customer_collection == "Sales Partner": customers = frappe.get_list( "Customer", - fields=["name", "email_id"], + fields=["name", "customer_name", "email_id"], filters=[["default_sales_partner", "=", collection_name]], ) else: @@ -244,7 +245,12 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory): continue 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 diff --git a/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json index dd04dc1b3c6..8bffd6a93b9 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json +++ b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json @@ -1,12 +1,12 @@ { "actions": [], - "allow_workflow": 1, "creation": "2020-08-03 16:35:21.852178", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ "customer", + "customer_name", "billing_email", "primary_email" ], @@ -30,11 +30,18 @@ "fieldtype": "Read Only", "in_list_view": 1, "label": "Billing Email" + }, + { + "fetch_from": "customer.customer_name", + "fieldname": "customer_name", + "fieldtype": "Data", + "label": "Customer Name", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2020-08-03 22:55:38.875601", + "modified": "2023-03-13 00:12:34.508086", "modified_by": "Administrator", "module": "Accounts", "name": "Process Statement Of Accounts Customer", @@ -43,5 +50,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 560b79243d7..9a3e82486af 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -138,7 +138,8 @@ def prepare_companywise_opening_balance(asset_data, liability_data, equity_data, for data in [asset_data, liability_data, equity_data]: if data: 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 @@ -155,7 +156,7 @@ def get_opening_balance(account_name, data, company): def get_root_account_name(root_type, company): - return frappe.get_all( + root_account = frappe.get_all( "Account", fields=["account_name"], filters={ @@ -165,7 +166,10 @@ def get_root_account_name(root_type, company): "parent_account": ("is", "not set"), }, as_list=1, - )[0][0] + ) + + if root_account: + return root_account[0][0] def get_profit_loss_data(fiscal_year, companies, columns, filters): diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 6d2cd8ed411..61bc58009a6 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -78,7 +78,6 @@ def validate_filters(filters): def get_data(filters): - accounts = frappe.db.sql( """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), ) - total_row = calculate_values( - accounts, gl_entries_by_account, opening_balances, filters, company_currency - ) + calculate_values(accounts, gl_entries_by_account, opening_balances) 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, 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 -def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, company_currency): +def calculate_values(accounts, gl_entries_by_account, opening_balances): init = { "opening_debit": 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, } - 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: d.update(init.copy()) @@ -261,8 +242,28 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, 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 @@ -274,7 +275,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name): 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 = [] 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 data.append(row) + total_row = calculate_total_row(accounts, company_currency) data.extend([{}, total_row]) return data diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index aa03f932b82..f3ea38c3915 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -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)] # 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, fields=fields, filters=[ diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index 8fae2a9a888..09484d33755 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -90,6 +90,7 @@ class LeaveAllocation(Document): if self.carry_forward: 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): if self.has_value_changed("new_leaves_allocated"): self.validate_against_leave_applications() @@ -99,7 +100,11 @@ class LeaveAllocation(Document): # run required validations again since total leaves are being updated 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 = { "leaves": leaves_to_be_added, "from_date": self.from_date, @@ -118,14 +123,13 @@ class LeaveAllocation(Document): "employee": self.employee, "company": self.company, "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): leaves_taken = get_approved_leaves_for_period( diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py index 48953003000..07792b5ea54 100644 --- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py @@ -18,6 +18,7 @@ class TestLeaveAllocation(FrappeTestCase): def setUp(self): frappe.db.delete("Leave Period") frappe.db.delete("Leave Allocation") + frappe.db.delete("Leave Ledger Entry") emp_id = make_employee("test_emp_leave_allocation@salary.com", company="_Test Company") self.employee = frappe.get_doc("Employee", emp_id) @@ -69,7 +70,6 @@ class TestLeaveAllocation(FrappeTestCase): def test_validation_for_over_allocation(self): leave_type = create_leave_type(leave_type_name="Test Over Allocation", is_carry_forward=1) - leave_type.save() doc = frappe.get_doc( { @@ -137,9 +137,9 @@ class TestLeaveAllocation(FrappeTestCase): ) ).insert() - leave_type = create_leave_type(leave_type_name="_Test Allocation Validation", is_carry_forward=1) - leave_type.max_leaves_allowed = 25 - leave_type.save() + leave_type = create_leave_type( + leave_type_name="_Test Allocation Validation", is_carry_forward=1, max_leaves_allowed=25 + ) # 15 leaves allocated in this period allocation = create_leave_allocation( @@ -174,9 +174,9 @@ class TestLeaveAllocation(FrappeTestCase): ) ).insert() - leave_type = create_leave_type(leave_type_name="_Test Allocation Validation", is_carry_forward=1) - leave_type.max_leaves_allowed = 30 - leave_type.save() + leave_type = create_leave_type( + leave_type_name="_Test Allocation Validation", is_carry_forward=1, max_leaves_allowed=30 + ) # 15 leaves allocated allocation = create_leave_allocation( @@ -207,7 +207,6 @@ class TestLeaveAllocation(FrappeTestCase): 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.save() # initial leave allocation = 15 leave_allocation = create_leave_allocation( @@ -235,10 +234,12 @@ class TestLeaveAllocation(FrappeTestCase): self.assertRaises(BackDatedAllocationError, leave_allocation.save) def test_carry_forward_calculation(self): - leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1) - leave_type.maximum_carry_forwarded_leaves = 10 - leave_type.max_leaves_allowed = 30 - leave_type.save() + leave_type = create_leave_type( + leave_type_name="_Test_CF_leave", + is_carry_forward=1, + maximum_carry_forwarded_leaves=10, + max_leaves_allowed=30, + ) # initial leave allocation = 15 leave_allocation = create_leave_allocation( @@ -286,7 +287,6 @@ class TestLeaveAllocation(FrappeTestCase): is_carry_forward=1, expire_carry_forwarded_leaves_after_days=90, ) - leave_type.save() # initial leave allocation leave_allocation = create_leave_allocation( @@ -352,12 +352,51 @@ class TestLeaveAllocation(FrappeTestCase): ) leave_allocation.submit() 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.submit() + leave_allocation.save() 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): leave_allocation = create_leave_allocation( @@ -365,12 +404,49 @@ class TestLeaveAllocation(FrappeTestCase): ) leave_allocation.submit() 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.submit() 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): from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index fff0967f25a..08bc93760a3 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -856,6 +856,7 @@ def get_leave_allocation_records(employee, date, leave_type=None): Min(Ledger.from_date).as_("from_date"), Max(Ledger.to_date).as_("to_date"), Ledger.leave_type, + Ledger.employee, ) .where( (Ledger.from_date <= date) @@ -895,6 +896,7 @@ def get_leave_allocation_records(employee, date, leave_type=None): "unused_leaves": d.cf_leaves, "new_leaves_allocated": d.new_leaves, "leave_type": d.leave_type, + "employee": d.employee, } ), ) @@ -933,26 +935,51 @@ def get_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: + # 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): - # carry forwarded leave expiry date passed + # carry forwarded leaves have expired cf_leaves = remaining_cf_leaves = 0 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) - leave_balance = flt(allocation.new_leaves_allocated) + flt(cf_leaves) - leave_balance_for_consumption = flt(allocation.new_leaves_allocated) + flt(remaining_cf_leaves) + # new leaves allocated - new leaves taken + cf leave balance + # 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) 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( employee: str, leave_type: str, from_date: str, to_date: str, skip_expired_leaves: bool = True ) -> float: diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 45e9a87428e..e30b84bbf34 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -28,6 +28,7 @@ from erpnext.hr.doctype.leave_application.leave_application import ( get_leave_allocation_records, get_leave_balance_on, get_leave_details, + get_new_and_cf_leaves_taken, ) from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import ( create_assignment_for_multiple_employees, @@ -96,6 +97,9 @@ class TestLeaveApplication(unittest.TestCase): from_date = get_year_start(getdate()) to_date = get_year_ending(getdate()) 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"): frappe.get_doc( @@ -698,7 +702,7 @@ class TestLeaveApplication(unittest.TestCase): leave_type_name="_Test_CF_leave_expiry", is_carry_forward=1, expire_carry_forwarded_leaves_after_days=90, - ).insert() + ) create_carry_forwarded_allocation(employee, leave_type) details = get_leave_balance_on( @@ -770,7 +774,6 @@ class TestLeaveApplication(unittest.TestCase): 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 @@ -813,7 +816,6 @@ class TestLeaveApplication(unittest.TestCase): expire_carry_forwarded_leaves_after_days=90, include_holiday=True, ) - leave_type.submit() create_carry_forwarded_allocation(employee, leave_type) @@ -852,7 +854,6 @@ class TestLeaveApplication(unittest.TestCase): is_carry_forward=1, expire_carry_forwarded_leaves_after_days=90, ) - leave_type.submit() create_carry_forwarded_allocation(employee, leave_type) @@ -990,25 +991,29 @@ class TestLeaveApplication(unittest.TestCase): } 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): + """Tests leave details: + Case 1: All leaves available before cf leave expiry + Case 2: Remaining Leaves after cf leave expiry + """ 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, - ).insert() + ) 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" ) - # 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)) 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)) expected_data = { "total_leaves": 30.0, @@ -1017,6 +1022,119 @@ class TestLeaveApplication(unittest.TestCase): "leaves_pending_approval": 0.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) @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", is_carry_forward=1, expire_carry_forwarded_leaves_after_days=90, - ).insert() + ) leave_alloc = create_carry_forwarded_allocation(employee, leave_type) cf_expiry = frappe.db.get_value( @@ -1043,6 +1161,7 @@ class TestLeaveApplication(unittest.TestCase): "unused_leaves": 15.0, "new_leaves_allocated": 15.0, "leave_type": leave_type.name, + "employee": employee.name, } self.assertEqual(details.get(leave_type.name), expected_data) diff --git a/erpnext/hr/doctype/leave_type/test_leave_type.py b/erpnext/hr/doctype/leave_type/test_leave_type.py index 69f9e125203..56bf641d261 100644 --- a/erpnext/hr/doctype/leave_type/test_leave_type.py +++ b/erpnext/hr/doctype/leave_type/test_leave_type.py @@ -9,7 +9,8 @@ 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) + frappe.delete_doc("Leave Type", args.leave_type_name, force=True) + leave_type = frappe.get_doc( { "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, "encashment_threshold_days": args.encashment_threshold_days or 5, "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: leave_type.fraction_of_daily_salary_per_leave = args.fraction_of_daily_salary_per_leave or 0.5 + leave_type.insert() + return leave_type diff --git a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py index d167d1d86c9..75cb4991299 100644 --- a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py @@ -154,7 +154,6 @@ class TestEmployeeLeaveBalance(unittest.TestCase): @set_holiday_list("_Test Emp Balance Holiday List", "_Test Company") 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.insert() # 30 leaves allocated for first half of the year allocation1 = make_allocation_record( diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 403b1475770..36bac4c6840 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -31,7 +31,7 @@ class BOMTree: # specifying the attributes to save resources # 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__( 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): bom = frappe.get_cached_doc("BOM", self.name) self.item_code = bom.item + self.bom_qty = bom.quantity 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 if item.bom_no: child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index b1ee3cd7a1b..0e8c9bcec49 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -476,7 +476,7 @@ frappe.ui.form.on("Work Order Item", { callback: function(r) { if (r.message) { frappe.model.set_value(cdt, cdn, { - "required_qty": 1, + "required_qty": row.required_qty || 1, "item_name": r.message.item_name, "description": r.message.description, "source_warehouse": r.message.default_warehouse, diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 574ca185fee..b2957b207cf 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -690,7 +690,7 @@ class WorkOrder(Document): for node in bom_traversal: 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") operations.extend(_get_operations(self.bom_no, qty=1.0 / bom_qty)) diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py index cdf1541f888..3573a3a93d8 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py @@ -4,7 +4,8 @@ import frappe 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 @@ -34,57 +35,55 @@ def get_columns(): def get_bom_stock(filters): - qty_to_produce = filters.get("qty_to_produce") or 1 - if int(qty_to_produce) < 0: - frappe.throw(_("Quantity to Produce can not be less than Zero")) + qty_to_produce = filters.get("qty_to_produce") + if cint(qty_to_produce) <= 0: + frappe.throw(_("Quantity to Produce should be greater than zero.")) if filters.get("show_exploded_view"): bom_item_table = "BOM Explosion Item" else: bom_item_table = "BOM Item" - bin = frappe.qb.DocType("Bin") - bom = frappe.qb.DocType("BOM") - 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) + warehouse_details = frappe.db.get_value( + "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 ) - if filters.get("warehouse"): - warehouse_details = frappe.db.get_value( - "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 - ) + BOM = frappe.qb.DocType("BOM") + BOM_ITEM = frappe.qb.DocType(bom_item_table) + BIN = frappe.qb.DocType("Bin") + WH = frappe.qb.DocType("Warehouse") + CONDITIONS = () - if warehouse_details: - wh = frappe.qb.DocType("Warehouse") - query = query.where( - ExistsCriterion( - frappe.qb.from_(wh) - .select(wh.name) - .where( - (wh.lft >= warehouse_details.lft) - & (wh.rgt <= warehouse_details.rgt) - & (bin.warehouse == wh.name) - ) - ) + if warehouse_details: + CONDITIONS = ExistsCriterion( + frappe.qb.from_(WH) + .select(WH.name) + .where( + (WH.lft >= warehouse_details.lft) + & (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() diff --git a/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py new file mode 100644 index 00000000000..24e42cda064 --- /dev/null +++ b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py @@ -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 diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 302c0d2853a..a3ec0386ad5 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -324,6 +324,8 @@ class SalarySlip(TransactionBase): 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): working_days -= len(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")) 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 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: lwp = actual_lwp @@ -461,7 +467,10 @@ class SalarySlip(TransactionBase): def get_holidays_for_employee(self, 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 daily_wages_fraction_for_half_day = ( @@ -469,6 +478,9 @@ class SalarySlip(TransactionBase): ) 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) if leave: @@ -488,10 +500,15 @@ class SalarySlip(TransactionBase): 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 absent = 0 + end_date = self.end_date + + if relieving_date: + end_date = relieving_date + daily_wages_fraction_for_half_day = ( 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: leave_type_map[leave_type.name] = leave_type - attendances = frappe.db.sql( + attendances = frappe.db.sql( # nosemgrep """ SELECT attendance_date, status, leave_type FROM `tabAttendance` @@ -516,7 +533,7 @@ class SalarySlip(TransactionBase): AND docstatus = 1 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, ) diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 32d0c7ed08f..5ad549fea28 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -267,7 +267,6 @@ class TestSalarySlip(FrappeTestCase): 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.save() alloc = create_leave_allocation( employee=emp_id, @@ -1128,6 +1127,35 @@ class TestSalarySlip(FrappeTestCase): if deduction.salary_component == "TDS": 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(): 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) -def make_holiday_list(list_name=None, from_date=None, to_date=None): - if not (from_date and to_date): - fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company()) +def make_holiday_list(list_name=None, from_date=None, to_date=None, add_weekly_offs=True): + fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company()) name = list_name or "Salary Slip Test Holiday List" 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, "from_date": from_date or fiscal_year[1], "to_date": to_date or fiscal_year[2], - "weekly_off": "Sunday", } ).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 = holiday_list.name diff --git a/erpnext/stock/doctype/item_alternative/item_alternative.py b/erpnext/stock/doctype/item_alternative/item_alternative.py index 0f93bb9e95b..14a5b9877ea 100644 --- a/erpnext/stock/doctype/item_alternative/item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/item_alternative.py @@ -54,7 +54,7 @@ class ItemAlternative(Document): if not item_data.allow_alternative_item: frappe.throw(alternate_item_check_msg.format(self.item_code)) 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): if frappe.db.get_value( diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 314a3549cc1..0384c24503a 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -831,7 +831,7 @@ def update_billing_percentage(pr_doc, update_modified=True): # Update Billing % based on pending accepted qty total_amount, total_billed_amount = 0, 0 for item in pr_doc.items: - return_data = frappe.db.get_list( + return_data = frappe.get_all( "Purchase Receipt", fields=["sum(abs(`tabPurchase Receipt Item`.qty)) as qty"], filters=[ diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index 3ba5ade6299..35ccb7d888b 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -2801,7 +2801,7 @@ Stock Ledger Entries and GL Entries are reposted for the selected Purchase Recei Stock Levels,Niveaux du Stocks, Stock Liabilities,Passif 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 Reports,Rapports de stock, Stock Summary,Résumé du Stock,