Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion csf_tz/api/selcom.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def create_order_minimal():
# API endpoint
orderPath = "/checkout/create-order-minimal"

# Send order request
response = {}
try:
response = client.postFunc(orderPath, orderDict)
if response.get("resultcode") != "000":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
"fieldtype": "Column Break"
},
{
"fetch_from": "payment_entry.paid_amount",
"fieldname": "payment_amount_foreign",
"fieldtype": "Currency",
"in_list_view": 1,
Expand All @@ -58,7 +57,6 @@
"fieldtype": "Section Break"
},
{
"fetch_from": "payment_entry.source_exchange_rate",
"fieldname": "payment_exchange_rate",
"fieldtype": "Float",
"label": "Payment Exchange Rate",
Expand All @@ -70,12 +68,14 @@
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "exchange_difference",
"fieldtype": "Currency",
"label": "Exchange Difference",
"read_only": 1
},
{
"allow_on_submit": 1,
"default": "0",
"fieldname": "journal_entry_created",
"fieldtype": "Check",
Expand All @@ -96,4 +96,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
"read_only": 1
},
{
"allow_on_submit": 1,
"default": "Draft",
"fieldname": "status",
"fieldtype": "Select",
Expand Down Expand Up @@ -267,7 +268,7 @@
"write": 1
}
],
"sort_field": "modified",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "purchase_invoice",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from frappe.model.document import Document
from frappe.utils import flt

from csf_tz.csftz_hooks.exchange_calculations import get_payment_foreign_amount_and_rate


class ForeignImportTransaction(Document):
def validate(self):
Expand All @@ -15,6 +17,10 @@ def validate(self):
def on_submit(self):
self.update_status("Active")

def before_update_after_submit(self):
self.calculate_totals()
self.set_status()

def on_cancel(self):
self.cancel_related_journal_entries()
self.update_status("Cancelled")
Expand Down Expand Up @@ -107,14 +113,12 @@ def add_payment_detail(self, payment_entry):
payment_row = self.append("payments", {})
payment_row.payment_entry = payment_entry
payment_row.payment_date = payment_doc.posting_date
payment_row.payment_amount_foreign = payment_doc.paid_amount
paid_amount, payment_rate = get_payment_foreign_amount_and_rate(self, payment_doc)
payment_row.payment_amount_foreign = paid_amount
payment_row.payment_amount_base = payment_doc.base_paid_amount
payment_row.payment_exchange_rate = payment_doc.source_exchange_rate
payment_row.payment_exchange_rate = payment_rate

# Calculate exchange difference
original_rate = flt(self.original_exchange_rate)
payment_rate = flt(payment_doc.source_exchange_rate)
paid_amount = flt(payment_doc.paid_amount)

if original_rate != payment_rate:
exchange_diff = paid_amount * (payment_rate - original_rate)
Expand All @@ -131,7 +135,7 @@ def add_lcv_detail(self, lcv_name):
lcv_row.landed_cost_voucher = lcv_name
lcv_row.lcv_date = lcv_doc.posting_date
lcv_row.lcv_amount_base = lcv_doc.total_taxes_and_charges
lcv_row.exchange_rate_used = flt(lcv_doc.get("conversion_rate", 1))
lcv_row.exchange_rate_used = flt(lcv_doc.get("conversion_rate")) or 1

# Get allocated amount from LCV items
allocated_amount = 0
Expand Down
264 changes: 129 additions & 135 deletions csf_tz/csf_tz/doctype/vehicle_fine_record/vehicle_fine_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import hashlib
import json
import re
from time import sleep

import frappe
import requests
Expand Down Expand Up @@ -112,140 +111,135 @@ def check_fine_all_vehicles(batch_size=20):


def sync_vehicle_fines(number_plate):
number_plate = normalize_number_plate(number_plate)

if not number_plate:
return {
"status": "invalid",
"message": "Missing number plate",
"fine_list": [],
}

if not is_valid_number_plate(number_plate):
return {
"status": "invalid",
"message": f"Skipping invalid plate: {number_plate}",
"fine_list": [],
}

url = "https://tms.tpf.go.tz/api/OffenceCheck"
headers = {
"Content-Type": "application/json",
"Accept": "*/*",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
"Origin": "https://tms.tpf.go.tz",
"Referer": "https://tms.tpf.go.tz/",
"Connection": "keep-alive",
}
payload = {"vehicle": number_plate}

try:
response = requests.post(url, json=payload, headers=headers, timeout=30)
if response.status_code == 429:
return {
"status": "rate_limited",
"message": f"TPF rate limited {number_plate}",
"fine_list": [],
}
response.raise_for_status()
except requests.exceptions.RequestException as exc:
frappe.logger().warning(f"[VehicleFine] TPF request failed for {number_plate}: {exc}")
return {
"status": "retryable_error",
"message": str(exc),
"fine_list": [],
}

try:
result = response.json()
result = decode_tpf_response(result)
except Exception:
frappe.log_error(
title="TPF API: Invalid JSON",
message=(
f"Non-JSON response for {number_plate}: "
f"{response.text[:500]}"
),
)
return {
"status": "error",
"message": "Invalid JSON response",
"fine_list": [],
}

data = result.get("pending_transactions", [])
fine_list = []

if data:
fine_list = [fine.get("reference") for fine in data if fine.get("reference")]
if not fine_list:
return {"status": "success", "message": "No fine references", "fine_list": fine_list}

stale_filters = {
"vehicle": number_plate,
"status": ["!=", "PAID"],
"reference": ["not in", fine_list],
}
for record in frappe.get_all(
"Vehicle Fine Record", filters=stale_filters, pluck="name"
):
old_status = frappe.db.get_value("Vehicle Fine Record", record, "status")
frappe.db.set_value("Vehicle Fine Record", record, "status", "PAID")
_notify_vehicle_fine_status_change(record, number_plate, old_status, "PAID")

