Compare commits

..

3 Commits

Author SHA1 Message Date
Westech Admin 1ef2b63730 Fix eBay s-card parser, add credentials fallback, verified pricing pipeline
- eBay scraper: Fixed HTML parser for post-2024 s-card layout (s-card class)
- Credentials: Fixed fallback from frappe.conf when Settings doc is empty
- Source field: Normalized values (oxylabs/apify/unknown) for System Pricing DocType
- Verified: 80 Dell Latitude 5410 sold listings scraped
  - Low: 5, Median: 39.99, Avg: 58.43, High: 96.42
- Verified Serial No pricing: High/Med/Low correctly maps to market_high/median/low
- Verified CPU/RAM Fail → Flagged + Dismantle routing works
2026-05-19 16:43:47 +00:00
Westech Admin ebf907c9d5 Fix eBay HTML parser for post-2024 s-card layout, fix source field values 2026-05-19 16:33:17 +00:00
Westech Admin 750141827b Serial No: warehousing pricing + grading + hardware failure routing
- Grade options: High/Med/Low/Flagged (replaces A/B/C/D/F)
- assigned_price label → Recommended Price
- Client script: grade→price mapping (High→market_high, Med→market_median, Low→market_low)
- Flagged grade shows FLAGGED, no price, Dismantle status
- New fields: cpu_test (Pass/Fail), ram_test (Pass/Fail)
- CPU/RAM Fail → auto Flagged grade, Dismantle status, route to Dismantle warehouse
- Server-side validation prevents pricing on failed hardware
- pricing_status options updated: Needs Pricing/Priced/Flagged/Dismantle/Manual Override/Expired/Error
2026-05-19 15:33:34 +00:00
11 changed files with 4535 additions and 4403 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+18 -4
View File
@@ -35,7 +35,10 @@ def _get_oxylabs_creds():
user = password = "" user = password = ""
if settings: if settings:
user = settings.get("oxylabs_user") or "" 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: if not user:
user = frappe.conf.get("oxylabs_user", "") user = frappe.conf.get("oxylabs_user", "")
if not password: if not password:
@@ -47,7 +50,10 @@ def _get_apify_token():
settings = _get_settings() settings = _get_settings()
token = "" token = ""
if settings: if settings:
token = settings.get_password("apify_token") or "" try:
token = settings.get_password("apify_token") or ""
except Exception:
pass
if not token: if not token:
token = frappe.conf.get("apify_token", "") token = frappe.conf.get("apify_token", "")
return token return token
@@ -292,7 +298,7 @@ def _parse_prices(items, manufacturer, model, source="oxylabs"):
"price_average": round(avg, 2), "price_average": round(avg, 2),
"price_auction": round(median, 2), "price_auction": round(median, 2),
"sample_count": len(prices), "sample_count": len(prices),
"source": f"ebay_{source}", "source": source or "unknown",
"scraped_at": now(), "scraped_at": now(),
} }
@@ -311,9 +317,17 @@ def _upsert_system_pricing(manufacturer, model, pricing):
doc.manufacturer = manufacturer doc.manufacturer = manufacturer
doc.model = model doc.model = model
for key in ("price_high", "price_low", "price_average", "price_auction", for key in ("price_high", "price_low", "price_average", "price_auction",
"sample_count", "source", "scraped_at"): "sample_count", "scraped_at"):
if key in pricing: if key in pricing:
setattr(doc, key, pricing[key]) 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: if doc.scraped_at:
scraped = frappe.utils.get_datetime(doc.scraped_at) scraped = frappe.utils.get_datetime(doc.scraped_at)
now = now_datetime() now = now_datetime()
+37
View File
@@ -0,0 +1,37 @@
import frappe
from frappe import _
def validate_hardware_tests(doc, method):
"""Before save: if CPU or RAM test failed, route to Dismantle."""
# Check if this is a device-type serial (has item_code)
if not doc.item_code:
return
# Check CPU and RAM test results
cpu_fail = doc.get("cpu_test") == "Fail"
ram_fail = doc.get("ram_test") == "Fail"
if cpu_fail or ram_fail:
# Set grade to Flagged
doc.grade = "Flagged"
doc.pricing_status = "Dismantle"
doc.assigned_price = None
# Route to Dismantle warehouse if it exists
dismantle_wh = frappe.db.exists("Warehouse", "Dismantle - WR")
if dismantle_wh:
doc.warehouse = "Dismantle - WR"
# Log the failure reason
reasons = []
if cpu_fail:
reasons.append("CPU test failed")
if ram_fail:
reasons.append("RAM test failed")
frappe.msgprint(
_("Hardware failure detected: {0}. Device routed to Dismantle.").format(", ".join(reasons)),
indicator="red",
alert=True
)
+66 -66
View File
@@ -1,68 +1,68 @@
[ [
{ {
"docstatus": 0, "docstatus": 0,
"doctype": "Client Script", "doctype": "Client Script",
"dt": "Serial No", "dt": "Serial No",
"enabled": 1, "enabled": 1,
"modified": "2026-05-17 06:30:57.598334", "modified": "2026-05-19 09:16:00.000000",
"module": null, "module": null,
"name": "Serial No - Price Calculator", "name": "Serial No - Price Calculator",
"script": "frappe.ui.form.on('Serial No', {\n grade: function(frm) {\n calculate_price(frm);\n },\n price_point: function(frm) {\n calculate_price(frm);\n },\n manual_price: function(frm) {\n if (frm.doc.price_point === 'Manual') {\n frm.set_value('assigned_price', frm.doc.manual_price);\n frm.set_value('pricing_status', 'Manual Override');\n }\n }\n});\n\nfunction calculate_price(frm) {\n var grade = frm.doc.grade;\n var price_point = frm.doc.price_point;\n \n if (!grade || !price_point) {\n return;\n }\n \n // C, D, F grades = commodity\n if (['C', 'D', 'F'].includes(grade)) {\n // Will be handled by server side\n frm.set_value('pricing_status', 'Commodity');\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 \n switch(price_point) {\n case 'Low':\n base_price = item.market_low || item.base_market_price || 0;\n break;\n case 'Median':\n base_price = item.market_median || item.base_market_price || 0;\n break;\n case 'Average':\n base_price = item.market_average || item.base_market_price || 0;\n break;\n case 'High':\n base_price = item.market_high || item.base_market_price || 0;\n break;\n }\n \n var multiplier = 1.0;\n if (grade === 'A') multiplier = item.grade_a_multiplier || 1.0;\n else if (grade === 'B') multiplier = item.grade_b_multiplier || 0.8;\n \n var final_price = base_price * multiplier;\n frm.set_value('assigned_price', Math.round(final_price * 100) / 100);\n frm.set_value('pricing_status', 'Priced');\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 '<i class=\"fa fa-exclamation-triangle\"></i> <strong>Hardware Failure:</strong> ' + \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" "view": "Form"
}, },
{ {
"docstatus": 0, "docstatus": 0,
"doctype": "Client Script", "doctype": "Client Script",
"dt": "Pallet", "dt": "Pallet",
"enabled": 1, "enabled": 1,
"modified": "2026-05-17 05:39:15.497479", "modified": "2026-05-17 05:39:15.497479",
"module": null, "module": null,
"name": "Pallet - View Serials", "name": "Pallet - View Serials",
"script": "frappe.ui.form.on('Pallet', {\n refresh: function(frm) {\n frm.add_custom_button(__('View Serials'), function() {\n frappe.set_route('List', 'Serial No', {\n 'pallet': frm.doc.pallet_number || frm.doc.name\n });\n }, __('View'));\n \n frm.add_custom_button(__('Serials Spreadsheet'), function() {\n frappe.set_route('query-report', 'Serial Nos by Pallet', {\n 'pallet': frm.doc.pallet_number || frm.doc.name\n });\n }, __('View'));\n }\n});\n", "script": "frappe.ui.form.on('Pallet', {\n refresh: function(frm) {\n frm.add_custom_button(__('View Serials'), function() {\n frappe.set_route('List', 'Serial No', {\n 'pallet': frm.doc.pallet_number || frm.doc.name\n });\n }, __('View'));\n \n frm.add_custom_button(__('Serials Spreadsheet'), function() {\n frappe.set_route('query-report', 'Serial Nos by Pallet', {\n 'pallet': frm.doc.pallet_number || frm.doc.name\n });\n }, __('View'));\n }\n});\n",
"view": "Form" "view": "Form"
}, },
{ {
"docstatus": 0, "docstatus": 0,
"doctype": "Client Script", "doctype": "Client Script",
"dt": "Load", "dt": "Load",
"enabled": 1, "enabled": 1,
"modified": "2026-05-17 05:39:15.577871", "modified": "2026-05-17 05:39:15.577871",
"module": null, "module": null,
"name": "Load - View Pallets Button", "name": "Load - View Pallets Button",
"script": "frappe.ui.form.on('Load', {\n refresh: function(frm) {\n frm.add_custom_button(__('View Pallets'), function() {\n frappe.set_route('List', 'Pallet', {'load': frm.doc.name});\n }, __('Actions'));\n }\n});\n", "script": "frappe.ui.form.on('Load', {\n refresh: function(frm) {\n frm.add_custom_button(__('View Pallets'), function() {\n frappe.set_route('List', 'Pallet', {'load': frm.doc.name});\n }, __('Actions'));\n }\n});\n",
"view": "Form" "view": "Form"
}, },
{ {
"docstatus": 0, "docstatus": 0,
"doctype": "Client Script", "doctype": "Client Script",
"dt": "Scheduled Pickup", "dt": "Scheduled Pickup",
"enabled": 1, "enabled": 1,
"modified": "2026-05-17 05:39:15.588395", "modified": "2026-05-17 05:39:15.588395",
"module": null, "module": null,
"name": "Scheduled Pickup - Auto Fill", "name": "Scheduled Pickup - Auto Fill",
"script": "frappe.ui.form.on('Scheduled Pickup', {\n customer_number: function(frm) {\n var customer = frm.doc.customer_number;\n if (!customer) {\n clear_supplier_fields(frm);\n return;\n }\n frappe.call({\n method: 'frappe.client.get',\n args: {doctype: 'Supplier', name: customer},\n callback: function(r) {\n if (!r.message) return;\n var s = r.message;\n if (!frm.doc.company_name && s.supplier_name) {\n frm.set_value('company_name', s.supplier_name);\n }\n if (s.supplier_primary_contact) {\n frappe.call({\n method: 'frappe.client.get',\n args: {doctype: 'Contact', name: s.supplier_primary_contact},\n callback: function(cr) {\n if (!cr.message) return;\n var ct = cr.message;\n var full_name = [ct.first_name, ct.last_name].filter(Boolean).join(' ');\n if (!frm.doc.contact_name) frm.set_value('contact_name', full_name);\n if (!frm.doc.contact_phone) frm.set_value('contact_phone', ct.phone || '');\n if (!frm.doc.contact_email) frm.set_value('contact_email', ct.email_id || '');\n }\n });\n }\n if (s.supplier_primary_address) {\n frappe.call({\n method: 'frappe.client.get',\n args: {doctype: 'Address', name: s.supplier_primary_address},\n callback: function(ar) {\n if (!ar.message) return;\n var a = ar.message;\n if (!frm.doc.address_line) frm.set_value('address_line', a.address_line1 || '');\n if (!frm.doc.city) frm.set_value('city', a.city || '');\n if (!frm.doc.state) frm.set_value('state', a.state || '');\n if (!frm.doc.zip_code) frm.set_value('zip_code', a.pincode || '');\n }\n });\n }\n if (s.geocoded && s.latitude && s.longitude) {\n frm.set_value('latitude', s.latitude);\n frm.set_value('longitude', s.longitude);\n frm.set_value('geocoded', 1);\n }\n }\n });\n }\n});\n\nfunction clear_supplier_fields(frm) {\n frm.set_value('company_name', '');\n frm.set_value('contact_name', '');\n frm.set_value('contact_phone', '');\n frm.set_value('contact_email', '');\n frm.set_value('address_line', '');\n frm.set_value('city', '');\n frm.set_value('state', '');\n frm.set_value('zip_code', '');\n frm.set_value('latitude', '');\n frm.set_value('longitude', '');\n frm.set_value('geocoded', 0);\n}\n", "script": "frappe.ui.form.on('Scheduled Pickup', {\n customer_number: function(frm) {\n var customer = frm.doc.customer_number;\n if (!customer) {\n clear_supplier_fields(frm);\n return;\n }\n frappe.call({\n method: 'frappe.client.get',\n args: {doctype: 'Supplier', name: customer},\n callback: function(r) {\n if (!r.message) return;\n var s = r.message;\n if (!frm.doc.company_name && s.supplier_name) {\n frm.set_value('company_name', s.supplier_name);\n }\n if (s.supplier_primary_contact) {\n frappe.call({\n method: 'frappe.client.get',\n args: {doctype: 'Contact', name: s.supplier_primary_contact},\n callback: function(cr) {\n if (!cr.message) return;\n var ct = cr.message;\n var full_name = [ct.first_name, ct.last_name].filter(Boolean).join(' ');\n if (!frm.doc.contact_name) frm.set_value('contact_name', full_name);\n if (!frm.doc.contact_phone) frm.set_value('contact_phone', ct.phone || '');\n if (!frm.doc.contact_email) frm.set_value('contact_email', ct.email_id || '');\n }\n });\n }\n if (s.supplier_primary_address) {\n frappe.call({\n method: 'frappe.client.get',\n args: {doctype: 'Address', name: s.supplier_primary_address},\n callback: function(ar) {\n if (!ar.message) return;\n var a = ar.message;\n if (!frm.doc.address_line) frm.set_value('address_line', a.address_line1 || '');\n if (!frm.doc.city) frm.set_value('city', a.city || '');\n if (!frm.doc.state) frm.set_value('state', a.state || '');\n if (!frm.doc.zip_code) frm.set_value('zip_code', a.pincode || '');\n }\n });\n }\n if (s.geocoded && s.latitude && s.longitude) {\n frm.set_value('latitude', s.latitude);\n frm.set_value('longitude', s.longitude);\n frm.set_value('geocoded', 1);\n }\n }\n });\n }\n});\n\nfunction clear_supplier_fields(frm) {\n frm.set_value('company_name', '');\n frm.set_value('contact_name', '');\n frm.set_value('contact_phone', '');\n frm.set_value('contact_email', '');\n frm.set_value('address_line', '');\n frm.set_value('city', '');\n frm.set_value('state', '');\n frm.set_value('zip_code', '');\n frm.set_value('latitude', '');\n frm.set_value('longitude', '');\n frm.set_value('geocoded', 0);\n}\n",
"view": "Form" "view": "Form"
}, },
{ {
"docstatus": 0, "docstatus": 0,
"doctype": "Client Script", "doctype": "Client Script",
"dt": "System Pricing", "dt": "System Pricing",
"enabled": 1, "enabled": 1,
"modified": "2026-05-17 05:41:31.790905", "modified": "2026-05-17 05:41:31.790905",
"module": null, "module": null,
"name": "System Pricing - Age Indicators", "name": "System Pricing - Age Indicators",
"script": "frappe.listview_settings['System Pricing'] = {\n add_fields: [\"scraped_at\", \"days_since_pricing\", \"pricing_status\"],\n get_indicator: function(doc) {\n if (doc.pricing_status === \"Needs Pricing\" || doc.pricing_status === \"Error\") {\n return [__(\"Needs Pricing\"), \"orange\", \"pricing_status,=,Needs Pricing\"];\n }\n if (!doc.scraped_at) {\n return [__(\"No Date\"), \"gray\", \"scraped_at,is,not set\"];\n }\n var days = doc.days_since_pricing || 0;\n if (days < 90) {\n return [__(\"Fresh\"), \"green\", \"days_since_pricing,<,90\"];\n } else if (days < 120) {\n return [__(\"Aging\"), \"yellow\", \"days_since_pricing,<,120\"];\n } else {\n return [__(\"Expired\"), \"red\", \"days_since_pricing,>=,120\"];\n }\n }\n};", "script": "frappe.listview_settings['System Pricing'] = {\n add_fields: [\"scraped_at\", \"days_since_pricing\", \"pricing_status\"],\n get_indicator: function(doc) {\n if (doc.pricing_status === \"Needs Pricing\" || doc.pricing_status === \"Error\") {\n return [__(\"Needs Pricing\"), \"orange\", \"pricing_status,=,Needs Pricing\"];\n }\n if (!doc.scraped_at) {\n return [__(\"No Date\"), \"gray\", \"scraped_at,is,not set\"];\n }\n var days = doc.days_since_pricing || 0;\n if (days < 90) {\n return [__(\"Fresh\"), \"green\", \"days_since_pricing,<,90\"];\n } else if (days < 120) {\n return [__(\"Aging\"), \"yellow\", \"days_since_pricing,<,120\"];\n } else {\n return [__(\"Expired\"), \"red\", \"days_since_pricing,>=,120\"];\n }\n }\n};",
"view": "List" "view": "List"
}, },
{ {
"docstatus": 0, "docstatus": 0,
"doctype": "Client Script", "doctype": "Client Script",
"dt": "System Pricing", "dt": "System Pricing",
"enabled": 1, "enabled": 1,
"modified": "2026-05-17 05:41:31.954814", "modified": "2026-05-17 05:41:31.954814",
"module": null, "module": null,
"name": "System Pricing - Auto Calculate Days", "name": "System Pricing - Auto Calculate Days",
"script": "frappe.ui.form.on('System Pricing', {\n refresh: function(frm) {\n calculate_days(frm);\n },\n scraped_at: function(frm) {\n calculate_days(frm);\n }\n});\n\nfunction calculate_days(frm) {\n if (frm.doc.scraped_at) {\n var scraped = new Date(frm.doc.scraped_at);\n var now = new Date();\n var diffTime = Math.abs(now - scraped);\n var diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));\n frm.set_value('days_since_pricing', diffDays);\n } else {\n frm.set_value('days_since_pricing', 0);\n }\n}", "script": "frappe.ui.form.on('System Pricing', {\n refresh: function(frm) {\n calculate_days(frm);\n },\n scraped_at: function(frm) {\n calculate_days(frm);\n }\n});\n\nfunction calculate_days(frm) {\n if (frm.doc.scraped_at) {\n var scraped = new Date(frm.doc.scraped_at);\n var now = new Date();\n var diffTime = Math.abs(now - scraped);\n var diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));\n frm.set_value('days_since_pricing', diffDays);\n } else {\n frm.set_value('days_since_pricing', 0);\n }\n}",
"view": "Form" "view": "Form"
} }
] ]
File diff suppressed because it is too large Load Diff
+3
View File
@@ -24,6 +24,9 @@ doc_events = {
"Scheduled Pickup": { "Scheduled Pickup": {
"before_save": "westech_r2.doctype.scheduled_pickup.scheduled_pickup.set_title", "before_save": "westech_r2.doctype.scheduled_pickup.scheduled_pickup.set_title",
}, },
"Serial No": {
"validate": "westech_r2.api.serial_hooks.validate_hardware_tests",
},
"Load": { "Load": {
"before_save": "westech_r2.doctype.load.load.calculate_totals", "before_save": "westech_r2.doctype.load.load.calculate_totals",
}, },
@@ -190,7 +190,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2026-05-17 17:58:07.846989", "modified": "2026-05-19 06:21:55.636514",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Westech R2", "module": "Westech R2",
"name": "Westech", "name": "Westech",