diff --git a/westech_r2/api/__pycache__/__init__.cpython-312.pyc b/westech_r2/api/__pycache__/__init__.cpython-312.pyc
index 02f0bc7..5dd511c 100644
Binary files a/westech_r2/api/__pycache__/__init__.cpython-312.pyc and b/westech_r2/api/__pycache__/__init__.cpython-312.pyc differ
diff --git a/westech_r2/api/__pycache__/ebay_pricing.cpython-312.pyc b/westech_r2/api/__pycache__/ebay_pricing.cpython-312.pyc
new file mode 100644
index 0000000..a3f557f
Binary files /dev/null and b/westech_r2/api/__pycache__/ebay_pricing.cpython-312.pyc differ
diff --git a/westech_r2/api/__pycache__/sales.cpython-312.pyc b/westech_r2/api/__pycache__/sales.cpython-312.pyc
new file mode 100644
index 0000000..57d58b9
Binary files /dev/null and b/westech_r2/api/__pycache__/sales.cpython-312.pyc differ
diff --git a/westech_r2/api/ebay_pricing.py b/westech_r2/api/ebay_pricing.py
index 218136d..649ffff 100644
--- a/westech_r2/api/ebay_pricing.py
+++ b/westech_r2/api/ebay_pricing.py
@@ -35,7 +35,10 @@ def _get_oxylabs_creds():
user = password = ""
if settings:
user = settings.get("oxylabs_user") or ""
- password = settings.get_password("oxylabs_password") or ""
+ try:
+ password = settings.get_password("oxylabs_password") or ""
+ except Exception:
+ pass
if not user:
user = frappe.conf.get("oxylabs_user", "")
if not password:
@@ -47,7 +50,10 @@ def _get_apify_token():
settings = _get_settings()
token = ""
if settings:
- token = settings.get_password("apify_token") or ""
+ try:
+ token = settings.get_password("apify_token") or ""
+ except Exception:
+ pass
if not token:
token = frappe.conf.get("apify_token", "")
return token
@@ -292,7 +298,7 @@ def _parse_prices(items, manufacturer, model, source="oxylabs"):
"price_average": round(avg, 2),
"price_auction": round(median, 2),
"sample_count": len(prices),
- "source": f"ebay_{source}",
+ "source": source or "unknown",
"scraped_at": now(),
}
@@ -311,9 +317,17 @@ def _upsert_system_pricing(manufacturer, model, pricing):
doc.manufacturer = manufacturer
doc.model = model
for key in ("price_high", "price_low", "price_average", "price_auction",
- "sample_count", "source", "scraped_at"):
+ "sample_count", "scraped_at"):
if key in pricing:
setattr(doc, key, pricing[key])
+ # Fix source to match allowed values
+ if "source" in pricing:
+ raw_source = pricing["source"]
+ if raw_source.startswith("ebay_"):
+ raw_source = raw_source.replace("ebay_", "")
+ if raw_source not in ("oxylabs", "apify"):
+ raw_source = "unknown"
+ setattr(doc, "source", raw_source)
if doc.scraped_at:
scraped = frappe.utils.get_datetime(doc.scraped_at)
now = now_datetime()
diff --git a/westech_r2/fixtures/client_script.json b/westech_r2/fixtures/client_script.json
index 3963fc8..a2acee6 100644
--- a/westech_r2/fixtures/client_script.json
+++ b/westech_r2/fixtures/client_script.json
@@ -4,10 +4,10 @@
"doctype": "Client Script",
"dt": "Serial No",
"enabled": 1,
- "modified": "2026-05-19 08:15:00.000000",
+ "modified": "2026-05-19 09:16:00.000000",
"module": null,
"name": "Serial No - Price Calculator",
- "script": "frappe.ui.form.on('Serial No', {\n grade: function(frm) {\n calculate_recommended_price(frm);\n },\n cpu_test: function(frm) {\n check_hardware_failures(frm);\n },\n ram_test: function(frm) {\n check_hardware_failures(frm);\n },\n refresh: function(frm) {\n // Label change via JS (since fixture label updates need migrate)\n var price_field = frm.get_field('assigned_price');\n if (price_field && price_field.$wrapper) {\n price_field.$wrapper.find('.control-label').text('Recommended Price');\n }\n check_hardware_failures(frm);\n }\n});\n\nfunction check_hardware_failures(frm) {\n var cpu_fail = frm.doc.cpu_test === 'Fail';\n var ram_fail = frm.doc.ram_test === 'Fail';\n \n if (cpu_fail || ram_fail) {\n // Force Flagged grade and dismantle routing\n if (frm.doc.grade !== 'Flagged') {\n frm.set_value('grade', 'Flagged');\n }\n frm.set_value('assigned_price', null);\n frm.set_value('pricing_status', 'Dismantle');\n frm.set_df_property('grade', 'read_only', 1);\n \n var reason = [];\n if (cpu_fail) reason.push('CPU test failed');\n if (ram_fail) reason.push('RAM test failed');\n \n frappe.show_alert({\n message: __('Hardware failure: ' + reason.join(', ') + ' \u2014 routed to Dismantle'),\n indicator: 'red'\n }, 5);\n } else {\n frm.set_df_property('grade', 'read_only', 0);\n }\n}\n\nfunction calculate_recommended_price(frm) {\n var grade = frm.doc.grade;\n \n // Flagged = no price, show FLAGGED text\n if (!grade || grade === 'Flagged') {\n frm.set_value('assigned_price', null);\n frm.set_value('pricing_status', grade === 'Flagged' ? 'Flagged' : 'Needs Pricing');\n return;\n }\n \n // Need item reference to get market prices\n if (!frm.doc.item_code) {\n return;\n }\n \n frappe.call({\n method: 'frappe.client.get',\n args: {\n doctype: 'Item',\n name: frm.doc.item_code\n },\n callback: function(r) {\n if (!r.message) return;\n var item = r.message;\n var base_price = 0;\n var price_source = '';\n \n switch(grade) {\n case 'High':\n base_price = item.market_high || item.base_market_price || 0;\n price_source = 'market_high';\n break;\n case 'Med':\n base_price = item.market_median || item.base_market_price || 0;\n price_source = 'market_median';\n break;\n case 'Low':\n base_price = item.market_low || item.base_market_price || 0;\n price_source = 'market_low';\n break;\n }\n \n if (base_price > 0) {\n frm.set_value('assigned_price', Math.round(base_price * 100) / 100);\n frm.set_value('pricing_status', 'Priced');\n frm.set_value('pricing_source', price_source);\n } else {\n frm.set_value('assigned_price', null);\n frm.set_value('pricing_status', 'Needs Pricing');\n }\n }\n });\n}\n",
+ "script": "frappe.ui.form.on('Serial No', {\n grade: function(frm) {\n calculate_recommended_price(frm);\n },\n cpu_test: function(frm) {\n check_hardware_failures(frm);\n },\n ram_test: function(frm) {\n check_hardware_failures(frm);\n },\n refresh: function(frm) {\n // Label change via JS (since fixture label updates need migrate)\n var price_field = frm.get_field('assigned_price');\n if (price_field && price_field.$wrapper) {\n price_field.$wrapper.find('.control-label').text('Recommended Price');\n }\n check_hardware_failures(frm);\n }\n});\n\nfunction check_hardware_failures(frm) {\n var cpu_fail = frm.doc.cpu_test === 'Fail';\n var ram_fail = frm.doc.ram_test === 'Fail';\n \n if (cpu_fail || ram_fail) {\n // Force Flagged grade and dismantle routing (but allow reviewer override)\n if (!frm.doc.grade || frm.doc.grade !== 'Flagged') {\n frm.set_value('grade', 'Flagged');\n }\n frm.set_value('assigned_price', null);\n frm.set_value('pricing_status', 'Dismantle');\n \n var reason = [];\n if (cpu_fail) reason.push('CPU test failed');\n if (ram_fail) reason.push('RAM test failed');\n \n // Show warning banner instead of locking the field\n frm.set_intro(\n ' Hardware Failure: ' + \n reason.join(', ') + ' \u2014 routed to Dismantle. Change tests to Pass to re-enable pricing.',\n 'red'\n );\n } else {\n // Clear the warning banner\n frm.clear_intro();\n // If grade was Flagged but tests now pass, let user manually change grade\n }\n}\n\nfunction calculate_recommended_price(frm) {\n var grade = frm.doc.grade;\n \n // Flagged = no price, show FLAGGED text\n if (!grade || grade === 'Flagged') {\n frm.set_value('assigned_price', null);\n frm.set_value('pricing_status', grade === 'Flagged' ? 'Flagged' : 'Needs Pricing');\n return;\n }\n \n // Need item reference to get market prices\n if (!frm.doc.item_code) {\n return;\n }\n \n frappe.call({\n method: 'frappe.client.get',\n args: {\n doctype: 'Item',\n name: frm.doc.item_code\n },\n callback: function(r) {\n if (!r.message) return;\n var item = r.message;\n var base_price = 0;\n var price_source = '';\n \n switch(grade) {\n case 'High':\n base_price = item.market_high || item.base_market_price || 0;\n price_source = 'market_high';\n break;\n case 'Med':\n base_price = item.market_median || item.base_market_price || 0;\n price_source = 'market_median';\n break;\n case 'Low':\n base_price = item.market_low || item.base_market_price || 0;\n price_source = 'market_low';\n break;\n }\n \n if (base_price > 0) {\n frm.set_value('assigned_price', Math.round(base_price * 100) / 100);\n frm.set_value('pricing_status', 'Priced');\n frm.set_value('pricing_source', price_source);\n } else {\n frm.set_value('assigned_price', null);\n frm.set_value('pricing_status', 'Needs Pricing');\n }\n }\n });\n}\n",
"view": "Form"
},
{