diff --git a/av_tools/av_tools/doctype/implementation_handover_snapshot/__init__.py b/av_tools/av_tools/doctype/implementation_handover_snapshot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/av_tools/av_tools/doctype/implementation_handover_snapshot/implementation_handover_snapshot.js b/av_tools/av_tools/doctype/implementation_handover_snapshot/implementation_handover_snapshot.js new file mode 100644 index 0000000..7b6dc34 --- /dev/null +++ b/av_tools/av_tools/doctype/implementation_handover_snapshot/implementation_handover_snapshot.js @@ -0,0 +1,71 @@ +// Copyright (c) 2026, Aakvatech and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Implementation Handover Snapshot", { + refresh(frm) { + frm.remove_custom_button(__("Add All Sections")); + frm.remove_custom_button(__("Generate Snapshot")); + + frm.add_custom_button(__("Add All Sections"), () => { + frm.trigger("add_all_sections"); + }); + + frm.add_custom_button(__("Generate Snapshot"), () => { + frm.trigger("generate_snapshot"); + }); + }, + + add_all_sections(frm) { + return get_snapshot_section_options().then((sections) => { + frm.clear_table("table_qvkt"); + + sections.forEach((section) => { + const row = frm.add_child("table_qvkt"); + row.reference = section; + }); + + frm.refresh_field("table_qvkt"); + }); + }, + + generate_snapshot(frm) { + return ensure_sections(frm) + .then(() => frm.call("generate_snapshot")) + .then((r) => { + if (!r.message) { + return; + } + + frm.doc.name = r.message.name || frm.doc.name; + frm.doc.__islocal = 0; + frm.set_value("site_name", r.message.site_name); + frm.set_value("generated_on", r.message.generated_on); + frm.set_value("generated_by", r.message.generated_by); + frm.clear_table("table_ldiu"); + + (r.message.table_ldiu || []).forEach((snapshot_json) => { + const row = frm.add_child("table_ldiu"); + row.type = snapshot_json.type; + row.json_type = snapshot_json.json_type; + }); + + frm.refresh_field("table_ldiu"); + frm.doc.__unsaved = 0; + frm.refresh_fields(); + }); + }, +}); + +function ensure_sections(frm) { + if ((frm.doc.table_qvkt || []).length) { + return Promise.resolve(); + } + + return frm.trigger("add_all_sections"); +} + +function get_snapshot_section_options() { + return frappe + .call("av_tools.av_tools.doctype.implementation_handover_snapshot.implementation_handover_snapshot.get_snapshot_section_options") + .then((r) => r.message || []); +} diff --git a/av_tools/av_tools/doctype/implementation_handover_snapshot/implementation_handover_snapshot.json b/av_tools/av_tools/doctype/implementation_handover_snapshot/implementation_handover_snapshot.json new file mode 100644 index 0000000..a723f59 --- /dev/null +++ b/av_tools/av_tools/doctype/implementation_handover_snapshot/implementation_handover_snapshot.json @@ -0,0 +1,112 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "format:CHS-{####}", + "creation": "2026-07-27 00:00:00", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "customer", + "site_name", + "column_break_lwng", + "generated_on", + "generated_by", + "from_date", + "to_date", + "snapshot_sections_section", + "table_qvkt", + "json_section_section", + "table_ldiu" + ], + "fields": [ + { + "fieldname": "customer", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Customer", + "reqd": 1 + }, + { + "fieldname": "site_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Site Name", + "read_only": 1 + }, + { + "fieldname": "column_break_lwng", + "fieldtype": "Column Break" + }, + { + "fieldname": "generated_on", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Generated On", + "read_only": 1 + }, + { + "fieldname": "generated_by", + "fieldtype": "Link", + "label": "Generated By", + "options": "User", + "read_only": 1 + }, + { + "fieldname": "snapshot_sections_section", + "fieldtype": "Section Break", + "label": "Snapshot Sections" + }, + { + "fieldname": "table_qvkt", + "fieldtype": "Table", + "options": "Snapshot" + }, + { + "fieldname": "json_section_section", + "fieldtype": "Section Break", + "label": "JSON Section" + }, + { + "fieldname": "table_ldiu", + "fieldtype": "Table", + "options": "Snapshot Json" + }, + { + "fieldname": "from_date", + "fieldtype": "Date", + "label": "From Date" + }, + { + "fieldname": "to_date", + "fieldtype": "Date", + "label": "To date" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-08-07 12:25:31.984378", + "modified_by": "Administrator", + "module": "Av Tools", + "name": "Implementation Handover Snapshot", + "naming_rule": "Expression (old style)", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/av_tools/av_tools/doctype/implementation_handover_snapshot/implementation_handover_snapshot.py b/av_tools/av_tools/doctype/implementation_handover_snapshot/implementation_handover_snapshot.py new file mode 100644 index 0000000..2956329 --- /dev/null +++ b/av_tools/av_tools/doctype/implementation_handover_snapshot/implementation_handover_snapshot.py @@ -0,0 +1,300 @@ +# Copyright (c) 2026, Aakvatech and contributors +# For license information, please see license.txt + +import json + +import frappe +from frappe.model.document import Document +from frappe.utils import getdate, now + +SNAPSHOT_TABLE = "table_qvkt" +JSON_TABLE = "table_ldiu" + +COMMON_AUDIT_FIELDS = ["modified", "modified_by", "owner"] + + +SECTION_DEFINITIONS = [ + { + "label": "Client Script", + "doctype": "Client Script", + "fields": ["name", "dt", "view", "enabled", "script", *COMMON_AUDIT_FIELDS], + "order_by": "dt asc, name asc", + }, + { + "label": "Server Script", + "doctype": "Server Script", + "fields": [ + "name", + "script_type", + "reference_doctype", + "event_frequency", + "api_method", + "disabled", + "script", + *COMMON_AUDIT_FIELDS, + ], + "order_by": "script_type asc, name asc", + }, + { + "label": "Custom Field", + "doctype": "Custom Field", + "fields": [ + "name", + "dt", + "fieldname", + "label", + "fieldtype", + "options", + "insert_after", + "reqd", + "read_only", + "hidden", + "depends_on", + "mandatory_depends_on", + *COMMON_AUDIT_FIELDS, + ], + "order_by": "dt asc, fieldname asc", + }, + { + "label": "Property Setter", + "doctype": "Property Setter", + "fields": [ + "name", + "doc_type", + "field_name", + "property", + "property_type", + "value", + "default_value", + *COMMON_AUDIT_FIELDS, + ], + "order_by": "doc_type asc, field_name asc, property asc", + }, + { + "label": "Workflow", + "doctype": "Workflow", + "fields": ["name", "document_type", "is_active", "workflow_state_field", *COMMON_AUDIT_FIELDS], + "order_by": "document_type asc, name asc", + }, + { + "label": "Workflow State", + "doctype": "Workflow State", + "fields": ["name", "workflow_state_name", "style", *COMMON_AUDIT_FIELDS], + "order_by": "workflow_state_name asc", + }, + { + "label": "Workflow Action", + "doctype": "Workflow Action Master", + "fields": ["name", "workflow_action_name", *COMMON_AUDIT_FIELDS], + "order_by": "workflow_action_name asc", + }, + { + "label": "Print Format", + "doctype": "Print Format", + "fields": ["name", "doc_type", "print_format_type", "standard", "disabled", "html", "css", *COMMON_AUDIT_FIELDS], + "filters": {"standard": "No"}, + "order_by": "doc_type asc, name asc", + }, + { + "label": "Report", + "doctype": "Report", + "fields": [ + "name", + "ref_doctype", + "report_type", + "is_standard", + "disabled", + "json", + "query", + "script", + *COMMON_AUDIT_FIELDS, + ], + "filters": {"is_standard": "No"}, + "order_by": "ref_doctype asc, name asc", + }, + { + "label": "Dashboard", + "doctype": "Dashboard", + "fields": ["name", "is_standard", "module", *COMMON_AUDIT_FIELDS], + "filters": {"is_standard": 0}, + "order_by": "name asc", + }, + { + "label": "Notification", + "doctype": "Notification", + "fields": ["name", "document_type", "event", "enabled", "condition", "subject", "message", *COMMON_AUDIT_FIELDS], + "order_by": "document_type asc, name asc", + }, + { + "label": "Web Form", + "doctype": "Web Form", + "fields": ["name", "title", "doc_type", "route", "published", "login_required", *COMMON_AUDIT_FIELDS], + "order_by": "doc_type asc, name asc", + }, + { + "label": "Custom Permission", + "doctype": "Custom DocPerm", + "fields": [ + "name", + "parent", + "role", + "permlevel", + "read", + "write", + "create", + "delete", + "submit", + "cancel", + "amend", + *COMMON_AUDIT_FIELDS, + ], + "order_by": "parent asc, role asc, permlevel asc", + }, + { + "label": "Custom DocType", + "doctype": "DocType", + "fields": ["name", "module", "custom", "istable", "issingle", "is_submittable", "autoname", *COMMON_AUDIT_FIELDS], + "filters": {"custom": 1}, + "order_by": "name asc", + }, +] + +SECTION_BY_LABEL = {section["label"]: section for section in SECTION_DEFINITIONS} + + +class ImplementationHandoverSnapshot(Document): + def before_insert(self): + if not self.site_name: + self.site_name = frappe.local.site + + def validate(self): + validate_date_range(self) + + @frappe.whitelist() + def generate_snapshot(self): + validate_date_range(self) + + self.site_name = frappe.local.site + self.generated_on = now() + self.generated_by = frappe.session.user + + self.set(JSON_TABLE, []) + for section in build_customization_snapshot(self): + self.append( + JSON_TABLE, + { + "type": section["label"], + "json_type": dump_json(section["records"]), + }, + ) + + self.save() + + response = { + "name": self.name, + "generated_on": self.generated_on, + "generated_by": self.generated_by, + "site_name": self.site_name, + JSON_TABLE: [row.as_dict() for row in self.get(JSON_TABLE)], + } + + return response + + +@frappe.whitelist() +def get_snapshot_section_options(): + return [section["label"] for section in SECTION_DEFINITIONS] + + +def build_customization_snapshot(doc=None): + sections = [] + + for label in get_selected_sections(doc): + section = SECTION_BY_LABEL.get(label) + if not section: + continue + + sections.append( + { + "label": section["label"], + "records": get_section_records(section, doc), + } + ) + + return sections + + +def dump_json(value): + return json.dumps(value, indent=2, sort_keys=True, default=str) + + +def get_selected_sections(doc): + if not doc: + return [section["label"] for section in SECTION_DEFINITIONS] + + seen = set() + selected = [] + for row in doc.get(SNAPSHOT_TABLE): + label = row.get("reference") + if label and label not in seen: + selected.append(label) + seen.add(label) + + return selected or [section["label"] for section in SECTION_DEFINITIONS] + + +def get_section_records(section, doc=None): + filters = dict(section.get("filters") or {}) + filters.update(get_date_filters(doc, section["doctype"])) + + return get_records( + section["doctype"], + section["fields"], + filters=filters, + order_by=section.get("order_by"), + ) + + +def get_date_filters(doc, doctype): + if not doc or not frappe.db.has_column(doctype, "modified"): + return {} + + from_date = doc.get("from_date") + to_date = doc.get("to_date") + if from_date and to_date: + return {"modified": ["between", [start_of_day(from_date), end_of_day(to_date)]]} + if from_date: + return {"modified": [">=", start_of_day(from_date)]} + if to_date: + return {"modified": ["<=", end_of_day(to_date)]} + + return {} + + +def validate_date_range(doc): + if doc.get("from_date") and doc.get("to_date") and getdate(doc.from_date) > getdate(doc.to_date): + frappe.throw("From Date cannot be after To Date") + + +def start_of_day(date): + return f"{getdate(date)} 00:00:00" + + +def end_of_day(date): + return f"{getdate(date)} 23:59:59" + + +def get_records(doctype, fields, filters=None, order_by=None): + if not frappe.db.exists("DocType", doctype): + return [] + + fields = [field for field in fields if frappe.db.has_column(doctype, field)] + if not fields: + return [] + + return frappe.get_all( + doctype, + fields=fields, + filters=filters, + order_by=order_by, + ) diff --git a/av_tools/av_tools/doctype/implementation_handover_snapshot/test_implementation_handover_snapshot.py b/av_tools/av_tools/doctype/implementation_handover_snapshot/test_implementation_handover_snapshot.py new file mode 100644 index 0000000..61c94b1 --- /dev/null +++ b/av_tools/av_tools/doctype/implementation_handover_snapshot/test_implementation_handover_snapshot.py @@ -0,0 +1,79 @@ +# Copyright (c) 2026, Aakvatech and Contributors +# See license.txt + +import json + +import frappe +from frappe.tests.utils import FrappeTestCase + +from av_tools.av_tools.doctype.implementation_handover_snapshot.implementation_handover_snapshot import ( + get_date_filters, + validate_date_range, +) + + +class TestImplementationHandoverSnapshot(FrappeTestCase): + def test_generate_snapshot_sets_json_rows_for_selected_sections(self): + doc = frappe.get_doc( + { + "doctype": "Implementation Handover Snapshot", + "customer": "_Test Customer", + "table_qvkt": [ + { + "reference": "Custom Field", + } + ], + } + ) + + doc.generate_snapshot() + + self.assertEqual(doc.site_name, frappe.local.site) + self.assertEqual(len(doc.table_ldiu), 1) + self.assertEqual(doc.table_ldiu[0].type, "Custom Field") + + payload = json.loads(doc.table_ldiu[0].json_type) + self.assertIsInstance(payload, list) + if payload: + self.assertIn("name", payload[0]) + self.assertIn("dt", payload[0]) + + def test_generate_snapshot_defaults_to_all_sections(self): + doc = frappe.get_doc( + { + "doctype": "Implementation Handover Snapshot", + "customer": "_Test Customer", + } + ) + + doc.generate_snapshot() + + self.assertGreater(len(doc.table_ldiu), 1) + self.assertEqual(doc.table_ldiu[0].type, "Client Script") + + def test_date_range_filters_modified_inclusive_days(self): + doc = frappe.get_doc( + { + "doctype": "Implementation Handover Snapshot", + "customer": "_Test Customer", + "from_date": "2026-08-01", + "to_date": "2026-08-07", + } + ) + + self.assertEqual( + get_date_filters(doc, "Custom Field"), + {"modified": ["between", ["2026-08-01 00:00:00", "2026-08-07 23:59:59"]]}, + ) + + def test_rejects_reversed_date_range(self): + doc = frappe.get_doc( + { + "doctype": "Implementation Handover Snapshot", + "customer": "_Test Customer", + "from_date": "2026-08-07", + "to_date": "2026-08-01", + } + ) + + self.assertRaises(frappe.ValidationError, validate_date_range, doc) diff --git a/av_tools/av_tools/doctype/snapshot/__init__.py b/av_tools/av_tools/doctype/snapshot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/av_tools/av_tools/doctype/snapshot/snapshot.json b/av_tools/av_tools/doctype/snapshot/snapshot.json new file mode 100644 index 0000000..458da3f --- /dev/null +++ b/av_tools/av_tools/doctype/snapshot/snapshot.json @@ -0,0 +1,35 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-07-31 09:30:37.809698", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "reference" + ], + "fields": [ + { + "fieldname": "reference", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Reference", + "options": "\nClient Script\nServer Script\nCustom Field\nProperty Setter\nWorkflow\nWorkflow State\nWorkflow Action\nPrint Format\nReport\nDashboard\nNotification\nWeb Form\nCustom Permission\nCustom DocType" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2026-07-31 09:40:03.097769", + "modified_by": "Administrator", + "module": "Av Tools", + "name": "Snapshot", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} diff --git a/av_tools/av_tools/doctype/snapshot/snapshot.py b/av_tools/av_tools/doctype/snapshot/snapshot.py new file mode 100644 index 0000000..985e89d --- /dev/null +++ b/av_tools/av_tools/doctype/snapshot/snapshot.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Aakvatech and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class Snapshot(Document): + pass diff --git a/av_tools/av_tools/doctype/snapshot_json/__init__.py b/av_tools/av_tools/doctype/snapshot_json/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/av_tools/av_tools/doctype/snapshot_json/snapshot_json.json b/av_tools/av_tools/doctype/snapshot_json/snapshot_json.json new file mode 100644 index 0000000..83a9586 --- /dev/null +++ b/av_tools/av_tools/doctype/snapshot_json/snapshot_json.json @@ -0,0 +1,46 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-07-31 09:42:02.229273", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "type", + "column_break_rcvm", + "json_type" + ], + "fields": [ + { + "fieldname": "type", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Type" + }, + { + "fieldname": "column_break_rcvm", + "fieldtype": "Column Break" + }, + { + "fieldname": "json_type", + "fieldtype": "JSON", + "label": "Json Type", + "read_only": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2026-07-31 10:16:29.706449", + "modified_by": "Administrator", + "module": "Av Tools", + "name": "Snapshot Json", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} diff --git a/av_tools/av_tools/doctype/snapshot_json/snapshot_json.py b/av_tools/av_tools/doctype/snapshot_json/snapshot_json.py new file mode 100644 index 0000000..34f7bf8 --- /dev/null +++ b/av_tools/av_tools/doctype/snapshot_json/snapshot_json.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Aakvatech and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class SnapshotJson(Document): + pass