mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-17 00:18:39 +00:00
fix(projects): use Coalesce for timesheet portal sales_invoice (not bitwise OR)
get_timesheets_list selected `timesheet.sales_invoice | detail.sales_invoice`, intending COALESCE (pick the parent timesheet's invoice, else the detail's) -- the original raw SQL was COALESCE(ts.sales_invoice, tsd.sales_invoice). pypika's `|` is a bitwise OR, not a coalesce: - Postgres: `varchar | varchar` -> "operator does not exist" (hard error). - MariaDB: bitwise OR coerces the operands to integers; with a NULL detail invoice the result is NULL, so the portal showed no invoice even when the timesheet was billed. Replace with Coalesce(table.sales_invoice, child_table.sales_invoice). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -391,6 +391,42 @@ class TestTimesheet(ERPNextTestSuite):
|
||||
self.assertEqual(timesheet.time_logs[1].sales_invoice, sales_invoice2.name)
|
||||
self.assertEqual(timesheet.status, "Billed")
|
||||
|
||||
def test_get_timesheets_list_portal_sales_invoice(self):
|
||||
# get_timesheets_list selects COALESCE(timesheet.sales_invoice, detail.sales_invoice). The earlier
|
||||
# `timesheet.sales_invoice | detail.sales_invoice` bitwise-ORed two varchars -- it errored on
|
||||
# Postgres and returned 0 (names cast to int) on MariaDB.
|
||||
from erpnext.projects.doctype.timesheet.timesheet import get_timesheets_list
|
||||
|
||||
customer = "_Test Customer"
|
||||
|
||||
# tie the current user (Administrator) to the customer so the portal resolves it
|
||||
contact = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Contact",
|
||||
"first_name": "_Test Timesheet Portal Contact",
|
||||
"user": "Administrator",
|
||||
"links": [{"link_doctype": "Customer", "link_name": customer}],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
self.addCleanup(self._delete_if_exists, "Contact", contact.name)
|
||||
|
||||
si = create_sales_invoice(customer=customer)
|
||||
|
||||
employee = make_employee("_test_timesheet_portal@example.com", company="_Test Company")
|
||||
timesheet = make_timesheet(employee, is_billable=0)
|
||||
frappe.db.set_value("Timesheet", timesheet.name, "sales_invoice", si.name)
|
||||
|
||||
rows = get_timesheets_list("Timesheet", None, {}, 0, 500)
|
||||
|
||||
row = next((r for r in rows if r.name == timesheet.name), None)
|
||||
self.assertIsNotNone(row, "billed timesheet not returned by portal list")
|
||||
self.assertEqual(row.sales_invoice, si.name)
|
||||
|
||||
@staticmethod
|
||||
def _delete_if_exists(doctype, name):
|
||||
if frappe.db.exists(doctype, name):
|
||||
frappe.delete_doc(doctype, name, force=True)
|
||||
|
||||
|
||||
def make_timesheet(
|
||||
employee,
|
||||
|
||||
@@ -7,7 +7,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Concat, Date, Round
|
||||
from frappe.query_builder.functions import Coalesce, Concat, Date, Round
|
||||
from frappe.utils import flt, get_datetime, getdate
|
||||
from frappe.utils.deprecations import deprecated
|
||||
|
||||
@@ -562,7 +562,7 @@ def get_timesheets_list(doctype, txt, filters, limit_start, limit_page_length=20
|
||||
child_table.activity_type,
|
||||
table.status,
|
||||
child_table.billing_hours,
|
||||
(table.sales_invoice | child_table.sales_invoice).as_("sales_invoice"),
|
||||
Coalesce(table.sales_invoice, child_table.sales_invoice).as_("sales_invoice"),
|
||||
child_table.project,
|
||||
)
|
||||
.orderby(table.end_date)
|
||||
|
||||
Reference in New Issue
Block a user