Reimplements get_ancestor_boms, BOM.traverse_tree, Task.check_recursion and the BOM Explorer report as single recursive-CTE queries (frappe.qb recursive=True), replacing query-per-node walks (N->1). Task cycle detection now catches cycles at any depth (was capped at 15 nodes). Requires the framework recursive-CTE support (frappe#40464).
Convert test-only raw frappe.db.sql calls that have an exact ORM
equivalent: full-table/filtered deletes -> frappe.db.delete, count ->
frappe.db.count, row-existence assertions -> frappe.db.exists,
single-row scalar fetches -> frappe.db.get_value, and simple
equality/range-filter selects -> frappe.get_all. No behaviour change.
Raw SQL that genuinely needs it is left as-is (dynamic identifiers,
aggregates/group-by, positional as_list consumers, DB-catalog
introspection).
Conservative cleanup of frappe.throw/msgprint messages per the message style
guide; meaning, severity, and .format() arguments are unchanged:
- index bare {} placeholders as {0}/{1}/... so translators can reorder
- move f-strings / .format() / concatenation out of _() (they break gettext
extraction and never translate)
- wrap translatable dynamic values (DocType/Select labels) in _()
- fix grammar and colloquialisms
- drop no-op _() wrapping runtime-built strings
Part of #53976.
Conservative cleanup of frappe.throw/msgprint messages per the message style
guide; meaning, severity, and .format() arguments are unchanged:
- index bare {} placeholders as {0}/{1}/... so translators can reorder
- move f-strings / .format() / concatenation out of _() (they break gettext
extraction and never translate)
- wrap translatable dynamic values (DocType/Select labels) in _()
- fix grammar and colloquialisms
- drop no-op _() wrapping runtime-built strings
Part of #53976.
The calendar view sends filters as a list of [doctype, field, op, value]
conditions (filter_area.get()); get_filter_conditions_qb accepts dict or list.
get_project_list builds a single-table query (no joins) with fields="*",
which always selects the unique PK `name`, so distinct=True can never
deduplicate any rows. It is a no-op on the result set for both engines.
It is not a no-op on ordering, though: frappe.db drops the ORDER BY clause
for distinct queries on Postgres (Postgres requires every ORDER BY term to
appear in the select list under DISTINCT), so the website project list came
back unordered on Postgres while MariaDB returned it ordered by `order_by`.
Removing the redundant flag leaves the MariaDB result and order untouched
and restores the same ordering on Postgres.
Covers the sales/billing roll-up (total_sales_amount, total_billed_amount,
gross margin) via the whitelisted update_costing_and_billing, and
consumed-material cost from a project-linked Stock Entry issue. The
purchase-cost roll-up is already covered by the Purchase Invoice tests.
Covers the no-employee path (title set to the activity type and the
default-cost duplication guard) and employee_name being fetched for the
title. Brings activity_cost.py to full coverage.
A template task that depends on another task requires that dependency to
also be present in the template's task list; covers both the rejection
and the valid case.
Covers get_activity_cost falling back to the Activity Type rates (and the
empty result for an unknown type), plus get_timesheet_data and
get_timesheet_detail_rate for a billable timesheet detail.
Covers the whitelisted set_multiple_status and add_multiple_tasks helpers
(including the blank-subject skip), the template-task dependency
validation, the on_trash child-exists guard, and a child task
registering itself in its parent's depends_on.
Both are whitelisted, UI-triggered functions that had no server tests.
Covers duplicating a project with its tasks (and the same-name guard),
and bulk-setting a project plus its tasks to a terminal status (and the
invalid-status guard).
Adds assertions for the four percent_complete_method paths (Manual is
already covered), plus the status transitions: 100% flips a project to
Completed, reopening a task flips it back to Open, and a Cancelled
project keeps its status. The method was previously unasserted.
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>
get_timeline_data grouped Timesheet Detail by Date(from_time) but selected
UnixTimestamp(from_time) (the full timestamp, ungrouped). MariaDB
arbitrary-picks a row's timestamp; Postgres rejects it ("must appear in the
GROUP BY clause"), so the Project timeline (calendar heatmap) is broken on PG.
Select UnixTimestamp(Date(from_time)) — the day's epoch — which is the
timeline key and matches the GROUP BY. CurDate() - Interval(years=1) is
portable and kept as-is.
Adds a test (no coverage existed) that records a timesheet against a project
and asserts get_timeline_data returns day-bucketed counts, on both engines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
daily_reminder/email_sending used raw frappe.db.sql with two portability
and correctness problems:
- The update query selected `progress` and `progress_details` from
`tabProject Update`, but those columns do not exist on the Project
Update doctype, so the query raised on BOTH MariaDB and Postgres
(the function is whitelisted-only, so the bug was latent). Drop the
non-existent columns and the corresponding "Project Status"/"Notes"
cells from the summary table.
- `DATE_ADD(CURRENT_DATE, INTERVAL -1 DAY)` (MySQL-only) and a
`CURRENT_DATE` Holiday lookup are not valid on Postgres.
Convert to ORM: frappe.get_all for Project/Project Update/Project User,
frappe.db.count for drafts, frappe.db.exists for the holiday check, and
add_days(today(), -1) for the date filter. Also str() the frequency in the
message so a NULL/empty frequency (Postgres returns None) does not raise.
Adds a test (the file was an empty stub) that creates a project + an update
dated yesterday and asserts the reminder finds it and runs end to end on
both engines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
make_timesheet(simulate=True) logs at now_datetime(); when the suite runs late
in the day under the site timezone the 2h log crosses midnight, so its to_time
falls outside the report's `to_time <= end-of-day` bound and the submitted
timesheet is (correctly) excluded — making test_submitted_timesheet_in_summary
fail in an evening window (observed at 22:51 IST in CI). Pin the log to a fixed
mid-day window on today so the assertion is time-of-day independent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pin TimesheetBillingService behaviour before refactor: billing a timesheet
into a Sales Invoice links the timesheet detail and marks it Billed on submit,
and clears the link / reverts status on cancel; an unsubmitted timesheet
cannot be invoiced.
Convert raw `frappe.db.sql` across the Projects module to `frappe.qb` / the ORM
so the same code runs on MariaDB and Postgres. Behaviour is preserved on
MariaDB; the conversions also make these paths valid under Postgres' stricter
SQL (GROUP BY, case-sensitivity, empty-string handling).
Conversions of note (behaviour kept identical to the MariaDB original):
- project.get_users_for_project: search selects the stored full_name instead of
concat_ws(first, middle, last) (concat_ws diverges on Postgres, where empty
Data fields are NULL) and wraps Locate in LOWER() to keep MariaDB's
case-insensitive result ordering.
- project costing: percent-complete and sales/billed-amount aggregates rebuilt
as Sum() query-builder selects.
- task.reschedule_dependent_tasks: the correlated subquery is split into a
`Task Depends On` parent-pluck + a Task lookup (same rows, no nested SQL).
- timesheet.get_events: user-permission match conditions move to the query-builder
form via get_event_conditions_qb; calendar columns rebuilt with Concat/Round.
- report/project_wise_stock_tracking & report/daily_timesheet_summary: GROUP BY
cost aggregates and the timesheet date window (timestamp(to_date,'24:00:00') ->
end-of-day via get_combine_datetime) rebuilt to satisfy Postgres.
- search helpers (query_task, get_project, get_timesheet) use frappe.qb.get_query
with ignore_permissions=False in place of build_match_conditions/get_match_cond.
Tests (run on both MariaDB and Postgres, --lightmode):
- Existing project/task/timesheet/activity_cost suites kept green (27 tests).
- New project_wise_stock_tracking test drives all three cost aggregates with
positive data (purchased / issued / delivered GROUP BY) plus get_project_details.
- New daily_timesheet_summary test covers the date-window join.
Not included: project_update.py is deferred. Its daily_reminder()/email_sending()
select `progress`/`progress_details`, columns that do not exist on the Project
Update doctype, so the function errors when invoked regardless of backend - a
pre-existing bug that needs an email-rework, not just a query port.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The second Project in test_sales_order_link_is_not_overwritten_by_second_project
was inserted without a company, which only succeeds when a default company is
configured. On a fresh CI site this raised MandatoryError. Set company from the
sales order explicitly.
refactor(manufacturing, projects): make raw SQL portable to PostgreSQL
Convert the MariaDB-only raw `frappe.db.sql` in the Manufacturing and Projects
modules to the cross-database query builder / ORM, and fix the non-portable
constructs that remain. Every change is a no-op on MariaDB (identical rendered
SQL / identical results) and only brings PostgreSQL — standards-strict where
MySQL is lax — in line.
Areas: BOM (cost/where-used/explosion), Work Order (operations, required items,
mapper, stock report), Workstation, Production Plan sub-assembly/explosion
queries, BOM Stock Analysis / Process Loss / Work Order Stock reports; Projects
(project, task, timesheet, activity cost, project update), Daily Timesheet
Summary and Project-wise Stock Tracking reports.
Part of the staged MariaDB<->PostgreSQL parity rollout (module 2 of 9).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Creating a Project with a `sales_order` set used `frappe.db.set_value`
to unconditionally write the Sales Order's `project` field. When several
projects were created for the same Sales Order, each new project silently
overwrote the previous link, leaving earlier projects orphaned.
Only back-link the Sales Order when it is not already tied to another
project, and write the value through the document's `db_set` so the
modified timestamp and realtime update are handled. A warning is shown
when an existing link is left untouched.
Fixes#52179
Resolve 4 conflicts from Phase 7 service/mapper extraction vs upstream:
- asset.py: take extraction; repoint dangling make_asset_movement JS to mapper
- job_card: port upstream field_no_map(naming_series) into mapper.make_subcontracting_po
- sales_order: port upstream rows-index fix into mapper.make_delivery_note
- sales_invoice (Phase 7): take service delegations; port upstream SQL->QB/ORM
changes for get_warehouse, get_all_mode_of_payments, get_discounting_status,
clear_unallocated_mode_of_payments, and set_pos_fields(POS DN skip) into services
Repoint all JS method strings and Python imports for mapper functions
across 18 doctypes from the doctype module to its mapper module, and
remove the now-unused re-export shims from each doctype file (keeping
only names used internally).