From bde31b2f1a6c8706ad93b9cbc2b176bdec3317be Mon Sep 17 00:00:00 2001 From: Sydney Kibanga Date: Thu, 13 Aug 2026 08:14:48 +0300 Subject: [PATCH] feat(weigh_bridge): add configurable transport items setting for target sales invoice generation --- av_tools/av_tools_hooks/parallel_approval.py | 159 +++++++++++++----- av_tools/weigh_bridge/api.py | 11 ++ .../weighbridge_settings.json | 15 +- .../weighbridge_transport_item/__init__.py | 1 + .../weighbridge_transport_item.json | 53 ++++++ .../weighbridge_transport_item.py | 8 + 6 files changed, 202 insertions(+), 45 deletions(-) create mode 100644 av_tools/weigh_bridge/doctype/weighbridge_transport_item/__init__.py create mode 100644 av_tools/weigh_bridge/doctype/weighbridge_transport_item/weighbridge_transport_item.json create mode 100644 av_tools/weigh_bridge/doctype/weighbridge_transport_item/weighbridge_transport_item.py diff --git a/av_tools/av_tools_hooks/parallel_approval.py b/av_tools/av_tools_hooks/parallel_approval.py index cf3f61a..5892438 100644 --- a/av_tools/av_tools_hooks/parallel_approval.py +++ b/av_tools/av_tools_hooks/parallel_approval.py @@ -2,7 +2,6 @@ from frappe import _ from frappe.custom.doctype.custom_field.custom_field import ( create_custom_fields, - delete_custom_fields, ) from frappe import share as frappe_share @@ -17,7 +16,9 @@ def _get_approval_doctypes() -> set: if getattr(frappe.flags, "in_migrate", False): return set() - if not frappe.db.get_single_value("AV Tools Settings", "enable_multi_approval_document"): + if not frappe.db.get_single_value( + "AV Tools Settings", "enable_multi_approval_document" + ): return set() if not frappe.db.table_exists("Approval Doctypes"): @@ -31,7 +32,9 @@ def _get_approval_doctypes() -> set: return set(rows) except Exception: - frappe.log_error(frappe.get_traceback(), "Parallel Approval: _get_approval_doctypes failed") + frappe.log_error( + frappe.get_traceback(), "Parallel Approval: _get_approval_doctypes failed" + ) return set() @@ -61,12 +64,21 @@ def sync_approver_shares(doc, method=None): for row in approver_rows: if row.approver: desired.add(row.approver) - user_info[row.approver] = {"name": row.approver_name or row.approver, "position": row.position or ""} + user_info[row.approver] = { + "name": row.approver_name or row.approver, + "position": row.position or "", + } if row.delegate_to: desired.add(row.delegate_to) if row.delegate_to not in user_info: - delegate_name = frappe.get_cached_value("User", row.delegate_to, "full_name") or row.delegate_to - user_info[row.delegate_to] = {"name": delegate_name, "position": row.position or ""} + delegate_name = ( + frappe.get_cached_value("User", row.delegate_to, "full_name") + or row.delegate_to + ) + user_info[row.delegate_to] = { + "name": delegate_name, + "position": row.position or "", + } # Capture existing DocShare users BEFORE adding new ones to detect new additions existing = frappe.get_all( @@ -78,7 +90,9 @@ def sync_approver_shares(doc, method=None): flags = {"ignore_share_permission": True} for user in desired: - frappe_share.add_docshare(doc.doctype, doc.name, user, read=1, write=0, flags=flags) + frappe_share.add_docshare( + doc.doctype, doc.name, user, read=1, write=0, flags=flags + ) if user not in existing_users: _notify_approver_added(user, user_info.get(user, {}), doc) @@ -97,11 +111,20 @@ def _notify_approver_added(user, info, doc): route = frappe.scrub(doc.doctype).replace("_", "-") doc_url = f"{site_url}/app/{route}/{doc.name}" - added_by = frappe.get_cached_value("User", frappe.session.user, "full_name") or frappe.session.user + added_by = ( + frappe.get_cached_value("User", frappe.session.user, "full_name") + or frappe.session.user + ) - subject = _("You have been added as an approver — {0} {1}").format(doc.doctype, doc.name) + subject = _("You have been added as an approver — {0} {1}").format( + doc.doctype, doc.name + ) - position_line = f"

{_('Position')}: {frappe.utils.escape_html(position)}

" if position else "" + position_line = ( + f"

{_('Position')}: {frappe.utils.escape_html(position)}

" + if position + else "" + ) message = f"""

{_("Dear")} {frappe.utils.escape_html(display_name)},

