192 lines
7.1 KiB
Python
192 lines
7.1 KiB
Python
import frappe
|
|
from frappe import _
|
|
|
|
# Mapping of material types to service categories
|
|
HDD_MATERIALS = {
|
|
"Loose Hard Drive", "External Hard Drive", "Printers/Copiers Hard Drives",
|
|
"Loose SSD or mSATA Drive", "Server", "Desktop", "Laptop", "Chromebook / Notebook",
|
|
"All In One", "HPStream", "Thin Clients", "Tablet", "Cell Phone / Smart Phone",
|
|
"Gaming Systems", "Smart TV", "POS", "POS Terminals", "DVR", "Switch",
|
|
"Network / Modems / Routers", "Office/ IP Phone", "Personal Electronics / PDA",
|
|
"CRT TV", "Printers/Copiers", "USB Drive", "SD Cards", "GPS"
|
|
}
|
|
|
|
TAPE_MATERIALS = {"CD / Floppy / DVD / Tapes"}
|
|
|
|
def _get_service_item(destruction_method, has_hardware=True):
|
|
"""Map destruction method to service item code."""
|
|
if destruction_method == "Wipe":
|
|
if has_hardware:
|
|
return "SVR-HDD-WIPE-1PASS" # Default to 1-pass; user can override
|
|
return "SVR-HDD-WIPE-3PASS-NOHW"
|
|
elif destruction_method in ("Shred", "Degauss"):
|
|
if has_hardware:
|
|
return "SVR-HDD-SERIAL-WIPE-HW"
|
|
return "SVR-HDD-SERIAL-WIPE-NOHW"
|
|
elif destruction_method == "None":
|
|
return None
|
|
return "SVR-HDD-WIPE-1PASS"
|
|
|
|
def _get_tape_item(destruction_method, has_hardware=True):
|
|
if has_hardware:
|
|
return "SVR-TAPE-SHRED"
|
|
return "SVR-TAPE-SHRED-NOHW"
|
|
|
|
def _get_onsite_item(has_hardware=True):
|
|
return "SVR-HDD-ONSITE-HW" if has_hardware else "SVR-HDD-ONSITE-NOHW"
|
|
|
|
def _calculate_tier_price(item_code, qty):
|
|
"""Return unit rate for given qty based on tier pricing."""
|
|
tiers = {
|
|
"SVR-HDD-WIPE-1PASS": [(1,10,7.00), (11,30,6.00), (31,50,4.50), (51,99,3.50), (100,999999,3.00)],
|
|
"SVR-HDD-WIPE-3PASS-HW": [(1,10,8.50), (11,30,7.00), (31,50,5.25), (51,99,4.25), (100,999999,3.50)],
|
|
"SVR-HDD-WIPE-3PASS-NOHW": [(1,10,14.00), (11,30,11.50), (31,50,8.40), (51,99,7.25), (100,999999,6.00)],
|
|
"SVR-HDD-SERIAL-WIPE-HW": [(1,10,9.00), (11,30,7.50), (31,50,6.00), (51,99,5.00), (100,999999,4.25)],
|
|
"SVR-HDD-SERIAL-WIPE-NOHW": [(1,10,14.50), (11,30,12.00), (31,50,10.00), (51,99,8.00), (100,999999,6.50)],
|
|
"SVR-HDD-ONSITE-HW": [(1,100,500.00), (101,999999,3.50)],
|
|
"SVR-HDD-ONSITE-NOHW": [(1,100,850.00), (101,999999,6.00)],
|
|
"SVR-TAPE-SHRED": [(1,10,4.00), (11,30,3.30), (31,50,2.70), (51,99,2.00), (100,999999,1.50)],
|
|
"SVR-TAPE-SHRED-NOHW": [(1,10,6.65), (11,30,5.50), (31,50,4.50), (51,99,3.35), (100,999999,2.50)],
|
|
"SVR-VIDEO-RECORD": [(1,999999,3.50)],
|
|
}
|
|
for item, tlist in tiers.items():
|
|
if item == item_code:
|
|
for min_qty, max_qty, rate in tlist:
|
|
if min_qty <= qty <= max_qty:
|
|
return rate
|
|
# Fallback to Item Price
|
|
rate = frappe.db.get_value("Item Price", {"item_code": item_code, "price_list": "2025 Service Rates", "selling": 1}, "price_list_rate")
|
|
return rate or 0
|
|
|
|
@frappe.whitelist()
|
|
def generate_service_invoice(load_name):
|
|
"""Generate a Sales Invoice from a Load document."""
|
|
load = frappe.get_doc("Load", load_name)
|
|
|
|
if load.invoice_generated:
|
|
frappe.throw(_("Invoice already generated for this load."))
|
|
|
|
if not load.customer:
|
|
frappe.throw(_("Load must have a Customer linked."))
|
|
|
|
# Gather quantities per service item
|
|
service_qty = {}
|
|
hdd_count = 0
|
|
tape_count = 0
|
|
total_items = 0
|
|
|
|
for item in load.material_items or []:
|
|
mt = item.material_type or ""
|
|
qty = item.total_count or 0
|
|
if qty <= 0:
|
|
continue
|
|
total_items += qty
|
|
if mt in HDD_MATERIALS:
|
|
hdd_count += qty
|
|
elif mt in TAPE_MATERIALS:
|
|
tape_count += qty
|
|
|
|
# Determine if on-site
|
|
is_onsite = (load.service_type or "") == "On-site"
|
|
destruction = load.destruction_method or "Wipe"
|
|
|
|
# For simplicity, assume "has hardware" = True unless explicitly set otherwise.
|
|
# TODO: Add custom field `has_hardware` on Load if needed.
|
|
has_hardware = True
|
|
|
|
invoice_items = []
|
|
|
|
if is_onsite and hdd_count > 0:
|
|
onsite_item = _get_onsite_item(has_hardware)
|
|
base_rate = _calculate_tier_price(onsite_item, hdd_count)
|
|
# Onsite: base fee + per-drive for extras above 100
|
|
if hdd_count <= 100:
|
|
invoice_items.append({
|
|
"item_code": onsite_item,
|
|
"qty": 1,
|
|
"rate": base_rate,
|
|
"description": f"On-site shredding for {hdd_count} drives"
|
|
})
|
|
else:
|
|
# One base fee + per-drive extras
|
|
invoice_items.append({
|
|
"item_code": onsite_item,
|
|
"qty": 1,
|
|
"rate": base_rate,
|
|
"description": f"On-site base fee (1-100 drives)"
|
|
})
|
|
extra = hdd_count - 100
|
|
per_drive_rate = _calculate_tier_price(onsite_item, hdd_count)
|
|
invoice_items.append({
|
|
"item_code": onsite_item,
|
|
"qty": extra,
|
|
"rate": per_drive_rate,
|
|
"description": f"Additional on-site drives ({extra})"
|
|
})
|
|
else:
|
|
# Standard pickup/mail-in pricing
|
|
if hdd_count > 0:
|
|
hdd_item = _get_service_item(destruction, has_hardware)
|
|
if hdd_item:
|
|
rate = _calculate_tier_price(hdd_item, hdd_count)
|
|
invoice_items.append({
|
|
"item_code": hdd_item,
|
|
"qty": hdd_count,
|
|
"rate": rate,
|
|
"description": f"{hdd_item} for {hdd_count} drives"
|
|
})
|
|
|
|
if tape_count > 0:
|
|
tape_item = _get_tape_item(destruction, has_hardware)
|
|
rate = _calculate_tier_price(tape_item, tape_count)
|
|
invoice_items.append({
|
|
"item_code": tape_item,
|
|
"qty": tape_count,
|
|
"rate": rate,
|
|
"description": f"{tape_item} for {tape_count} tapes"
|
|
})
|
|
|
|
# Video recording surcharge
|
|
if load.video_recording and total_items > 0:
|
|
vid_rate = _calculate_tier_price("SVR-VIDEO-RECORD", total_items)
|
|
invoice_items.append({
|
|
"item_code": "SVR-VIDEO-RECORD",
|
|
"qty": total_items,
|
|
"rate": vid_rate,
|
|
"description": f"Video recording surcharge for {total_items} items"
|
|
})
|
|
|
|
if not invoice_items:
|
|
frappe.throw(_("No billable items found in this load."))
|
|
|
|
# Create Sales Invoice
|
|
si = frappe.new_doc("Sales Invoice")
|
|
si.customer = load.customer
|
|
si.posting_date = frappe.utils.today()
|
|
si.due_date = frappe.utils.today()
|
|
si.price_list = "2025 Service Rates"
|
|
si.selling_price_list = "2025 Service Rates"
|
|
si.currency = "USD"
|
|
si.set_warehouse = None
|
|
si.update_stock = 0
|
|
|
|
for it in invoice_items:
|
|
si.append("items", {
|
|
"item_code": it["item_code"],
|
|
"qty": it["qty"],
|
|
"rate": it["rate"],
|
|
"description": it.get("description", ""),
|
|
"uom": "Unit"
|
|
})
|
|
|
|
si.save()
|
|
# Do NOT submit automatically; let user review
|
|
|
|
# Update Load
|
|
load.invoice_generated = 1
|
|
load.sales_invoice = si.name
|
|
load.save()
|
|
|
|
frappe.db.commit()
|
|
return {"status": "ok", "sales_invoice": si.name}
|