mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-14 15:11:52 +00:00
Merge pull request #56040 from mihir-kandoi/pg-fn-swap
fix(postgres): replace MySQL-only SQL functions with portable equivalents
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import CustomFunction
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
|
||||
|
||||
@@ -93,7 +93,6 @@ def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filter
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
ifelse = CustomFunction("IF", ["condition", "then", "else"])
|
||||
pe = qb.DocType("Payment Entry")
|
||||
doctype_name = ConstantColumn("Payment Entry")
|
||||
payments = (
|
||||
@@ -101,7 +100,10 @@ def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filter
|
||||
.select(
|
||||
doctype_name.as_("doctype"),
|
||||
pe.name,
|
||||
ifelse(pe.paid_from.eq(filters.account), pe.paid_amount, pe.received_amount).as_("amount"),
|
||||
Case()
|
||||
.when(pe.paid_from.eq(filters.account), pe.paid_amount)
|
||||
.else_(pe.received_amount)
|
||||
.as_("amount"),
|
||||
pe.payment_type,
|
||||
pe.party_type,
|
||||
pe.posting_date,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.report.cheques_and_deposits_incorrectly_cleared.cheques_and_deposits_incorrectly_cleared import (
|
||||
execute,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestChequesAndDepositsIncorrectlyCleared(ERPNextTestSuite):
|
||||
def test_report_executes_with_case_amount(self):
|
||||
# Exercises the Payment Entry branch whose amount column uses a db-aware CASE expression
|
||||
# (previously a MySQL-only IF()). IF() does not compile on postgres, so running the report
|
||||
# query guards the portability fix on both databases.
|
||||
company = frappe.db.get_value("Company", {}, "name")
|
||||
account = frappe.db.get_value(
|
||||
"Account", {"account_type": "Bank", "company": company, "is_group": 0}, "name"
|
||||
)
|
||||
columns, data = execute(frappe._dict({"account": account, "report_date": nowdate()}))
|
||||
self.assertTrue(columns)
|
||||
self.assertIsInstance(data, list)
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import CustomFunction
|
||||
from frappe.query_builder.functions import CurDate, DateDiff
|
||||
from frappe.utils import cint
|
||||
|
||||
|
||||
@@ -102,11 +102,11 @@ def get_sales_details(filters):
|
||||
child_doctype = "Sales Order Item" if filters["based_on"] == "Sales Order" else "Sales Invoice Item"
|
||||
child = frappe.qb.DocType(child_doctype)
|
||||
|
||||
date_diff = CustomFunction("DATEDIFF", ["d1", "d2"])
|
||||
current_date = CustomFunction("CURRENT_DATE", [])
|
||||
|
||||
date_col = parent.transaction_date if filters["based_on"] == "Sales Order" else parent.posting_date
|
||||
days_since_last_order = date_diff(current_date(), date_col)
|
||||
|
||||
# DateDiff is cross-database (DATEDIFF on MariaDB, date subtraction on postgres); CurDate()
|
||||
# renders the bare CURRENT_DATE keyword. Yields the integer number of days.
|
||||
days_since_last_order = DateDiff(CurDate(), date_col)
|
||||
|
||||
sales_data = (
|
||||
frappe.qb.from_(parent)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.report.inactive_sales_items.inactive_sales_items import execute
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestInactiveSalesItems(ERPNextTestSuite):
|
||||
def test_days_since_last_order_is_computed(self):
|
||||
# Exercises the date-arithmetic path (DATEDIFF/CURRENT_DATE on mariadb, date subtraction on
|
||||
# postgres) which must produce the same integer day count on both databases.
|
||||
item = make_item("_Test Inactive Sales Item").name
|
||||
old_date = add_days(today(), -120)
|
||||
so = make_sales_order(item=item, qty=3, rate=150, transaction_date=old_date)
|
||||
so.items[0].delivery_date = add_days(old_date, 7)
|
||||
so.save()
|
||||
so.submit()
|
||||
|
||||
columns, data = execute(frappe._dict({"based_on": "Sales Order", "days": 30}))
|
||||
self.assertTrue(columns)
|
||||
row = next((r for r in data if r.get("item") == item and r.get("days_since_last_order")), None)
|
||||
self.assertIsNotNone(row, "Inactive item should appear in the report")
|
||||
self.assertGreaterEqual(row["days_since_last_order"], 30)
|
||||
|
||||
def test_report_runs_for_sales_invoice(self):
|
||||
columns, _data = execute(frappe._dict({"based_on": "Sales Invoice", "days": 30}))
|
||||
self.assertTrue(columns)
|
||||
@@ -7,7 +7,7 @@ import sys
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.query_builder.functions import DateDiff, Sum
|
||||
from frappe.utils import getdate
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ def get_item_workdays(scorecard):
|
||||
frappe.qb.from_(PO_Item)
|
||||
.join(PO)
|
||||
.on(PO_Item.parent == PO.name)
|
||||
.select(Sum(frappe.qb.fn.DATEDIFF(scorecard.end_date, PO_Item.schedule_date) * (PO_Item.qty)))
|
||||
.select(Sum(DateDiff(scorecard.end_date, PO_Item.schedule_date) * (PO_Item.qty)))
|
||||
.where(PO.supplier == scorecard.supplier)
|
||||
.where(PO_Item.received_qty < PO_Item.qty)
|
||||
.where(PO_Item.schedule_date[scorecard.start_date : scorecard.end_date]) # Équivalent du BETWEEN
|
||||
@@ -153,7 +153,7 @@ def get_total_days_late(scorecard):
|
||||
.on(PR_Item.purchase_order_item == PO_Item.name)
|
||||
.join(PO)
|
||||
.on(PO_Item.parent == PO.name)
|
||||
.select(Sum(frappe.qb.fn.DATEDIFF(PR.posting_date, PO_Item.schedule_date) * PR_Item.qty))
|
||||
.select(Sum(DateDiff(PR.posting_date, PO_Item.schedule_date) * PR_Item.qty))
|
||||
.where(PO.supplier == scorecard.supplier)
|
||||
.where(PO_Item.schedule_date[scorecard.start_date : scorecard.end_date])
|
||||
.where(PO_Item.schedule_date < PR.posting_date)
|
||||
@@ -170,10 +170,7 @@ def get_total_days_late(scorecard):
|
||||
.join(PO)
|
||||
.on(PO_Item.parent == PO.name)
|
||||
.select(
|
||||
Sum(
|
||||
frappe.qb.fn.DATEDIFF(scorecard.end_date, PO_Item.schedule_date)
|
||||
* (PO_Item.qty - PO_Item.received_qty)
|
||||
)
|
||||
Sum(DateDiff(scorecard.end_date, PO_Item.schedule_date) * (PO_Item.qty - PO_Item.received_qty))
|
||||
)
|
||||
.where(PO.supplier == scorecard.supplier)
|
||||
.where(PO_Item.received_qty < PO_Item.qty)
|
||||
@@ -530,7 +527,7 @@ def get_rfq_response_days(scorecard):
|
||||
.on(sq_item.request_for_quotation_item == rfq_item.name)
|
||||
.join(sq)
|
||||
.on(sq_item.parent == sq.name)
|
||||
.select(frappe.qb.fn.Sum(frappe.qb.fn.Datediff(sq.transaction_date, rfq.transaction_date)))
|
||||
.select(frappe.qb.fn.Sum(DateDiff(sq.transaction_date, rfq.transaction_date)))
|
||||
.where(rfq_sup.supplier == scorecard.supplier)
|
||||
.where(sq.supplier == scorecard.supplier)
|
||||
.where(rfq.transaction_date[scorecard.start_date : scorecard.end_date])
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.query_builder.custom import GROUP_CONCAT
|
||||
from frappe.query_builder.functions import Date
|
||||
from frappe.query_builder.functions import Date, GroupConcat
|
||||
|
||||
Opportunity = DocType("Opportunity")
|
||||
OpportunityLostReasonDetail = DocType("Opportunity Lost Reason Detail")
|
||||
@@ -72,6 +71,9 @@ def get_columns():
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
# db-aware GROUP_CONCAT (MariaDB) / STRING_AGG (postgres) with a ", " separator
|
||||
lost_reasons = GroupConcat(OpportunityLostReasonDetail.lost_reason, ", ", alias="lost_reason")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(Opportunity)
|
||||
.left_join(OpportunityLostReasonDetail)
|
||||
@@ -85,7 +87,7 @@ def get_data(filters):
|
||||
Opportunity.party_name,
|
||||
Opportunity.customer_name,
|
||||
Opportunity.opportunity_type,
|
||||
GROUP_CONCAT(OpportunityLostReasonDetail.lost_reason, alias="lost_reason").separator(", "),
|
||||
lost_reasons,
|
||||
Opportunity.sales_stage,
|
||||
Opportunity.territory,
|
||||
)
|
||||
|
||||
22
erpnext/crm/report/lost_opportunity/test_lost_opportunity.py
Normal file
22
erpnext/crm/report/lost_opportunity/test_lost_opportunity.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.crm.report.lost_opportunity.lost_opportunity import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestLostOpportunity(ERPNextTestSuite):
|
||||
def test_report_aggregates_lost_reasons(self):
|
||||
# Exercises the db-aware GROUP_CONCAT (MariaDB) / STRING_AGG (postgres) aggregation of the
|
||||
# child "Opportunity Lost Reason Detail" rows. The MySQL-only GROUP_CONCAT term would fail to
|
||||
# compile on postgres, so simply running the report query guards the portability fix on both
|
||||
# databases.
|
||||
company = frappe.db.get_value("Company", {}, "name")
|
||||
columns, data = execute(
|
||||
frappe._dict({"company": company, "from_date": add_days(today(), -365), "to_date": today()})
|
||||
)
|
||||
self.assertTrue(columns)
|
||||
self.assertIsInstance(data, list)
|
||||
@@ -11,6 +11,7 @@ are called from other modules.
|
||||
import frappe
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import CombineDatetime
|
||||
from frappe.utils import (
|
||||
cint,
|
||||
date_diff,
|
||||
@@ -268,13 +269,17 @@ class OperationsService:
|
||||
self.doc.actual_end_date = max(end_dates)
|
||||
|
||||
def _set_dates_from_stock_entries(self):
|
||||
data = frappe.get_all(
|
||||
"Stock Entry",
|
||||
fields=[{"TIMESTAMP": ["posting_date", "posting_time"], "as": "posting_datetime"}],
|
||||
filters={
|
||||
"work_order": self.doc.name,
|
||||
"purpose": ("in", ["Material Transfer for Manufacture", "Manufacture"]),
|
||||
},
|
||||
# {"TIMESTAMP": [...]} renders MySQL's TIMESTAMP(date, time), invalid on postgres; use the
|
||||
# portable CombineDatetime via query builder instead.
|
||||
se = frappe.qb.DocType("Stock Entry")
|
||||
data = (
|
||||
frappe.qb.from_(se)
|
||||
.select(CombineDatetime(se.posting_date, se.posting_time).as_("posting_datetime"))
|
||||
.where(
|
||||
(se.work_order == self.doc.name)
|
||||
& (se.purpose.isin(["Material Transfer for Manufacture", "Manufacture"]))
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
if not data:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user