Files
erpnext/erpnext/utilities/activation.py
Mihir Kandoi 96d4c48357 refactor(postgres): port Setup/Utilities/Templates/Regional queries to the query builder
Convert raw `frappe.db.sql` in the Setup, Utilities, Templates and Regional
areas 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, reserved words).

Conversions of note (behaviour kept identical to the MariaDB original):
- email_digest: ToDo ordering replicated with a CASE that mirrors MySQL
  `field(priority,'High','Medium','Low')` (unknown/NULL -> 0, sorts first),
  NULL-date-first and a `name` tie-break for a deterministic LIMIT.
- company.get_all_transactions_annual_history: the cross-DocType UNION + GROUP BY
  is replaced by one grouped query per DocType merged with a Counter, so two
  different DocTypes sharing a transaction_date still collapse into one bucket.
- templates/utils.send_message: contact lookup wraps both sides in LOWER() to
  keep MariaDB's case-insensitive email match on case-sensitive Postgres.
- regional/irs_1099 & uae_vat_201: address ranking and emirate aggregation
  rebuilt with CASE/aggregate selects that satisfy Postgres GROUP BY, with a
  deterministic tie-break on the LIMIT-1 address lookups.
- utilities/product.get_item_codes_by_attributes: numeric attribute values are
  cast with cstr() so Postgres doesn't reject `varchar = numeric`.

Tests (run on both MariaDB and Postgres, --lightmode):
- New: company merge test, authorization_rule duplicate-check, youtube report,
  templates/utils, and utilities/templates page reports (partners, rfq,
  material_request_info, product, utilities __init__).
- Existing suites kept green: company, email_digest, transaction_deletion_record,
  irs_1099, uae_vat_201.

Deferred (tracked separately):
- setup/doctype/authorization_control.py still has raw `.format()` SELECTs;
  left for its own PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 14:37:50 +05:30

152 lines
4.0 KiB
Python

# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe import _
from frappe.core.doctype.installed_applications.installed_applications import get_setup_wizard_completed_apps
import erpnext
def get_level(site_info):
activation_level = site_info.get("activation", {}).get("activation_level", 0)
sales_data = site_info.get("activation", {}).get("sales_data", [])
min_count = 0
doctypes = {
"Asset": 5,
"BOM": 3,
"Customer": 5,
"Delivery Note": 5,
"Employee": 3,
"Issue": 5,
"Item": 5,
"Journal Entry": 3,
"Lead": 3,
"Material Request": 5,
"Opportunity": 5,
"Payment Entry": 2,
"Project": 5,
"Purchase Order": 2,
"Purchase Invoice": 5,
"Purchase Receipt": 5,
"Quotation": 3,
"Sales Order": 2,
"Sales Invoice": 2,
"Stock Entry": 3,
"Supplier": 5,
"Task": 5,
"User": 5,
"Work Order": 5,
}
for doctype, min_count in doctypes.items():
count = frappe.db.count(doctype)
if count > min_count:
activation_level += 1
sales_data.append({doctype: count})
if "erpnext" in get_setup_wizard_completed_apps():
activation_level += 1
communication_number = frappe.db.count("Communication", dict(communication_medium="Email"))
if communication_number > 10:
activation_level += 1
sales_data.append({"Communication": communication_number})
# recent login
if frappe.db.exists(
"User", {"last_login": [">", frappe.utils.add_to_date(frappe.utils.now_datetime(), days=-2)]}
):
activation_level += 1
level = {"activation_level": activation_level, "sales_data": sales_data}
return level
def get_help_messages():
"""Returns help messages to be shown on Desktop"""
if get_level() > 6:
return []
domain = frappe.get_cached_value("Company", erpnext.get_default_company(), "domain")
messages = []
message_settings = [
frappe._dict(
doctype="Lead",
title=_("Create Leads"),
description=_("Leads help you get business, add all your contacts and more as your leads"),
action=_("Create Lead"),
route="List/Lead",
domain=("Manufacturing", "Retail", "Services", "Distribution"),
target=3,
),
frappe._dict(
doctype="Quotation",
title=_("Create customer quotes"),
description=_("Quotations are proposals, bids you have sent to your customers"),
action=_("Create Quotation"),
route="List/Quotation",
domain=("Manufacturing", "Retail", "Services", "Distribution"),
target=3,
),
frappe._dict(
doctype="Sales Order",
title=_("Manage your orders"),
description=_("Create Sales Orders to help you plan your work and deliver on-time"),
action=_("Create Sales Order"),
route="List/Sales Order",
domain=("Manufacturing", "Retail", "Services", "Distribution"),
target=3,
),
frappe._dict(
doctype="Purchase Order",
title=_("Create Purchase Orders"),
description=_("Purchase orders help you plan and follow up on your purchases"),
action=_("Create Purchase Order"),
route="List/Purchase Order",
domain=("Manufacturing", "Retail", "Services", "Distribution"),
target=3,
),
frappe._dict(
doctype="User",
title=_("Create Users"),
description=_(
"Add the rest of your organization as your users. You can also add invite Customers to your portal by adding them from Contacts"
),
action=_("Create User"),
route="List/User",
domain=("Manufacturing", "Retail", "Services", "Distribution"),
target=3,
),
frappe._dict(
doctype="Timesheet",
title=_("Add Timesheets"),
description=_(
"Timesheets help keep track of time, cost and billing for activities done by your team"
),
action=_("Create Timesheet"),
route="List/Timesheet",
domain=("Services",),
target=5,
),
frappe._dict(
doctype="Employee",
title=_("Create Employee Records"),
description=_("Create Employee records."),
action=_("Create Employee"),
route="List/Employee",
target=3,
),
]
for m in message_settings:
if not m.domain or domain in m.domain:
m.count = frappe.db.count(m.doctype)
if m.count < m.target:
messages.append(m)
return messages