snapshot of https://github.com/frappe/crm.git (develop, 2026-09-18; blobs > 131072B omitted)
Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
This commit is contained in:
102
crm/integrations/erpnext/item.py
Normal file
102
crm/integrations/erpnext/item.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import frappe
|
||||
|
||||
from crm.fcrm.doctype.crm_product.sync_utils import payload_differs
|
||||
from crm.integrations.erpnext.utils import (
|
||||
CASCADE_FLAG,
|
||||
cascade_rename,
|
||||
in_cascade,
|
||||
set_links,
|
||||
should_sync,
|
||||
validate_rename_conflict,
|
||||
)
|
||||
|
||||
CATALOGUE_FIELDS = ("standard_rate", "image", "disabled", "description")
|
||||
|
||||
|
||||
def _should_sync(doc) -> bool:
|
||||
return should_sync() and not doc.flags.get("ignore_crm_sync")
|
||||
|
||||
|
||||
def get_item_price_rate(item_code: str, uom: str | None = None):
|
||||
from erpnext.stock.get_item_details import get_item_price
|
||||
|
||||
price_list = frappe.db.get_single_value("Selling Settings", "selling_price_list")
|
||||
if not price_list:
|
||||
return None
|
||||
pctx = frappe._dict({"price_list": price_list, "uom": uom, "transaction_date": frappe.utils.nowdate()})
|
||||
rows = get_item_price(pctx, item_code)
|
||||
if not rows:
|
||||
return None
|
||||
row = rows[0]
|
||||
|
||||
return row.price_list_rate if isinstance(row, dict) else row[1]
|
||||
|
||||
|
||||
def _catalogue_data(doc) -> dict:
|
||||
data = {f: doc.get(f) for f in CATALOGUE_FIELDS}
|
||||
# Give preference to Item Price over standard rate
|
||||
# keep standard_rate as fallback
|
||||
rate = get_item_price_rate(doc.name, doc.get("stock_uom"))
|
||||
if rate is not None:
|
||||
data["standard_rate"] = rate
|
||||
# standard_rate is a NOT NULL Currency column and frappe.db.set_value writes
|
||||
# raw values (no flt coercion, unlike doc.insert), so never pass None through
|
||||
data["standard_rate"] = frappe.utils.flt(data["standard_rate"])
|
||||
return data
|
||||
|
||||
|
||||
def after_insert(doc, method=None):
|
||||
if not _should_sync(doc):
|
||||
return
|
||||
if frappe.db.get_value("CRM Product", {"erpnext_item_code": doc.name}):
|
||||
return
|
||||
product = frappe.get_doc(
|
||||
{
|
||||
"doctype": "CRM Product",
|
||||
"product_code": doc.item_code,
|
||||
"product_name": doc.item_name,
|
||||
**_catalogue_data(doc),
|
||||
}
|
||||
)
|
||||
product.flags.ignore_erpnext_sync = True
|
||||
product.insert(ignore_permissions=True)
|
||||
set_links(doc.name, product.name)
|
||||
|
||||
|
||||
def on_update(doc, method=None):
|
||||
if not _should_sync(doc):
|
||||
return
|
||||
product_name = frappe.db.get_value("CRM Product", {"erpnext_item_code": doc.name})
|
||||
if not product_name:
|
||||
return
|
||||
data = {
|
||||
"product_name": doc.item_name,
|
||||
**_catalogue_data(doc),
|
||||
}
|
||||
current = frappe.db.get_value("CRM Product", product_name, list(data.keys()), as_dict=True) or {}
|
||||
if not payload_differs(data, current):
|
||||
return
|
||||
frappe.db.set_value("CRM Product", product_name, data)
|
||||
|
||||
|
||||
def before_rename(doc, method=None, olddn=None, newdn=None, merge=False):
|
||||
validate_rename_conflict("Item", olddn, newdn, merge)
|
||||
|
||||
|
||||
def after_rename(doc, method=None, olddn=None, newdn=None, merge=False):
|
||||
cascade_rename("Item", olddn, newdn, merge)
|
||||
|
||||
|
||||
def on_trash(doc, method=None):
|
||||
if in_cascade() or not _should_sync(doc):
|
||||
return
|
||||
product = frappe.db.get_value("CRM Product", {"erpnext_item_code": doc.name})
|
||||
if not product:
|
||||
return
|
||||
# Clear back link first so CRMProduct.on_trash won't try to delete this Item
|
||||
frappe.db.set_value("CRM Product", product, "erpnext_item_code", None)
|
||||
frappe.flags[CASCADE_FLAG] = True
|
||||
try:
|
||||
frappe.delete_doc("CRM Product", product, ignore_permissions=True)
|
||||
finally:
|
||||
frappe.flags[CASCADE_FLAG] = False
|
||||
Reference in New Issue
Block a user