@@ -126,7 +149,9 @@ def _notify_approver_added(user, info, doc): now=True, ) except Exception: - frappe.log_error(frappe.get_traceback(), "Parallel Approval: approver email failed") + frappe.log_error( + frappe.get_traceback(), "Parallel Approval: approver email failed" + ) def block_submit_if_not_approved(doc, method=None): @@ -196,7 +221,9 @@ def create_approval_fields(doctype: str) -> None: def delete_approval_fields(doctype: str) -> None: - delete_custom_fields({doctype: [_APPROVER_TAB, _APPROVER_TABLE]}) + for fieldname in [_APPROVER_TAB, _APPROVER_TABLE]: + frappe.db.delete("Custom Field", {"dt": doctype, "fieldname": fieldname}) + frappe.clear_cache(doctype=doctype) def clear_approval_cache() -> None: @@ -285,13 +312,15 @@ def create_approver_qr_print_format(doctype: str) -> None: continue if any(f.get("fieldname") == _QR_FIELD_NAME for f in fd): continue - fd.append({ - "fieldname": _QR_FIELD_NAME, - "fieldtype": "HTML", - "label": "Approver QR Codes", - "print_hide": 0, - "options": _QR_PRINT_FORMAT_HTML, - }) + fd.append( + { + "fieldname": _QR_FIELD_NAME, + "fieldtype": "HTML", + "label": "Approver QR Codes", + "print_hide": 0, + "options": _QR_PRINT_FORMAT_HTML, + } + ) pf.format_data = json.dumps(fd) pf.save(ignore_permissions=True) @@ -334,7 +363,9 @@ def boot_session(bootinfo): try: bootinfo.parallel_approval_doctypes = list(_get_approval_doctypes()) except Exception: - frappe.log_error(frappe.get_traceback(), "Parallel Approval: boot_session failed") + frappe.log_error( + frappe.get_traceback(), "Parallel Approval: boot_session failed" + ) bootinfo.parallel_approval_doctypes = [] @@ -352,7 +383,7 @@ def submit_approval_action(doctype, docname, action, reason=None): current_user = frappe.session.user approver_row = None - for row in (getattr(doc, _APPROVER_TABLE, None) or []): + for row in getattr(doc, _APPROVER_TABLE, None) or []: if row.approver == current_user or row.delegate_to == current_user: approver_row = row break @@ -360,50 +391,90 @@ def submit_approval_action(doctype, docname, action, reason=None): if not approver_row: frappe.throw(_("You are not listed as an approver for this document.")) - is_expired = approver_row.expiry_date and frappe.utils.getdate(approver_row.expiry_date) < frappe.utils.getdate(frappe.utils.nowdate()) - if is_expired and current_user == approver_row.approver and approver_row.delegate_to: - frappe.throw(_( - "Your approval authority for this document expired on {0}. " - "This has been escalated to {1}." - ).format(frappe.utils.formatdate(approver_row.expiry_date), approver_row.delegate_to)) + is_expired = approver_row.expiry_date and frappe.utils.getdate( + approver_row.expiry_date + ) < frappe.utils.getdate(frappe.utils.nowdate()) + if ( + is_expired + and current_user == approver_row.approver + and approver_row.delegate_to + ): + frappe.throw( + _( + "Your approval authority for this document expired on {0}. " + "This has been escalated to {1}." + ).format( + frappe.utils.formatdate(approver_row.expiry_date), + approver_row.delegate_to, + ) + ) - acting_name = frappe.get_cached_value("User", current_user, "full_name") or current_user + acting_name = ( + frappe.get_cached_value("User", current_user, "full_name") or current_user + ) - acting_as_delegate = current_user == approver_row.delegate_to and current_user != approver_row.approver + acting_as_delegate = ( + current_user == approver_row.delegate_to + and current_user != approver_row.approver + ) if acting_as_delegate: - approver_name = frappe.get_cached_value("User", approver_row.approver, "full_name") or approver_row.approver + approver_name = ( + frappe.get_cached_value("User", approver_row.approver, "full_name") + or approver_row.approver + ) delegate_suffix = _(" (as delegate for {0})").format(approver_name) else: delegate_suffix = "" if action == "approve": - updates = {"approved": 1, "rejected": 0, "rejection_reason": "", "date": frappe.utils.now()} - comment_html = _("{0} reviewed and approved this document{1}.").format(acting_name, delegate_suffix) + updates = { + "approved": 1, + "rejected": 0, + "rejection_reason": "", + "date": frappe.utils.now(), + } + comment_html = _("{0} reviewed and approved this document{1}.").format( + acting_name, delegate_suffix + ) elif action == "reject": if not reason: frappe.throw(_("A rejection reason is required.")) safe_reason = frappe.utils.escape_html(reason) - updates = {"approved": 0, "rejected": 1, "rejection_reason": reason, "date": frappe.utils.now()} + updates = { + "approved": 0, + "rejected": 1, + "rejection_reason": reason, + "date": frappe.utils.now(), + } comment_html = _( "{0} rejected this document{1}.
Reason: {2}" ).format(acting_name, delegate_suffix, safe_reason) else: # clear_rejection - updates = {"approved": 0, "rejected": 0, "rejection_reason": "", "date": frappe.utils.now()} - comment_html = _("{0} cleared their rejection{1}.").format(acting_name, delegate_suffix) + updates = { + "approved": 0, + "rejected": 0, + "rejection_reason": "", + "date": frappe.utils.now(), + } + comment_html = _("{0} cleared their rejection{1}.").format( + acting_name, delegate_suffix + ) approver_row.update(updates) doc.save(ignore_permissions=True) - frappe.get_doc({ - "doctype": "Comment", - "comment_type": "Comment", - "reference_doctype": doctype, - "reference_name": docname, - "comment_email": current_user, - "comment_by": acting_name, - "content": comment_html, - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Comment", + "comment_type": "Comment", + "reference_doctype": doctype, + "reference_name": docname, + "comment_email": current_user, + "comment_by": acting_name, + "content": comment_html, + } + ).insert(ignore_permissions=True) return True diff --git a/av_tools/weigh_bridge/api.py b/av_tools/weigh_bridge/api.py index 172a8a7..b8965ce 100644 --- a/av_tools/weigh_bridge/api.py +++ b/av_tools/weigh_bridge/api.py @@ -62,11 +62,22 @@ def _apply_ticket_items_to_target(target_doc, ticket_doc): continue ticket_by_item_code.setdefault(item_code, []).append(row) + settings = frappe.get_single("Weighbridge Settings") + allowed_transport_items = { + (row.item_code or "").strip() + for row in (settings.get("transport_items") or []) + if row.get("item_code") + } + kept_rows = [] for row in (target_doc.get("items") or []): item_code = (row.get("item_code") or "").strip() matches = ticket_by_item_code.get(item_code) if item_code else None if not matches: + if item_code: + is_stock_item = frappe.db.get_value("Item", item_code, "is_stock_item") + if not is_stock_item and item_code in allowed_transport_items: + kept_rows.append(row) continue ticket_row = matches.pop(0) diff --git a/av_tools/weigh_bridge/doctype/weighbridge_settings/weighbridge_settings.json b/av_tools/weigh_bridge/doctype/weighbridge_settings/weighbridge_settings.json index 42a3944..fa0bebf 100644 --- a/av_tools/weigh_bridge/doctype/weighbridge_settings/weighbridge_settings.json +++ b/av_tools/weigh_bridge/doctype/weighbridge_settings/weighbridge_settings.json @@ -10,6 +10,8 @@ "timeout_seconds", "gateway_url", "read_weight_url", + "section_break_transport", + "transport_items", "section_break_device", "device_ip", "device_port", @@ -51,6 +53,17 @@ "fieldtype": "Data", "label": "Read Weight URL" }, + { + "fieldname": "section_break_transport", + "fieldtype": "Section Break", + "label": "Transport Items Configuration" + }, + { + "fieldname": "transport_items", + "fieldtype": "Table", + "label": "Allowed Transport / Service Items", + "options": "Weighbridge Transport Item" + }, { "fieldname": "section_break_device", "fieldtype": "Section Break", @@ -81,7 +94,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-02-09 00:00:00.000000", + "modified": "2026-08-13 08:00:00.000000", "modified_by": "Administrator", "module": "Weigh Bridge", "name": "Weighbridge Settings", diff --git a/av_tools/weigh_bridge/doctype/weighbridge_transport_item/__init__.py b/av_tools/weigh_bridge/doctype/weighbridge_transport_item/__init__.py new file mode 100644 index 0000000..03d47fc --- /dev/null +++ b/av_tools/weigh_bridge/doctype/weighbridge_transport_item/__init__.py @@ -0,0 +1 @@ +# Package init diff --git a/av_tools/weigh_bridge/doctype/weighbridge_transport_item/weighbridge_transport_item.json b/av_tools/weigh_bridge/doctype/weighbridge_transport_item/weighbridge_transport_item.json new file mode 100644 index 0000000..9454493 --- /dev/null +++ b/av_tools/weigh_bridge/doctype/weighbridge_transport_item/weighbridge_transport_item.json @@ -0,0 +1,53 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "hash", + "creation": "2026-08-13 08:00:00.000000", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "item_name", + "description" + ], + "fields": [ + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Item Name", + "read_only": 1 + }, + { + "fetch_from": "item_code.description", + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 0, + "label": "Description", + "read_only": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2026-08-13 08:00:00.000000", + "modified_by": "Administrator", + "module": "Weigh Bridge", + "name": "Weighbridge Transport Item", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} diff --git a/av_tools/weigh_bridge/doctype/weighbridge_transport_item/weighbridge_transport_item.py b/av_tools/weigh_bridge/doctype/weighbridge_transport_item/weighbridge_transport_item.py new file mode 100644 index 0000000..79fa7b7 --- /dev/null +++ b/av_tools/weigh_bridge/doctype/weighbridge_transport_item/weighbridge_transport_item.py @@ -0,0 +1,8 @@ +# Copyright (c) 2026, Av Tools and contributors +# For license information, please see license.txt + +from frappe.model.document import Document + + +class WeighbridgeTransportItem(Document): + pass