mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-17 16:38:41 +00:00
Merge pull request #56139 from nabinhait/refactor-si-timesheet-billing
refactor(sales_invoice): simplify TimesheetBillingService link decision
This commit is contained in:
@@ -99,23 +99,24 @@ class TimesheetBillingService:
|
||||
doc.total_billing_hours = sum(flt(ts.billing_hours) for ts in doc.timesheets)
|
||||
|
||||
def _update_time_sheet_detail(self, timesheet, args, sales_invoice: str | None) -> None:
|
||||
doc = self.doc
|
||||
for data in timesheet.time_logs:
|
||||
if (
|
||||
(doc.project and args.timesheet_detail == data.name)
|
||||
or (not doc.project and not data.sales_invoice and args.timesheet_detail == data.name)
|
||||
or (
|
||||
not sales_invoice
|
||||
and data.sales_invoice == doc.name
|
||||
and args.timesheet_detail == data.name
|
||||
)
|
||||
or (
|
||||
doc.is_return
|
||||
and doc.return_against
|
||||
and data.sales_invoice
|
||||
and data.sales_invoice == doc.return_against
|
||||
and not sales_invoice
|
||||
and args.timesheet_detail == data.name
|
||||
)
|
||||
):
|
||||
if args.timesheet_detail == data.name and self._should_set_sales_invoice(data, sales_invoice):
|
||||
data.sales_invoice = sales_invoice
|
||||
|
||||
def _should_set_sales_invoice(self, time_log, sales_invoice: str | None) -> bool:
|
||||
"""Whether this time log's sales-invoice link should be (re)set to sales_invoice."""
|
||||
doc = self.doc
|
||||
if doc.project:
|
||||
return True
|
||||
if not time_log.sales_invoice:
|
||||
return True
|
||||
if not sales_invoice and time_log.sales_invoice == doc.name:
|
||||
# clearing the link on cancellation of this invoice
|
||||
return True
|
||||
# clearing the link on a return raised against the original invoice
|
||||
return bool(
|
||||
doc.is_return
|
||||
and doc.return_against
|
||||
and not sales_invoice
|
||||
and time_log.sales_invoice == doc.return_against
|
||||
)
|
||||
|
||||
@@ -126,6 +126,80 @@ class TestTimesheet(ERPNextTestSuite):
|
||||
self.assertEqual(ts.per_billed, 100)
|
||||
self.assertEqual(ts.time_logs[0].sales_invoice, sales_invoice.name)
|
||||
|
||||
def _bill_timesheet_into_invoice(self, emp):
|
||||
"""Submit a billable timesheet into a Sales Invoice; return (timesheet, invoice)."""
|
||||
timesheet = make_timesheet(emp, simulate=True, is_billable=1)
|
||||
sales_invoice = make_sales_invoice(timesheet.name, "_Test Item", "_Test Customer", currency="INR")
|
||||
sales_invoice.due_date = nowdate()
|
||||
sales_invoice.submit()
|
||||
timesheet.reload()
|
||||
# Submitting links the timesheet detail to the invoice and marks it billed
|
||||
self.assertEqual(timesheet.time_logs[0].sales_invoice, sales_invoice.name)
|
||||
self.assertEqual(timesheet.status, "Billed")
|
||||
return timesheet, sales_invoice
|
||||
|
||||
def test_timesheet_billing_link_lifecycle(self):
|
||||
emp = make_employee("test_employee_6@salary.com", company="_Test Company")
|
||||
|
||||
with self.subTest("link released on cancel"):
|
||||
timesheet, sales_invoice = self._bill_timesheet_into_invoice(emp)
|
||||
sales_invoice.reload()
|
||||
sales_invoice.cancel()
|
||||
timesheet.reload()
|
||||
self.assertFalse(timesheet.time_logs[0].sales_invoice)
|
||||
self.assertNotEqual(timesheet.status, "Billed")
|
||||
|
||||
with self.subTest("link released on sales return"):
|
||||
timesheet, sales_invoice = self._bill_timesheet_into_invoice(emp)
|
||||
sales_return = make_sales_return(sales_invoice.name)
|
||||
sales_return.insert()
|
||||
sales_return.submit()
|
||||
timesheet.reload()
|
||||
self.assertFalse(timesheet.time_logs[0].sales_invoice)
|
||||
|
||||
def test_timesheet_billing_validations(self):
|
||||
emp = make_employee("test_employee_6@salary.com", company="_Test Company")
|
||||
|
||||
with self.subTest("unsubmitted timesheet is rejected"):
|
||||
draft = make_timesheet(emp, simulate=True, is_billable=1, do_not_submit=True)
|
||||
sales_invoice = self._invoice_with_timesheet_row(draft.name, draft.time_logs[0].name)
|
||||
self.assertRaises(frappe.ValidationError, sales_invoice.save)
|
||||
|
||||
with self.subTest("already invoiced detail is rejected"):
|
||||
timesheet, _ = self._bill_timesheet_into_invoice(emp)
|
||||
sales_invoice = self._invoice_with_timesheet_row(timesheet.name, timesheet.time_logs[0].name)
|
||||
self.assertRaises(frappe.ValidationError, sales_invoice.save)
|
||||
|
||||
@ERPNextTestSuite.change_settings("Projects Settings", {"fetch_timesheet_in_sales_invoice": 1})
|
||||
def test_timesheet_billing_data_population(self):
|
||||
emp = make_employee("test_employee_6@salary.com", company="_Test Company")
|
||||
|
||||
with self.subTest("blank hours/amount are back-filled from the timesheet"):
|
||||
timesheet = make_timesheet(emp, simulate=True, is_billable=1)
|
||||
sales_invoice = self._invoice_with_timesheet_row(
|
||||
timesheet.name, timesheet.time_logs[0].name, with_amounts=False
|
||||
)
|
||||
sales_invoice.save()
|
||||
self.assertEqual(sales_invoice.timesheets[0].billing_hours, 2)
|
||||
self.assertEqual(sales_invoice.timesheets[0].billing_amount, 100)
|
||||
|
||||
with self.subTest("project invoice auto-fetches the project's timesheets"):
|
||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
make_timesheet(emp, simulate=True, is_billable=1, project=project, company="_Test Company")
|
||||
sales_invoice = create_sales_invoice(do_not_save=True)
|
||||
sales_invoice.project = project
|
||||
sales_invoice.set("timesheets", [])
|
||||
sales_invoice.save()
|
||||
self.assertTrue(sales_invoice.timesheets)
|
||||
|
||||
def _invoice_with_timesheet_row(self, time_sheet, timesheet_detail, with_amounts=True):
|
||||
sales_invoice = create_sales_invoice(do_not_save=True)
|
||||
row = {"time_sheet": time_sheet, "timesheet_detail": timesheet_detail}
|
||||
if with_amounts:
|
||||
row.update({"billing_hours": 2, "billing_amount": 100})
|
||||
sales_invoice.append("timesheets", row)
|
||||
return sales_invoice
|
||||
|
||||
def test_timesheet_time_overlap(self):
|
||||
emp = make_employee("test_employee_6@salary.com", company="_Test Company")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user