mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-29 06:38:24 +00:00
Merge pull request #56019 from frappe/revert-55994-pg-selling-buying
Revert "refactor(selling, buying): make raw SQL portable to PostgreSQL (parity rollout 1/9)"
This commit is contained in:
@@ -477,8 +477,10 @@ class TestPurchaseOrder(ERPNextTestSuite):
|
|||||||
item_doc.save()
|
item_doc.save()
|
||||||
else:
|
else:
|
||||||
# update valid from
|
# update valid from
|
||||||
frappe.db.set_value(
|
frappe.db.sql(
|
||||||
"Item Tax", {"parent": item, "item_tax_template": tax_template}, "valid_from", nowdate()
|
"""UPDATE `tabItem Tax` set valid_from = CURRENT_DATE
|
||||||
|
where parent = %(item)s and item_tax_template = %(tax)s""",
|
||||||
|
{"item": item, "tax": tax_template},
|
||||||
)
|
)
|
||||||
|
|
||||||
po = create_purchase_order(item_code=item, qty=1, do_not_save=1)
|
po = create_purchase_order(item_code=item, qty=1, do_not_save=1)
|
||||||
@@ -525,8 +527,10 @@ class TestPurchaseOrder(ERPNextTestSuite):
|
|||||||
self.assertEqual(po.taxes[1].total, 840)
|
self.assertEqual(po.taxes[1].total, 840)
|
||||||
|
|
||||||
# teardown
|
# teardown
|
||||||
frappe.db.set_value(
|
frappe.db.sql(
|
||||||
"Item Tax", {"parent": item, "item_tax_template": tax_template}, "valid_from", None
|
"""UPDATE `tabItem Tax` set valid_from = NULL
|
||||||
|
where parent = %(item)s and item_tax_template = %(tax)s""",
|
||||||
|
{"item": item, "tax": tax_template},
|
||||||
)
|
)
|
||||||
po.cancel()
|
po.cancel()
|
||||||
po.delete()
|
po.delete()
|
||||||
@@ -648,7 +652,7 @@ class TestPurchaseOrder(ERPNextTestSuite):
|
|||||||
|
|
||||||
def test_purchase_order_on_hold(self):
|
def test_purchase_order_on_hold(self):
|
||||||
po = create_purchase_order(item_code="_Test Product Bundle Item")
|
po = create_purchase_order(item_code="_Test Product Bundle Item")
|
||||||
po.db_set("status", "On Hold")
|
po.db_set("Status", "On Hold")
|
||||||
pi = make_pi_from_po(po.name)
|
pi = make_pi_from_po(po.name)
|
||||||
pr = make_purchase_receipt(po.name)
|
pr = make_purchase_receipt(po.name)
|
||||||
self.assertRaises(frappe.ValidationError, pr.submit)
|
self.assertRaises(frappe.ValidationError, pr.submit)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import sys
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.query_builder.functions import DateDiff, Sum
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import getdate
|
from frappe.utils import getdate
|
||||||
|
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ def get_item_workdays(scorecard):
|
|||||||
frappe.qb.from_(PO_Item)
|
frappe.qb.from_(PO_Item)
|
||||||
.join(PO)
|
.join(PO)
|
||||||
.on(PO_Item.parent == PO.name)
|
.on(PO_Item.parent == PO.name)
|
||||||
.select(Sum(DateDiff(scorecard.end_date, PO_Item.schedule_date) * (PO_Item.qty)))
|
.select(Sum(frappe.qb.fn.DATEDIFF(scorecard.end_date, PO_Item.schedule_date) * (PO_Item.qty)))
|
||||||
.where(PO.supplier == scorecard.supplier)
|
.where(PO.supplier == scorecard.supplier)
|
||||||
.where(PO_Item.received_qty < PO_Item.qty)
|
.where(PO_Item.received_qty < PO_Item.qty)
|
||||||
.where(PO_Item.schedule_date[scorecard.start_date : scorecard.end_date]) # Équivalent du BETWEEN
|
.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)
|
.on(PR_Item.purchase_order_item == PO_Item.name)
|
||||||
.join(PO)
|
.join(PO)
|
||||||
.on(PO_Item.parent == PO.name)
|
.on(PO_Item.parent == PO.name)
|
||||||
.select(Sum(DateDiff(PR.posting_date, PO_Item.schedule_date) * PR_Item.qty))
|
.select(Sum(frappe.qb.fn.DATEDIFF(PR.posting_date, PO_Item.schedule_date) * PR_Item.qty))
|
||||||
.where(PO.supplier == scorecard.supplier)
|
.where(PO.supplier == scorecard.supplier)
|
||||||
.where(PO_Item.schedule_date[scorecard.start_date : scorecard.end_date])
|
.where(PO_Item.schedule_date[scorecard.start_date : scorecard.end_date])
|
||||||
.where(PO_Item.schedule_date < PR.posting_date)
|
.where(PO_Item.schedule_date < PR.posting_date)
|
||||||
@@ -170,7 +170,10 @@ def get_total_days_late(scorecard):
|
|||||||
.join(PO)
|
.join(PO)
|
||||||
.on(PO_Item.parent == PO.name)
|
.on(PO_Item.parent == PO.name)
|
||||||
.select(
|
.select(
|
||||||
Sum(DateDiff(scorecard.end_date, PO_Item.schedule_date) * (PO_Item.qty - PO_Item.received_qty))
|
Sum(
|
||||||
|
frappe.qb.fn.DATEDIFF(scorecard.end_date, PO_Item.schedule_date)
|
||||||
|
* (PO_Item.qty - PO_Item.received_qty)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.where(PO.supplier == scorecard.supplier)
|
.where(PO.supplier == scorecard.supplier)
|
||||||
.where(PO_Item.received_qty < PO_Item.qty)
|
.where(PO_Item.received_qty < PO_Item.qty)
|
||||||
@@ -527,7 +530,7 @@ def get_rfq_response_days(scorecard):
|
|||||||
.on(sq_item.request_for_quotation_item == rfq_item.name)
|
.on(sq_item.request_for_quotation_item == rfq_item.name)
|
||||||
.join(sq)
|
.join(sq)
|
||||||
.on(sq_item.parent == sq.name)
|
.on(sq_item.parent == sq.name)
|
||||||
.select(frappe.qb.fn.Sum(DateDiff(sq.transaction_date, rfq.transaction_date)))
|
.select(frappe.qb.fn.Sum(frappe.qb.fn.Datediff(sq.transaction_date, rfq.transaction_date)))
|
||||||
.where(rfq_sup.supplier == scorecard.supplier)
|
.where(rfq_sup.supplier == scorecard.supplier)
|
||||||
.where(sq.supplier == scorecard.supplier)
|
.where(sq.supplier == scorecard.supplier)
|
||||||
.where(rfq.transaction_date[scorecard.start_date : scorecard.end_date])
|
.where(rfq.transaction_date[scorecard.start_date : scorecard.end_date])
|
||||||
|
|||||||
@@ -305,9 +305,7 @@ def get_po_entries(filters):
|
|||||||
& (parent.name == child.parent)
|
& (parent.name == child.parent)
|
||||||
& (parent.status.notin(("Closed", "Completed", "Cancelled")))
|
& (parent.status.notin(("Closed", "Completed", "Cancelled")))
|
||||||
)
|
)
|
||||||
# This is one row per PO item; the selected child.* columns are only functionally dependent
|
.groupby(parent.name, child.material_request_item)
|
||||||
# on the child PK, which postgres requires in the GROUP BY (MariaDB allows omitting it).
|
|
||||||
.groupby(parent.name, child.material_request_item, child.name)
|
|
||||||
)
|
)
|
||||||
query = apply_filters_on_query(filters, parent, child, query)
|
query = apply_filters_on_query(filters, parent, child, query)
|
||||||
|
|
||||||
|
|||||||
@@ -71,9 +71,7 @@ def get_data(filters):
|
|||||||
po_item.name,
|
po_item.name,
|
||||||
)
|
)
|
||||||
.where((po_item.parent == po.name) & (po.status.notin(("Stopped", "On Hold"))) & (po.docstatus == 1))
|
.where((po_item.parent == po.name) & (po.status.notin(("Stopped", "On Hold"))) & (po.docstatus == 1))
|
||||||
# the selected po.* columns need the Purchase Order PK grouped on postgres; po.name is 1:1
|
.groupby(po_item.name)
|
||||||
# with the grouped po_item.name, so groups are unchanged.
|
|
||||||
.groupby(po_item.name, po.name)
|
|
||||||
.orderby(po.transaction_date)
|
.orderby(po.transaction_date)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import copy
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.query_builder.functions import Coalesce, Max, Sum
|
from frappe.query_builder.functions import Coalesce, Sum
|
||||||
from frappe.utils import cint, date_diff, flt, getdate
|
from frappe.utils import cint, date_diff, flt, getdate
|
||||||
|
|
||||||
|
|
||||||
@@ -44,15 +44,13 @@ def get_data(filters):
|
|||||||
.on(mr_item.parent == mr.name)
|
.on(mr_item.parent == mr.name)
|
||||||
.select(
|
.select(
|
||||||
mr.name.as_("material_request"),
|
mr.name.as_("material_request"),
|
||||||
# non-grouped columns are constant per grouped mr.name / item_code -> Max() keeps the
|
mr.transaction_date.as_("date"),
|
||||||
# GROUP BY valid on postgres while returning the same value MySQL picked.
|
mr_item.schedule_date.as_("required_date"),
|
||||||
Max(mr.transaction_date).as_("date"),
|
|
||||||
Max(mr_item.schedule_date).as_("required_date"),
|
|
||||||
mr_item.item_code.as_("item_code"),
|
mr_item.item_code.as_("item_code"),
|
||||||
Sum(Coalesce(mr_item.qty, 0)).as_("qty"),
|
Sum(Coalesce(mr_item.qty, 0)).as_("qty"),
|
||||||
Sum(Coalesce(mr_item.stock_qty, 0)).as_("stock_qty"),
|
Sum(Coalesce(mr_item.stock_qty, 0)).as_("stock_qty"),
|
||||||
Max(Coalesce(mr_item.uom, "")).as_("uom"),
|
Coalesce(mr_item.uom, "").as_("uom"),
|
||||||
Max(Coalesce(mr_item.stock_uom, "")).as_("stock_uom"),
|
Coalesce(mr_item.stock_uom, "").as_("stock_uom"),
|
||||||
Sum(Coalesce(mr_item.ordered_qty, 0)).as_("ordered_qty"),
|
Sum(Coalesce(mr_item.ordered_qty, 0)).as_("ordered_qty"),
|
||||||
Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
|
Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
|
||||||
(Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.received_qty, 0))).as_(
|
(Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.received_qty, 0))).as_(
|
||||||
@@ -60,9 +58,9 @@ def get_data(filters):
|
|||||||
),
|
),
|
||||||
Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
|
Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
|
||||||
(Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.ordered_qty, 0))).as_("qty_to_order"),
|
(Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.ordered_qty, 0))).as_("qty_to_order"),
|
||||||
Max(mr_item.item_name).as_("item_name"),
|
mr_item.item_name,
|
||||||
Max(mr_item.description).as_("description"),
|
mr_item.description,
|
||||||
Max(mr.company).as_("company"),
|
mr.company,
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(mr.material_request_type == "Purchase")
|
(mr.material_request_type == "Purchase")
|
||||||
@@ -74,7 +72,7 @@ def get_data(filters):
|
|||||||
|
|
||||||
query = get_conditions(filters, query, mr, mr_item) # add conditional conditions
|
query = get_conditions(filters, query, mr, mr_item) # add conditional conditions
|
||||||
|
|
||||||
query = query.groupby(mr.name, mr_item.item_code).orderby(Max(mr.transaction_date), Max(mr.schedule_date))
|
query = query.groupby(mr.name, mr_item.item_code).orderby(mr.transaction_date, mr.schedule_date)
|
||||||
data = query.run(as_dict=True)
|
data = query.run(as_dict=True)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from frappe.model.document import Document
|
|||||||
from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options
|
from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options
|
||||||
from frappe.model.utils.rename_doc import update_linked_doctypes
|
from frappe.model.utils.rename_doc import update_linked_doctypes
|
||||||
from frappe.query_builder import CustomFunction, Field, functions
|
from frappe.query_builder import CustomFunction, Field, functions
|
||||||
from frappe.query_builder.functions import Cast, Coalesce, Max
|
from frappe.query_builder.functions import Cast, Coalesce, Max, Substring
|
||||||
from frappe.utils import cint, cstr, flt, get_formatted_email, today
|
from frappe.utils import cint, cstr, flt, get_formatted_email, today
|
||||||
from frappe.utils.user import get_users_with_role
|
from frappe.utils.user import get_users_with_role
|
||||||
|
|
||||||
@@ -128,11 +128,9 @@ class Customer(TransactionBase):
|
|||||||
Customer = frappe.qb.DocType("Customer")
|
Customer = frappe.qb.DocType("Customer")
|
||||||
|
|
||||||
if frappe.db.db_type == "postgres":
|
if frappe.db.db_type == "postgres":
|
||||||
# Postgres: take the token after the last space (mirrors MariaDB
|
# Postgres: extract trailing digits (e.g. "Customer - 3") and cast to int.
|
||||||
# SUBSTRING_INDEX(name, ' ', -1)) and cast to int. (pypika's Substring is start/length,
|
# NOTE: PostgreSQL is strict about types; MySQL's UNSIGNED cast does not exist.
|
||||||
# not a regex, so it can't be used here; UNSIGNED also doesn't exist on postgres.)
|
extracted_part = Substring(Customer.name, r"\d+$")
|
||||||
regexp_replace = CustomFunction("regexp_replace", ["source", "pattern", "replacement"])
|
|
||||||
extracted_part = regexp_replace(Customer.name, "^.* ", "")
|
|
||||||
casted_part = Cast(extracted_part, "INTEGER")
|
casted_part = Cast(extracted_part, "INTEGER")
|
||||||
else:
|
else:
|
||||||
# MariaDB/MySQL: keep existing behavior.
|
# MariaDB/MySQL: keep existing behavior.
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import getdate, nowdate
|
from frappe.utils import getdate, nowdate
|
||||||
from pypika.terms import ExistsCriterion
|
|
||||||
|
|
||||||
from erpnext.controllers.selling_controller import SellingController
|
from erpnext.controllers.selling_controller import SellingController
|
||||||
|
|
||||||
@@ -359,31 +358,22 @@ def get_list_context(context=None):
|
|||||||
|
|
||||||
|
|
||||||
def set_expired_status():
|
def set_expired_status():
|
||||||
quotation = frappe.qb.DocType("Quotation")
|
# filter out submitted non expired quotations whose validity has been ended
|
||||||
so = frappe.qb.DocType("Sales Order")
|
cond = "`tabQuotation`.docstatus = 1 and `tabQuotation`.status NOT IN ('Expired', 'Lost') and `tabQuotation`.valid_till < %s"
|
||||||
so_item = frappe.qb.DocType("Sales Order Item")
|
# check if those QUO have SO against it
|
||||||
|
so_against_quo = """
|
||||||
|
SELECT
|
||||||
|
so.name FROM `tabSales Order` so, `tabSales Order Item` so_item
|
||||||
|
WHERE
|
||||||
|
so_item.docstatus = 1 and so.docstatus = 1
|
||||||
|
and so_item.parent = so.name
|
||||||
|
and so_item.prevdoc_docname = `tabQuotation`.name"""
|
||||||
|
|
||||||
# submitted Sales Orders raised against the quotation (correlated to the quotation being updated)
|
# if not exists any SO, set status as Expired
|
||||||
so_against_quo = (
|
frappe.db.multisql(
|
||||||
frappe.qb.from_(so)
|
{
|
||||||
.from_(so_item)
|
"mariadb": f"""UPDATE `tabQuotation` SET `tabQuotation`.status = 'Expired' WHERE {cond} and not exists({so_against_quo})""",
|
||||||
.select(so.name)
|
"postgres": f"""UPDATE `tabQuotation` SET status = 'Expired' FROM `tabSales Order`, `tabSales Order Item` WHERE {cond} and not exists({so_against_quo})""",
|
||||||
.where(
|
},
|
||||||
(so_item.docstatus == 1)
|
(nowdate()),
|
||||||
& (so.docstatus == 1)
|
|
||||||
& (so_item.parent == so.name)
|
|
||||||
& (so_item.prevdoc_docname == quotation.name)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# expire submitted, non-expired/lost quotations whose validity has ended and that have no SO
|
|
||||||
(
|
|
||||||
frappe.qb.update(quotation)
|
|
||||||
.set(quotation.status, "Expired")
|
|
||||||
.where(
|
|
||||||
(quotation.docstatus == 1)
|
|
||||||
& (quotation.status.notin(["Expired", "Lost"]))
|
|
||||||
& (quotation.valid_till < nowdate())
|
|
||||||
& ExistsCriterion(so_against_quo).negate()
|
|
||||||
)
|
|
||||||
).run()
|
|
||||||
|
|||||||
@@ -1082,19 +1082,13 @@ class TestQuotation(ERPNextTestSuite):
|
|||||||
|
|
||||||
@ERPNextTestSuite.change_settings("Accounts Settings", {"allow_pegged_currencies_exchange_rates": True})
|
@ERPNextTestSuite.change_settings("Accounts Settings", {"allow_pegged_currencies_exchange_rates": True})
|
||||||
def test_make_quotation_qar_to_inr(self):
|
def test_make_quotation_qar_to_inr(self):
|
||||||
# Seed the pegged base rate in cache so the conversion is deterministic and doesn't depend
|
|
||||||
# on the external exchange-rate API (flaky/unreachable in CI) or on a record left by another
|
|
||||||
# test. get_exchange_rate reads this key, applies the QAR peg (/3.64); the assertion below
|
|
||||||
# reads the same key.
|
|
||||||
cache = frappe.cache()
|
|
||||||
key = "currency_exchange_rate_{}:{}:{}".format("2026-01-01", "QAR", "INR")
|
|
||||||
cache.setex(name=key, time=21600, value=flt(218.4))
|
|
||||||
|
|
||||||
quotation = make_quotation(
|
quotation = make_quotation(
|
||||||
currency="QAR",
|
currency="QAR",
|
||||||
transaction_date="2026-01-01",
|
transaction_date="2026-01-01",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cache = frappe.cache()
|
||||||
|
key = "currency_exchange_rate_{}:{}:{}".format("2026-01-01", "QAR", "INR")
|
||||||
value = cache.get(key)
|
value = cache.get(key)
|
||||||
expected_rate = flt(value) / 3.64
|
expected_rate = flt(value) / 3.64
|
||||||
|
|
||||||
|
|||||||
@@ -907,8 +907,10 @@ class TestSalesOrder(ERPNextTestSuite):
|
|||||||
item_doc.save()
|
item_doc.save()
|
||||||
else:
|
else:
|
||||||
# update valid from
|
# update valid from
|
||||||
frappe.db.set_value(
|
frappe.db.sql(
|
||||||
"Item Tax", {"parent": item, "item_tax_template": tax_template}, "valid_from", nowdate()
|
"""UPDATE `tabItem Tax` set valid_from = CURRENT_DATE
|
||||||
|
where parent = %(item)s and item_tax_template = %(tax)s""",
|
||||||
|
{"item": item, "tax": tax_template},
|
||||||
)
|
)
|
||||||
|
|
||||||
so = make_sales_order(item_code=item, qty=1, do_not_save=1)
|
so = make_sales_order(item_code=item, qty=1, do_not_save=1)
|
||||||
@@ -958,8 +960,10 @@ class TestSalesOrder(ERPNextTestSuite):
|
|||||||
self.assertEqual(so.taxes[1].total, 480)
|
self.assertEqual(so.taxes[1].total, 480)
|
||||||
|
|
||||||
# teardown
|
# teardown
|
||||||
frappe.db.set_value(
|
frappe.db.sql(
|
||||||
"Item Tax", {"parent": item, "item_tax_template": tax_template}, "valid_from", None
|
"""UPDATE `tabItem Tax` set valid_from = NULL
|
||||||
|
where parent = %(item)s and item_tax_template = %(tax)s""",
|
||||||
|
{"item": item, "tax": tax_template},
|
||||||
)
|
)
|
||||||
so.cancel()
|
so.cancel()
|
||||||
so.delete()
|
so.delete()
|
||||||
@@ -1553,12 +1557,11 @@ class TestSalesOrder(ERPNextTestSuite):
|
|||||||
|
|
||||||
# Check if Work Orders were raised
|
# Check if Work Orders were raised
|
||||||
for item in so_item_name:
|
for item in so_item_name:
|
||||||
wo_qty = frappe.get_all(
|
wo_qty = frappe.db.sql(
|
||||||
"Work Order",
|
"select sum(qty) from `tabWork Order` where sales_order=%s and sales_order_item=%s",
|
||||||
filters={"sales_order": so.name, "sales_order_item": item},
|
(so.name, item),
|
||||||
fields=[{"SUM": "qty", "as": "qty"}],
|
|
||||||
)
|
)
|
||||||
self.assertEqual(wo_qty[0].qty, so_item_name.get(item))
|
self.assertEqual(wo_qty[0][0], so_item_name.get(item))
|
||||||
|
|
||||||
def test_advance_payment_entry_unlink_against_sales_order(self):
|
def test_advance_payment_entry_unlink_against_sales_order(self):
|
||||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
@@ -1737,7 +1740,9 @@ class TestSalesOrder(ERPNextTestSuite):
|
|||||||
mr_dict["include_exploded_items"] = 0
|
mr_dict["include_exploded_items"] = 0
|
||||||
mr_dict["ignore_existing_ordered_qty"] = 1
|
mr_dict["ignore_existing_ordered_qty"] = 1
|
||||||
make_raw_material_request(mr_dict, so.company, so.name)
|
make_raw_material_request(mr_dict, so.company, so.name)
|
||||||
mr = frappe.get_all("Material Request", fields=["name"], order_by="creation desc", limit=1)[0]
|
mr = frappe.db.sql(
|
||||||
|
"""select name from `tabMaterial Request` ORDER BY creation DESC LIMIT 1""", as_dict=1
|
||||||
|
)[0]
|
||||||
mr_doc = frappe.get_doc("Material Request", mr.get("name"))
|
mr_doc = frappe.get_doc("Material Request", mr.get("name"))
|
||||||
self.assertEqual(mr_doc.items[0].sales_order, so.name)
|
self.assertEqual(mr_doc.items[0].sales_order, so.name)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.query_builder import Criterion, DocType, Order
|
from frappe.query_builder import DocType, Order
|
||||||
from frappe.utils import cint, get_datetime
|
from frappe.utils import cint, get_datetime
|
||||||
from frappe.utils.nestedset import get_root_of
|
from frappe.utils.nestedset import get_root_of
|
||||||
|
|
||||||
@@ -155,55 +155,50 @@ def get_items(
|
|||||||
if not frappe.db.exists("Item Group", item_group):
|
if not frappe.db.exists("Item Group", item_group):
|
||||||
item_group = get_root_of("Item Group")
|
item_group = get_root_of("Item Group")
|
||||||
|
|
||||||
|
condition = get_conditions(search_term)
|
||||||
|
condition += get_item_group_condition(pos_profile)
|
||||||
|
|
||||||
lft, rgt = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"])
|
lft, rgt = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"])
|
||||||
|
|
||||||
item = frappe.qb.DocType("Item")
|
bin_join_selection, bin_join_condition = "", ""
|
||||||
item_group_dt = frappe.qb.DocType("Item Group")
|
if hide_unavailable_items:
|
||||||
|
bin_join_selection = "LEFT JOIN `tabBin` bin ON bin.item_code = item.name"
|
||||||
|
bin_join_condition = "AND (item.is_stock_item = 0 OR (item.is_stock_item = 1 AND bin.warehouse = %(warehouse)s AND bin.actual_qty > 0))"
|
||||||
|
|
||||||
item_group_subquery = (
|
items_data = frappe.db.sql(
|
||||||
frappe.qb.from_(item_group_dt)
|
"""
|
||||||
.select(item_group_dt.name)
|
SELECT
|
||||||
.where((item_group_dt.lft >= lft) & (item_group_dt.rgt <= rgt))
|
item.name AS item_code,
|
||||||
)
|
|
||||||
|
|
||||||
query = (
|
|
||||||
frappe.qb.from_(item)
|
|
||||||
.select(
|
|
||||||
item.name.as_("item_code"),
|
|
||||||
item.item_name,
|
item.item_name,
|
||||||
item.description,
|
item.description,
|
||||||
item.stock_uom,
|
item.stock_uom,
|
||||||
item.image.as_("item_image"),
|
item.image AS item_image,
|
||||||
item.is_stock_item,
|
item.is_stock_item,
|
||||||
item.sales_uom,
|
item.sales_uom
|
||||||
)
|
FROM
|
||||||
.where(
|
`tabItem` item {bin_join_selection}
|
||||||
(item.disabled == 0)
|
WHERE
|
||||||
& (item.has_variants == 0)
|
item.disabled = 0
|
||||||
& (item.is_sales_item == 1)
|
AND item.has_variants = 0
|
||||||
& (item.is_fixed_asset == 0)
|
AND item.is_sales_item = 1
|
||||||
& (item.item_group.isin(item_group_subquery))
|
AND item.is_fixed_asset = 0
|
||||||
& get_conditions(search_term, item)
|
AND item.item_group in (SELECT name FROM `tabItem Group` WHERE lft >= {lft} AND rgt <= {rgt})
|
||||||
)
|
AND {condition}
|
||||||
)
|
{bin_join_condition}
|
||||||
|
ORDER BY
|
||||||
item_group_condition = get_item_group_condition(pos_profile, item)
|
item.name asc
|
||||||
if item_group_condition is not None:
|
LIMIT
|
||||||
query = query.where(item_group_condition)
|
{page_length} offset {start}""".format(
|
||||||
|
start=cint(start),
|
||||||
if hide_unavailable_items:
|
page_length=cint(page_length),
|
||||||
bin = frappe.qb.DocType("Bin")
|
lft=cint(lft),
|
||||||
query = (
|
rgt=cint(rgt),
|
||||||
query.left_join(bin)
|
condition=condition,
|
||||||
.on(bin.item_code == item.name)
|
bin_join_selection=bin_join_selection,
|
||||||
.where(
|
bin_join_condition=bin_join_condition,
|
||||||
(item.is_stock_item == 0)
|
),
|
||||||
| ((item.is_stock_item == 1) & (bin.warehouse == warehouse) & (bin.actual_qty > 0))
|
{"warehouse": warehouse},
|
||||||
)
|
as_dict=1,
|
||||||
)
|
|
||||||
|
|
||||||
items_data = (
|
|
||||||
query.orderby(item.name, order=Order.asc).limit(cint(page_length)).offset(cint(start)).run(as_dict=1)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# return (empty) list if there are no results
|
# return (empty) list if there are no results
|
||||||
@@ -274,62 +269,56 @@ def search_for_serial_or_batch_or_barcode_number(search_value: str) -> dict[str,
|
|||||||
return scan_barcode(search_value)
|
return scan_barcode(search_value)
|
||||||
|
|
||||||
|
|
||||||
def get_conditions(search_term, item=None):
|
def get_conditions(search_term):
|
||||||
if item is None:
|
condition = "("
|
||||||
item = frappe.qb.DocType("Item")
|
condition += """item.name like {search_term}
|
||||||
|
or item.item_name like {search_term}""".format(search_term=frappe.db.escape("%" + search_term + "%"))
|
||||||
|
condition += add_search_fields_condition(search_term)
|
||||||
|
condition += ")"
|
||||||
|
|
||||||
pattern = f"%{search_term}%"
|
return condition
|
||||||
conditions = [item.name.like(pattern), item.item_name.like(pattern)]
|
|
||||||
conditions += add_search_fields_condition(search_term, item)
|
|
||||||
|
|
||||||
return Criterion.any(conditions)
|
|
||||||
|
|
||||||
|
|
||||||
def add_search_fields_condition(search_term, item=None):
|
def add_search_fields_condition(search_term):
|
||||||
if item is None:
|
condition = ""
|
||||||
item = frappe.qb.DocType("Item")
|
|
||||||
|
|
||||||
pattern = f"%{search_term}%"
|
|
||||||
conditions = []
|
|
||||||
search_fields = frappe.get_all("POS Search Fields", fields=["fieldname"])
|
search_fields = frappe.get_all("POS Search Fields", fields=["fieldname"])
|
||||||
for field in search_fields:
|
if search_fields:
|
||||||
if not field.get("fieldname"):
|
for field in search_fields:
|
||||||
continue
|
if not field.get("fieldname"):
|
||||||
conditions.append(item[field["fieldname"]].like(pattern))
|
continue
|
||||||
|
condition += " or item.`{}` like {}".format(
|
||||||
return conditions
|
field["fieldname"], frappe.db.escape("%" + search_term + "%")
|
||||||
|
)
|
||||||
|
return condition
|
||||||
|
|
||||||
|
|
||||||
def get_item_group_condition(pos_profile, item=None):
|
def get_item_group_condition(pos_profile):
|
||||||
if item is None:
|
cond = "and 1=1"
|
||||||
item = frappe.qb.DocType("Item")
|
|
||||||
|
|
||||||
item_groups = get_item_groups(pos_profile)
|
item_groups = get_item_groups(pos_profile)
|
||||||
if item_groups:
|
if item_groups:
|
||||||
return item.item_group.isin(item_groups)
|
cond = "and item.item_group in (%s)" % (", ".join(["%s"] * len(item_groups)))
|
||||||
|
|
||||||
return None
|
return cond % tuple(item_groups)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
def item_group_query(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
|
def item_group_query(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
|
||||||
|
item_groups = []
|
||||||
|
cond = "1=1"
|
||||||
pos_profile = filters.get("pos_profile")
|
pos_profile = filters.get("pos_profile")
|
||||||
|
|
||||||
item_filters = [["name", "like", f"%{txt}%"]]
|
|
||||||
if pos_profile:
|
if pos_profile:
|
||||||
item_groups = get_item_groups(pos_profile)
|
item_groups = get_item_groups(pos_profile)
|
||||||
if item_groups:
|
|
||||||
item_filters.append(["name", "in", item_groups])
|
|
||||||
|
|
||||||
return frappe.get_all(
|
if item_groups:
|
||||||
"Item Group",
|
cond = "name in (%s)" % (", ".join(["%s"] * len(item_groups)))
|
||||||
filters=item_filters,
|
cond = cond % tuple(item_groups)
|
||||||
fields=["name"],
|
|
||||||
distinct=True,
|
return frappe.db.sql(
|
||||||
limit_start=start,
|
f""" select distinct name from `tabItem Group`
|
||||||
limit_page_length=page_len,
|
where {cond} and (name like %(txt)s) limit {page_len} offset {start}""",
|
||||||
as_list=True,
|
{"txt": "%%%s%%" % txt},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from itertools import groupby
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.query_builder.functions import Count, Date
|
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
|
|
||||||
from erpnext.accounts.report.utils import convert
|
from erpnext.accounts.report.utils import convert
|
||||||
@@ -23,47 +22,33 @@ def validate_filters(from_date, to_date, company):
|
|||||||
def get_funnel_data(from_date: str, to_date: str, company: str):
|
def get_funnel_data(from_date: str, to_date: str, company: str):
|
||||||
validate_filters(from_date, to_date, company)
|
validate_filters(from_date, to_date, company)
|
||||||
|
|
||||||
lead = frappe.qb.DocType("Lead")
|
active_leads = frappe.db.sql(
|
||||||
active_leads = (
|
"""select count(*) from `tabLead`
|
||||||
frappe.qb.from_(lead)
|
where (date(`creation`) between %s and %s)
|
||||||
.select(Count("*"))
|
and company=%s""",
|
||||||
.where(Date(lead.creation).between(from_date, to_date) & (lead.company == company))
|
(from_date, to_date, company),
|
||||||
.run()
|
|
||||||
)[0][0]
|
)[0][0]
|
||||||
|
|
||||||
opportunity = frappe.qb.DocType("Opportunity")
|
opportunities = frappe.db.sql(
|
||||||
opportunities = (
|
"""select count(*) from `tabOpportunity`
|
||||||
frappe.qb.from_(opportunity)
|
where (date(`creation`) between %s and %s)
|
||||||
.select(Count("*"))
|
and opportunity_from='Lead' and company=%s""",
|
||||||
.where(
|
(from_date, to_date, company),
|
||||||
Date(opportunity.creation).between(from_date, to_date)
|
|
||||||
& (opportunity.opportunity_from == "Lead")
|
|
||||||
& (opportunity.company == company)
|
|
||||||
)
|
|
||||||
.run()
|
|
||||||
)[0][0]
|
)[0][0]
|
||||||
|
|
||||||
quotation = frappe.qb.DocType("Quotation")
|
quotations = frappe.db.sql(
|
||||||
quotations = (
|
"""select count(*) from `tabQuotation`
|
||||||
frappe.qb.from_(quotation)
|
where docstatus = 1 and (date(`creation`) between %s and %s)
|
||||||
.select(Count("*"))
|
and (opportunity!="" or quotation_to="Lead") and company=%s""",
|
||||||
.where(
|
(from_date, to_date, company),
|
||||||
(quotation.docstatus == 1)
|
|
||||||
& Date(quotation.creation).between(from_date, to_date)
|
|
||||||
& ((quotation.opportunity != "") | (quotation.quotation_to == "Lead"))
|
|
||||||
& (quotation.company == company)
|
|
||||||
)
|
|
||||||
.run()
|
|
||||||
)[0][0]
|
)[0][0]
|
||||||
|
|
||||||
customer = frappe.qb.DocType("Customer")
|
converted = frappe.db.sql(
|
||||||
converted = (
|
"""select count(*) from `tabCustomer`
|
||||||
frappe.qb.from_(customer)
|
JOIN `tabLead` ON `tabLead`.name = `tabCustomer`.lead_name
|
||||||
.inner_join(lead)
|
WHERE (date(`tabCustomer`.creation) between %s and %s)
|
||||||
.on(lead.name == customer.lead_name)
|
and `tabLead`.company=%s""",
|
||||||
.select(Count("*"))
|
(from_date, to_date, company),
|
||||||
.where(Date(customer.creation).between(from_date, to_date) & (lead.company == company))
|
|
||||||
.run()
|
|
||||||
)[0][0]
|
)[0][0]
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ def execute(filters=None):
|
|||||||
|
|
||||||
columns = get_columns()
|
columns = get_columns()
|
||||||
iwq_map = get_item_warehouse_quantity_map()
|
iwq_map = get_item_warehouse_quantity_map()
|
||||||
item_map = get_item_details(list(iwq_map.keys()))
|
item_map = get_item_details()
|
||||||
data = []
|
data = []
|
||||||
for sbom, warehouse in iwq_map.items():
|
for sbom, warehouse in iwq_map.items():
|
||||||
total = 0
|
total = 0
|
||||||
@@ -53,67 +53,48 @@ def get_columns():
|
|||||||
return columns
|
return columns
|
||||||
|
|
||||||
|
|
||||||
def get_item_details(item_codes):
|
def get_item_details():
|
||||||
# only the bundle items actually shown in the report need detail lookup, not the whole catalogue
|
|
||||||
if not item_codes:
|
|
||||||
return {}
|
|
||||||
item_map = {}
|
item_map = {}
|
||||||
for item in frappe.get_all(
|
for item in frappe.db.sql(
|
||||||
"Item",
|
"""SELECT name, item_name, description, stock_uom
|
||||||
filters={"name": ["in", item_codes]},
|
from `tabItem`""",
|
||||||
fields=["name", "item_name", "description", "stock_uom"],
|
as_dict=1,
|
||||||
):
|
):
|
||||||
item_map.setdefault(item.name, item)
|
item_map.setdefault(item.name, item)
|
||||||
return item_map
|
return item_map
|
||||||
|
|
||||||
|
|
||||||
def get_item_warehouse_quantity_map():
|
def get_item_warehouse_quantity_map():
|
||||||
# Components of every active product bundle: (bundle item code, component item, qty per bundle)
|
query = """SELECT parent, warehouse, MIN(qty) AS qty
|
||||||
pb = frappe.qb.DocType("Product Bundle")
|
FROM (SELECT b.parent, bi.item_code, bi.warehouse,
|
||||||
pbi = frappe.qb.DocType("Product Bundle Item")
|
sum(bi.projected_qty) / b.qty AS qty
|
||||||
bundle_components = (
|
FROM tabBin AS bi, (SELECT pb.new_item_code as parent, b.item_code, b.qty, w.name
|
||||||
frappe.qb.from_(pbi)
|
FROM `tabProduct Bundle Item` b, `tabWarehouse` w,
|
||||||
.inner_join(pb)
|
`tabProduct Bundle` pb
|
||||||
.on(pbi.parent == pb.name)
|
where b.parent = pb.name
|
||||||
.select(pb.new_item_code.as_("parent"), pbi.item_code, pbi.qty)
|
and pb.is_active = 1 and pb.docstatus = 1) AS b
|
||||||
.where((pb.is_active == 1) & (pb.docstatus == 1))
|
WHERE bi.item_code = b.item_code
|
||||||
.run(as_dict=True)
|
AND bi.warehouse = b.name
|
||||||
)
|
GROUP BY b.parent, b.item_code, bi.warehouse
|
||||||
|
UNION ALL
|
||||||
if not bundle_components:
|
SELECT b.parent, b.item_code, b.name, 0 AS qty
|
||||||
return {}
|
FROM (SELECT pb.new_item_code as parent, b.item_code, b.qty, w.name
|
||||||
|
FROM `tabProduct Bundle Item` b, `tabWarehouse` w,
|
||||||
component_items = list({c.item_code for c in bundle_components})
|
`tabProduct Bundle` pb
|
||||||
|
where b.parent = pb.name
|
||||||
bin_projected = {
|
and pb.is_active = 1 and pb.docstatus = 1) AS b
|
||||||
(b.item_code, b.warehouse): flt(b.projected_qty)
|
WHERE NOT EXISTS(SELECT *
|
||||||
for b in frappe.get_all(
|
FROM `tabBin` AS bi
|
||||||
"Bin",
|
WHERE bi.item_code = b.item_code
|
||||||
filters={"item_code": ["in", component_items]},
|
AND bi.warehouse = b.name)) AS r
|
||||||
fields=["item_code", "warehouse", "projected_qty"],
|
GROUP BY parent, warehouse
|
||||||
)
|
HAVING MIN(qty) != 0"""
|
||||||
}
|
result = frappe.db.sql(query, as_dict=1)
|
||||||
|
last_sbom = ""
|
||||||
# Only warehouses that hold at least one component can yield a non-zero packable qty; a warehouse
|
|
||||||
# missing any component yields MIN()=0 and is dropped below, so scanning every warehouse in the
|
|
||||||
# system is wasted work. Scope the loop to warehouses present in the Bin result.
|
|
||||||
bin_warehouses = {wh for (_, wh) in bin_projected}
|
|
||||||
|
|
||||||
# For each (bundle, warehouse) the number of complete bundles that can be packed is the
|
|
||||||
# MIN over components of (component projected_qty in that warehouse / component qty per bundle).
|
|
||||||
# A component with no Bin in a warehouse contributes 0 (the original UNION ALL/NOT EXISTS branch).
|
|
||||||
packable_qty = {}
|
|
||||||
for component in bundle_components:
|
|
||||||
if not component.qty:
|
|
||||||
continue
|
|
||||||
for warehouse in bin_warehouses:
|
|
||||||
qty = bin_projected.get((component.item_code, warehouse), 0) / flt(component.qty)
|
|
||||||
key = (component.parent, warehouse)
|
|
||||||
packable_qty[key] = min(packable_qty[key], qty) if key in packable_qty else qty
|
|
||||||
|
|
||||||
sbom_map = {}
|
sbom_map = {}
|
||||||
for (parent, warehouse), qty in packable_qty.items():
|
for line in result:
|
||||||
if qty != 0: # HAVING MIN(qty) != 0
|
if line.get("parent") != last_sbom:
|
||||||
sbom_map.setdefault(parent, {})[warehouse] = qty
|
last_sbom = line.get("parent")
|
||||||
|
actual_dict = sbom_map.setdefault(last_sbom, {})
|
||||||
|
actual_dict.setdefault(line.get("warehouse"), line.get("qty"))
|
||||||
return sbom_map
|
return sbom_map
|
||||||
|
|||||||
@@ -112,8 +112,8 @@ def get_data_by_territory(filters, common_columns):
|
|||||||
customers_in = get_customer_stats(filters, tree_view=True)
|
customers_in = get_customer_stats(filters, tree_view=True)
|
||||||
|
|
||||||
territory_dict = {}
|
territory_dict = {}
|
||||||
for t in frappe.get_all(
|
for t in frappe.db.sql(
|
||||||
"Territory", fields=["name", "lft", "parent_territory", "is_group"], order_by="lft"
|
"""SELECT name, lft, parent_territory, is_group FROM `tabTerritory` ORDER BY lft""", as_dict=1
|
||||||
):
|
):
|
||||||
territory_dict.update({t.name: {"parent": t.parent_territory, "is_group": t.is_group}})
|
territory_dict.update({t.name: {"parent": t.parent_territory, "is_group": t.is_group}})
|
||||||
|
|
||||||
@@ -155,18 +155,19 @@ def get_data_by_territory(filters, common_columns):
|
|||||||
|
|
||||||
def get_customer_stats(filters, tree_view=False):
|
def get_customer_stats(filters, tree_view=False):
|
||||||
"""Calculates number of new and repeated customers and revenue."""
|
"""Calculates number of new and repeated customers and revenue."""
|
||||||
|
company_condition = ""
|
||||||
|
if filters.get("company"):
|
||||||
|
company_condition = " and company=%(company)s"
|
||||||
|
|
||||||
customers = []
|
customers = []
|
||||||
customers_in = {}
|
customers_in = {}
|
||||||
|
|
||||||
si_filters = {"docstatus": 1, "posting_date": ["<=", filters.get("to_date")]}
|
for si in frappe.db.sql(
|
||||||
if filters.get("company"):
|
f"""select territory, posting_date, customer, base_grand_total from `tabSales Invoice`
|
||||||
si_filters["company"] = filters.get("company")
|
where docstatus=1 and posting_date <= %(to_date)s
|
||||||
|
{company_condition} order by posting_date""",
|
||||||
for si in frappe.get_all(
|
filters,
|
||||||
"Sales Invoice",
|
as_dict=1,
|
||||||
filters=si_filters,
|
|
||||||
fields=["territory", "posting_date", "customer", "base_grand_total"],
|
|
||||||
order_by="posting_date",
|
|
||||||
):
|
):
|
||||||
key = si.territory if tree_view else si.posting_date.strftime("%Y-%m")
|
key = si.territory if tree_view else si.posting_date.strftime("%Y-%m")
|
||||||
new_or_repeat = "new" if si.customer not in customers else "repeat"
|
new_or_repeat = "new" if si.customer not in customers else "repeat"
|
||||||
|
|||||||
@@ -77,18 +77,17 @@ def get_columns(customer_naming_type):
|
|||||||
|
|
||||||
|
|
||||||
def get_details(filters):
|
def get_details(filters):
|
||||||
c = frappe.qb.DocType("Customer")
|
sql_query = """SELECT
|
||||||
ccl = frappe.qb.DocType("Customer Credit Limit")
|
c.name, c.customer_name,
|
||||||
query = (
|
ccl.bypass_credit_limit_check,
|
||||||
frappe.qb.from_(c)
|
c.is_frozen, c.disabled
|
||||||
.inner_join(ccl)
|
FROM `tabCustomer` c, `tabCustomer Credit Limit` ccl
|
||||||
.on(c.name == ccl.parent)
|
WHERE
|
||||||
.select(c.name, c.customer_name, ccl.bypass_credit_limit_check, c.is_frozen, c.disabled)
|
c.name = ccl.parent
|
||||||
.where(ccl.company == filters.get("company"))
|
AND ccl.company = %(company)s"""
|
||||||
)
|
|
||||||
|
|
||||||
# customer filter is optional.
|
# customer filter is optional.
|
||||||
if filters.get("customer"):
|
if filters.get("customer"):
|
||||||
query = query.where(c.name == filters.get("customer"))
|
sql_query += " AND c.name = %(customer)s"
|
||||||
|
|
||||||
return query.run(as_dict=1)
|
return frappe.db.sql(sql_query, filters, as_dict=1)
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.query_builder import Case
|
from frappe.query_builder import Case, CustomFunction
|
||||||
from frappe.query_builder.functions import Count, CurDate, DateDiff, Max, Sum
|
from frappe.query_builder.functions import Count, Max, Sum
|
||||||
from frappe.utils import cint
|
from frappe.utils import cint
|
||||||
|
|
||||||
|
|
||||||
@@ -37,6 +37,9 @@ def get_sales_details(doctype):
|
|||||||
customer = frappe.qb.DocType("Customer")
|
customer = frappe.qb.DocType("Customer")
|
||||||
sales_doctype = frappe.qb.DocType(doctype)
|
sales_doctype = frappe.qb.DocType(doctype)
|
||||||
|
|
||||||
|
date_diff = CustomFunction("DATEDIFF", ["d1", "d2"])
|
||||||
|
current_date = CustomFunction("CURRENT_DATE", [])
|
||||||
|
|
||||||
if doctype == "Sales Order":
|
if doctype == "Sales Order":
|
||||||
total_considered = Sum(
|
total_considered = Sum(
|
||||||
Case()
|
Case()
|
||||||
@@ -52,9 +55,7 @@ def get_sales_details(doctype):
|
|||||||
date_col = sales_doctype.posting_date
|
date_col = sales_doctype.posting_date
|
||||||
|
|
||||||
last_order_date = Max(date_col)
|
last_order_date = Max(date_col)
|
||||||
# DateDiff is cross-database (DATEDIFF on MariaDB, date subtraction on postgres); CurDate()
|
days_since_last_order = date_diff(current_date(), last_order_date)
|
||||||
# renders the bare CURRENT_DATE keyword. Yields the integer number of days.
|
|
||||||
days_since_last_order = DateDiff(CurDate(), last_order_date)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
frappe.qb.from_(customer)
|
frappe.qb.from_(customer)
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, qb, query_builder
|
from frappe import _, qb, query_builder
|
||||||
from frappe.query_builder import Criterion
|
from frappe.query_builder import Criterion, functions
|
||||||
from frappe.query_builder.functions import Max
|
|
||||||
from frappe.utils.dateutils import getdate
|
from frappe.utils.dateutils import getdate
|
||||||
|
|
||||||
|
|
||||||
@@ -186,6 +185,9 @@ def get_so_with_invoices(filters):
|
|||||||
conditions = get_conditions(filters)
|
conditions = get_conditions(filters)
|
||||||
filter_criterions = build_filter_criterions(filters)
|
filter_criterions = build_filter_criterions(filters)
|
||||||
|
|
||||||
|
datediff = query_builder.CustomFunction("DATEDIFF", ["cur_date", "due_date"])
|
||||||
|
ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"])
|
||||||
|
|
||||||
query_so = (
|
query_so = (
|
||||||
qb.from_(so)
|
qb.from_(so)
|
||||||
.join(soi)
|
.join(soi)
|
||||||
@@ -197,8 +199,7 @@ def get_so_with_invoices(filters):
|
|||||||
.select(
|
.select(
|
||||||
so.customer,
|
so.customer,
|
||||||
so.transaction_date.as_("submitted"),
|
so.transaction_date.as_("submitted"),
|
||||||
# CASE + a Python date is portable; MySQL's IF()/DATEDIFF()/CURDATE() don't exist on postgres
|
ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"),
|
||||||
query_builder.Case().when(ps.due_date < getdate(), "Overdue").else_("Unpaid").as_("status"),
|
|
||||||
ps.payment_term,
|
ps.payment_term,
|
||||||
ps.description,
|
ps.description,
|
||||||
ps.due_date,
|
ps.due_date,
|
||||||
@@ -229,13 +230,7 @@ def get_so_with_invoices(filters):
|
|||||||
.on(si.name == sii.parent)
|
.on(si.name == sii.parent)
|
||||||
.inner_join(soi)
|
.inner_join(soi)
|
||||||
.on(soi.name == sii.so_detail)
|
.on(soi.name == sii.so_detail)
|
||||||
.select(
|
.select(sii.sales_order, sii.parent.as_("invoice"), si.base_grand_total.as_("invoice_amount"))
|
||||||
# grouped by the invoice (sii.parent); sales_order is arbitrary per invoice on MySQL and
|
|
||||||
# base_grand_total is constant per invoice -> Max() keeps the GROUP BY postgres-valid.
|
|
||||||
Max(sii.sales_order).as_("sales_order"),
|
|
||||||
sii.parent.as_("invoice"),
|
|
||||||
Max(si.base_grand_total).as_("invoice_amount"),
|
|
||||||
)
|
|
||||||
.where((sii.sales_order.isin([x.name for x in sorders])) & (si.docstatus == 1))
|
.where((sii.sales_order.isin([x.name for x in sorders])) & (si.docstatus == 1))
|
||||||
.groupby(sii.parent)
|
.groupby(sii.parent)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.query_builder.functions import Max, Sum
|
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
|
|
||||||
|
|
||||||
@@ -50,28 +49,27 @@ def get_columns():
|
|||||||
|
|
||||||
|
|
||||||
def get_data():
|
def get_data():
|
||||||
so = frappe.qb.DocType("Sales Order")
|
sales_order_entry = frappe.db.sql(
|
||||||
so_item = frappe.qb.DocType("Sales Order Item")
|
"""
|
||||||
sales_order_entry = (
|
SELECT
|
||||||
frappe.qb.from_(so)
|
|
||||||
.inner_join(so_item)
|
|
||||||
.on(so.name == so_item.parent)
|
|
||||||
.select(
|
|
||||||
so_item.item_code,
|
so_item.item_code,
|
||||||
# non-grouped columns are constant per grouped so.name / item_code -> Max() keeps the
|
so_item.item_name,
|
||||||
# GROUP BY valid on postgres while returning the same value MySQL picked.
|
so_item.description,
|
||||||
Max(so_item.item_name).as_("item_name"),
|
|
||||||
Max(so_item.description).as_("description"),
|
|
||||||
so.name,
|
so.name,
|
||||||
Max(so.transaction_date).as_("transaction_date"),
|
so.transaction_date,
|
||||||
Max(so.customer).as_("customer"),
|
so.customer,
|
||||||
Max(so.territory).as_("territory"),
|
so.territory,
|
||||||
Sum(so_item.qty).as_("total_qty"),
|
sum(so_item.qty) as total_qty,
|
||||||
Max(so.company).as_("company"),
|
so.company
|
||||||
)
|
FROM `tabSales Order` so, `tabSales Order Item` so_item
|
||||||
.where((so.docstatus == 1) & so.status.notin(["Closed", "Completed", "Cancelled"]))
|
WHERE
|
||||||
.groupby(so.name, so_item.item_code)
|
so.docstatus = 1
|
||||||
.run(as_dict=1)
|
and so.name = so_item.parent
|
||||||
|
and so.status not in ('Closed','Completed','Cancelled')
|
||||||
|
GROUP BY
|
||||||
|
so.name,so_item.item_code
|
||||||
|
""",
|
||||||
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
sales_orders = [row.name for row in sales_order_entry]
|
sales_orders = [row.name for row in sales_order_entry]
|
||||||
|
|||||||
@@ -510,10 +510,10 @@ class Analytics:
|
|||||||
|
|
||||||
self.depth_map = frappe._dict()
|
self.depth_map = frappe._dict()
|
||||||
|
|
||||||
self.group_entries = frappe.get_all(
|
self.group_entries = frappe.db.sql(
|
||||||
self.filters.tree_type,
|
f"""select name, lft, rgt , {parent} as parent
|
||||||
fields=["name", "lft", "rgt", f"{parent} as parent"],
|
from `tab{self.filters.tree_type}` order by lft""",
|
||||||
order_by="lft",
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
for d in self.group_entries:
|
for d in self.group_entries:
|
||||||
@@ -528,19 +528,14 @@ class Analytics:
|
|||||||
if not frappe.db.exists("DocType", self.filters.doc_type):
|
if not frappe.db.exists("DocType", self.filters.doc_type):
|
||||||
frappe.throw(_("Invalid Document Type {0}").format(self.filters.doc_type))
|
frappe.throw(_("Invalid Document Type {0}").format(self.filters.doc_type))
|
||||||
|
|
||||||
order_types = frappe.get_all(
|
self.group_entries = frappe.db.sql(
|
||||||
self.filters.doc_type,
|
f""" select * from (select "Order Types" as name, 0 as lft,
|
||||||
filters={"order_type": ["is", "set"]},
|
2 as rgt, '' as parent union select distinct order_type as name, 1 as lft, 1 as rgt, "Order Types" as parent
|
||||||
pluck="order_type",
|
from `tab{self.filters.doc_type}` where ifnull(order_type, '') != '') as b order by lft, name
|
||||||
distinct=True,
|
""",
|
||||||
order_by="order_type",
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.group_entries = [frappe._dict(name="Order Types", lft=0, rgt=2, parent="")]
|
|
||||||
self.group_entries += [
|
|
||||||
frappe._dict(name=order_type, lft=1, rgt=1, parent="Order Types") for order_type in order_types
|
|
||||||
]
|
|
||||||
|
|
||||||
for d in self.group_entries:
|
for d in self.group_entries:
|
||||||
if d.parent:
|
if d.parent:
|
||||||
self.depth_map.setdefault(d.name, self.depth_map.get(d.parent) + 1)
|
self.depth_map.setdefault(d.name, self.depth_map.get(d.parent) + 1)
|
||||||
@@ -549,7 +544,7 @@ class Analytics:
|
|||||||
|
|
||||||
def get_supplier_parent_child_map(self):
|
def get_supplier_parent_child_map(self):
|
||||||
self.parent_child_map = frappe._dict(
|
self.parent_child_map = frappe._dict(
|
||||||
frappe.get_all("Supplier", fields=["name", "supplier_group"], as_list=True)
|
frappe.db.sql(""" select name, supplier_group from `tabSupplier`""")
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_chart_data(self):
|
def get_chart_data(self):
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ from collections import OrderedDict
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, qb
|
from frappe import _, qb
|
||||||
from frappe.query_builder import Case, CustomFunction
|
from frappe.query_builder import CustomFunction
|
||||||
from frappe.query_builder.functions import Coalesce, DateDiff, Max, Sum
|
from frappe.query_builder.functions import Max
|
||||||
from frappe.utils import date_diff, flt, getdate, nowdate
|
from frappe.utils import date_diff, flt, getdate
|
||||||
|
|
||||||
|
|
||||||
def execute(filters=None):
|
def execute(filters=None):
|
||||||
@@ -18,7 +18,8 @@ def execute(filters=None):
|
|||||||
validate_filters(filters)
|
validate_filters(filters)
|
||||||
|
|
||||||
columns = get_columns(filters)
|
columns = get_columns(filters)
|
||||||
data = get_data(filters)
|
conditions = get_conditions(filters)
|
||||||
|
data = get_data(conditions, filters)
|
||||||
so_elapsed_time = get_so_elapsed_time(data)
|
so_elapsed_time = get_so_elapsed_time(data)
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
@@ -38,66 +39,64 @@ def validate_filters(filters):
|
|||||||
frappe.throw(_("To Date cannot be before From Date."))
|
frappe.throw(_("To Date cannot be before From Date."))
|
||||||
|
|
||||||
|
|
||||||
def get_data(filters):
|
def get_conditions(filters):
|
||||||
so = qb.DocType("Sales Order")
|
conditions = ""
|
||||||
soi = qb.DocType("Sales Order Item")
|
if filters.get("from_date") and filters.get("to_date"):
|
||||||
sii = qb.DocType("Sales Invoice Item")
|
conditions += " and so.transaction_date between %(from_date)s and %(to_date)s"
|
||||||
|
|
||||||
# Use the application's today (nowdate, System Settings timezone) rather than the database
|
if filters.get("company"):
|
||||||
# server's CURRENT_DATE: the two differ by a day when the DB server runs in a different timezone
|
conditions += " and so.company = %(company)s"
|
||||||
# (e.g. UTC DB + IST app near midnight), which made delay_days non-deterministic on postgres CI.
|
|
||||||
# DateDiff is cross-database: DATEDIFF() on MariaDB, date subtraction on postgres; it casts the
|
|
||||||
# string date to a date on postgres. delivery_date is functionally dependent on the grouped
|
|
||||||
# soi.name primary key, so this is valid under both.
|
|
||||||
delay = DateDiff(nowdate(), soi.delivery_date)
|
|
||||||
conversion_rate = Coalesce(so.conversion_rate, 1)
|
|
||||||
|
|
||||||
query = (
|
if filters.get("sales_order"):
|
||||||
qb.from_(so)
|
conditions += " and so.name in %(sales_order)s"
|
||||||
.join(soi)
|
|
||||||
.on(soi.parent == so.name)
|
if filters.get("status"):
|
||||||
.left_join(sii)
|
conditions += " and so.status in %(status)s"
|
||||||
.on((sii.so_detail == soi.name) & (sii.docstatus == 1))
|
|
||||||
.select(
|
if filters.get("warehouse"):
|
||||||
so.transaction_date.as_("date"),
|
conditions += " and soi.warehouse = %(warehouse)s"
|
||||||
soi.delivery_date.as_("delivery_date"),
|
|
||||||
so.name.as_("sales_order"),
|
return conditions
|
||||||
so.status,
|
|
||||||
so.customer,
|
|
||||||
soi.item_code,
|
def get_data(conditions, filters):
|
||||||
delay.as_("delay_days"),
|
data = frappe.db.sql(
|
||||||
Case().when(so.status.isin(["Completed", "To Bill"]), 0).else_(delay).as_("delay"),
|
f"""
|
||||||
soi.qty,
|
SELECT
|
||||||
soi.delivered_qty,
|
so.transaction_date as date,
|
||||||
(soi.qty - soi.delivered_qty).as_("pending_qty"),
|
soi.delivery_date as delivery_date,
|
||||||
Coalesce(Sum(sii.qty), 0).as_("billed_qty"),
|
so.name as sales_order,
|
||||||
soi.base_amount.as_("amount"),
|
so.status, so.customer, soi.item_code,
|
||||||
(soi.delivered_qty * soi.base_rate).as_("delivered_qty_amount"),
|
DATEDIFF(CURRENT_DATE, soi.delivery_date) as delay_days,
|
||||||
(soi.billed_amt * conversion_rate).as_("billed_amount"),
|
IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay,
|
||||||
(soi.base_amount - (soi.billed_amt * conversion_rate)).as_("pending_amount"),
|
soi.qty, soi.delivered_qty,
|
||||||
soi.warehouse.as_("warehouse"),
|
(soi.qty - soi.delivered_qty) AS pending_qty,
|
||||||
so.company,
|
IFNULL(SUM(sii.qty), 0) as billed_qty,
|
||||||
soi.name,
|
soi.base_amount as amount,
|
||||||
soi.description.as_("description"),
|
(soi.delivered_qty * soi.base_rate) as delivered_qty_amount,
|
||||||
)
|
(soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount,
|
||||||
.where((so.status.notin(["Stopped", "On Hold"])) & (so.docstatus == 1))
|
(soi.base_amount - (soi.billed_amt * IFNULL(so.conversion_rate, 1))) as pending_amount,
|
||||||
.groupby(soi.name, so.name)
|
soi.warehouse as warehouse,
|
||||||
.orderby(so.transaction_date)
|
so.company, soi.name,
|
||||||
.orderby(soi.item_code)
|
soi.description as description
|
||||||
|
FROM
|
||||||
|
`tabSales Order` so,
|
||||||
|
`tabSales Order Item` soi
|
||||||
|
LEFT JOIN `tabSales Invoice Item` sii
|
||||||
|
ON sii.so_detail = soi.name and sii.docstatus = 1
|
||||||
|
WHERE
|
||||||
|
soi.parent = so.name
|
||||||
|
and so.status not in ('Stopped', 'On Hold')
|
||||||
|
and so.docstatus = 1
|
||||||
|
{conditions}
|
||||||
|
GROUP BY soi.name
|
||||||
|
ORDER BY so.transaction_date ASC, soi.item_code ASC
|
||||||
|
""",
|
||||||
|
filters,
|
||||||
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
if filters.get("from_date") and filters.get("to_date"):
|
return data
|
||||||
query = query.where(so.transaction_date[filters.get("from_date") : filters.get("to_date")])
|
|
||||||
if filters.get("company"):
|
|
||||||
query = query.where(so.company == filters.get("company"))
|
|
||||||
if filters.get("sales_order"):
|
|
||||||
query = query.where(so.name.isin(filters.get("sales_order")))
|
|
||||||
if filters.get("status"):
|
|
||||||
query = query.where(so.status.isin(filters.get("status")))
|
|
||||||
if filters.get("warehouse"):
|
|
||||||
query = query.where(soi.warehouse == filters.get("warehouse"))
|
|
||||||
|
|
||||||
return query.run(as_dict=True)
|
|
||||||
|
|
||||||
|
|
||||||
def get_so_elapsed_time(data):
|
def get_so_elapsed_time(data):
|
||||||
@@ -113,17 +112,7 @@ def get_so_elapsed_time(data):
|
|||||||
dn = qb.DocType("Delivery Note")
|
dn = qb.DocType("Delivery Note")
|
||||||
dni = qb.DocType("Delivery Note Item")
|
dni = qb.DocType("Delivery Note Item")
|
||||||
|
|
||||||
# TO_SECONDS is MariaDB-only. On postgres, subtracting dates yields days, so multiply
|
to_seconds = CustomFunction("TO_SECONDS", ["date"])
|
||||||
# by 86400 for the equivalent second delta. so.transaction_date is neither aggregated nor
|
|
||||||
# in the GROUP BY, but it is selectable under postgres' strict GROUP BY because it is
|
|
||||||
# functionally dependent on the grouped so.name (a doctype's `name` is always the PK).
|
|
||||||
if frappe.db.db_type == "postgres":
|
|
||||||
elapsed_seconds = ((Max(dn.posting_date) - so.transaction_date) * 86400).as_("elapsed_seconds")
|
|
||||||
else:
|
|
||||||
to_seconds = CustomFunction("TO_SECONDS", ["date"])
|
|
||||||
elapsed_seconds = (to_seconds(Max(dn.posting_date)) - to_seconds(so.transaction_date)).as_(
|
|
||||||
"elapsed_seconds"
|
|
||||||
)
|
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
qb.from_(so)
|
qb.from_(so)
|
||||||
@@ -136,11 +125,11 @@ def get_so_elapsed_time(data):
|
|||||||
.select(
|
.select(
|
||||||
so.name.as_("sales_order"),
|
so.name.as_("sales_order"),
|
||||||
soi.item_code.as_("so_item_code"),
|
soi.item_code.as_("so_item_code"),
|
||||||
elapsed_seconds,
|
(to_seconds(Max(dn.posting_date)) - to_seconds(so.transaction_date)).as_("elapsed_seconds"),
|
||||||
)
|
)
|
||||||
.where((so.name.isin(sales_orders)) & (dn.docstatus == 1))
|
.where((so.name.isin(sales_orders)) & (dn.docstatus == 1))
|
||||||
.orderby(so.name, soi.name)
|
.orderby(so.name, soi.name)
|
||||||
.groupby(soi.name, so.name)
|
.groupby(soi.name)
|
||||||
)
|
)
|
||||||
dn_elapsed_time = query.run(as_dict=True)
|
dn_elapsed_time = query.run(as_dict=True)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user