From d83931e5070e04c3208c13103a39fdfcc1ba989e Mon Sep 17 00:00:00 2001 From: MariamMabele Date: Thu, 6 Aug 2026 15:01:52 +0300 Subject: [PATCH 01/10] fix(vehicle-sync): defer failed retries until pending queue is processed --- .../doctype/vehicle_sync_task/processor.py | 4 +- .../csf_tz/doctype/vehicle_sync_task/queue.py | 59 ++++++++++++++++--- 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/csf_tz/csf_tz/doctype/vehicle_sync_task/processor.py b/csf_tz/csf_tz/doctype/vehicle_sync_task/processor.py index e1e1c01d..6cf0960a 100644 --- a/csf_tz/csf_tz/doctype/vehicle_sync_task/processor.py +++ b/csf_tz/csf_tz/doctype/vehicle_sync_task/processor.py @@ -85,11 +85,11 @@ def run_vehicle_batch(): continue if status in {"rate_limited", "retryable_error"}: - attempts, _ = queue.bump_attempts(TASK_DOCTYPE, task) + current_attempts = frappe.db.get_value(TASK_DOCTYPE, task["name"], "attempts") or 0 queue.schedule_next( TASK_DOCTYPE, task, - _backoff_seconds(attempts), + _backoff_seconds(current_attempts + 1), result.get("message") or status, ) errors += 1 diff --git a/csf_tz/csf_tz/doctype/vehicle_sync_task/queue.py b/csf_tz/csf_tz/doctype/vehicle_sync_task/queue.py index 30c7c680..c724221d 100644 --- a/csf_tz/csf_tz/doctype/vehicle_sync_task/queue.py +++ b/csf_tz/csf_tz/doctype/vehicle_sync_task/queue.py @@ -4,7 +4,7 @@ # ------------ CONFIGURATION ------------ BATCH_SIZE = 1 TIME_BUDGET_SEC = 50 -MAX_ATTEMPTS = 4 +MAX_ATTEMPTS = 2 BASE_BACKOFF = 300 BACKOFF_JITTER = 0.2 SUCCESS_INTERVAL_SECONDS = 60 * 60 * 2 @@ -22,6 +22,21 @@ def _jitter(seconds): jitter_factor = 1 + (random_factor * BACKOFF_JITTER) return int(seconds * jitter_factor) +def get_pending_cycle_delay(doctype): + try: + Task = frappe.qb.DocType(doctype) + pending = ( + frappe.qb.from_(Task) + .select(Task.name) + .where( + (Task.status == "Pending") & + ((Task.is_deleted.isnull()) | (Task.is_deleted == 0)) + ) + ).run() + return max(len(pending), 1) * 60 + except Exception: + return 60 + # ------------ CORE QUEUE OPERATIONS ------------ def claim_batch(doctype, limit=BATCH_SIZE): try: @@ -34,13 +49,27 @@ def claim_batch(doctype, limit=BATCH_SIZE): .where( (Task.status == "Pending") & ((Task.next_run_at.isnull()) | (Task.next_run_at <= now)) & - ((Task.is_deleted.isnull()) | (Task.is_deleted == 0)) # ← IGNORE DELETED TASKS + ((Task.is_deleted.isnull()) | (Task.is_deleted == 0)) ) .orderby(Task.priority, order=frappe.qb.terms.Order.desc) .orderby(Task.name) .limit(limit) ).run(as_dict=True) + if not rows: + rows = ( + frappe.qb.from_(Task) + .select(Task.name) + .where( + (Task.status == "Failed") & + (Task.next_run_at <= now) & + ((Task.is_deleted.isnull()) | (Task.is_deleted == 0)) + ) + .orderby(Task.priority, order=frappe.qb.terms.Order.desc) + .orderby(Task.name) + .limit(limit) + ).run(as_dict=True) + if not rows: return [] @@ -80,16 +109,22 @@ def mark_done(doctype, task): message=f"Error marking task {task.get('name')} as done in {doctype}: {str(e)}" ) -def mark_failed(doctype, task, err_msg): +def mark_failed(doctype, task, err_msg, next_run_at=None, reset_attempts=False): try: - frappe.db.set_value(doctype, task["name"], { + values = { "status": "Failed", "last_error": err_msg[:1000], "last_run_at": _now(), "claimed_by": "", "claimed_at": None, - "next_run_at": None, - }) + "next_run_at": next_run_at, + } + if reset_attempts: + values.update({ + "attempts": 0, + "backoff_exp": 0, + }) + frappe.db.set_value(doctype, task["name"], values) except Exception as e: frappe.log_error( title="Queue Mark Failed Error", @@ -119,10 +154,18 @@ def bump_attempts(doctype, task): def schedule_next(doctype, task, backoff_seconds, error_msg=""): try: attempts, _ = bump_attempts(doctype, task) + cycle_delay = get_pending_cycle_delay(doctype) + next_delay = max(backoff_seconds, cycle_delay) + next_run = frappe.utils.add_to_date(_now(), seconds=_jitter(next_delay)) if attempts >= MAX_ATTEMPTS: - mark_failed(doctype, task, error_msg or "Max attempts exceeded") + mark_failed( + doctype, + task, + error_msg or "Max attempts exceeded", + next_run_at=next_run, + reset_attempts=True, + ) return - next_run = frappe.utils.add_to_date(_now(), seconds=_jitter(backoff_seconds)) frappe.db.set_value(doctype, task["name"], { "status": "Pending", "claimed_by": "", From 63abe8b6188b1e0467207a3fe9ce6b46b5083341 Mon Sep 17 00:00:00 2001 From: MariamMabele Date: Fri, 21 Aug 2026 15:51:19 +0300 Subject: [PATCH 02/10] fix(vehicle-fines): simplify daily sync queue and failed task handling --- .../vehicle_fine_record.py | 76 ++-------------- .../doctype/vehicle_sync_task/processor.py | 29 +----- .../csf_tz/doctype/vehicle_sync_task/queue.py | 91 +------------------ 3 files changed, 16 insertions(+), 180 deletions(-) diff --git a/csf_tz/csf_tz/doctype/vehicle_fine_record/vehicle_fine_record.py b/csf_tz/csf_tz/doctype/vehicle_fine_record/vehicle_fine_record.py index 60e3d737..d82565aa 100644 --- a/csf_tz/csf_tz/doctype/vehicle_fine_record/vehicle_fine_record.py +++ b/csf_tz/csf_tz/doctype/vehicle_fine_record/vehicle_fine_record.py @@ -18,7 +18,6 @@ send_authority_notification, ) import re -from time import sleep from frappe.utils import now_datetime from cryptography.hazmat.primitives import padding from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes @@ -144,77 +143,20 @@ def sync_vehicle_fines(number_plate): } payload = {"vehicle": number_plate} - max_retries = 3 - response = None - - for attempt in range(max_retries): - try: - if attempt > 0: - sleep(5 * attempt) - - 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() - break - - except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as exc: - if attempt < max_retries - 1: - continue - frappe.logger().warning( - f"[VehicleFine] Connection timeout for {number_plate} " - f"after {max_retries} retries" - ) - return { - "status": "retryable_error", - "message": str(exc), - "fine_list": [], - } - - except requests.exceptions.HTTPError: - status = response.status_code if response is not None else 0 - if status in (408,) or status >= 500: - if attempt < max_retries - 1: - continue - frappe.logger().warning( - f"[VehicleFine] HTTP {status} for {number_plate} " - f"after {max_retries} retries" - ) - return { - "status": "retryable_error", - "message": f"HTTP {status}", - "fine_list": [], - } - - frappe.log_error( - title="TPF API Error", - message=( - f"HTTP {status} for {number_plate}: " - f"{response.text[:500] if response is not None else ''}" - ), - ) - return { - "status": "error", - "message": f"HTTP {status}", - "fine_list": [], - } - - except requests.exceptions.RequestException as exc: - frappe.log_error(title="TPF API Error", message=str(exc)) + try: + response = requests.post(url, json=payload, headers=headers, timeout=30) + if response.status_code == 429: return { - "status": "error", - "message": str(exc), + "status": "rate_limited", + "message": f"TPF rate limited {number_plate}", "fine_list": [], } - - if response is None: + 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": "No response from TPF", + "message": str(exc), "fine_list": [], } diff --git a/csf_tz/csf_tz/doctype/vehicle_sync_task/processor.py b/csf_tz/csf_tz/doctype/vehicle_sync_task/processor.py index 6cf0960a..cf458012 100644 --- a/csf_tz/csf_tz/doctype/vehicle_sync_task/processor.py +++ b/csf_tz/csf_tz/doctype/vehicle_sync_task/processor.py @@ -46,14 +46,8 @@ def _acquire_rate_limit_slot(): return True -def _backoff_seconds(attempts): - exponent = max(attempts - 1, 0) - return queue.BASE_BACKOFF * (2 ** exponent) - - @frappe.whitelist() def run_vehicle_batch(): - started_at = time.monotonic() processed = 0 errors = 0 @@ -64,16 +58,8 @@ def run_vehicle_batch(): return {"status": "no_tasks", "message": "No pending vehicle sync tasks"} for task in tasks: - if (time.monotonic() - started_at) >= queue.TIME_BUDGET_SEC: - break - if not _acquire_rate_limit_slot(): - queue.schedule_next( - TASK_DOCTYPE, - task, - 60, - "TPF per-minute limit reached for this site", - ) + queue.mark_failed(TASK_DOCTYPE, task, "TPF per-minute limit reached for this site") continue result = sync_vehicle_fines(task["vehicle_no"]) @@ -84,21 +70,10 @@ def run_vehicle_batch(): processed += 1 continue - if status in {"rate_limited", "retryable_error"}: - current_attempts = frappe.db.get_value(TASK_DOCTYPE, task["name"], "attempts") or 0 - queue.schedule_next( - TASK_DOCTYPE, - task, - _backoff_seconds(current_attempts + 1), - result.get("message") or status, - ) - errors += 1 - continue - queue.mark_failed( TASK_DOCTYPE, task, - result.get("message") or "Unhandled sync error", + result.get("message") or status or "Unhandled sync error", ) errors += 1 diff --git a/csf_tz/csf_tz/doctype/vehicle_sync_task/queue.py b/csf_tz/csf_tz/doctype/vehicle_sync_task/queue.py index c724221d..f2ad85d2 100644 --- a/csf_tz/csf_tz/doctype/vehicle_sync_task/queue.py +++ b/csf_tz/csf_tz/doctype/vehicle_sync_task/queue.py @@ -1,43 +1,13 @@ -import secrets import frappe -# ------------ CONFIGURATION ------------ BATCH_SIZE = 1 -TIME_BUDGET_SEC = 50 -MAX_ATTEMPTS = 2 -BASE_BACKOFF = 300 -BACKOFF_JITTER = 0.2 -SUCCESS_INTERVAL_SECONDS = 60 * 60 * 2 +SUCCESS_INTERVAL_SECONDS = 60 * 60 * 24 MAX_CALLS_PER_MINUTE = 1 WORKER_ID = frappe.local.site -# ------------ INTERNAL HELPERS ------------ def _now(): return frappe.utils.now_datetime() -def _jitter(seconds): - # Generate cryptographically secure random jitter for backoff timing - # Range: -BACKOFF_JITTER to +BACKOFF_JITTER - random_factor = (secrets.randbelow(10000) / 10000.0) * 2 - 1 # -1 to 1 - jitter_factor = 1 + (random_factor * BACKOFF_JITTER) - return int(seconds * jitter_factor) - -def get_pending_cycle_delay(doctype): - try: - Task = frappe.qb.DocType(doctype) - pending = ( - frappe.qb.from_(Task) - .select(Task.name) - .where( - (Task.status == "Pending") & - ((Task.is_deleted.isnull()) | (Task.is_deleted == 0)) - ) - ).run() - return max(len(pending), 1) * 60 - except Exception: - return 60 - -# ------------ CORE QUEUE OPERATIONS ------------ def claim_batch(doctype, limit=BATCH_SIZE): try: now = _now() @@ -109,21 +79,18 @@ def mark_done(doctype, task): message=f"Error marking task {task.get('name')} as done in {doctype}: {str(e)}" ) -def mark_failed(doctype, task, err_msg, next_run_at=None, reset_attempts=False): +def mark_failed(doctype, task, err_msg): try: values = { "status": "Failed", + "attempts": 0, + "backoff_exp": 0, "last_error": err_msg[:1000], "last_run_at": _now(), "claimed_by": "", "claimed_at": None, - "next_run_at": next_run_at, + "next_run_at": _now(), } - if reset_attempts: - values.update({ - "attempts": 0, - "backoff_exp": 0, - }) frappe.db.set_value(doctype, task["name"], values) except Exception as e: frappe.log_error( @@ -131,54 +98,6 @@ def mark_failed(doctype, task, err_msg, next_run_at=None, reset_attempts=False): message=f"Error marking task {task.get('name')} as failed in {doctype}: {str(e)}" ) -def bump_attempts(doctype, task): - try: - current = frappe.db.get_value( - doctype, task["name"], ["attempts", "backoff_exp"], as_dict=True - ) - attempts = (current.attempts or 0) + 1 - backoff_exp = min((current.backoff_exp or 0) + 1, 6) - frappe.db.set_value(doctype, task["name"], { - "attempts": attempts, - "backoff_exp": backoff_exp, - "last_run_at": _now() - }) - return attempts, backoff_exp - except Exception as e: - frappe.log_error( - title="Queue Bump Attempts Failed", - message=f"Error bumping attempts for task {task.get('name')} in {doctype}: {str(e)}" - ) - return 1, 1 # Return default values - -def schedule_next(doctype, task, backoff_seconds, error_msg=""): - try: - attempts, _ = bump_attempts(doctype, task) - cycle_delay = get_pending_cycle_delay(doctype) - next_delay = max(backoff_seconds, cycle_delay) - next_run = frappe.utils.add_to_date(_now(), seconds=_jitter(next_delay)) - if attempts >= MAX_ATTEMPTS: - mark_failed( - doctype, - task, - error_msg or "Max attempts exceeded", - next_run_at=next_run, - reset_attempts=True, - ) - return - frappe.db.set_value(doctype, task["name"], { - "status": "Pending", - "claimed_by": "", - "claimed_at": None, - "next_run_at": next_run, - "last_error": error_msg[:500] if error_msg else "", - }) - except Exception as e: - frappe.log_error( - title="Queue Schedule Next Failed", - message=f"Error scheduling next run for task {task.get('name')} in {doctype}: {str(e)}" - ) - def reset_stuck_tasks(doctype, timeout_minutes=10): try: timeout_time = frappe.utils.add_to_date(_now(), minutes=-timeout_minutes) From 797a6e05403f9d1f9134c34db8520f957d54d481 Mon Sep 17 00:00:00 2001 From: aakvatech <35020381+aakvatech@users.noreply.github.com> Date: Sat, 22 Aug 2026 23:47:09 +0000 Subject: [PATCH 03/10] chore: add Frappe maintenance workflow --- .github/workflows/modernize-frappe.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/workflows/modernize-frappe.yml diff --git a/.github/workflows/modernize-frappe.yml b/.github/workflows/modernize-frappe.yml new file mode 100644 index 00000000..02b15b99 --- /dev/null +++ b/.github/workflows/modernize-frappe.yml @@ -0,0 +1,12 @@ +name: Modernize Frappe Packaging + +on: + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + modernize: + uses: Aakvatech-Limited/frappe-maintenance/.github/workflows/modernize-frappe-reusable.yml@main \ No newline at end of file From f96905070af159c0dcff75751fed2dfb3f94b119 Mon Sep 17 00:00:00 2001 From: aakvatech <35020381+aakvatech@users.noreply.github.com> Date: Sun, 23 Aug 2026 07:07:36 +0300 Subject: [PATCH 04/10] chore: Add CSF TZ specification document Added comprehensive specification document for CSF TZ application, outlining goals, application model, functional domains, extension model, integration model, and development rules. --- SPEC.md | 681 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 681 insertions(+) create mode 100644 SPEC.md diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 00000000..97763d4b --- /dev/null +++ b/SPEC.md @@ -0,0 +1,681 @@ +# CSF TZ Spec + +CSF TZ provides country-specific functionality for Tanzania on top of Frappe and ERPNext. + +The application extends standard ERPNext behaviour rather than replacing ERPNext. Tanzanian statutory requirements, integrations, local business rules, accounting extensions, payroll extensions, banking functionality, and other reusable Tanzania-specific functionality belong in CSF TZ when they cannot reasonably be implemented through standard ERPNext configuration. + +The application should preserve ERPNext conventions and upgradeability wherever possible. + +## Goals + +* Provide reusable Tanzania-specific functionality for ERPNext. +* Support Tanzanian statutory, taxation, fiscal, payroll, banking, regulatory, and business requirements. +* Extend standard ERPNext transactions without unnecessarily duplicating ERPNext functionality. +* Keep integrations with Tanzanian authorities, fiscal systems, banks, and payment providers isolated behind clear interfaces. +* Keep custom business logic deterministic, auditable, and maintainable. +* Make upgrades between supported Frappe and ERPNext versions predictable. +* Keep site-specific or customer-specific functionality outside the common CSF TZ application unless it is genuinely reusable. +* Prefer standard Frappe extension mechanisms over modifications to Frappe or ERPNext source code. + +## Application Model + +CSF TZ is an extension application running inside a Frappe/ERPNext site. + +The important architectural areas are: + +* **DocTypes** own persistent CSF TZ business entities and configuration. +* **Hooks** connect CSF TZ behaviour to Frappe and ERPNext lifecycle events. +* **Overrides** replace standard document controller behaviour only when extension through hooks is insufficient. +* **Client scripts and bundled JavaScript** extend standard Desk behaviour. +* **APIs** expose explicitly supported server-side operations and integrations. +* **Integrations** communicate with banks, payment providers, fiscal systems, government authorities, and other external services. +* **Scheduled jobs** perform recurring reconciliation, synchronization, notification, regulatory, and maintenance work. +* **Patches** perform controlled schema, metadata, configuration, and data migrations. +* **Reports** expose business, accounting, operational, and statutory information. +* **Workspaces** provide user-facing entry points into Tanzania-specific functionality. + +Business rules should live as close as possible to the domain that owns them. + +Do not place substantial business logic in `hooks.py`. Hooks should primarily map framework events to appropriately grouped implementation functions. + +## Functional Domains + +The application may contain functionality covering areas including: + +* Tanzania tax and fiscal compliance +* VFD/EFD integrations +* Sales and receivables extensions +* Purchasing and payables extensions +* Withholding taxes +* Banking and reconciliation +* Payroll and employee-related localization +* Inventory and stock controls +* Importation and landed-cost processes +* Payment provider integrations +* Tanzanian geographic and regulatory data +* Vehicle and authority integrations +* Education-related extensions where required by supported deployments +* Operational utilities and reusable ERPNext enhancements + +A feature does not belong in CSF TZ merely because it was developed for a Tanzanian customer. + +New functionality should normally satisfy at least one of these conditions: + +1. It implements a Tanzanian statutory or regulatory requirement. +2. It integrates with a Tanzania-specific service or institution. +3. It represents a business requirement broadly reusable by CSF TZ installations. +4. It provides infrastructure required by another legitimate CSF TZ feature. + +Customer-specific workflows, reports, integrations, fields, naming conventions, or business rules should normally live in a customer-specific application. + +## Extension Model + +Use Frappe's standard extension mechanisms in this order of preference: + +1. Configuration and standard ERPNext functionality +2. Custom fields and property setters managed by the application +3. Document events +4. Client-side DocType extensions +5. Whitelisted methods and APIs +6. Scheduler events +7. Controller extension or override where required + +Direct modification of Frappe or ERPNext source code is not part of the CSF TZ architecture. + +### Document Events + +Use `doc_events` when logic belongs to a standard Frappe or ERPNext document lifecycle. + +Event handlers should: + +* receive the document and event using standard Frappe conventions; +* perform one clearly identifiable business responsibility; +* avoid duplicating ERPNext controller logic; +* avoid committing or rolling back database transactions independently unless specifically required; +* raise meaningful validation errors when a transaction cannot proceed; +* remain safe when called during normal framework lifecycle processing. + +Large handlers should delegate to domain-specific modules. + +### Controller Overrides + +Controller overrides are a high-impact extension mechanism. + +Use `override_doctype_class` only when the required behaviour cannot safely be implemented through events or supported extension points. + +An override should inherit from the corresponding upstream controller wherever practical. + +When overriding a standard controller: + +* preserve upstream behaviour unless the specification explicitly changes it; +* call the superclass implementation where appropriate; +* document why an override is required; +* consider upstream changes during every major ERPNext upgrade; +* keep the override narrowly scoped. + +Controller overrides should not become independent copies of ERPNext controllers. + +## Client-Side Extensions + +JavaScript attached through `doctype_js`, `doctype_list_js`, application bundles, or other Frappe hooks should enhance the standard UI rather than reproduce server-side business logic. + +Client-side code may: + +* improve data entry; +* provide validations for user convenience; +* calculate previews; +* add buttons and actions; +* call approved server methods; +* adapt standard forms to CSF TZ workflows. + +Business-critical validation must also exist server-side. + +Never rely only on browser-side validation for accounting, compliance, authorization, statutory, or data-integrity controls. + +## API Model + +Server APIs should be grouped by domain rather than accumulating unrelated behaviour in large generic modules. + +New APIs should preferably live in a dedicated package or domain module. + +Whitelisted methods must explicitly consider: + +* authentication; +* authorization; +* input validation; +* document permissions; +* idempotency; +* transaction boundaries; +* external-service failures; +* logging; +* exposure of confidential data. + +Do not make a method guest-accessible unless anonymous access is a genuine integration requirement. + +Public or integration-facing APIs should have stable request and response contracts. + +Breaking API changes should be treated as compatibility changes. + +## Integration Model + +External systems should be treated as unreliable network dependencies. + +Integrations may include: + +* VFD/EFD providers; +* TRA-related services; +* banks; +* payment gateways; +* SFTP endpoints; +* vehicle and licensing authorities; +* regulatory services; +* other approved third-party systems. + +Integration code should separate: + +1. configuration; +2. authentication; +3. request construction; +4. transport; +5. response parsing; +6. business processing; +7. retry/reconciliation behaviour; +8. logging. + +Provider-specific behaviour should remain inside provider-specific modules wherever possible. + +Do not spread provider-specific conditionals throughout Sales Invoice, Payment Entry, Payroll Entry, or other unrelated domains. + +### Credentials + +Credentials, tokens, private keys, API secrets, passwords, and similar material must never be hard-coded in source files. + +Use Frappe configuration or password fields appropriate to the sensitivity of the credential. + +Logs must not expose credentials or sensitive authentication material. + +### External Calls + +External calls performed during document submission should be used carefully. + +Where an external operation can safely occur asynchronously, prefer a background or reconciliation process rather than making the external provider's availability a prerequisite for completing an ERPNext transaction. + +Where synchronous communication is legally or operationally required, failure behaviour must be explicit. + +## VFD and Fiscal Processing + +Fiscal processing is compliance-sensitive functionality. + +VFD functionality should maintain a clear distinction between: + +* ERPNext transaction state; +* fiscal submission state; +* provider request state; +* provider response state; +* retries; +* successful fiscalization; +* failure; +* cancellation or reversal. + +A Sales Invoice being submitted in ERPNext does not by itself prove successful fiscal submission. + +Fiscal operations should preserve enough information to determine: + +* what was submitted; +* when it was submitted; +* which provider was used; +* what response was received; +* whether the operation succeeded; +* whether retry is required; +* whether subsequent cancellation or adjustment occurred. + +Provider communication and fiscal business rules should be kept separate wherever practical. + +## Accounting Integrity + +Any CSF TZ functionality that creates or alters accounting consequences must respect ERPNext's accounting model. + +Examples include: + +* withholding tax; +* bank charges; +* exchange differences; +* landed costs; +* import tracking; +* additional salary accounting; +* payment integrations. + +Accounting logic must: + +* use submitted documents where ERPNext requires submission; +* preserve company and currency context; +* preserve debit/credit integrity; +* respect cancellation; +* avoid orphan accounting references; +* avoid duplicate GL consequences; +* remain reproducible from the underlying business transaction. + +Do not update accounting tables directly when an ERPNext document or accounting API should own the transaction. + +## Scheduled Jobs + +Recurring processing is registered through Frappe scheduler hooks. + +Scheduled jobs are appropriate for work including: + +* synchronization; +* reconciliation; +* retries; +* token renewal; +* regulatory data refreshes; +* notifications; +* queue seeding; +* periodic cleanup; +* maintenance; +* delayed transaction processing. + +Scheduler methods must be safe to execute repeatedly. + +Where possible they should be idempotent: running the same job again should not create duplicate financial, regulatory, or operational consequences. + +A scheduled job should not assume that the previous invocation completed successfully. + +Jobs processing potentially large datasets should operate in bounded batches. + +Do not load an unbounded number of documents into memory. + +Failures affecting one record should not unnecessarily prevent all other independent records from processing. + +## Background Work + +Operations involving significant network communication, file processing, large datasets, or long-running calculations should normally use Frappe background jobs. + +Queue work when synchronous execution would: + +* make a user transaction unnecessarily slow; +* risk HTTP timeouts; +* depend on unreliable third-party services; +* process large numbers of records; +* perform retryable work. + +Background jobs must receive enough identifiers to reload authoritative state rather than depending on stale in-memory documents. + +## Configuration + +CSF TZ configuration should use Frappe DocTypes or supported site configuration. + +Configuration belongs at the narrowest appropriate scope: + +* system-wide; +* company; +* provider; +* bank; +* fiscal device; +* user; +* transaction. + +Do not introduce global settings for configuration that legitimately varies by Company. + +Configuration fields should have clear defaults and should fail explicitly when mandatory configuration is missing. + +Settings DocTypes should be preferred over scattered custom fields when a feature has substantial configuration of its own. + +## Custom Fields and Property Setters + +CSF TZ may extend standard DocTypes using Custom Fields and Property Setters. + +Application-owned metadata must be reproducible from source. + +Do not rely on production sites containing manually created Custom Fields that are absent from application setup or migration logic. + +Field creation must be idempotent. + +Before changing or deleting existing fields, account for installations that may already contain data. + +Fieldnames should be stable after release wherever possible. + +## Data Model + +A CSF TZ DocType should exist when a concept has an independent lifecycle, configuration role, transactional role, integration role, or audit requirement. + +Do not create a new DocType merely to avoid using an appropriate ERPNext model. + +Links to ERPNext documents should use proper Link or Dynamic Link fields wherever possible. + +Child tables should be used for records that exist only as part of their parent document. + +Integration logs should retain identifiers required to trace the corresponding ERPNext transaction and external transaction. + +## Migrations and Patches + +Database and metadata migrations are part of the application contract. + +Use `patches.txt` for one-time migration work. + +Use install or migrate hooks for operations that genuinely need to remain repeatable. + +A patch should: + +* be safe for existing production data; +* be deterministic; +* preferably be idempotent; +* avoid assumptions about optional modules or data; +* handle already-migrated records safely; +* avoid silently destroying business data; +* complete in reasonable bounded operations. + +Do not rewrite the behaviour of a previously released patch after installations may already have executed it. + +Create a new patch for subsequent corrections. + +Destructive migrations require particular care and should be explicitly documented. + +## Installation and Migration Hooks + +`after_install` prepares newly installed sites. + +`after_migrate` may enforce application-owned metadata or configuration that must remain synchronized. + +Do not put expensive recurring business processing in migration hooks. + +Migration hooks must not rely on external services being available. + +A failed external provider must not prevent a normal `bench migrate` unless that provider is fundamentally required to make the schema valid. + +## Version Compatibility + +Each maintained branch must explicitly declare the supported Frappe and ERPNext major versions in `pyproject.toml`. + +A branch should target a defined framework generation. + +Do not make one branch silently support incompatible framework majors through extensive version-condition logic. + +Compatibility changes involving: + +* controller APIs; +* DocType fields; +* hooks; +* accounting behaviour; +* scheduler behaviour; +* framework APIs; +* JavaScript APIs + +must be checked against the targeted Frappe and ERPNext versions. + +Upstream APIs should not be assumed stable across major releases. + +## Modules + +Functional modules should group related business behaviour. + +Current module boundaries may include areas such as: + +* CSF TZ +* Purchase and Stock Management +* Sales and Marketing +* Meal Count +* Stanbic +* KCB +* VFD Providers +* VFD Settings + +New modules should only be introduced when they represent a coherent functional domain. + +Do not create a module for every small feature. + +## Public Surfaces + +The important public surfaces of CSF TZ include: + +* DocTypes +* reports +* workspaces +* whitelisted methods +* hooks into ERPNext documents +* scheduled jobs +* integrations consumed by external systems +* configuration DocTypes +* print and Jinja helpers where explicitly exposed + +Changes to these surfaces may affect installed sites even when no Python import API changes. + +Treat fieldnames, DocType names, integration contracts, and externally consumed endpoints as compatibility-sensitive. + +## Permissions and Authorization + +Server-side permission checks remain authoritative. + +Creating a custom form button does not grant permission to perform the corresponding operation. + +APIs that read or modify ERPNext documents must respect Frappe permissions unless the integration explicitly requires privileged system processing. + +Any deliberate permission bypass must: + +* have a documented reason; +* be scoped narrowly; +* validate the caller or integration; +* avoid accepting arbitrary document access from untrusted input. + +## Security Model + +CSF TZ runs with the privileges of the Frappe application process and has access to site data. + +Application code therefore belongs inside the site's trusted computing boundary. + +Assume that server-side CSF TZ code can potentially access: + +* accounting information; +* customer and supplier records; +* employee information; +* payroll information; +* integration credentials; +* regulatory records; +* uploaded files. + +From this: + +* validate untrusted input; +* avoid arbitrary SQL construction; +* avoid arbitrary filesystem access; +* do not execute user-supplied code; +* protect integration credentials; +* restrict guest endpoints; +* validate uploaded files; +* avoid logging unnecessary personal or financial information. + +External responses must be treated as untrusted input. + +## SQL and Database Access + +Prefer Frappe ORM, Query Builder, and standard document APIs. + +Direct SQL is acceptable when there is a clear technical reason such as reporting, performance, migration, or functionality not reasonably expressible through supported APIs. + +Direct SQL must: + +* parameterize dynamic values; +* respect `docstatus` where relevant; +* consider Company boundaries; +* consider permissions when used in user-facing operations; +* avoid direct writes to framework-owned accounting or stock ledgers unless explicitly required by framework architecture. + +Database writes should normally occur through document APIs. + +## Error Handling + +Errors shown to users should explain the business problem and, where possible, the corrective action. + +Do not expose raw provider credentials, tokens, SQL, or internal stack details through user-facing errors. + +Integration errors should preserve enough technical information in appropriate logs for diagnosis. + +Retryable errors should be distinguishable from permanent validation failures. + +## Logging and Auditability + +Compliance-sensitive and integration-sensitive operations should be traceable. + +Where appropriate, preserve: + +* source document; +* external reference; +* timestamp; +* provider; +* operation; +* result; +* error; +* retry information. + +Do not use unrestricted console output as the primary production logging mechanism. + +Use Frappe logging, integration log DocTypes, or purpose-built audit records. + +## Cancellation and Reversal + +Any feature that creates downstream records must explicitly consider cancellation. + +When a source ERPNext document is cancelled, CSF TZ must determine whether downstream records should: + +* be cancelled; +* be reversed; +* be unlinked; +* remain as immutable audit evidence; +* trigger an external cancellation; +* require manual intervention. + +Cancellation logic must not silently leave active financial or compliance consequences behind. + +## Idempotency + +Operations that may be retried must protect against duplicate execution. + +This particularly applies to: + +* scheduled jobs; +* webhook/API callbacks; +* fiscal submissions; +* payment processing; +* bank reconciliation; +* journal creation; +* background jobs; +* authority synchronization. + +Where an external system provides a transaction identifier, persist and use it for duplicate detection when practical. + +## Performance + +Code running in transaction hooks must remain bounded. + +Avoid: + +* queries inside large loops; +* loading complete tables unnecessarily; +* performing expensive external calls repeatedly; +* processing entire transaction histories during ordinary document validation; +* synchronous bulk processing where a background job is appropriate. + +Use batching for high-volume scheduled operations. + +Performance optimizations must not compromise accounting or compliance correctness. + +## Testing + +Business-critical features should have automated tests. + +Priority areas include: + +* accounting consequences; +* taxation; +* VFD/fiscalization; +* payroll calculations; +* document submission and cancellation; +* integration request/response handling; +* migration patches; +* scheduled job idempotency; +* duplicate prevention. + +Tests should exercise business outcomes rather than merely whether a function executes. + +Where an external provider is involved, provider calls should normally be mocked in automated tests. + +Tests must not depend on live banking, fiscal, payment, or authority services. + +## Development Rules + +When changing existing functionality: + +1. Identify the owning domain. +2. Check existing hooks and overrides before adding another extension point. +3. Reuse ERPNext behaviour where possible. +4. Preserve submission and cancellation semantics. +5. Consider multi-company behaviour. +6. Consider permissions. +7. Consider migration requirements. +8. Consider scheduled or asynchronous execution. +9. Consider integration retry and duplicate behaviour. +10. Add or update tests for material business logic. + +Avoid adding unrelated convenience functions to `custom_api.py` or other already broad modules. + +New substantial features should use dedicated domain modules. + +## Naming + +Use names that describe the business concept rather than a customer or temporary implementation. + +Provider-specific functionality may use the provider name where the provider itself defines the integration. + +Avoid abbreviations unless they are established domain terminology such as VAT, VFD, TRA, PAYE, or NSSF. + +Do not encode one customer's name into reusable CSF TZ business logic. + +## Source of Truth + +For application behaviour: + +* Python source is the source of truth for server-side logic. +* JavaScript source is the source of truth for client-side behaviour. +* DocType JSON is the source of truth for application-owned DocType metadata. +* patch modules and migration hooks are the source of truth for migrations. +* `hooks.py` is the source of truth for registered framework extensions and schedules. +* `pyproject.toml` is the source of truth for Python and Frappe/ERPNext compatibility declarations. + +Production-site manual customizations are not substitutes for source-controlled application behaviour. + +## Contribution Boundary + +Before adding functionality to CSF TZ, ask: + +**Is this Tanzania-specific or reusable across a substantial number of CSF TZ installations?** + +If no, it probably belongs in: + +* standard ERPNext configuration; +* another reusable application; +* an industry-specific application; or +* a customer-specific application. + +CSF TZ should not become a collection of unrelated customer customizations. + +## Documentation Map + +Documentation should progressively cover: + +* Architecture +* Installation and upgrade +* Tanzanian statutory configuration +* VFD configuration and providers +* Tax and withholding configuration +* Banking integrations +* Payroll localization +* Purchase and import processes +* Scheduled jobs +* API and integration contracts +* Migration and compatibility guidance +* Troubleshooting + +`SPEC.md` defines architectural and development rules. + +`README.md` should remain the high-level introduction and installation entry point. + +Detailed operational and developer documentation should live under `docs/` as the repository grows. From 2fe9e73dd14b1ce6c9b0553e8c751ce78e42b8b3 Mon Sep 17 00:00:00 2001 From: aakvatech <35020381+aakvatech@users.noreply.github.com> Date: Sun, 23 Aug 2026 12:58:55 +0300 Subject: [PATCH 05/10] fix: avoid monkey patch loading without site context (cherry picked from commit 3ca3fc525d89f42ff1a2bc2380f5f979cb0be7d7) --- csf_tz/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/csf_tz/__init__.py b/csf_tz/__init__.py index 95acbb0d..3f1c0388 100755 --- a/csf_tz/__init__.py +++ b/csf_tz/__init__.py @@ -19,11 +19,17 @@ def load_monkey_patches(): if patches_loaded: return - patches_loaded = True + # Bench-level commands such as asset builds can run without a site context. + # Avoid querying installed apps in that case, because it attempts a database + # connection and fails with "site must be fully initialized, db_name missing". + if not getattr(frappe.local, "site", None): + return if app_name not in frappe.get_installed_apps(): return + patches_loaded = True + for module_name in os.listdir(frappe.get_app_path(app_name, "monkey_patches")): if not module_name.endswith(".py") or module_name == "__init__.py": continue From 1c6b4f1176cddeae481e29c702c05a48f6540925 Mon Sep 17 00:00:00 2001 From: aakvatech <35020381+aakvatech@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:05:34 +0300 Subject: [PATCH 06/10] fix: use dedicated group for multicurrency bank charges (cherry picked from commit a25700f178558cae8fbf163874ba7eff0e1603f3) --- csf_tz/setup_data/accounts.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/csf_tz/setup_data/accounts.json b/csf_tz/setup_data/accounts.json index 1e542ea8..41c998cd 100644 --- a/csf_tz/setup_data/accounts.json +++ b/csf_tz/setup_data/accounts.json @@ -31,7 +31,7 @@ }, { "doctype": "Account", - "account_name": "Bank Charges", + "account_name": "Bank Charges Accounts", "company": "{company}", "account_currency": "TZS", "parent_account": "Financial Charges - {abbr}", @@ -147,7 +147,7 @@ "account_name": "Bank Charges TZS", "company": "{company}", "account_currency": "TZS", - "parent_account": "Bank Charges - {abbr}", + "parent_account": "Bank Charges Accounts - {abbr}", "root_type": "Expense", "report_type": "Profit and Loss" }, From c9fb01110b1b64c99e34b51da367c90af7de049f Mon Sep 17 00:00:00 2001 From: MariamMabele Date: Mon, 24 Aug 2026 11:47:36 +0300 Subject: [PATCH 07/10] fix(settings): remove duplicate authority notification field migration --- csf_tz/hooks.py | 1 - .../authority_notification_settings_fields.py | 106 ------------------ 2 files changed, 107 deletions(-) delete mode 100644 csf_tz/utils/authority_notification_settings_fields.py diff --git a/csf_tz/hooks.py b/csf_tz/hooks.py index b91bc488..9e0725d3 100755 --- a/csf_tz/hooks.py +++ b/csf_tz/hooks.py @@ -122,7 +122,6 @@ after_migrate = [ "csf_tz.utils.create_custom_fields.execute", - "csf_tz.utils.authority_notification_settings_fields.execute", "csf_tz.utils.create_property_setter.execute", "csf_tz.patches.custom_fields.vfd_providers_updated_custom_fields.execute", "csf_tz.patches.migrate_vfd_providers_to_csf_tz.execute", diff --git a/csf_tz/utils/authority_notification_settings_fields.py b/csf_tz/utils/authority_notification_settings_fields.py deleted file mode 100644 index 49420c03..00000000 --- a/csf_tz/utils/authority_notification_settings_fields.py +++ /dev/null @@ -1,106 +0,0 @@ -from frappe.custom.doctype.custom_field.custom_field import create_custom_fields - - -def execute(): - fields = { - "CSF TZ Settings": [ - { - "fieldname": "authority_notification_section", - "fieldtype": "Section Break", - "label": "Authority Notifications", - "insert_after": "tz_regions_populated", - }, - { - "fieldname": "enable_latra_license_notifications", - "fieldtype": "Check", - "label": "Enable LATRA License Notifications", - "default": "0", - "insert_after": "authority_notification_section", - }, - { - "fieldname": "enable_latra_offence_notifications", - "fieldtype": "Check", - "label": "Enable LATRA Offence Notifications", - "default": "0", - "insert_after": "enable_latra_license_notifications", - }, - { - "fieldname": "enable_tira_notifications", - "fieldtype": "Check", - "label": "Enable TIRA Notifications", - "default": "0", - "insert_after": "enable_latra_offence_notifications", - }, - { - "fieldname": "enable_vehicle_fine_notifications", - "fieldtype": "Check", - "label": "Enable Vehicle Fine Notifications", - "default": "0", - "insert_after": "enable_tira_notifications", - }, - { - "fieldname": "column_break_authority_notification", - "fieldtype": "Column Break", - "insert_after": "enable_vehicle_fine_notifications", - }, - { - "fieldname": "latra_license_notify_before_days", - "fieldtype": "Int", - "label": "LATRA License Notify Before Days", - "default": "7", - "depends_on": "eval:doc.enable_latra_license_notifications", - "mandatory_depends_on": "eval:doc.enable_latra_license_notifications", - "insert_after": "column_break_authority_notification", - }, - { - "fieldname": "latra_offence_notify_on_new", - "fieldtype": "Check", - "label": "LATRA Offence Notify On New", - "default": "1", - "depends_on": "eval:doc.enable_latra_offence_notifications", - "insert_after": "latra_license_notify_before_days", - }, - { - "fieldname": "latra_offence_notify_on_status_change", - "fieldtype": "Check", - "label": "LATRA Offence Notify On Status Change", - "default": "0", - "depends_on": "eval:doc.enable_latra_offence_notifications", - "insert_after": "latra_offence_notify_on_new", - }, - { - "fieldname": "tira_notify_before_days", - "fieldtype": "Int", - "label": "TIRA Notify Before Days", - "default": "7", - "depends_on": "eval:doc.enable_tira_notifications", - "mandatory_depends_on": "eval:doc.enable_tira_notifications", - "insert_after": "latra_offence_notify_on_status_change", - }, - { - "fieldname": "vehicle_fine_notify_on_new", - "fieldtype": "Check", - "label": "Vehicle Fine Notify On New", - "default": "1", - "depends_on": "eval:doc.enable_vehicle_fine_notifications", - "insert_after": "tira_notify_before_days", - }, - { - "fieldname": "vehicle_fine_notify_on_status_change", - "fieldtype": "Check", - "label": "Vehicle Fine Notify On Status Change", - "default": "0", - "depends_on": "eval:doc.enable_vehicle_fine_notifications", - "insert_after": "vehicle_fine_notify_on_new", - }, - { - "fieldname": "authority_notification_roles", - "fieldtype": "Table", - "label": "Authority Notification Roles", - "options": "Authority Notification Role", - "insert_after": "vehicle_fine_notify_on_status_change", - }, - ] - } - - create_custom_fields(fields, update=True) From 8250ef1487b895c47eca3a835e369ba59a942aaa Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Wed, 26 Aug 2026 17:40:42 +0300 Subject: [PATCH 08/10] ci: standardise pre-commit, lint and release tooling Add a pre-push hook that runs pre-commit over the whole repository, so a push is rejected when any file is unclean. The commit hook keeps checking staged files only. Replace the divergent per-app setups with one shared toolchain: ruff for lint and format, the self-contained frappe-semgrep hook, and commitlint for commit messages. Anchor the exclude regex so .github/ is no longer skipped by an unanchored .git pattern. Add the pre-commit and semantic-commits workflows, and scripts/setup-git-hooks.sh for a one-command developer bootstrap. Remove ci.yml. Building a bench and migrating a throwaway site cost five to eight minutes per pull request and went red for upstream and runner problems unrelated to the change. Frappe tests continue to run locally. Remove release.yml and .releaserc.json. tag-and-promote-from-pr-label.yml is now the only owner of tags, releases and promotion; running semantic-release alongside it made both tag the same version at different commits. --- .github/workflows/linter.yml | 34 ++++--------- .github/workflows/modernize-frappe.yml | 2 +- .github/workflows/pre-commit.yml | 31 ++++++++++++ .github/workflows/release.yml | 34 ------------- .github/workflows/semantic-commits.yml | 1 + .pre-commit-config.yaml | 69 +++++++++++++++++++++----- .releaserc.json | 22 -------- commitlint.config.js | 1 + pyproject.toml | 5 ++ scripts/setup-git-hooks.sh | 24 +++++++++ 10 files changed, 129 insertions(+), 94 deletions(-) create mode 100644 .github/workflows/pre-commit.yml delete mode 100644 .github/workflows/release.yml delete mode 100644 .releaserc.json create mode 100755 scripts/setup-git-hooks.sh diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 4282edc8..50b5ccd7 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -1,4 +1,3 @@ - name: Linters on: @@ -9,35 +8,23 @@ permissions: contents: read concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: linters-csf_tz-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: - linter: - name: 'Frappe Linter' + semgrep: + name: Frappe Linter runs-on: ubuntu-latest - if: github.event_name == 'pull_request' steps: - uses: actions/checkout@v4 with: fetch-depth: 0 + - uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: "3.11" cache: pip - - name: Install pre-commit - run: pip install pre-commit - # Semgrep is skipped here and run as a full-repo scan in the next steps - - name: Run pre-commit on changed files - env: - SKIP: frappe-semgrep-rules - run: | - pre-commit run \ - --show-diff-on-failure \ - --color=always \ - --from-ref origin/${{ github.base_ref }} \ - --to-ref HEAD - name: Download Semgrep rules run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules @@ -45,7 +32,7 @@ jobs: - name: Install Semgrep run: pip install semgrep - # Blocking: real bugs / security issues only + # Blocking: real bugs and security issues only - name: Run Semgrep rules run: | semgrep scan --config ./frappe-semgrep-rules/rules \ @@ -61,15 +48,15 @@ jobs: --severity=WARNING csf_tz || true deps-vulnerable-check: - name: 'Vulnerable Dependency Check' + name: Vulnerable Dependency Check runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: - python-version: '3.10' - - - uses: actions/checkout@v4 + python-version: "3.11" - name: Cache pip uses: actions/cache@v4 @@ -83,5 +70,4 @@ jobs: - name: Install and run pip-audit run: | pip install pip-audit - cd ${GITHUB_WORKSPACE} pip-audit --desc on . diff --git a/.github/workflows/modernize-frappe.yml b/.github/workflows/modernize-frappe.yml index 02b15b99..e6e37596 100644 --- a/.github/workflows/modernize-frappe.yml +++ b/.github/workflows/modernize-frappe.yml @@ -9,4 +9,4 @@ permissions: jobs: modernize: - uses: Aakvatech-Limited/frappe-maintenance/.github/workflows/modernize-frappe-reusable.yml@main \ No newline at end of file + uses: Aakvatech-Limited/frappe-maintenance/.github/workflows/modernize-frappe-reusable.yml@main diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 00000000..4cc2ffa1 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,31 @@ +name: Pre-commit + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: precommit-csf_tz-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + pre-commit: + name: pre-commit + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 3bc54c76..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Release - -on: - workflow_dispatch: - -permissions: - contents: write - issues: write - pull-requests: write - -concurrency: - group: release-${{ github.ref }} - cancel-in-progress: true - -jobs: - release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - persist-credentials: false - - - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Install semantic-release - run: npm install --no-save semantic-release @semantic-release/changelog @semantic-release/exec @semantic-release/git @semantic-release/github conventional-changelog-conventionalcommits - - - name: Run semantic-release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: npx semantic-release diff --git a/.github/workflows/semantic-commits.yml b/.github/workflows/semantic-commits.yml index 1370dfe8..7ec0d66a 100644 --- a/.github/workflows/semantic-commits.yml +++ b/.github/workflows/semantic-commits.yml @@ -14,6 +14,7 @@ jobs: commitlint: name: Check Commit Messages runs-on: ubuntu-latest + steps: - uses: actions/checkout@v4 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 12126078..f5f05285 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ -exclude: 'node_modules|.git|frappe-semgrep-rules' +exclude: "^(node_modules/|frappe-semgrep-rules/|[.]vscode/|.*/node_modules/|csf_tz/public/dist/|csf_tz/public/css/)" default_stages: [pre-commit] -default_install_hook_types: [pre-commit, commit-msg] +default_install_hook_types: [pre-commit, commit-msg, pre-push] fail_fast: false repos: @@ -8,30 +8,64 @@ repos: rev: v5.0.0 hooks: - id: trailing-whitespace - files: "csf_tz.*" - exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" + exclude: '\.(json|txt|csv|md|svg)$' - id: end-of-file-fixer - exclude: ".*json$" + exclude: '\.(json|csv|svg)$' - id: check-merge-conflict - id: check-ast - id: check-json - id: check-toml - id: check-yaml - id: debug-statements + - id: no-commit-to-branch + args: + - --branch + - main + - --branch + - master + - --branch + - production + - --branch + - version-14 + - --branch + - version-15 + - --branch + - version-16 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.0 + rev: v0.13.2 hooks: - id: ruff name: "Run ruff import sorter" args: ["--select=I", "--fix"] - + files: '^csf_tz/.*\.py$' - id: ruff name: "Run ruff linter" args: ["--fix"] - + files: '^csf_tz/.*\.py$' - id: ruff-format name: "Run ruff formatter" + files: '^csf_tz/.*\.py$' + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.7.1 + hooks: + - id: prettier + name: "Run prettier on frontend sources" + types_or: [javascript, vue, css, scss] + files: '^(csf_tz|frontend)/.*\.(js|vue|css|scss)$' + exclude: | + (?x)^( + .*/public/dist/.*| + .*/public/frontend/.*| + .*/public/node_modules/.*| + .*\.bundle\.js| + .*\.min\.js| + frontend/dist/.*| + cypress/.*| + .*node_modules.*| + .*boilerplate.* + )$ - repo: local hooks: @@ -41,18 +75,27 @@ repos: language: python additional_dependencies: ["semgrep"] types: [python] - files: "^csf_tz/.*\\.py$" + files: '^csf_tz/.*\.py$' pass_filenames: true require_serial: true + - id: full-repository-check + name: "Full repository check before push" + entry: bash -c 'if command -v pre-commit >/dev/null 2>&1; then exec pre-commit run --all-files --hook-stage pre-commit --show-diff-on-failure --color=always; else exec python3 -m pre_commit run --all-files --hook-stage pre-commit --show-diff-on-failure --color=always; fi' + language: system + stages: [pre-push] + pass_filenames: false + always_run: true + verbose: true + - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook rev: v9.22.0 hooks: - id: commitlint stages: [commit-msg] - additional_dependencies: ['conventional-changelog-conventionalcommits'] + additional_dependencies: ["@commitlint/config-conventional"] ci: - autoupdate_schedule: weekly - skip: [frappe-semgrep-rules] - submodules: false + autoupdate_schedule: weekly + skip: [frappe-semgrep-rules, full-repository-check] + submodules: false diff --git a/.releaserc.json b/.releaserc.json deleted file mode 100644 index e40d8fe7..00000000 --- a/.releaserc.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "branches": ["version-15"], - "plugins": [ - ["@semantic-release/commit-analyzer", { - "preset": "conventionalcommits" - }], - ["@semantic-release/release-notes-generator", { - "preset": "conventionalcommits" - }], - ["@semantic-release/changelog", { - "changelogFile": "CHANGELOG.md" - }], - ["@semantic-release/exec", { - "prepareCmd": "sed -i 's/^__version__ = .*/__version__ = \"${nextRelease.version}\"/' csf_tz/__init__.py" - }], - ["@semantic-release/git", { - "assets": ["CHANGELOG.md", "csf_tz/__init__.py"], - "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" - }], - "@semantic-release/github" - ] -} diff --git a/commitlint.config.js b/commitlint.config.js index 3acd5a31..300da21e 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,4 +1,5 @@ module.exports = { + extends: ["@commitlint/config-conventional"], rules: { "subject-empty": [2, "never"], "type-case": [2, "always", "lower-case"], diff --git a/pyproject.toml b/pyproject.toml index 1afc4bae..da8fd493 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,11 @@ dependencies = [ "selcom-apigw-client", ] +[project.optional-dependencies] +dev = [ + "pre-commit", +] + [build-system] requires = ["flit_core >=3.4,<4"] build-backend = "flit_core.buildapi" diff --git a/scripts/setup-git-hooks.sh b/scripts/setup-git-hooks.sh new file mode 100755 index 00000000..58aabf1f --- /dev/null +++ b/scripts/setup-git-hooks.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Installs pre-commit and wires up the commit and push hooks for this clone. +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" + +if ! command -v pre-commit >/dev/null 2>&1; then + echo "pre-commit not found, installing..." + if command -v uv >/dev/null 2>&1; then + uv tool install pre-commit + elif command -v pipx >/dev/null 2>&1; then + pipx install pre-commit + else + python3 -m pip install --user pre-commit + fi +fi + +pre-commit install --install-hooks --overwrite + +echo +echo "Hooks installed:" +echo " pre-commit staged files only, fast" +echo " commit-msg conventional commit message check" +echo " pre-push pre-commit run --all-files, blocks the push on any failure" From d750bfcaac90a0c3eb55ec0ad33ec0c85593498d Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Wed, 26 Aug 2026 17:40:42 +0300 Subject: [PATCH 09/10] style: apply ruff and prettier formatting Mechanical reformat produced by the standardised hooks. No behaviour change. --- csf_tz/check-all-git.sh | 1 - csf_tz/csf_tz/additional_salary.js | 97 ++- csf_tz/csf_tz/bank_reconciliation.js | 39 +- csf_tz/csf_tz/bom_addittional_costs.js | 39 +- csf_tz/csf_tz/company.js | 234 ++--- csf_tz/csf_tz/custom_field.js | 158 ++-- csf_tz/csf_tz/customer.js | 18 +- .../multi_account_balance_timeline.js | 2 +- csf_tz/csf_tz/delivery_note.js | 223 ++--- .../bank_charges_pattern.js | 3 +- .../csf_api_response_log.js | 3 +- .../csf_tz_bank_charges.js | 3 +- .../csf_tz_settings/csf_tz_settings.js | 16 +- .../doctype/efd_z_report/efd_z_report.js | 67 +- .../doctype/efd_z_report/test_efd_z_report.js | 14 +- .../efd_z_report_invoice.js | 6 +- .../test_efd_z_report_invoice.js | 14 +- .../electronic_fiscal_device.js | 6 +- .../test_electronic_fiscal_device.js | 14 +- .../foreign_import_transaction.js | 424 ++++----- .../doctype/nmb_callback/nmb_callback.js | 24 +- .../student_applicant_fees.js | 80 +- .../csf_tz/doctype/tra_tax_inv/tra_tax_inv.js | 194 +++-- .../csf_tz/doctype/tz_district/tz_district.js | 3 +- .../tz_insurance_cover_note.js | 3 +- csf_tz/csf_tz/doctype/tz_region/tz_region.js | 3 +- .../csf_tz/doctype/tz_village/tz_village.js | 3 +- csf_tz/csf_tz/doctype/tz_ward/tz_ward.js | 3 +- .../vehicle_fine_record.js | 3 +- .../vehicle_fine_record.py | 264 +++--- .../doctype/vehicle_sync_task/processor.py | 72 +- .../csf_tz/doctype/vehicle_sync_task/queue.py | 141 +-- csf_tz/csf_tz/employee_advance.js | 112 +-- csf_tz/csf_tz/employee_contact_qr.js | 50 +- csf_tz/csf_tz/fees.js | 48 +- csf_tz/csf_tz/landed_cost_voucher.js | 72 +- csf_tz/csf_tz/material_request.js | 54 +- csf_tz/csf_tz/page/jobcards/jobcards.js | 21 +- csf_tz/csf_tz/page/scan_qrcode/scan_qrcode.js | 64 +- csf_tz/csf_tz/payment_entry.js | 784 +++++++++-------- csf_tz/csf_tz/payment_entry_list.js | 45 +- csf_tz/csf_tz/payroll_entry.js | 275 +++--- csf_tz/csf_tz/program_enrollment.js | 60 +- csf_tz/csf_tz/program_enrollment_tool.js | 73 +- csf_tz/csf_tz/property_setter.js | 88 +- csf_tz/csf_tz/purchase_invoice.js | 335 +++---- csf_tz/csf_tz/purchase_order.js | 125 ++- csf_tz/csf_tz/purchase_receipt.js | 38 +- csf_tz/csf_tz/quotation.js | 77 +- .../accounts_receivable_multi_currency.js | 271 +++--- ...ounts_receivable_summary_multi_currency.js | 148 ++-- .../av_sales_invoice_trend.js | 5 +- .../credit_note_list/credit_note_list.js | 54 +- .../csf_tz_stock_movement.js | 58 +- ...salary_register_with_monthly_comparison.js | 104 +-- .../excise_duty_detailed_report.js | 34 +- .../excise_duty_report/exise_duty_report.js | 34 +- .../excise_duty_stock/excise_duty_stock.js | 111 ++- .../general_ledger_pro/general_ledger_pro.js | 215 ++--- .../gross_profit_pro/gross_profit_pro.js | 53 +- .../import_exchange_differences.js | 96 +- .../item_price_by_price_list.js | 38 +- .../itemwise_stock_movement.js | 60 +- ...\342\200\223_withholding_tax_statement.js" | 48 +- .../loan_repayment_details.js | 14 +- .../monthly_account_balance.js | 26 +- .../monthly_timesheet_report.js | 34 +- .../multi_currency_ledger.js | 226 ++--- .../output_vat_reconciliation.js | 44 +- .../particular_item_history_report.js | 52 +- .../paye_report_mapping.js | 26 +- .../salary_register_csf.js | 171 ++-- .../salary_register_ctc.js | 116 +-- .../salary_register_summary.js | 104 +-- ...salary_register_summary_with_components.js | 104 +-- ...egister_summary_with_monthly_comparison.js | 190 ++-- .../stock_balance_pivot_warehouse.js | 68 +- .../stock_balance_pro/stock_balance_pro.js | 111 ++- .../tra_input_vat_returns_efiling.js | 26 +- .../trial_balance_report_in_usd.js | 132 +-- .../vat_efiling_returns.js | 40 +- .../warehouse_wise_item_balance_and_value.js | 110 +-- .../withholding_tax_payment_summary.js | 16 +- .../withholding_tax_summary_on_sales.js | 28 +- csf_tz/csf_tz/salary_slip.js | 126 ++- csf_tz/csf_tz/sales_invoice.js | 471 +++++----- csf_tz/csf_tz/sales_order.js | 206 +++-- csf_tz/csf_tz/stock_entry.js | 281 +++--- csf_tz/csf_tz/stock_reconciliation.js | 17 +- csf_tz/csf_tz/student_applicant.js | 48 +- csf_tz/csf_tz/supplier.js | 18 +- csf_tz/csf_tz/travel_request.js | 265 +++--- csf_tz/csf_tz/warehouse.js | 17 +- csf_tz/kcb/payroll_entry.js | 18 +- .../csf_tz_biometric_device.js | 3 +- .../csf_tz_biometric_log.js | 3 +- .../csf_tz_biometric_user.js | 3 +- .../csf_tz_biometric_user_type.js | 3 +- .../csf_tz_meal_type/csf_tz_meal_type.js | 3 +- csf_tz/public/js/budget_check_utils.js | 44 +- csf_tz/public/js/jobcards/Card.vue | 821 +++++++++--------- csf_tz/public/js/jobcards/JobCards.vue | 263 +++--- csf_tz/public/js/jobcards/jobcards.js | 86 +- csf_tz/public/js/po_shortcuts.js | 209 ++--- csf_tz/public/js/select_dialog.js | 491 ++++++----- csf_tz/public/js/shortcuts.js | 294 ++++--- csf_tz/public/js/to_console.js | 6 +- .../doctype/bin_setup/bin_setup.js | 6 +- .../doctype/bin_setup/test_bin_setup.js | 14 +- .../doctype/item_number/item_number.js | 6 +- .../doctype/item_number/test_item_number.js | 14 +- .../doctype/order_track/order_track.js | 102 +-- .../doctype/order_track/test_order_track.js | 14 +- .../purchase_and_stock_management_test.js | 6 +- ...test_purchase_and_stock_management_test.js | 14 +- .../ordered_items_to_be_delivered.js | 62 +- .../pending_ordered_items.js | 62 +- .../purchase_history/purchase_history.js | 54 +- .../reordering_items/reordering_items.js | 46 +- .../shipment_tracking/shipment_tracking.js | 52 +- .../supplier_contacts/supplier_contacts.js | 42 +- .../doctype/allert_custom/allert_custom.js | 6 +- .../allert_custom/test_allert_custom.js | 14 +- .../doctype/communications/communications.js | 6 +- .../communications/test_communications.js | 14 +- .../doctype/marketing_dept/marketing_dept.js | 6 +- .../marketing_dept/test_marketing_dept.js | 14 +- .../doctype/past_sales/past_sales.js | 6 +- .../doctype/past_sales/test_past_sales.js | 14 +- .../doctype/past_serial_no/past_serial_no.js | 6 +- .../past_serial_no/test_past_serial_no.js | 14 +- .../brand_sales_report/brand_sales_report.js | 52 +- .../customer_loan_assistance_report.js | 41 +- .../item_wise_leads_report.js | 48 +- .../items_marked_for_delivery.js | 26 +- .../previous_ams_customer_report.js | 52 +- .../sales_details_report.js | 60 +- .../spare_sales_report/spare_sales_report.js | 52 +- .../stanbic_payments_initiation.js | 3 +- .../stanbic_setting/stanbic_setting.js | 3 +- csf_tz/stanbic/payroll_entry.js | 88 +- .../simplify_vfd_settings.js | 6 +- .../total_vfd_setting/total_vfd_setting.js | 3 +- .../doctype/vfd_provider/vfd_provider.js | 3 +- .../vfd_provider_posting.js | 3 +- .../vfdplus_settings/vfdplus_settings.js | 3 +- .../company_vfd_provider.js | 3 +- csf_tz/vfd_support/customer.js | 56 +- csf_tz/vfd_support/sales_invoice.js | 525 +++++------ 149 files changed, 6475 insertions(+), 6304 deletions(-) diff --git a/csf_tz/check-all-git.sh b/csf_tz/check-all-git.sh index 5f9aaf50..21342bbf 100755 --- a/csf_tz/check-all-git.sh +++ b/csf_tz/check-all-git.sh @@ -8,4 +8,3 @@ for dir in apps/* ; do cd ../.. fi done - diff --git a/csf_tz/csf_tz/additional_salary.js b/csf_tz/csf_tz/additional_salary.js index 1b6318f5..26dacbe6 100644 --- a/csf_tz/csf_tz/additional_salary.js +++ b/csf_tz/csf_tz/additional_salary.js @@ -1,47 +1,50 @@ -frappe.ui.form.on('Additional Salary', { - refresh: function(frm) { - cur_frm.add_custom_button(__("Generate Additional Salary Records"), function() { - frappe.call({ - method: "csf_tz.csftz_hooks.additional_salary.generate_additional_salary_records", - args: {}, - callback: function () { - cur_frm.reload_doc(); - } - }); - }); - }, - payroll_date: function(frm) { - if (!frm.doc.payroll_date) { - frm.set_value("no_of_hours", null); - } - }, - employee: function(frm) { - if (!frm.doc.employee) { - frm.set_value("no_of_hours", null); - } - }, - salary_component: function(frm) { - if (!frm.doc.salary_component) { - frm.set_value("based_on_hourly_rate", null); - frm.set_value("hourly_rate", null); - } - }, - no_of_hours: function(frm) { - if (frm.doc.employee && frm.doc.payroll_date) { - frappe.call({ - method: "csf_tz.csftz_hooks.additional_salary.get_employee_base_salary_in_hours", - args: { - employee: frm.doc.employee, - payroll_date: frm.doc.payroll_date - }, - async: false, - callback: function(r) { - console.log(r.message) - if(r.message) { - frm.set_value("amount", frm.doc.hourly_rate / 100 * frm.doc.no_of_hours * r.message.base_salary_in_hours); - } - } - }); - } - }, -}); +frappe.ui.form.on("Additional Salary", { + refresh: function (frm) { + cur_frm.add_custom_button(__("Generate Additional Salary Records"), function () { + frappe.call({ + method: "csf_tz.csftz_hooks.additional_salary.generate_additional_salary_records", + args: {}, + callback: function () { + cur_frm.reload_doc(); + }, + }); + }); + }, + payroll_date: function (frm) { + if (!frm.doc.payroll_date) { + frm.set_value("no_of_hours", null); + } + }, + employee: function (frm) { + if (!frm.doc.employee) { + frm.set_value("no_of_hours", null); + } + }, + salary_component: function (frm) { + if (!frm.doc.salary_component) { + frm.set_value("based_on_hourly_rate", null); + frm.set_value("hourly_rate", null); + } + }, + no_of_hours: function (frm) { + if (frm.doc.employee && frm.doc.payroll_date) { + frappe.call({ + method: "csf_tz.csftz_hooks.additional_salary.get_employee_base_salary_in_hours", + args: { + employee: frm.doc.employee, + payroll_date: frm.doc.payroll_date, + }, + async: false, + callback: function (r) { + console.log(r.message); + if (r.message) { + frm.set_value( + "amount", + (frm.doc.hourly_rate / 100) * frm.doc.no_of_hours * r.message.base_salary_in_hours + ); + } + }, + }); + } + }, +}); diff --git a/csf_tz/csf_tz/bank_reconciliation.js b/csf_tz/csf_tz/bank_reconciliation.js index fec5229c..926b8ed5 100644 --- a/csf_tz/csf_tz/bank_reconciliation.js +++ b/csf_tz/csf_tz/bank_reconciliation.js @@ -1,21 +1,20 @@ -frappe.ui.form.on('Bank Reconciliation', { - get_payment_entries: function (frm) { - frappe.call({ - method: 'erpnext.accounts.utils.get_balance_on', - args: { - account: frm.doc.account, - date: frappe.datetime.add_days(frm.doc.from_date, -1), - }, - async: false, - callback: function (r) { - if (r.message) { - frm.set_value("opening_balance", r.message || 0); - } - else { - frm.set_value("opening_balance", 0); - } - } - }); - frm.set_value("closing_balance", frm.doc.total_amount + frm.doc.opening_balance); - }, +frappe.ui.form.on("Bank Reconciliation", { + get_payment_entries: function (frm) { + frappe.call({ + method: "erpnext.accounts.utils.get_balance_on", + args: { + account: frm.doc.account, + date: frappe.datetime.add_days(frm.doc.from_date, -1), + }, + async: false, + callback: function (r) { + if (r.message) { + frm.set_value("opening_balance", r.message || 0); + } else { + frm.set_value("opening_balance", 0); + } + }, + }); + frm.set_value("closing_balance", frm.doc.total_amount + frm.doc.opening_balance); + }, }); diff --git a/csf_tz/csf_tz/bom_addittional_costs.js b/csf_tz/csf_tz/bom_addittional_costs.js index 60b1007e..d94041bb 100644 --- a/csf_tz/csf_tz/bom_addittional_costs.js +++ b/csf_tz/csf_tz/bom_addittional_costs.js @@ -1,22 +1,21 @@ frappe.ui.form.on("BOM", { - refresh: function (frm) { - frm.set_query("expense_account", "additional_costs", function () { - return { - filters: { - account_type: [ - "in", - [ - "Tax", - "Chargeable", - "Income Account", - "Expenses Included In Valuation", - "Expenses Included In Asset Valuation", - ], - ], - company: frm.doc.company, - }, - }; - }); - }, - + refresh: function (frm) { + frm.set_query("expense_account", "additional_costs", function () { + return { + filters: { + account_type: [ + "in", + [ + "Tax", + "Chargeable", + "Income Account", + "Expenses Included In Valuation", + "Expenses Included In Asset Valuation", + ], + ], + company: frm.doc.company, + }, + }; + }); + }, }); diff --git a/csf_tz/csf_tz/company.js b/csf_tz/csf_tz/company.js index 3de366b9..580d746e 100644 --- a/csf_tz/csf_tz/company.js +++ b/csf_tz/csf_tz/company.js @@ -1,153 +1,165 @@ frappe.ui.form.on("Company", { - - setup: function(frm) { - frm.set_query("default_withholding_payable_account", function() { + setup: function (frm) { + frm.set_query("default_withholding_payable_account", function () { return { - "filters": { - "company": frm.doc.name, - "account_type": "Payable", - } + filters: { + company: frm.doc.name, + account_type: "Payable", + }, }; }); - frm.set_query("default_withholding_receivable_account", function() { + frm.set_query("default_withholding_receivable_account", function () { return { - "filters": { - "company": frm.doc.name, - "account_type": "Receivable", - } + filters: { + company: frm.doc.name, + account_type: "Receivable", + }, }; }); - frm.set_query("fee_bank_account", function() { + frm.set_query("fee_bank_account", function () { return { - "filters": { - "company": frm.doc.name, - "account_type": ["in",["Cash","Bank"]], - "account_currency": frm.doc.default_currency, - } + filters: { + company: frm.doc.name, + account_type: ["in", ["Cash", "Bank"]], + account_currency: frm.doc.default_currency, + }, }; }); - frm.set_query("student_applicant_fees_revenue_account", function() { + frm.set_query("student_applicant_fees_revenue_account", function () { return { - "filters": { - "company": frm.doc.name, - "account_type": "Income Account", - "account_currency": frm.doc.default_currency, - } + filters: { + company: frm.doc.name, + account_type: "Income Account", + account_currency: frm.doc.default_currency, + }, }; - }); + }); }, - - refresh: function(frm) { - frm.add_custom_button(__('Auto create accounts'), function() { - frm.trigger("auto_create_account"); - }, __("Setup")); - frm.add_custom_button(__('create Item Tax Template'), function() { - frm.trigger("create_tax_template"); - }, __("Setup")); - frm.add_custom_button(__('Create Tax Category'), function() { - frm.trigger("make_tax_category"); - }, __("Setup")); - frm.add_custom_button(__('Create Salary Component'), function() { - frm.trigger("make_salary_components_and_structure"); - }, __("Setup")); - frm.add_custom_button(__('Link Item Tax Template'), function() { - let d = new frappe.ui.Dialog({ - title: 'Enter details', - fields: [ - { - fieldtype: 'Link', - options: 'Item Tax Template', - label: __('Item Tax Category'), - fieldname: 'default_tax_template', - reqd: 1 - } - ], - primary_action_label: 'Submit', - primary_action(values) { - console.log(values); - - frappe.call({ - method: 'csf_tz.custom_api.linking_tax_template', - args: { - abbr: frm.doc.abbr, - doctype: 'Item', - default_tax_template: { - default_tax_template: values.default_tax_template - } + refresh: function (frm) { + frm.add_custom_button( + __("Auto create accounts"), + function () { + frm.trigger("auto_create_account"); + }, + __("Setup") + ); + frm.add_custom_button( + __("create Item Tax Template"), + function () { + frm.trigger("create_tax_template"); + }, + __("Setup") + ); + frm.add_custom_button( + __("Create Tax Category"), + function () { + frm.trigger("make_tax_category"); + }, + __("Setup") + ); + frm.add_custom_button( + __("Create Salary Component"), + function () { + frm.trigger("make_salary_components_and_structure"); + }, + __("Setup") + ); + frm.add_custom_button( + __("Link Item Tax Template"), + function () { + let d = new frappe.ui.Dialog({ + title: "Enter details", + fields: [ + { + fieldtype: "Link", + options: "Item Tax Template", + label: __("Item Tax Category"), + fieldname: "default_tax_template", + reqd: 1, }, - callback: function(response) { - if (response.message) { - frappe.msgprint(__('Item Tax Template Linked successfully.')); - } - } - }); - - d.hide(); - } - }); + ], + primary_action_label: "Submit", + primary_action(values) { + console.log(values); - d.show(); - }, __("Setup")); + frappe.call({ + method: "csf_tz.custom_api.linking_tax_template", + args: { + abbr: frm.doc.abbr, + doctype: "Item", + default_tax_template: { + default_tax_template: values.default_tax_template, + }, + }, + callback: function (response) { + if (response.message) { + frappe.msgprint(__("Item Tax Template Linked successfully.")); + } + }, + }); + d.hide(); + }, + }); + d.show(); + }, + __("Setup") + ); }, - auto_create_account: function(frm) { + auto_create_account: function (frm) { frappe.call({ - method: 'csf_tz.custom_api.auto_create_account', - args:{ - abbr: frm.doc.abbr - + method: "csf_tz.custom_api.auto_create_account", + args: { + abbr: frm.doc.abbr, }, - callback: function(response) { + callback: function (response) { if (response.message) { - frappe.msgprint(__('Accounts created successfully.')); + frappe.msgprint(__("Accounts created successfully.")); } - } - }) + }, + }); }, - create_tax_template: function(frm) { + create_tax_template: function (frm) { frappe.call({ - method: 'csf_tz.custom_api.create_item_tax_template', - args:{ - abbr: frm.doc.abbr - + method: "csf_tz.custom_api.create_item_tax_template", + args: { + abbr: frm.doc.abbr, }, - callback: function(response) { + callback: function (response) { if (response.message) { - frappe.msgprint(__('Item Tax Templates created successfully.')); + frappe.msgprint(__("Item Tax Templates created successfully.")); } - } - }) + }, + }); }, - make_tax_category: function(frm) { + make_tax_category: function (frm) { frappe.call({ - method: 'csf_tz.custom_api.create_tax_category', - args:{ - abbr: frm.doc.abbr - + method: "csf_tz.custom_api.create_tax_category", + args: { + abbr: frm.doc.abbr, }, - callback: function(response) { + callback: function (response) { if (response.message) { - frappe.msgprint(__('Tax Category created successfully.')); + frappe.msgprint(__("Tax Category created successfully.")); } - } - }) + }, + }); }, - make_salary_components_and_structure: function(frm) { + make_salary_components_and_structure: function (frm) { frappe.call({ - method: 'csf_tz.custom_api.make_salary_components_and_structure', - args:{ - abbr: frm.doc.abbr - + method: "csf_tz.custom_api.make_salary_components_and_structure", + args: { + abbr: frm.doc.abbr, }, - callback: function(response) { + callback: function (response) { if (response.message) { - frappe.msgprint(__('Salary Components and Structure are created successfully.')); + frappe.msgprint(__("Salary Components and Structure are created successfully.")); } - } - }) + }, + }); }, }); diff --git a/csf_tz/csf_tz/custom_field.js b/csf_tz/csf_tz/custom_field.js index ce24a929..0d50f289 100644 --- a/csf_tz/csf_tz/custom_field.js +++ b/csf_tz/csf_tz/custom_field.js @@ -1,82 +1,84 @@ -frappe.listview_settings['Custom Field'] = { - onload: function (listview) { - listview.page.add_menu_item(__('Export Selected'), async function () { - const selected_docs = listview.get_checked_items(); - if (selected_docs.length === 0) { - frappe.msgprint(__('Please select at least one document.')); - return; - } +frappe.listview_settings["Custom Field"] = { + onload: function (listview) { + listview.page.add_menu_item(__("Export Selected"), async function () { + const selected_docs = listview.get_checked_items(); + if (selected_docs.length === 0) { + frappe.msgprint(__("Please select at least one document.")); + return; + } - const detailed_docs = await Promise.all(selected_docs.map(doc => - fetch(`/api/resource/Custom Field/${doc.name}`) - .then(response => response.json()) - .then(data => data.data) - )); + const detailed_docs = await Promise.all( + selected_docs.map((doc) => + fetch(`/api/resource/Custom Field/${doc.name}`) + .then((response) => response.json()) + .then((data) => data.data) + ) + ); - const data_to_export = detailed_docs.map(doc => { - return { - name: doc.name, - owner: doc.owner, - creation: doc.creation, - modified: doc.modified, - modified_by: doc.modified_by, - docstatus: doc.docstatus, - idx: doc.idx, - is_system_generated: doc.is_system_generated, - dt: doc.dt, - label: doc.label, - fieldname: doc.fieldname, - insert_after: doc.insert_after, - length: doc.length, - fieldtype: doc.fieldtype, - precision: doc.precision, - hide_seconds: doc.hide_seconds, - hide_days: doc.hide_days, - options: doc.options, - sort_options: doc.sort_options, - fetch_if_empty: doc.fetch_if_empty, - fetch_from: doc.fetch_from, - collapsible: doc.collapsible, - non_negative: doc.non_negative, - mandatory_depends_on: doc.mandatory_depends_on, - depends_on: doc.depends_on, - reqd: doc.reqd, - unique: doc.unique, - is_virtual: doc.is_virtual, - read_only: doc.read_only, - ignore_user_permissions: doc.ignore_user_permissions, - hidden: doc.hidden, - print_hide: doc.print_hide, - print_hide_if_no_value: doc.print_hide_if_no_value, - no_copy: doc.no_copy, - allow_on_submit: doc.allow_on_submit, - in_list_view: doc.in_list_view, - in_standard_filter: doc.in_standard_filter, - in_global_search: doc.in_global_search, - in_preview: doc.in_preview, - bold: doc.bold, - report_hide: doc.report_hide, - search_index: doc.search_index, - allow_in_quick_entry: doc.allow_in_quick_entry, - ignore_xss_filter: doc.ignore_xss_filter, - translatable: doc.translatable, - hide_border: doc.hide_border, - show_dashboard: doc.show_dashboard, - permlevel: doc.permlevel, - columns: doc.columns, - doctype: doc.doctype, - __last_sync_on: doc.__last_sync_on - }; - }); + const data_to_export = detailed_docs.map((doc) => { + return { + name: doc.name, + owner: doc.owner, + creation: doc.creation, + modified: doc.modified, + modified_by: doc.modified_by, + docstatus: doc.docstatus, + idx: doc.idx, + is_system_generated: doc.is_system_generated, + dt: doc.dt, + label: doc.label, + fieldname: doc.fieldname, + insert_after: doc.insert_after, + length: doc.length, + fieldtype: doc.fieldtype, + precision: doc.precision, + hide_seconds: doc.hide_seconds, + hide_days: doc.hide_days, + options: doc.options, + sort_options: doc.sort_options, + fetch_if_empty: doc.fetch_if_empty, + fetch_from: doc.fetch_from, + collapsible: doc.collapsible, + non_negative: doc.non_negative, + mandatory_depends_on: doc.mandatory_depends_on, + depends_on: doc.depends_on, + reqd: doc.reqd, + unique: doc.unique, + is_virtual: doc.is_virtual, + read_only: doc.read_only, + ignore_user_permissions: doc.ignore_user_permissions, + hidden: doc.hidden, + print_hide: doc.print_hide, + print_hide_if_no_value: doc.print_hide_if_no_value, + no_copy: doc.no_copy, + allow_on_submit: doc.allow_on_submit, + in_list_view: doc.in_list_view, + in_standard_filter: doc.in_standard_filter, + in_global_search: doc.in_global_search, + in_preview: doc.in_preview, + bold: doc.bold, + report_hide: doc.report_hide, + search_index: doc.search_index, + allow_in_quick_entry: doc.allow_in_quick_entry, + ignore_xss_filter: doc.ignore_xss_filter, + translatable: doc.translatable, + hide_border: doc.hide_border, + show_dashboard: doc.show_dashboard, + permlevel: doc.permlevel, + columns: doc.columns, + doctype: doc.doctype, + __last_sync_on: doc.__last_sync_on, + }; + }); - const jsonStr = JSON.stringify(data_to_export); - let blob = new Blob([jsonStr], { type: "application/json" }); - let a = document.createElement("a"); - a.href = URL.createObjectURL(blob); - a.download = "exported_custom_fields.json"; - a.click(); - URL.revokeObjectURL(a.href); - a.remove(); - }); - } + const jsonStr = JSON.stringify(data_to_export); + let blob = new Blob([jsonStr], { type: "application/json" }); + let a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = "exported_custom_fields.json"; + a.click(); + URL.revokeObjectURL(a.href); + a.remove(); + }); + }, }; diff --git a/csf_tz/csf_tz/customer.js b/csf_tz/csf_tz/customer.js index 3751e049..5404b64d 100644 --- a/csf_tz/csf_tz/customer.js +++ b/csf_tz/csf_tz/customer.js @@ -2,20 +2,16 @@ // For license information, please see license.txt /* eslint-disable */ - frappe.ui.form.on("Customer", { - - - refresh: function(frm) { - - if(!frm.doc.__islocal) { + refresh: function (frm) { + if (!frm.doc.__islocal) { // custom buttons - frm.add_custom_button(__('Multi-Currency Ledger'), function() { - frappe.set_route('query-report', 'Multi-Currency Ledger', - {party_type:'Customer', party:frm.doc.name}); + frm.add_custom_button(__("Multi-Currency Ledger"), function () { + frappe.set_route("query-report", "Multi-Currency Ledger", { + party_type: "Customer", + party: frm.doc.name, + }); }); - } }, - }); diff --git a/csf_tz/csf_tz/dashboard_chart_source/multi_account_balance_timeline/multi_account_balance_timeline.js b/csf_tz/csf_tz/dashboard_chart_source/multi_account_balance_timeline/multi_account_balance_timeline.js index dd8cc988..e154230d 100644 --- a/csf_tz/csf_tz/dashboard_chart_source/multi_account_balance_timeline/multi_account_balance_timeline.js +++ b/csf_tz/csf_tz/dashboard_chart_source/multi_account_balance_timeline/multi_account_balance_timeline.js @@ -29,6 +29,6 @@ frappe.dashboards.chart_sources["Multi_Account Balance Timeline"] = { label: __("Include Inactive Accounts"), fieldtype: "Check", default: 0, - } + }, ], }; diff --git a/csf_tz/csf_tz/delivery_note.js b/csf_tz/csf_tz/delivery_note.js index 4abb786c..41c4dc38 100644 --- a/csf_tz/csf_tz/delivery_note.js +++ b/csf_tz/csf_tz/delivery_note.js @@ -1,18 +1,18 @@ frappe.ui.keys.add_shortcut({ - shortcut: 'ctrl+q', - action: () => { - const current_doc = $('.data-row.editable-row').parent().attr("data-name"); - const item_row = locals["Delivery Note Item"][current_doc]; - frappe.call({ - method: 'csf_tz.custom_api.get_item_info', - args: {item_code: item_row.item_code}, - callback: function(r) { - if (r.message.length > 0){ - const d = new frappe.ui.Dialog({ - title: __('Item Balance'), - width: 600 - }); - $(`