existing_refs = frappe.get_all(
"Vehicle Fine Record",
filters={"vehicle": number_plate, "reference": ["in", fine_list]},
pluck="reference",
)
for fine in data:
fine_ref = fine.get("reference")
if not fine_ref or fine_ref in existing_refs:
continue
charge = fine.get("charge") or fine.get("amount")
penalty = fine.get("penalty")
try:
doc = frappe.get_doc(
{
"doctype": "Vehicle Fine Record",
"vehicle": number_plate,
"reference": fine_ref,
"status": fine.get("status") or "PENDING",
"licence": fine.get("licence"),
"location": fine.get("location"),
"officer": fine.get("officer"),
"charge": charge,
"penalty": penalty,
"total": fine.get("total") or (flt(charge) + flt(penalty)),
"offence": fine.get("offence"),
"issued_date": fine.get("issued_date") or fine.get("date"),
}
)
doc.insert(ignore_permissions=True)
_notify_vehicle_fine_new(doc)
except frappe.exceptions.DuplicateEntryError:
pass
except Exception:
frappe.log_error(
title=f"Error creating fine record for {number_plate}",
message=frappe.get_traceback(),
)
else:
for record in frappe.get_all(
"Vehicle Fine Record",
filters={"vehicle": number_plate, "status": ["!=", "PAID"]},
pluck="name",
):
old_status = frappe.db.get_value("Vehicle Fine Record", record, "status")
frappe.db.set_value("Vehicle Fine Record", record, "status", "PAID")
_notify_vehicle_fine_status_change(record, number_plate, old_status, "PAID")

frappe.db.commit()
return {
"status": "success",
"message": f"Processed fine sync for {number_plate}",
"fine_list": fine_list,
}
number_plate = normalize_number_plate(number_plate)

if not number_plate:
return {
"status": "invalid",
"message": "Missing number plate",
"fine_list": [],
}

if not is_valid_number_plate(number_plate):
return {
"status": "invalid",
"message": f"Skipping invalid plate: {number_plate}",
"fine_list": [],
}

url = "https://tms.tpf.go.tz/api/OffenceCheck"
headers = {
"Content-Type": "application/json",
"Accept": "*/*",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
"Origin": "https://tms.tpf.go.tz",
"Referer": "https://tms.tpf.go.tz/",
"Connection": "keep-alive",
}
payload = {"vehicle": number_plate}

try:
response = requests.post(url, json=payload, headers=headers, timeout=30)
if response.status_code == 429:
return {
"status": "rate_limited",
"message": f"TPF rate limited {number_plate}",
"fine_list": [],
}
response.raise_for_status()
except requests.exceptions.RequestException as exc:
frappe.logger().warning(f"[VehicleFine] TPF request failed for {number_plate}: {exc}")
return {
"status": "retryable_error",
"message": str(exc),
"fine_list": [],
}

try:
result = response.json()
result = decode_tpf_response(result)
except Exception:
frappe.log_error(
title="TPF API: Invalid JSON",
message=(f"Non-JSON response for {number_plate}: {response.text[:500]}"),
)
return {
"status": "error",
"message": "Invalid JSON response",
"fine_list": [],
}

data = result.get("pending_transactions", [])
fine_list = []

if data:
fine_list = [fine.get("reference") for fine in data if fine.get("reference")]
if not fine_list:
return {"status": "success", "message": "No fine references", "fine_list": fine_list}

stale_filters = {
"vehicle": number_plate,
"status": ["!=", "PAID"],
"reference": ["not in", fine_list],
}
for record in frappe.get_all("Vehicle Fine Record", filters=stale_filters, pluck="name"):
old_status = frappe.db.get_value("Vehicle Fine Record", record, "status")
frappe.db.set_value("Vehicle Fine Record", record, "status", "PAID")
_notify_vehicle_fine_status_change(record, number_plate, old_status, "PAID")

existing_refs = frappe.get_all(
"Vehicle Fine Record",
filters={"vehicle": number_plate, "reference": ["in", fine_list]},
pluck="reference",
)
for fine in data:
fine_ref = fine.get("reference")
if not fine_ref or fine_ref in existing_refs:
continue
charge = fine.get("charge") or fine.get("amount")
penalty = fine.get("penalty")
try:
doc = frappe.get_doc(
{
"doctype": "Vehicle Fine Record",
"vehicle": number_plate,
"reference": fine_ref,
"status": fine.get("status") or "PENDING",
"licence": fine.get("licence"),
"location": fine.get("location"),
"officer": fine.get("officer"),
"charge": charge,
"penalty": penalty,
"total": fine.get("total") or (flt(charge) + flt(penalty)),
"offence": fine.get("offence"),
"issued_date": fine.get("issued_date") or fine.get("date"),
}
)
doc.insert(ignore_permissions=True)
_notify_vehicle_fine_new(doc)
except frappe.exceptions.DuplicateEntryError:
pass
except Exception:
frappe.log_error(
title=f"Error creating fine record for {number_plate}",
message=frappe.get_traceback(),
)
else:
for record in frappe.get_all(
"Vehicle Fine Record",
filters={"vehicle": number_plate, "status": ["!=", "PAID"]},
pluck="name",
):
old_status = frappe.db.get_value("Vehicle Fine Record", record, "status")
frappe.db.set_value("Vehicle Fine Record", record, "status", "PAID")
_notify_vehicle_fine_status_change(record, number_plate, old_status, "PAID")

frappe.db.commit()
return {
"status": "success",
"message": f"Processed fine sync for {number_plate}",
"fine_list": fine_list,
}


@frappe.whitelist()
Expand Down
Loading
Loading