From 147d1d12d5e3cd58c8360e8450145b64651e7383 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Wed, 6 May 2026 14:55:24 +0300 Subject: [PATCH] feat(compliance): add License Register DocType - Main register for tracking business licenses and certificates - Fields: license type, number, issuing authority, issue/expiry dates, fee paid, company, branch/premises, status, and attachment - Auto-updates status (Active/Pending Renewal/Expired) based on expiry date and configurable reminder days - Color-coded status indicators (green/red/orange/yellow/gray) - Links to Inspection Record for related inspections --- .../doctype/license_register/__init__.py | 2 + .../license_register/license_register.js | 8 + .../license_register/license_register.json | 262 ++++++++++++++++++ .../license_register/license_register.py | 65 +++++ .../license_register/test_license_register.py | 9 + .../dist/js/av_tools.bundle.3P725JMU.js | 62 +++++ .../dist/js/av_tools.bundle.3P725JMU.js.map | 7 + 7 files changed, 415 insertions(+) create mode 100644 av_tools/compliance/doctype/license_register/__init__.py create mode 100644 av_tools/compliance/doctype/license_register/license_register.js create mode 100644 av_tools/compliance/doctype/license_register/license_register.json create mode 100644 av_tools/compliance/doctype/license_register/license_register.py create mode 100644 av_tools/compliance/doctype/license_register/test_license_register.py create mode 100644 av_tools/public/dist/js/av_tools.bundle.3P725JMU.js create mode 100644 av_tools/public/dist/js/av_tools.bundle.3P725JMU.js.map diff --git a/av_tools/compliance/doctype/license_register/__init__.py b/av_tools/compliance/doctype/license_register/__init__.py new file mode 100644 index 0000000..af349a2 --- /dev/null +++ b/av_tools/compliance/doctype/license_register/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2026, Aakvatech and contributors +# For license information, please see license.txt diff --git a/av_tools/compliance/doctype/license_register/license_register.js b/av_tools/compliance/doctype/license_register/license_register.js new file mode 100644 index 0000000..fc58f2f --- /dev/null +++ b/av_tools/compliance/doctype/license_register/license_register.js @@ -0,0 +1,8 @@ +// Copyright (c) 2026, Aakvatech and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("License Register", { +// refresh(frm) { + +// }, +// }); diff --git a/av_tools/compliance/doctype/license_register/license_register.json b/av_tools/compliance/doctype/license_register/license_register.json new file mode 100644 index 0000000..a564fb3 --- /dev/null +++ b/av_tools/compliance/doctype/license_register/license_register.json @@ -0,0 +1,262 @@ +{ + "actions": [], + "autoname": "naming_series:", + "creation": "2026-05-06 13:12:00", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "details_tab", + "license_details_section", + "license_type", + "license_name", + "license_number", + "column_break_details", + "company", + "branch", + "fee_paid", + "column_break_ncpn", + "status", + "posting_date", + "posting_time", + "authority_dates_section", + "issuing_authority", + "issue_date", + "expiry_date", + "column_break_dates", + "reminder_days_before", + "notify_role", + "notes_section", + "license_attachment", + "naming_series", + "amended_from", + "column_break_mnbd", + "notes" + ], + "fields": [ + { + "fieldname": "details_tab", + "fieldtype": "Tab Break", + "label": "Details" + }, + { + "fieldname": "license_details_section", + "fieldtype": "Section Break", + "label": "License Details" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "options": "LIC-.YYYY.-.#####", + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "license_type", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "License Type", + "options": "License Type", + "reqd": 1 + }, + { + "fieldname": "license_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "License Name", + "reqd": 1 + }, + { + "fieldname": "column_break_details", + "fieldtype": "Column Break" + }, + { + "fieldname": "license_number", + "fieldtype": "Data", + "in_list_view": 1, + "label": "License / Certificate Number", + "reqd": 1 + }, + { + "default": "Active", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Active\nExpired\nPending Renewal\nSuspended\nCancelled" + }, + { + "fieldname": "authority_dates_section", + "fieldtype": "Section Break", + "label": "Authority & Dates" + }, + { + "fieldname": "issuing_authority", + "fieldtype": "Data", + "label": "Issuing Authority", + "reqd": 1 + }, + { + "fieldname": "issue_date", + "fieldtype": "Date", + "label": "Issue Date", + "reqd": 1 + }, + { + "fieldname": "column_break_dates", + "fieldtype": "Column Break" + }, + { + "fieldname": "expiry_date", + "fieldtype": "Date", + "label": "Expiry Date", + "reqd": 1 + }, + { + "default": "30", + "description": "Number of days before expiry to trigger a reminder notification", + "fieldname": "reminder_days_before", + "fieldtype": "Int", + "label": "Reminder Days Before Expiry", + "mandatory_depends_on": "eval: doc.reminder_days_before" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "branch", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Branch / Premises", + "options": "Branch" + }, + { + "fieldname": "fee_paid", + "fieldtype": "Currency", + "label": "Fee Paid" + }, + { + "collapsible": 1, + "fieldname": "notes_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "notes", + "fieldtype": "Text Editor", + "label": "Notes" + }, + { + "fieldname": "license_attachment", + "fieldtype": "Attach", + "label": "License / Certificate Attachment" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "License Register", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_ncpn", + "fieldtype": "Column Break" + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date", + "read_only": 1 + }, + { + "default": "now", + "fieldname": "posting_time", + "fieldtype": "Time", + "label": "Posting Time", + "read_only": 1 + }, + { + "description": "A role for users who will recieve reminder notification", + "fieldname": "notify_role", + "fieldtype": "Link", + "label": "Notify Role", + "options": "Role" + }, + { + "fieldname": "column_break_mnbd", + "fieldtype": "Column Break" + } + ], + "links": [ + { + "link_doctype": "Inspection Record", + "link_fieldname": "license_register" + } + ], + "modified": "2026-05-06 14:19:05.322331", + "modified_by": "Administrator", + "module": "Compliance", + "name": "License Register", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 + } + ], + "row_format": "Dynamic", + "show_title_field_in_link": 1, + "sort_field": "creation", + "sort_order": "DESC", + "states": [ + { + "color": "Green", + "title": "Active" + }, + { + "color": "Red", + "title": "Expired" + }, + { + "color": "Orange", + "title": "Pending Renewal" + }, + { + "color": "Yellow", + "title": "Suspended" + }, + { + "color": "Pink", + "title": "Cancelled" + } + ], + "title_field": "license_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/av_tools/compliance/doctype/license_register/license_register.py b/av_tools/compliance/doctype/license_register/license_register.py new file mode 100644 index 0000000..4290faf --- /dev/null +++ b/av_tools/compliance/doctype/license_register/license_register.py @@ -0,0 +1,65 @@ +# Copyright (c) 2026, Aakvatech and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import getdate, nowdate, date_diff + + +class LicenseRegister(Document): + def before_save(self): + self.validate_dates() + self.update_status() + + def validate_dates(self): + if self.issue_date and self.expiry_date: + if getdate(self.issue_date) > getdate(self.expiry_date): + frappe.throw( + _("Issue Date cannot be after Expiry Date"), + title=_("Invalid Dates"), + ) + + def update_status(self): + """Auto-update status based on expiry date.""" + if self.status in ("Suspended", "Cancelled"): + return + + today = getdate(nowdate()) + expiry = getdate(self.expiry_date) if self.expiry_date else None + + if not expiry: + return + + if expiry < today: + self.status = "Expired" + elif self.reminder_days_before and date_diff(expiry, today) <= self.reminder_days_before: + self.status = "Pending Renewal" + else: + self.status = "Active" + + +def update_license_statuses(): + """Scheduled job to auto-update license statuses daily. + Called via hooks.py scheduler_events. + """ + licenses = frappe.get_all( + "License Register", + filters={"status": ("in", ["Active", "Pending Renewal"])}, + fields=["name", "expiry_date", "reminder_days_before", "status"], + ) + + today = getdate(nowdate()) + + for lic in licenses: + expiry = getdate(lic.expiry_date) + new_status = lic.status + + if expiry < today: + new_status = "Expired" + elif lic.reminder_days_before and date_diff(expiry, today) <= lic.reminder_days_before: + new_status = "Pending Renewal" + + if new_status != lic.status: + frappe.db.set_value("License Register", lic.name, "status", new_status) + diff --git a/av_tools/compliance/doctype/license_register/test_license_register.py b/av_tools/compliance/doctype/license_register/test_license_register.py new file mode 100644 index 0000000..5c6c96d --- /dev/null +++ b/av_tools/compliance/doctype/license_register/test_license_register.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Aakvatech and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestLicenseRegister(FrappeTestCase): + pass diff --git a/av_tools/public/dist/js/av_tools.bundle.3P725JMU.js b/av_tools/public/dist/js/av_tools.bundle.3P725JMU.js new file mode 100644 index 0000000..e2200cf --- /dev/null +++ b/av_tools/public/dist/js/av_tools.bundle.3P725JMU.js @@ -0,0 +1,62 @@ +(() => { + // ../av_tools/av_tools/public/js/financial_statements_override.js + function install_financial_statements_override() { + if (typeof erpnext === "undefined" || !erpnext.financial_statements) { + return false; + } + if (erpnext.financial_statements.__av_tools_override_installed) { + return true; + } + const original_open_general_ledger = erpnext.financial_statements.open_general_ledger; + erpnext.financial_statements.open_general_ledger = function(data) { + if (!data.account && !data.accounts) + return; + function navigate_based_on_type(account_type) { + if (account_type === "Receivable") { + frappe.route_options = { + company: frappe.query_report.get_filter_value("company"), + report_date: data.to_date || data.year_end_date, + ageing_based_on: "Posting Date" + }; + frappe.set_route("query-report", "Accounts Receivable Summary"); + } else if (account_type === "Payable") { + frappe.route_options = { + company: frappe.query_report.get_filter_value("company"), + report_date: data.to_date || data.year_end_date, + ageing_based_on: "Posting Date" + }; + frappe.set_route("query-report", "Accounts Payable Summary"); + } else { + original_open_general_ledger(data); + } + } + if (data.account_type) { + navigate_based_on_type(data.account_type); + } else { + let account_name = data.account || data.accounts; + frappe.db.get_value("Account", account_name, "account_type", function(r) { + if (r && r.account_type) { + navigate_based_on_type(r.account_type); + } else { + original_open_general_ledger(data); + } + }); + } + }; + erpnext.financial_statements.__av_tools_override_installed = true; + return true; + } + $(document).on("app_ready startup", install_financial_statements_override); + $(function() { + if (install_financial_statements_override()) + return; + let attempts = 0; + const interval = setInterval(function() { + attempts++; + if (install_financial_statements_override() || attempts > 50) { + clearInterval(interval); + } + }, 200); + }); +})(); +//# sourceMappingURL=av_tools.bundle.3P725JMU.js.map diff --git a/av_tools/public/dist/js/av_tools.bundle.3P725JMU.js.map b/av_tools/public/dist/js/av_tools.bundle.3P725JMU.js.map new file mode 100644 index 0000000..c5fb58f --- /dev/null +++ b/av_tools/public/dist/js/av_tools.bundle.3P725JMU.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../../../../apps/av_tools/av_tools/public/js/financial_statements_override.js"], + "sourcesContent": ["function install_financial_statements_override() {\n\tif (typeof erpnext === \"undefined\" || !erpnext.financial_statements) {\n\t\treturn false;\n\t}\n\tif (erpnext.financial_statements.__av_tools_override_installed) {\n\t\treturn true;\n\t}\n\n\tconst original_open_general_ledger = erpnext.financial_statements.open_general_ledger;\n\n\terpnext.financial_statements.open_general_ledger = function (data) {\n\t\tif (!data.account && !data.accounts) return;\n\n\t\tfunction navigate_based_on_type(account_type) {\n\t\t\tif (account_type === \"Receivable\") {\n\t\t\t\tfrappe.route_options = {\n\t\t\t\t\tcompany: frappe.query_report.get_filter_value(\"company\"),\n\t\t\t\t\treport_date: data.to_date || data.year_end_date,\n\t\t\t\t\tageing_based_on: \"Posting Date\",\n\t\t\t\t};\n\t\t\t\tfrappe.set_route(\"query-report\", \"Accounts Receivable Summary\");\n\t\t\t} else if (account_type === \"Payable\") {\n\t\t\t\tfrappe.route_options = {\n\t\t\t\t\tcompany: frappe.query_report.get_filter_value(\"company\"),\n\t\t\t\t\treport_date: data.to_date || data.year_end_date,\n\t\t\t\t\tageing_based_on: \"Posting Date\",\n\t\t\t\t};\n\t\t\t\tfrappe.set_route(\"query-report\", \"Accounts Payable Summary\");\n\t\t\t} else {\n\t\t\t\toriginal_open_general_ledger(data);\n\t\t\t}\n\t\t}\n\n\t\tif (data.account_type) {\n\t\t\tnavigate_based_on_type(data.account_type);\n\t\t} else {\n\t\t\tlet account_name = data.account || data.accounts;\n\t\t\tfrappe.db.get_value(\"Account\", account_name, \"account_type\", function (r) {\n\t\t\t\tif (r && r.account_type) {\n\t\t\t\t\tnavigate_based_on_type(r.account_type);\n\t\t\t\t} else {\n\t\t\t\t\toriginal_open_general_ledger(data);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t};\n\n\terpnext.financial_statements.__av_tools_override_installed = true;\n\treturn true;\n}\n\n$(document).on(\"app_ready startup\", install_financial_statements_override);\n\n$(function () {\n\tif (install_financial_statements_override()) return;\n\tlet attempts = 0;\n\tconst interval = setInterval(function () {\n\t\tattempts++;\n\t\tif (install_financial_statements_override() || attempts > 50) {\n\t\t\tclearInterval(interval);\n\t\t}\n\t}, 200);\n});\n"], + "mappings": ";;AAAA,WAAS,wCAAwC;AAChD,QAAI,OAAO,YAAY,eAAe,CAAC,QAAQ,sBAAsB;AACpE,aAAO;AAAA,IACR;AACA,QAAI,QAAQ,qBAAqB,+BAA+B;AAC/D,aAAO;AAAA,IACR;AAEA,UAAM,+BAA+B,QAAQ,qBAAqB;AAElE,YAAQ,qBAAqB,sBAAsB,SAAU,MAAM;AAClE,UAAI,CAAC,KAAK,WAAW,CAAC,KAAK;AAAU;AAErC,eAAS,uBAAuB,cAAc;AAC7C,YAAI,iBAAiB,cAAc;AAClC,iBAAO,gBAAgB;AAAA,YACtB,SAAS,OAAO,aAAa,iBAAiB,SAAS;AAAA,YACvD,aAAa,KAAK,WAAW,KAAK;AAAA,YAClC,iBAAiB;AAAA,UAClB;AACA,iBAAO,UAAU,gBAAgB,6BAA6B;AAAA,QAC/D,WAAW,iBAAiB,WAAW;AACtC,iBAAO,gBAAgB;AAAA,YACtB,SAAS,OAAO,aAAa,iBAAiB,SAAS;AAAA,YACvD,aAAa,KAAK,WAAW,KAAK;AAAA,YAClC,iBAAiB;AAAA,UAClB;AACA,iBAAO,UAAU,gBAAgB,0BAA0B;AAAA,QAC5D,OAAO;AACN,uCAA6B,IAAI;AAAA,QAClC;AAAA,MACD;AAEA,UAAI,KAAK,cAAc;AACtB,+BAAuB,KAAK,YAAY;AAAA,MACzC,OAAO;AACN,YAAI,eAAe,KAAK,WAAW,KAAK;AACxC,eAAO,GAAG,UAAU,WAAW,cAAc,gBAAgB,SAAU,GAAG;AACzE,cAAI,KAAK,EAAE,cAAc;AACxB,mCAAuB,EAAE,YAAY;AAAA,UACtC,OAAO;AACN,yCAA6B,IAAI;AAAA,UAClC;AAAA,QACD,CAAC;AAAA,MACF;AAAA,IACD;AAEA,YAAQ,qBAAqB,gCAAgC;AAC7D,WAAO;AAAA,EACR;AAEA,IAAE,QAAQ,EAAE,GAAG,qBAAqB,qCAAqC;AAEzE,IAAE,WAAY;AACb,QAAI,sCAAsC;AAAG;AAC7C,QAAI,WAAW;AACf,UAAM,WAAW,YAAY,WAAY;AACxC;AACA,UAAI,sCAAsC,KAAK,WAAW,IAAI;AAC7D,sBAAc,QAAQ;AAAA,MACvB;AAAA,IACD,GAAG,GAAG;AAAA,EACP,CAAC;", + "names": [] +}