Factorizer/Cluster ERP WikiCluster ERP Türkçe
Home / Decision records

Explanation

No hard delete

Decision record for the rule that Cluster ERP never physically deletes a record; deleting means deactivating with a reason, recorded in the change history.

Status: Accepted, stated in the project's written engineering rules. This record covers the rule that no record of Cluster ERP is removed by a user action. A delete becomes a deactivation with a reason, every deactivation is kept in core_change_history, and only a retention purge removes rows for good. The columns and routes are explained on soft delete and deactivation.

Context

The project's written engineering rules forbid physical deletion and list three deliberate exceptions: dependent child rows that are renewed wholesale with their owner, derived summary cells that are rebuilt, and event rows removed by the retention purge. An earlier mechanism, a database-trigger vault, was removed in July 2026. The migration that removed it states the reason: deactivations are recorded in the permanent change history, and the only physical deletion is the retention purge (migrations/versions/dropvault_0001_remove_deletion_vault.py:1-11).

Decision

  • A record is active while deleted_at is empty and deactivated when it holds a time. deleted_by, deactivation_reason, and deactivation_note record who and why (src/cluster_erp/core/models.py:49-72).
  • A user's delete deactivates; the record can be listed among passive records and reactivated (src/cluster_erp/modules/records/lifecycle_routes.py:115-133, src/cluster_erp/modules/records/lifecycle_routes.py:225-249).
  • Every deactivation and every change is recorded automatically by the change-history listener.
  • The retention purge is the only general physical deletion (src/cluster_erp/modules/records/retention/purge.py:1-6).

The flowchart shows the life of a record.

flowchart TD
 del["user deletes a record"] --> deact["deleted_at set, reason and note stamped"]
 deact --> hist["before_flush writes a deactivate row to core_change_history"]
 deact --> passive["record listed among passive records"]
 passive -->|activate| live["deleted_at cleared, activate row written"]
 passive -->|retention window passed| purge["purge_expired removes the row"]

In prose: when deleted_at moves from empty to a value, the listener stamps the actor, reason, and note on the row and writes a deactivate history row (src/cluster_erp/core/change_history.py:331-342). Activation clears deleted_at and writes an activate row. After the configured number of years, purge_expired deletes deactivated rows and old history (src/cluster_erp/modules/records/retention/purge.py:33-45).

Consequences

  • Bulk deactivation loads each object and sets deleted_at through the ORM, so the history and the reason stamp apply to every row (src/cluster_erp/core/bulk_operations.py:20-55).
  • Uniqueness applies only to live rows, through partial indexes with WHERE deleted_at IS NULL, so a deactivated value can be used again. Example: uq_core_deactivation_reasons_code_live (src/cluster_erp/modules/records/deactivation_reasons/models.py:18-25).
  • Queries over business data filter out deactivated rows; analytics tests state this as a consequence of the rule (tests/test_analytics_gates.py:423).
  • The deliberate exceptions go through the ORM so the history still records each removal:
Exception Rows Source
Child rows renewed with their owner Calendar template lines and exception shifts src/cluster_erp/modules/calendars/services.py:186-190, src/cluster_erp/modules/calendars/services.py:596-600
Child rows renewed with their owner Supplier quotation condition and scale lines src/cluster_erp/modules/purchasing_quotations/quotations/services.py:931-937
Child rows renewed with their owner Costing breakdown items and rate snapshots src/cluster_erp/modules/costing/costing_analyses/services.py:315-329
Child rows renewed with their owner Work centre operation types src/cluster_erp/modules/production/work_centers/services.py:152-163
Derived summary cells Analytics summary cells, rebuilt src/cluster_erp/modules/analytics/summaries.py:245
Retention purge Deactivated rows and old history src/cluster_erp/modules/records/retention/purge.py:33-45

orm_delete_where is the shared helper for the child-row cases (src/cluster_erp/core/bulk_operations.py:58-75).

Where the code enforces it

Mechanism What it checks Source
BaseModel soft-delete columns Every business table carries the deactivation state src/cluster_erp/core/models.py:49-72
_capture_changes listener Every deactivation and every ORM delete is written to history src/cluster_erp/core/change_history.py:318-351
tests/test_live_row_uniqueness.py Model and database partial unique indexes agree; no plain unique on user values tests/test_live_row_uniqueness.py:1-15
test_the_screen_never_offers_a_delete_action The analytics statistics screen holds no delete or update statement tests/test_analytics_statistics.py:206-217

Not determined from code: a repository-wide test that rejects DELETE statements outside the purge and the listed exceptions. None was found in tests/.


Verified against commit 92f8051 on 2026-11-13.

Verified against commit 92f8051 · 2026-11-13