fix sms
This commit is contained in:
1 parent
1dea3509a7
commit
e3a6d1f9d4
42 files changed
+33084
-15777
No files matched your search
+66
@@ -0,0 +1,66 @@
|
|||||||
|
# ==========================================
|
||||||
|
# 1. Operational Style & Communication (Claude Code Style)
|
||||||
|
# ==========================================
|
||||||
|
You are an expert engineer. Deliver high-signal, structured technical execution with zero conversational fluff.
|
||||||
|
|
||||||
|
## Communication Rules
|
||||||
|
- Match the user's language (Persian or English).
|
||||||
|
- Be direct, terse, and technical. Never use pleasantries, greetings, apologies, or robotic affirmations.
|
||||||
|
- Do NOT output preamble text or explanations before invoking tools; execute immediately.
|
||||||
|
- Never run interactive or long-running commands (e.g., ensure test commands include non-blocking flags).
|
||||||
|
|
||||||
|
## Task Wrap-Up Format
|
||||||
|
- Minor fixes (single file/few lines): Output a single sentence or "Done."
|
||||||
|
- Complex or multi-step tasks: Output ONLY this structured summary:
|
||||||
|
- 1-sentence summary of the fix/feature.
|
||||||
|
- Bullet points of key modified files and core changes.
|
||||||
|
- Test/build verification status.
|
||||||
|
- Required manual actions (e.g., .env keys or migrations), only if strictly necessary.
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 2. Project Operating Guidelines
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
## Codebase Navigation & Analysis (Ripwire vs. Graphify)
|
||||||
|
- Default to Ripwire for all hands-on coding, AST, edits, call graphs, and tests.
|
||||||
|
- Reserve Graphify strictly for high-level architectural understanding, cross-artifact relations, and non-code assets.
|
||||||
|
- CLI-only (never use MCP). Route strictly by question and artifact type.
|
||||||
|
- Never query both tools for the same sub-task; never execute in parallel. Always prefer `--expand` over reading entire files.
|
||||||
|
- Trivial edits (<= 1 file, no exported symbols): Modify directly without querying either tool.
|
||||||
|
- Routing gate:
|
||||||
|
- Code, symbols, AST, usages, impact, tests -> Ripwire.
|
||||||
|
- Architecture discovery, cross-artifact concepts (PDF, media, docs, ADR/RFC), SQL schemas -> Graphify (.sql symbols use Ripwire; schema semantics use Graphify).
|
||||||
|
- Ambiguous/Mixed -> run `ripwire . --help-task="task"` to classify.
|
||||||
|
- Graphify rules: Requires `graphify-out/graph.json` (if absent, fallback to Ripwire). Match terms directly against graph vocabulary. Cross-verify graph findings against source code before editing.
|
||||||
|
- Pre-edit gate: Inspect target symbol with both `ripwire . --impact=SYM` and `ripwire . --uses=SYM`.
|
||||||
|
- Post-edit pipeline: Run `ripwire . --test-gate` followed by `ripwire . --quality-delta`.
|
||||||
|
|
||||||
|
## Dynamic Help, Self-Correction & Staleness
|
||||||
|
- Help precedence: Inspect `ripwire --help` or `ripwire --help=--FLAG` when uncertain; CLI help is authoritative.
|
||||||
|
- Retries: Cap at 1 retry on failures.
|
||||||
|
- Stale data: Run `graphify update .` or `ripwire . --no-cache` and retry once.
|
||||||
|
- Missing binary: Report missing dependency clearly; do not guess syntax.
|
||||||
|
|
||||||
|
## Git Operations & Safety
|
||||||
|
- Read-only for agent: Allowed commands are strictly `git status`, `git diff`, and `git log`.
|
||||||
|
- Human-gated: NEVER run `git add`, `commit`, `push`, `checkout`, `merge`, `rebase`, or branch commands. Ask the user to execute these and pause.
|
||||||
|
- Exception: `graphify hook install` is permitted (read-side only).
|
||||||
|
|
||||||
|
## Development Server Lifecycle
|
||||||
|
- Binding: Always bind Django to `0.0.0.0:8000` (LAN accessible). Never use `127.0.0.1` unless explicitly requested.
|
||||||
|
- Process management: Inspect port 8000 and terminate previous project server instances before launching.
|
||||||
|
- Health check: Confirm exactly one process listens on `0.0.0.0:8000`, then verify health via local fetch to `127.0.0.1:8000`.
|
||||||
|
- Asset verification: Verify served frontend content on live server before diagnosing browser cache issues.
|
||||||
|
|
||||||
|
## Environment, Frontend & PWA Safety
|
||||||
|
- Python venv: Strictly use `.venv` for all Python/Django operations (`.venv/Scripts/python.exe` on WSL, `.venv\Scripts\python.exe` on Windows). Run pip with `<venv-python> -m pip` and Django with `<venv-python> manage.py ...`.
|
||||||
|
- PyPI Mirror: Use Liara mirror: `https://package-mirror.liara.ir/repository/pypi/simple`.
|
||||||
|
- Asset versioning: Local frontend assets must include `static_asset_version` in URLs.
|
||||||
|
- PWA: Keep service worker registration URL versioned with `updateViaCache: 'none'`.
|
||||||
|
- Cache regression gate: Frontend/PWA cache changes must pass assertions in `tests/test_views.py` and `tests/e2e/test_pwa_versioning.py`.
|
||||||
|
|
||||||
|
## End-to-End Browser Tests (Playwright)
|
||||||
|
- Tests live in `tests/e2e/`, marked with `e2e`, run against Django's transactional `live_server`.
|
||||||
|
- Use Chromium and BrowserContext from `tests/e2e/conftest.py`. Function-scoped to avoid deadlocks.
|
||||||
|
- Page Objects: Encapsulate in `tests/e2e/pages/`. Assert user-visible outcomes only.
|
||||||
|
- Synchronization: Never use hardcoded sleeps; use Playwright locators and assertions.
|
||||||
+180
-19
@@ -1,11 +1,11 @@
|
|||||||
Project Root: khatibi
|
Project Root: khatibi
|
||||||
Generated At: 2026-10-10T06:32:47+00:00
|
Generated At: 2026-10-11T12:20:45+00:00
|
||||||
Python Files: 153
|
Python Files: 171
|
||||||
Django Files: 12
|
Django Files: 12
|
||||||
JavaScript/TypeScript Files: 14
|
JavaScript/TypeScript Files: 14
|
||||||
HTML Files: 54
|
HTML Files: 58
|
||||||
Total Files: 523
|
Total Files: 682
|
||||||
Total Directories: 74
|
Total Directories: 76
|
||||||
|
|
||||||
khatibi
|
khatibi
|
||||||
├── .cursor
|
├── .cursor
|
||||||
@@ -20,6 +20,10 @@ khatibi
|
|||||||
│ │ └── graphify.js
|
│ │ └── graphify.js
|
||||||
│ └── opencode.json
|
│ └── opencode.json
|
||||||
├── .run
|
├── .run
|
||||||
|
│ ├── delivery.pid
|
||||||
|
│ ├── scanner.pid
|
||||||
|
│ ├── web.pid
|
||||||
|
│ └── worker.pid
|
||||||
├── admin
|
├── admin
|
||||||
│ ├── migrations
|
│ ├── migrations
|
||||||
│ │ └── __init__.py
|
│ │ └── __init__.py
|
||||||
@@ -34,8 +38,11 @@ khatibi
|
|||||||
│ ├── management
|
│ ├── management
|
||||||
│ │ ├── commands
|
│ │ ├── commands
|
||||||
│ │ │ ├── __init__.py
|
│ │ │ ├── __init__.py
|
||||||
|
│ │ │ ├── fix_customer_phones.py
|
||||||
│ │ │ ├── gt_esh.py
|
│ │ │ ├── gt_esh.py
|
||||||
|
│ │ │ ├── recalc_last_bazdid.py
|
||||||
│ │ │ ├── send_daily_report.py
|
│ │ │ ├── send_daily_report.py
|
||||||
|
│ │ │ ├── sms_delivery.py
|
||||||
│ │ │ ├── sms_scan.py
|
│ │ │ ├── sms_scan.py
|
||||||
│ │ │ ├── sms_worker.py
|
│ │ │ ├── sms_worker.py
|
||||||
│ │ │ └── sqlite_db_backup.py
|
│ │ │ └── sqlite_db_backup.py
|
||||||
@@ -103,6 +110,15 @@ khatibi
|
|||||||
│ │ ├── 0060_sms_permission_label.py
|
│ │ ├── 0060_sms_permission_label.py
|
||||||
│ │ ├── 0061_sms_timing_and_primary.py
|
│ │ ├── 0061_sms_timing_and_primary.py
|
||||||
│ │ ├── 0062_sms_link_provider.py
|
│ │ ├── 0062_sms_link_provider.py
|
||||||
|
│ │ ├── 0063_alter_smsqueue_status.py
|
||||||
|
│ │ ├── 0064_sms_webservice_delivery.py
|
||||||
|
│ │ ├── 0065_smsproviderconfig_test_message.py
|
||||||
|
│ │ ├── 0066_smsdedup.py
|
||||||
|
│ │ ├── 0067_customer_notes.py
|
||||||
|
│ │ ├── 0068_auto_20261010_1305.py
|
||||||
|
│ │ ├── 0069_smstemplate_auto_send.py
|
||||||
|
│ │ ├── 0070_sms_types_seven.py
|
||||||
|
│ │ ├── 0071_sms_settings_permission.py
|
||||||
│ │ └── __init__.py
|
│ │ └── __init__.py
|
||||||
│ ├── models
|
│ ├── models
|
||||||
│ │ ├── __init__.py
|
│ │ ├── __init__.py
|
||||||
@@ -118,12 +134,16 @@ khatibi
|
|||||||
│ ├── sms
|
│ ├── sms
|
||||||
│ │ ├── __init__.py
|
│ │ ├── __init__.py
|
||||||
│ │ ├── enqueue.py
|
│ │ ├── enqueue.py
|
||||||
|
│ │ ├── notices.py
|
||||||
|
│ │ ├── parts.py
|
||||||
|
│ │ ├── phone_cleanup.py
|
||||||
│ │ ├── phones.py
|
│ │ ├── phones.py
|
||||||
│ │ ├── recipients.py
|
│ │ ├── recipients.py
|
||||||
│ │ ├── render.py
|
│ │ ├── render.py
|
||||||
│ │ ├── rules.py
|
│ │ ├── rules.py
|
||||||
│ │ ├── scanner.py
|
│ │ ├── scanner.py
|
||||||
│ │ └── timing.py
|
│ │ ├── timing.py
|
||||||
|
│ │ └── triggers.py
|
||||||
│ ├── static
|
│ ├── static
|
||||||
│ │ └── admin_panel
|
│ │ └── admin_panel
|
||||||
│ │ ├── DataTables
|
│ │ ├── DataTables
|
||||||
@@ -208,6 +228,8 @@ khatibi
|
|||||||
│ │ ├── customer
|
│ │ ├── customer
|
||||||
│ │ │ ├── customerNazarInc.html
|
│ │ │ ├── customerNazarInc.html
|
||||||
│ │ │ └── nazar.html
|
│ │ │ └── nazar.html
|
||||||
|
│ │ ├── _customer_info.html
|
||||||
|
│ │ ├── _customer_phones.html
|
||||||
│ │ ├── _customer_sms.html
|
│ │ ├── _customer_sms.html
|
||||||
│ │ ├── ActionList.html
|
│ │ ├── ActionList.html
|
||||||
│ │ ├── base.html
|
│ │ ├── base.html
|
||||||
@@ -234,6 +256,7 @@ khatibi
|
|||||||
│ │ ├── customers.html
|
│ │ ├── customers.html
|
||||||
│ │ ├── customerSarresid.html
|
│ │ ├── customerSarresid.html
|
||||||
│ │ ├── customersBK.html
|
│ │ ├── customersBK.html
|
||||||
|
│ │ ├── customerSmsHistory.html
|
||||||
│ │ ├── entezar.html
|
│ │ ├── entezar.html
|
||||||
│ │ ├── entezarBK.html
|
│ │ ├── entezarBK.html
|
||||||
│ │ ├── financial_dashboard.html
|
│ │ ├── financial_dashboard.html
|
||||||
@@ -253,6 +276,7 @@ khatibi
|
|||||||
│ │ ├── reportCheckNesiye.html
|
│ │ ├── reportCheckNesiye.html
|
||||||
│ │ ├── rezayat.html
|
│ │ ├── rezayat.html
|
||||||
│ │ ├── sms_queue.html
|
│ │ ├── sms_queue.html
|
||||||
|
│ │ ├── sms_report.html
|
||||||
│ │ ├── sms_settings.html
|
│ │ ├── sms_settings.html
|
||||||
│ │ ├── sms_templates.html
|
│ │ ├── sms_templates.html
|
||||||
│ │ ├── store.html
|
│ │ ├── store.html
|
||||||
@@ -299,19 +323,45 @@ khatibi
|
|||||||
│ │ ├── graph.json
|
│ │ ├── graph.json
|
||||||
│ │ ├── GRAPH_REPORT.md
|
│ │ ├── GRAPH_REPORT.md
|
||||||
│ │ └── manifest.json
|
│ │ └── manifest.json
|
||||||
|
│ ├── 2026-10-10
|
||||||
|
│ │ ├── .graphify_analysis.json
|
||||||
|
│ │ ├── .graphify_labels.json
|
||||||
|
│ │ ├── .graphify_semantic_marker
|
||||||
|
│ │ ├── graph.json
|
||||||
|
│ │ ├── GRAPH_REPORT.md
|
||||||
|
│ │ └── manifest.json
|
||||||
|
│ ├── 2026-10-11
|
||||||
|
│ │ ├── .graphify_analysis.json
|
||||||
|
│ │ ├── .graphify_labels.json
|
||||||
|
│ │ ├── .graphify_semantic_marker
|
||||||
|
│ │ ├── graph.json
|
||||||
|
│ │ ├── GRAPH_REPORT.md
|
||||||
|
│ │ └── manifest.json
|
||||||
│ ├── cache
|
│ ├── cache
|
||||||
│ │ ├── ast
|
│ │ ├── ast
|
||||||
│ │ │ └── v0.9.82-s7
|
│ │ │ └── v0.9.82-s7
|
||||||
│ │ │ ├── 0075bdcd95747f7c750eba8757614aab71d4d8c77bd6764e4c5a98fe18dc8997.json
|
│ │ │ ├── 0075bdcd95747f7c750eba8757614aab71d4d8c77bd6764e4c5a98fe18dc8997.json
|
||||||
|
│ │ │ ├── 0175e34128b5ac5119a5da4cc53d889dce334d8f20357631d54627c53cae9b8e.json
|
||||||
|
│ │ │ ├── 01850b7fad89dcb9e521fceed2c41e64e9909a0f4c4a54d818d346ccf63bbc5d.json
|
||||||
|
│ │ │ ├── 0348dd4662ebd96572421864ce77a266334a327f8bcbdc237d54c313d116bafb.json
|
||||||
│ │ │ ├── 03c6fb41770a9293658a5db5af36f7e4afe64ae04b3ce8cc6c5874320706fd22.json
|
│ │ │ ├── 03c6fb41770a9293658a5db5af36f7e4afe64ae04b3ce8cc6c5874320706fd22.json
|
||||||
|
│ │ │ ├── 0400cc0eadd706ff70ef627ffe39f401fc72a591dcbcd5463fca07fc271f080d.json
|
||||||
│ │ │ ├── 0442dd7e8445ba796e02aaefde16760c8ed37f63359d4319498be512f60266bd.json
|
│ │ │ ├── 0442dd7e8445ba796e02aaefde16760c8ed37f63359d4319498be512f60266bd.json
|
||||||
│ │ │ ├── 056f0cb55bfd5a1ddf744c7c824c342048c1db47f3bb350030a1533ab56bf8d7.json
|
│ │ │ ├── 056f0cb55bfd5a1ddf744c7c824c342048c1db47f3bb350030a1533ab56bf8d7.json
|
||||||
|
│ │ │ ├── 05b753e15288fbd02b721b00cfd03c3a523e9e234420330b980a462a1de2dad6.json
|
||||||
|
│ │ │ ├── 07eb6ef411f8e9b78d19cfd5f262bfc9fc482701f0834c8936cbb5ba2b14efab.json
|
||||||
|
│ │ │ ├── 0b48631fc75356bef276f694d7db5b457bd2315410fd4538680612fe10b95eb2.json
|
||||||
│ │ │ ├── 0b8f8e3b6e4d84256e784dabb96f5646fa6e7295adbd20648c235e7821b4ca1d.json
|
│ │ │ ├── 0b8f8e3b6e4d84256e784dabb96f5646fa6e7295adbd20648c235e7821b4ca1d.json
|
||||||
│ │ │ ├── 0e6bfa570caca1f79f549f5cdc37f8bdf9c9b43a023ca28a8fcb83351389b399.json
|
│ │ │ ├── 0e6bfa570caca1f79f549f5cdc37f8bdf9c9b43a023ca28a8fcb83351389b399.json
|
||||||
│ │ │ ├── 100f37182d082767395fcd09f1ec937fd74e57ed039885843bf81d349c3fc1f5.json
|
│ │ │ ├── 100f37182d082767395fcd09f1ec937fd74e57ed039885843bf81d349c3fc1f5.json
|
||||||
|
│ │ │ ├── 10615f5a999e04257afc03cdc52e1b9e4e11ae7818d7c64ae13b976e3974feb4.json
|
||||||
|
│ │ │ ├── 11042a6b38137d4e725c18d536cde9c98652782cb58390aa6216dacad2d18cf5.json
|
||||||
|
│ │ │ ├── 1285cdf9b89689e3ac99cea19470a3c14fdbf3a9d1bb2cd8da2cfdbd42ca940f.json
|
||||||
│ │ │ ├── 128b2b616c097fbfbcbc3fddafc224df0919ba261bb5ded7bcafa8b445fe118b.json
|
│ │ │ ├── 128b2b616c097fbfbcbc3fddafc224df0919ba261bb5ded7bcafa8b445fe118b.json
|
||||||
│ │ │ ├── 12d922593c75865b8068057f9244ad61a5ee573b7f3f38a16c31fa83abb9d668.json
|
│ │ │ ├── 12d922593c75865b8068057f9244ad61a5ee573b7f3f38a16c31fa83abb9d668.json
|
||||||
|
│ │ │ ├── 13a39530fbb076496d510363b40bf16093d8d117751e7240bcc09a447bdace35.json
|
||||||
│ │ │ ├── 13ca1f37c3eea3c07228d2d98d47f7db6a9c1d8468b259651f3fea5419651569.json
|
│ │ │ ├── 13ca1f37c3eea3c07228d2d98d47f7db6a9c1d8468b259651f3fea5419651569.json
|
||||||
|
│ │ │ ├── 14f64f9b156b6b520e7af189b303a2844728f3ab8bbe1e65fed49160b1431cb2.json
|
||||||
│ │ │ ├── 15be5091a5bfbf8f48c3f3ae344a980be5452eab00f054c001958aae96557514.json
|
│ │ │ ├── 15be5091a5bfbf8f48c3f3ae344a980be5452eab00f054c001958aae96557514.json
|
||||||
│ │ │ ├── 174e406c2649f2384921581cbf912945a95d54aab73707b9aa45752aaa7c54d7.json
|
│ │ │ ├── 174e406c2649f2384921581cbf912945a95d54aab73707b9aa45752aaa7c54d7.json
|
||||||
│ │ │ ├── 183467d5c1ee7daac78dfb428bd335c17e17272bd4510358f38fc5924140c816.json
|
│ │ │ ├── 183467d5c1ee7daac78dfb428bd335c17e17272bd4510358f38fc5924140c816.json
|
||||||
@@ -319,47 +369,88 @@ khatibi
|
|||||||
│ │ │ ├── 1d5a29a9dabb365c079039c788988636b66c3a4bd2dbe66b249df001c2b5396f.json
|
│ │ │ ├── 1d5a29a9dabb365c079039c788988636b66c3a4bd2dbe66b249df001c2b5396f.json
|
||||||
│ │ │ ├── 1d9f15f04104af07c60349f21bcd9f6f2c0e65c915e3e9c69040a10197b0fa6f.json
|
│ │ │ ├── 1d9f15f04104af07c60349f21bcd9f6f2c0e65c915e3e9c69040a10197b0fa6f.json
|
||||||
│ │ │ ├── 1dd0c6958a307a1e4e59c24f20225292277585dd8652b35537ede94fc04807ee.json
|
│ │ │ ├── 1dd0c6958a307a1e4e59c24f20225292277585dd8652b35537ede94fc04807ee.json
|
||||||
|
│ │ │ ├── 20652a8d348bf514ac266f30a6400f923674dae819d18046d008066943377804.json
|
||||||
│ │ │ ├── 21af53aa7322868a25b47458e59f339a8f6e58b7914fbe8e3389ab86e0b0b643.json
|
│ │ │ ├── 21af53aa7322868a25b47458e59f339a8f6e58b7914fbe8e3389ab86e0b0b643.json
|
||||||
│ │ │ ├── 22310d4d49f2beee611c83915933ea06e80ec562fd92c170345c556cd3772561.json
|
│ │ │ ├── 22310d4d49f2beee611c83915933ea06e80ec562fd92c170345c556cd3772561.json
|
||||||
│ │ │ ├── 2356cb9f067d43739312459bad2cb6d9ecbacac703e7e40a282d5b89d64332b3.json
|
│ │ │ ├── 2356cb9f067d43739312459bad2cb6d9ecbacac703e7e40a282d5b89d64332b3.json
|
||||||
|
│ │ │ ├── 23da13b308596ede7678e92e57041bcecdd8e9baff04b759ddf9a69803dd790a.json
|
||||||
│ │ │ ├── 23f4e3a62e03a38743659d94dd1350d17775da8be9175846e4ff3f38fa87c515.json
|
│ │ │ ├── 23f4e3a62e03a38743659d94dd1350d17775da8be9175846e4ff3f38fa87c515.json
|
||||||
|
│ │ │ ├── 24508e7a9b6fa93cdd28b9436bdb55fa24ad8f324baa95adb3afd37e301af9ac.json
|
||||||
│ │ │ ├── 2501a9885d6fa5292276ee8cc7db63a9a3c08605f38d199d425d33f650dac35e.json
|
│ │ │ ├── 2501a9885d6fa5292276ee8cc7db63a9a3c08605f38d199d425d33f650dac35e.json
|
||||||
│ │ │ ├── 25dbf169bbd8ae50f7cc568b4dbb7d259eb993ccddc9dbb20db96018ed093f5a.json
|
│ │ │ ├── 25dbf169bbd8ae50f7cc568b4dbb7d259eb993ccddc9dbb20db96018ed093f5a.json
|
||||||
│ │ │ ├── 28df8bbd275806fd77c086629211231bd1329fe8156f521b19b5c5979f0d3b51.json
|
│ │ │ ├── 28df8bbd275806fd77c086629211231bd1329fe8156f521b19b5c5979f0d3b51.json
|
||||||
│ │ │ ├── 294e2d390f56acc0445564d428d90c641d76496cbc457bcfb09876382e9d87ab.json
|
│ │ │ ├── 294e2d390f56acc0445564d428d90c641d76496cbc457bcfb09876382e9d87ab.json
|
||||||
|
│ │ │ ├── 2976c530804df2075019fedaf2b908eeeff37b2703310c7e031b653d1fec71e5.json
|
||||||
|
│ │ │ ├── 2c587e304ae13b0717d72f5099508220c82234a8a293219c2ed2c97df9152404.json
|
||||||
│ │ │ ├── 2f36d1565911fdcb414da716162085b0fdc4b92d9d2d3fbb9fecc403b1916766.json
|
│ │ │ ├── 2f36d1565911fdcb414da716162085b0fdc4b92d9d2d3fbb9fecc403b1916766.json
|
||||||
|
│ │ │ ├── 3089c169c5eb18e61b13a19751166813d18c7bbe35b938b2b149406ff349feda.json
|
||||||
|
│ │ │ ├── 3258762ed4d4077750967504aecb42df00c35a69571f5d4839d4cfebd9b1c838.json
|
||||||
|
│ │ │ ├── 3330ae8555dab8f7ff4e014183f014681feb3f40a0d8969f30a4b2e9658e27f8.json
|
||||||
│ │ │ ├── 338e7d5d2e7d3a7260ab74d9b7b19bbab6b9ce84e7036623060190d9e0cc4bcd.json
|
│ │ │ ├── 338e7d5d2e7d3a7260ab74d9b7b19bbab6b9ce84e7036623060190d9e0cc4bcd.json
|
||||||
|
│ │ │ ├── 33b32e27be802f61375110f7f4ac9419fd4eb633523e01e31774b874448849c1.json
|
||||||
│ │ │ ├── 3614ae46baf3e1b6efa17011c37407a5d2931aba1886d8f8bdf8e4e154ab6c3f.json
|
│ │ │ ├── 3614ae46baf3e1b6efa17011c37407a5d2931aba1886d8f8bdf8e4e154ab6c3f.json
|
||||||
|
│ │ │ ├── 370c10e9d6f01aa67799b1f2191d5749a69ce4541aa55cbf4a28d45732bd641b.json
|
||||||
|
│ │ │ ├── 38980449465279974120694e1c667f229049f75d656fe518196d9cadc2429155.json
|
||||||
|
│ │ │ ├── 3ba0d4c75ab8bfa5bc2a60dccb74197cb94bd161e526e54770a9b0a2aa757806.json
|
||||||
|
│ │ │ ├── 3c9f8987dc7426b442fe2a90c7de5d909fc85988e172e7722c11f24a2c331481.json
|
||||||
│ │ │ ├── 3ff644065147b53af40ea703202c0fd832114936fe67e6c9a30d15e3da915455.json
|
│ │ │ ├── 3ff644065147b53af40ea703202c0fd832114936fe67e6c9a30d15e3da915455.json
|
||||||
│ │ │ ├── 40e7fbe5e765171201ab8016acce76a28304e1339d9aeb5a9cc18da87c5604d2.json
|
│ │ │ ├── 40e7fbe5e765171201ab8016acce76a28304e1339d9aeb5a9cc18da87c5604d2.json
|
||||||
|
│ │ │ ├── 4162977f2c2251e7a8b331d7229bd43dff56df31356191b19886b5bf5d56377d.json
|
||||||
│ │ │ ├── 41952e7c846da15fe658603ff001dafdb0e12d3372a7f0306adfa3a71de8bee7.json
|
│ │ │ ├── 41952e7c846da15fe658603ff001dafdb0e12d3372a7f0306adfa3a71de8bee7.json
|
||||||
|
│ │ │ ├── 4204828f3cbb3465c469e28345e0fc4d7ff9a84e30e3dd9f00ab4dbe27014e0d.json
|
||||||
│ │ │ ├── 458c1b9a3ce257e789f75305939ab2e79029c557657102eebf2082e50a47a0e5.json
|
│ │ │ ├── 458c1b9a3ce257e789f75305939ab2e79029c557657102eebf2082e50a47a0e5.json
|
||||||
│ │ │ ├── 45f7e59ef3f291171180b6763e8da408d8b7579bb7cb96dd4ca859e48aa5ce14.json
|
│ │ │ ├── 45f7e59ef3f291171180b6763e8da408d8b7579bb7cb96dd4ca859e48aa5ce14.json
|
||||||
|
│ │ │ ├── 46616763804b2b938d73c75f7651158e94803e0b86b3a759ecdd59375759eea3.json
|
||||||
|
│ │ │ ├── 46bde576f6f1e69457e4ad33238fdbd602d2a7f027644a84cb81068215fbd5e1.json
|
||||||
|
│ │ │ ├── 4da7df08a0ef803551d404438fe24d6c8cc11d9a2c3cc8d85e37d5228ba3a166.json
|
||||||
|
│ │ │ ├── 4e24d52f79dfa76aa2406de05583fa49ce966a28fb653fa284fb8e8204797385.json
|
||||||
|
│ │ │ ├── 4e604dd03a70463ae25f6615ea3ce4fe8e98fab15d6b11ddafd51d601062e84d.json
|
||||||
|
│ │ │ ├── 506e815cbeaaf3aa6e448dcbf70c10b4d118dff41e05a292fb8ec6d76dd391c4.json
|
||||||
|
│ │ │ ├── 507e42a0bbe7a582ce1fcb77651e1cfde643907ca0429fedfe11afe7ae3b11c4.json
|
||||||
│ │ │ ├── 52608b276c6f0a2f190186e5bac578715ad888ff5ba3c10ff30879c3baf9d9ce.json
|
│ │ │ ├── 52608b276c6f0a2f190186e5bac578715ad888ff5ba3c10ff30879c3baf9d9ce.json
|
||||||
|
│ │ │ ├── 527de91bd8bbc5a7ab79951457a9f192909faa9bbf02aaec72bb3d26e2298b57.json
|
||||||
│ │ │ ├── 5316515ee7fc3e1038e676b361d9028edc735993c3d34791a77a14503deb064d.json
|
│ │ │ ├── 5316515ee7fc3e1038e676b361d9028edc735993c3d34791a77a14503deb064d.json
|
||||||
|
│ │ │ ├── 53bcc38a002a291b8a370a1c24dde42e704cbda5d409d77b8405599e45844cae.json
|
||||||
│ │ │ ├── 5730d72cc177474584197921cce656696e3177780028c4629950f5514b530c6d.json
|
│ │ │ ├── 5730d72cc177474584197921cce656696e3177780028c4629950f5514b530c6d.json
|
||||||
│ │ │ ├── 57a18b5350b4a567305ca01b2951efee6eeb3b289b5ffbbf390c3962ff29d61c.json
|
│ │ │ ├── 57a18b5350b4a567305ca01b2951efee6eeb3b289b5ffbbf390c3962ff29d61c.json
|
||||||
│ │ │ ├── 58ad02e0930221b4f44603dbcf01d3b5d07d9da2772edafe48a94a9894ea148a.json
|
│ │ │ ├── 58ad02e0930221b4f44603dbcf01d3b5d07d9da2772edafe48a94a9894ea148a.json
|
||||||
│ │ │ ├── 58bfae9cf8467d2bb9363884d517e98cae38a934f4c474e9e02aed520fdaab95.json
|
│ │ │ ├── 58bfae9cf8467d2bb9363884d517e98cae38a934f4c474e9e02aed520fdaab95.json
|
||||||
│ │ │ ├── 58f2e3419fb2de4a3bdc4a9c0bf48e50f2cc225506096109dbf2b9bfe4778bb5.json
|
│ │ │ ├── 58f2e3419fb2de4a3bdc4a9c0bf48e50f2cc225506096109dbf2b9bfe4778bb5.json
|
||||||
│ │ │ ├── 5901400fc36893a9ad85acd97eb73b691cb4b8d1440047c5f85c1534a2c7e492.json
|
│ │ │ ├── 5901400fc36893a9ad85acd97eb73b691cb4b8d1440047c5f85c1534a2c7e492.json
|
||||||
|
│ │ │ ├── 59dd2e5089bebfb0397e9c492c890c619fcaa4d73b9d035e533320f276e1ebf9.json
|
||||||
|
│ │ │ ├── 5a15a341e370fb512c0b3f5ae1c30ec0d8a6729a690f286369015d107210124e.json
|
||||||
|
│ │ │ ├── 5b59421d9713dc21f9f77603d9708edd8c3eb8ba198636f80f1cedcc79ffcaca.json
|
||||||
│ │ │ ├── 5b63b5d7fc7a09b2dbf5e2f7db6fe00ccab773762b3291bd666c8e29d32100d3.json
|
│ │ │ ├── 5b63b5d7fc7a09b2dbf5e2f7db6fe00ccab773762b3291bd666c8e29d32100d3.json
|
||||||
|
│ │ │ ├── 5c8593cf08ce32f0c569aacfaf83b8baeb352934f6f8749aa4f652d5ce2acff1.json
|
||||||
|
│ │ │ ├── 5c922734de6f6345b5230507bee5c43dc284c308901e84846666b939493e6258.json
|
||||||
│ │ │ ├── 5ce9ee869c507986bf00ae6929f3aae3ab7aa58b5c2bded4965374fcf7f5654b.json
|
│ │ │ ├── 5ce9ee869c507986bf00ae6929f3aae3ab7aa58b5c2bded4965374fcf7f5654b.json
|
||||||
│ │ │ ├── 5d5b6249e3cfa0aeee94ba2053fc73b4b7b5b8fa1d21df09d40bda7c90ef273d.json
|
│ │ │ ├── 5d5b6249e3cfa0aeee94ba2053fc73b4b7b5b8fa1d21df09d40bda7c90ef273d.json
|
||||||
|
│ │ │ ├── 5d994876cad71133f4bf804932fb2604ffdc8bb8cc434ca1434667ef789144e7.json
|
||||||
|
│ │ │ ├── 5e19aa3aba5509cd48a9a93b39db071cfc2c2728ccae46b34caa0e214cc6be01.json
|
||||||
|
│ │ │ ├── 5f050da3769e151e52138ee1ea5dcbcb6160cb57952c75665e607628d0e08d67.json
|
||||||
│ │ │ ├── 60002a01d6fc22147e2994d1a1a447d5e577c02aed2443789af20571ec2d2a19.json
|
│ │ │ ├── 60002a01d6fc22147e2994d1a1a447d5e577c02aed2443789af20571ec2d2a19.json
|
||||||
│ │ │ ├── 60e00560038df059b1696e54dcf055cd658677d6e1201570a8500531addc0ac6.json
|
│ │ │ ├── 60e00560038df059b1696e54dcf055cd658677d6e1201570a8500531addc0ac6.json
|
||||||
|
│ │ │ ├── 62d88411a61ee9d3b60dda6c4febf33539b8b9e8226b5d2b9205f8e2b0c4478a.json
|
||||||
│ │ │ ├── 63c38f2a8abe98c0d1e9cb5b7d9fd0451e1e46c2bcbf1d2c3b241b263ac0a9cd.json
|
│ │ │ ├── 63c38f2a8abe98c0d1e9cb5b7d9fd0451e1e46c2bcbf1d2c3b241b263ac0a9cd.json
|
||||||
│ │ │ ├── 6448d65fdcfe330b5d4278c8d1540e7b075238672b8101f9d2e1c4052f7c9c49.json
|
│ │ │ ├── 6448d65fdcfe330b5d4278c8d1540e7b075238672b8101f9d2e1c4052f7c9c49.json
|
||||||
│ │ │ ├── 64fc04d8c553b2121a71955d48257160a40f6dd34258e3287945873f02d897dc.json
|
│ │ │ ├── 64fc04d8c553b2121a71955d48257160a40f6dd34258e3287945873f02d897dc.json
|
||||||
│ │ │ ├── 65d5139792e0ee840c5b1710d7659ecfb5dbdcadbd02d205f7517a6a3a374211.json
|
│ │ │ ├── 65d5139792e0ee840c5b1710d7659ecfb5dbdcadbd02d205f7517a6a3a374211.json
|
||||||
|
│ │ │ ├── 6603c5eb7403f7426c429341d43018fdffcb3430f22972c426ac43956d229b33.json
|
||||||
|
│ │ │ ├── 6648bc2b25d0bd25846d48f406d3e9a6f89677542600e9f3f57533747ed9b08c.json
|
||||||
|
│ │ │ ├── 6691dea01045e5561ac1c72f902f96ba84564c221498829443f793fee8784021.json
|
||||||
|
│ │ │ ├── 6805914035b171d32a1b80948179da15f78c5741e5a80b216d51a6586185c0c3.json
|
||||||
│ │ │ ├── 6876f75a9cb4a5f5857c47e3b0d6b38e51941aff76bc6d92cb1b4eef1c51071e.json
|
│ │ │ ├── 6876f75a9cb4a5f5857c47e3b0d6b38e51941aff76bc6d92cb1b4eef1c51071e.json
|
||||||
│ │ │ ├── 6a88f82e5b9f9da12b50f8531eac30c10e1c2a0cf39601d275fd90ee3e28da55.json
|
│ │ │ ├── 6a88f82e5b9f9da12b50f8531eac30c10e1c2a0cf39601d275fd90ee3e28da55.json
|
||||||
│ │ │ ├── 6aca679f90689aa4c275389c8b4e2462a9456775e8b124071070af2c988ca0fc.json
|
│ │ │ ├── 6aca679f90689aa4c275389c8b4e2462a9456775e8b124071070af2c988ca0fc.json
|
||||||
|
│ │ │ ├── 6afa6136d9df8317bdd8d49f4109412776deb21ba123fdd2a97331b400a0230d.json
|
||||||
|
│ │ │ ├── 6bfbb9eed6842bf331dec78724d7e740919c76345b54be844964f44640482a87.json
|
||||||
│ │ │ ├── 6d4d7d7f829f13e46e4898aceb760d66d2255dc838eab0933f9a92d720f420a7.json
|
│ │ │ ├── 6d4d7d7f829f13e46e4898aceb760d66d2255dc838eab0933f9a92d720f420a7.json
|
||||||
│ │ │ ├── 6df9c1e88224e95c55dee4238fa91358360231ee47362294e884bc6e24d73612.json
|
│ │ │ ├── 6df9c1e88224e95c55dee4238fa91358360231ee47362294e884bc6e24d73612.json
|
||||||
│ │ │ ├── 6e262456570c174e207055c7c964fe85431addb0f59298b1e4d912500dfb23a3.json
|
│ │ │ ├── 6e262456570c174e207055c7c964fe85431addb0f59298b1e4d912500dfb23a3.json
|
||||||
│ │ │ ├── 6e2ed39911a83c75e432ffd0dab22fd095ce8c5bb186d6ab22d594375f3b32bc.json
|
│ │ │ ├── 6e2ed39911a83c75e432ffd0dab22fd095ce8c5bb186d6ab22d594375f3b32bc.json
|
||||||
|
│ │ │ ├── 6ed362dbf43e46eeb440246d52fdc4e0783c895114249ea33f3de7ddf6898a3a.json
|
||||||
│ │ │ ├── 71a07328d56403d55714d76cefc05db3e68c6498719e629e4e76889a1940efc2.json
|
│ │ │ ├── 71a07328d56403d55714d76cefc05db3e68c6498719e629e4e76889a1940efc2.json
|
||||||
|
│ │ │ ├── 72577fde1eae8cf079aff518cecce910ac310cefcc038fbbcb0a5d11f20fa20d.json
|
||||||
│ │ │ ├── 73a129efd08cc38274f3c56503c234eef17f58faf74dadbdbbe475efcbfc2777.json
|
│ │ │ ├── 73a129efd08cc38274f3c56503c234eef17f58faf74dadbdbbe475efcbfc2777.json
|
||||||
│ │ │ ├── 7483cdf00a2d7de8809278d9dcdf2f2330354b02117952da4b9452d4bbd6b81e.json
|
│ │ │ ├── 7483cdf00a2d7de8809278d9dcdf2f2330354b02117952da4b9452d4bbd6b81e.json
|
||||||
│ │ │ ├── 74efb913c62b374d818cfc5af6d7ee9bb46966eb2bce0e26e0bba461683b98a7.json
|
│ │ │ ├── 74efb913c62b374d818cfc5af6d7ee9bb46966eb2bce0e26e0bba461683b98a7.json
|
||||||
@@ -369,107 +460,176 @@ khatibi
|
|||||||
│ │ │ ├── 767dd58146a5d4a713611561a9e5c1b706e5f509a5621891c9b2e195f3b8ee0a.json
|
│ │ │ ├── 767dd58146a5d4a713611561a9e5c1b706e5f509a5621891c9b2e195f3b8ee0a.json
|
||||||
│ │ │ ├── 77a1520156879de714ee872c29b1387967bcc5ce470b47d37087ea2a110f3875.json
|
│ │ │ ├── 77a1520156879de714ee872c29b1387967bcc5ce470b47d37087ea2a110f3875.json
|
||||||
│ │ │ ├── 780977bca042ffdde1cca2811a3a46c36bd3fcff2723c94ac527428113131c64.json
|
│ │ │ ├── 780977bca042ffdde1cca2811a3a46c36bd3fcff2723c94ac527428113131c64.json
|
||||||
|
│ │ │ ├── 784dd791c21e8837a3e925331cf234b2138ded3d2012968739604d5535a18e09.json
|
||||||
│ │ │ ├── 7be7f428e6acfe4c99370707cc7de9253f801feff1564088f2584d5dc8c42a89.json
|
│ │ │ ├── 7be7f428e6acfe4c99370707cc7de9253f801feff1564088f2584d5dc8c42a89.json
|
||||||
|
│ │ │ ├── 7bf404104bb32fc04568c2d287891fa69955ac3f46718c99dcca64a4bfff6fe3.json
|
||||||
│ │ │ ├── 7c4fc90402a7836b364425c33cc2ffc98e7634391ebe78143df03c88945d8988.json
|
│ │ │ ├── 7c4fc90402a7836b364425c33cc2ffc98e7634391ebe78143df03c88945d8988.json
|
||||||
|
│ │ │ ├── 7d106c54b917d9e26357fa71aa871e94cb99c940b6c48808e93cd57415da91e3.json
|
||||||
|
│ │ │ ├── 7d59da18a8b644e48a164eb8a764efc9f4c3538b99d41c8e93ce38cc5556ba05.json
|
||||||
│ │ │ ├── 7e9bad82d0eb642b739dcf11e86c2fcf2005dfb12c94478af143deb9a8129bfa.json
|
│ │ │ ├── 7e9bad82d0eb642b739dcf11e86c2fcf2005dfb12c94478af143deb9a8129bfa.json
|
||||||
│ │ │ ├── 8055bf9fe36a9fa183d0c5dc152684e65410d58c5634e544e8f38f6c9216f6ad.json
|
│ │ │ ├── 8055bf9fe36a9fa183d0c5dc152684e65410d58c5634e544e8f38f6c9216f6ad.json
|
||||||
|
│ │ │ ├── 81d6d00c4c975b3289ac212d355856c3d6e60bccde0780fe2e3fc8cac31f3c19.json
|
||||||
│ │ │ ├── 82833f6235cb6559bad38e93372d42284eb4e9097e90519337266113da2495bf.json
|
│ │ │ ├── 82833f6235cb6559bad38e93372d42284eb4e9097e90519337266113da2495bf.json
|
||||||
│ │ │ ├── 828dacee98d6f71fcf53c8a051b3bf1bfc9d94c272a79ed8539801e3fb90b3df.json
|
│ │ │ ├── 828dacee98d6f71fcf53c8a051b3bf1bfc9d94c272a79ed8539801e3fb90b3df.json
|
||||||
|
│ │ │ ├── 837801d7d0e218e49de562a7e03711e75941d42ce5cc2f77552bc6293a45a4e8.json
|
||||||
│ │ │ ├── 84281ba94e70a71d52b5179cf2e933f8412da3a9eaf5e7177b815f8fadd0afc5.json
|
│ │ │ ├── 84281ba94e70a71d52b5179cf2e933f8412da3a9eaf5e7177b815f8fadd0afc5.json
|
||||||
│ │ │ ├── 846ba1087a1ee6b5cda2ebdf22d0bc61ad9175b21aba29783429277288dbfaaf.json
|
│ │ │ ├── 846ba1087a1ee6b5cda2ebdf22d0bc61ad9175b21aba29783429277288dbfaaf.json
|
||||||
|
│ │ │ ├── 84cef5656079ececae35930e1b26620186b50f25fd977c569537daad2d59db86.json
|
||||||
│ │ │ ├── 86e57806cd08cf765ce1f56f0638dd321e93c653bb6613aa35d7ad375808433c.json
|
│ │ │ ├── 86e57806cd08cf765ce1f56f0638dd321e93c653bb6613aa35d7ad375808433c.json
|
||||||
|
│ │ │ ├── 881adf7185e68d203906537117de576a11fba814941cf9eeedd0dfac2d65e536.json
|
||||||
│ │ │ ├── 88e349e879d601e1f5c75f3b019ffb4958e90639317ba893c54f82b0cc9066c2.json
|
│ │ │ ├── 88e349e879d601e1f5c75f3b019ffb4958e90639317ba893c54f82b0cc9066c2.json
|
||||||
|
│ │ │ ├── 8a70e1324d7f834d2c41a7c3b4cbb3b8ccad92d413970316bc95e13f5132ff76.json
|
||||||
|
│ │ │ ├── 8d658a3a8b1ae03cbc881e3c47cf2cfe111f9be2b564fb3739cf3d9367845a8b.json
|
||||||
│ │ │ ├── 8ffb83ce6f13d2f97d0197c7381748e9b66956c13f0158bc519350b7dd1b9ad0.json
|
│ │ │ ├── 8ffb83ce6f13d2f97d0197c7381748e9b66956c13f0158bc519350b7dd1b9ad0.json
|
||||||
│ │ │ ├── 908e0129cefe129d0b562243460fe20793c84876244cef2e99ca1becf8cf6a1f.json
|
│ │ │ ├── 908e0129cefe129d0b562243460fe20793c84876244cef2e99ca1becf8cf6a1f.json
|
||||||
|
│ │ │ ├── 909129924b48dede801eb946cf98bb93003ab8116be2b6acbd2f7fe8dc166358.json
|
||||||
|
│ │ │ ├── 92ecdeb228d91f4adb6c1e9bfd1b3f0eea65faec8dbcb27fca3211d1d264cb46.json
|
||||||
│ │ │ ├── 940d0349821b0fb7f2c3effcb81fd13bb1c046daa6f6c5f2cd1b157058c55877.json
|
│ │ │ ├── 940d0349821b0fb7f2c3effcb81fd13bb1c046daa6f6c5f2cd1b157058c55877.json
|
||||||
│ │ │ ├── 957f37be1d5d6a5207f90ad07704f8423c915c5bcdfcd130acc85a388fc89d19.json
|
│ │ │ ├── 957f37be1d5d6a5207f90ad07704f8423c915c5bcdfcd130acc85a388fc89d19.json
|
||||||
|
│ │ │ ├── 95e88ece49092778e7709f6b87db4c05dd055b838cfdf20d7d4705b6373739ba.json
|
||||||
│ │ │ ├── 96c3ba545db191ae5811d5dfdde74e7c6396a932b759513c94160cb7b7c81fc8.json
|
│ │ │ ├── 96c3ba545db191ae5811d5dfdde74e7c6396a932b759513c94160cb7b7c81fc8.json
|
||||||
|
│ │ │ ├── 96f5469edd566b92d9d38423a27779142fbe2b77cfc9d85005d43ffc99ce3de1.json
|
||||||
│ │ │ ├── 9766508e5a013c3d3cbb2602719391199e90af5720c9782a77ca71565aa346b1.json
|
│ │ │ ├── 9766508e5a013c3d3cbb2602719391199e90af5720c9782a77ca71565aa346b1.json
|
||||||
│ │ │ ├── 9880af8b991c19abe5ed019ad7b7291b8b281ed9dcb894a00c3fd8259a59b4a4.json
|
│ │ │ ├── 9880af8b991c19abe5ed019ad7b7291b8b281ed9dcb894a00c3fd8259a59b4a4.json
|
||||||
│ │ │ ├── 996411678d23c3cf8a0e6c1c5076701e8bf5f3dad21ca907ec4116d15366a668.json
|
│ │ │ ├── 996411678d23c3cf8a0e6c1c5076701e8bf5f3dad21ca907ec4116d15366a668.json
|
||||||
|
│ │ │ ├── 9c2c2f6f8d4d4c3a9a60da3ed38a26dc789e8cfaff5d46d140d2f208e5fef3c3.json
|
||||||
│ │ │ ├── 9cfb6e4f7b08a863675af032e7823c0523170f5201e686c5ef72b39c04b07a12.json
|
│ │ │ ├── 9cfb6e4f7b08a863675af032e7823c0523170f5201e686c5ef72b39c04b07a12.json
|
||||||
│ │ │ ├── 9eabdb0cacba38c8fb6b3e777932e06bd82172cdc0bcccf2fc0bfc23b5bd4974.json
|
│ │ │ ├── 9eabdb0cacba38c8fb6b3e777932e06bd82172cdc0bcccf2fc0bfc23b5bd4974.json
|
||||||
|
│ │ │ ├── a186e74ebd87e23212ffe7fd72c40e9383c0624a3efdb2fe1e3271d9ea96d9b9.json
|
||||||
│ │ │ ├── a31ab0e36b30049a602cc060d77c0715187f7c3838db91b8c20bfaa802f49899.json
|
│ │ │ ├── a31ab0e36b30049a602cc060d77c0715187f7c3838db91b8c20bfaa802f49899.json
|
||||||
|
│ │ │ ├── a43d68ff03b49e9153a73777c0bde346a6b1969f640d67eae500d60d410a02c0.json
|
||||||
|
│ │ │ ├── a7c605f57dd37c8725a92f08d4fe18701628701fa986cbb98d3a4aeb3a74abbe.json
|
||||||
│ │ │ ├── a823c9c28027c8d525714e6571bc3f3ca38f3bd0420aaf4f4921067cf0672475.json
|
│ │ │ ├── a823c9c28027c8d525714e6571bc3f3ca38f3bd0420aaf4f4921067cf0672475.json
|
||||||
│ │ │ ├── a8311f6e51027ea4606e35bf14d50edc1795e71b2d63cddeae8c7bb7ece215b8.json
|
│ │ │ ├── a8311f6e51027ea4606e35bf14d50edc1795e71b2d63cddeae8c7bb7ece215b8.json
|
||||||
|
│ │ │ ├── aabf6dbb2202699bb1b06fd582c88465b9cf2b8cc060bda2e2bc0c67fc493bed.json
|
||||||
│ │ │ ├── add004e78a62164d7565c102eb825debea20811f7fe76afa0567a1b6b289f264.json
|
│ │ │ ├── add004e78a62164d7565c102eb825debea20811f7fe76afa0567a1b6b289f264.json
|
||||||
│ │ │ ├── aeaae7b60339b1283cab517b4ab42806c64933c272d007a68431988ba8e0edb5.json
|
│ │ │ ├── aeaae7b60339b1283cab517b4ab42806c64933c272d007a68431988ba8e0edb5.json
|
||||||
|
│ │ │ ├── af128ca7fc8d02ae40fff36925a5b96034862ce0d581e22a8a0c88f41040e526.json
|
||||||
│ │ │ ├── b26c22033963df1727407c1fa6c79d8241bfc16056e7a8d3c4383fb2d050d3cd.json
|
│ │ │ ├── b26c22033963df1727407c1fa6c79d8241bfc16056e7a8d3c4383fb2d050d3cd.json
|
||||||
│ │ │ ├── b26e251bde84a040dbb2d1a4f700ebcd761f51a124a98a104704e82537e320eb.json
|
│ │ │ ├── b26e251bde84a040dbb2d1a4f700ebcd761f51a124a98a104704e82537e320eb.json
|
||||||
│ │ │ ├── b34cb0a1a28e753a900b3d23f01d3810f9c06635b0650d34be4ec9d98daa8fbb.json
|
│ │ │ ├── b34cb0a1a28e753a900b3d23f01d3810f9c06635b0650d34be4ec9d98daa8fbb.json
|
||||||
│ │ │ ├── b543952627caaf0a5c52bec09b33fae93443fca7234365680bbe75acc260dd1c.json
|
│ │ │ ├── b543952627caaf0a5c52bec09b33fae93443fca7234365680bbe75acc260dd1c.json
|
||||||
│ │ │ ├── b59999c2fd322c241c7bd37e6ed0a95c9124cbb79f8424f1fa7cb2e92fe63817.json
|
│ │ │ ├── b59999c2fd322c241c7bd37e6ed0a95c9124cbb79f8424f1fa7cb2e92fe63817.json
|
||||||
│ │ │ ├── b82d5f6840b4930eb62fdb1c7b3a58124fff805e529ede7684516a65322fcae0.json
|
│ │ │ ├── b82d5f6840b4930eb62fdb1c7b3a58124fff805e529ede7684516a65322fcae0.json
|
||||||
|
│ │ │ ├── b9f8ea31056fe0c0fc40a84640c58f29895312c65bf694567487204d8f1170e9.json
|
||||||
│ │ │ ├── bb140cfe12cfb3e32dc9815393035fc33c7649c2faeeccb82cdee56a930ea635.json
|
│ │ │ ├── bb140cfe12cfb3e32dc9815393035fc33c7649c2faeeccb82cdee56a930ea635.json
|
||||||
|
│ │ │ ├── bc24ddb05a8aa685edc1376cbd110423adf05bcc488fb97a30fe286e7e74c2da.json
|
||||||
|
│ │ │ ├── bc6f93c7938b3565d50cdfc4abe4fd62f0739fd6299f35d7c50075cd860cab58.json
|
||||||
│ │ │ ├── bcea7c584e5031a9c5734144f8169d016ef1ed316e6f4827609509ba2a79d3eb.json
|
│ │ │ ├── bcea7c584e5031a9c5734144f8169d016ef1ed316e6f4827609509ba2a79d3eb.json
|
||||||
|
│ │ │ ├── bd00f0b0db2eb10f7e889df5a0f08994e8c1eaa0eb69be8398e45e0e634724fd.json
|
||||||
│ │ │ ├── bd6e156d8e427b14676b4707710f2b340026e679cb7bd29b22b247a4b637e7bd.json
|
│ │ │ ├── bd6e156d8e427b14676b4707710f2b340026e679cb7bd29b22b247a4b637e7bd.json
|
||||||
│ │ │ ├── be1200d7c9b2881ba54c7fa95daf5f3e2ce993619b974c7760a9d44a416e00c0.json
|
│ │ │ ├── be1200d7c9b2881ba54c7fa95daf5f3e2ce993619b974c7760a9d44a416e00c0.json
|
||||||
|
│ │ │ ├── be72e639c12e860a2120c3ecf96b94c86e5c69b2f1f0715cd2804d0475409cd6.json
|
||||||
|
│ │ │ ├── c12baf851ec40542280cb62d2f878d5e7383357365281938cfb55a64c08a9a10.json
|
||||||
|
│ │ │ ├── c36b696b57c1deb51e47a057b32d1dd75e3f1560e83931aab1f8ba86abcc1543.json
|
||||||
│ │ │ ├── c46ad3fc8fd3db1315ab2e40add760b44e8f98c24c79e334c342c6a411fc671c.json
|
│ │ │ ├── c46ad3fc8fd3db1315ab2e40add760b44e8f98c24c79e334c342c6a411fc671c.json
|
||||||
│ │ │ ├── c52300dce4cf464158f8db56df5030359a501a81bbe4a5f36e50c8c38a5ac48c.json
|
│ │ │ ├── c52300dce4cf464158f8db56df5030359a501a81bbe4a5f36e50c8c38a5ac48c.json
|
||||||
|
│ │ │ ├── c535c8a7523b768d28903b84f4cfbce7d62181a2a05f3db08ec3a7c8cbf86ce8.json
|
||||||
|
│ │ │ ├── c56beb43cf063fd1cc92950ca14695b543b3971bf7e7dbd4a676242057258533.json
|
||||||
│ │ │ ├── c5c93ccc7715c31fc7538bcd9327c8fb7c1e495bd17d6444b384ec426b7a231f.json
|
│ │ │ ├── c5c93ccc7715c31fc7538bcd9327c8fb7c1e495bd17d6444b384ec426b7a231f.json
|
||||||
│ │ │ ├── c72226544540cc75217ba02f1d09d989eb810d879409ac086c76245f27220cfa.json
|
│ │ │ ├── c72226544540cc75217ba02f1d09d989eb810d879409ac086c76245f27220cfa.json
|
||||||
│ │ │ ├── c795f43bd1f10cf666ff987d18af538b2164c1683629ac689f0a54cb875118be.json
|
│ │ │ ├── c795f43bd1f10cf666ff987d18af538b2164c1683629ac689f0a54cb875118be.json
|
||||||
│ │ │ ├── c7eb97fa16701c6d9ee531fa032479a7c771292f3e9d8921d3ea12a4897fd43a.json
|
│ │ │ ├── c7eb97fa16701c6d9ee531fa032479a7c771292f3e9d8921d3ea12a4897fd43a.json
|
||||||
│ │ │ ├── c846b5de286163b5eed16ff6d6e00f2038f30bf7edd2ab3f8d831369e190ae18.json
|
│ │ │ ├── c846b5de286163b5eed16ff6d6e00f2038f30bf7edd2ab3f8d831369e190ae18.json
|
||||||
|
│ │ │ ├── c850d65440fa6ea38db7300c89c5052b3db430ea2c7d20d35cf38c6b41807d0d.json
|
||||||
|
│ │ │ ├── c8719ca6bbbd7a4815ce5f982745c94d54d2f753312c08b96964ab7ead661291.json
|
||||||
|
│ │ │ ├── ca0c19553fc1b40bdec0973b3e0c5363d6bbbf69d9961567483ba1c1cb9023eb.json
|
||||||
|
│ │ │ ├── cb3b2a1c1afd8d0837cc29b5040ec984dc0c0070af2135d0b0e5cb5ff726f2a7.json
|
||||||
│ │ │ ├── cdc5059cf09416d24d03fe57a48af5219727866c84b8109d986456fbaa789dc4.json
|
│ │ │ ├── cdc5059cf09416d24d03fe57a48af5219727866c84b8109d986456fbaa789dc4.json
|
||||||
|
│ │ │ ├── cf17cbc72af5f2b56eff6cc03157817d7552c6c5e4565e97278e02ae69a362f3.json
|
||||||
│ │ │ ├── cf6668c54cd4f65d268a17df44f100e1cb77f91eae90dd2dc052564731b2f195.json
|
│ │ │ ├── cf6668c54cd4f65d268a17df44f100e1cb77f91eae90dd2dc052564731b2f195.json
|
||||||
│ │ │ ├── d1289d5406d360e3ff617e213db4e262cd43a32711a61aba97bdcd0a906cbcb5.json
|
│ │ │ ├── d1289d5406d360e3ff617e213db4e262cd43a32711a61aba97bdcd0a906cbcb5.json
|
||||||
|
│ │ │ ├── d12cd2b31145213be1b4d89e7be64f3aecc122841cdeac7660eceae3dd871f01.json
|
||||||
|
│ │ │ ├── d12dccf07d58d7c0a2b5ea5b6aa821aa419fdc8b34a48939ebf9c565bc439551.json
|
||||||
|
│ │ │ ├── d1f2090208e1aab3b0e377278b02e9fc1007e5e1724c9000fb69d050365c8660.json
|
||||||
│ │ │ ├── d3456cd690135f9630e6b0bb79fef5d205804c7acabffc61cbd5ac4032b17bc7.json
|
│ │ │ ├── d3456cd690135f9630e6b0bb79fef5d205804c7acabffc61cbd5ac4032b17bc7.json
|
||||||
|
│ │ │ ├── d51b2eb414ee4ea432987cd45d6b623013e994a6080c89cde57b69004b576e9d.json
|
||||||
|
│ │ │ ├── d5676c133bc21ec531e7c17c5e9416781e3459852a4e6e1fac9c77ecdb5c05a2.json
|
||||||
|
│ │ │ ├── d606b856d99eb59fe3e5378e597bbeff679c812e9f1b173ecced4639ede8ad11.json
|
||||||
│ │ │ ├── d88e1e365cafd5d26ce47c2a0fe1f067dc3bfe5ceed1f75a21393fae950be939.json
|
│ │ │ ├── d88e1e365cafd5d26ce47c2a0fe1f067dc3bfe5ceed1f75a21393fae950be939.json
|
||||||
|
│ │ │ ├── d899e8cc575f125b908d78f0c8d34408a4af1972ee0544d0517f8d1933239214.json
|
||||||
│ │ │ ├── d94bebfe941bb0acc30b633423eaee11690652709e5f8f28d157473d31a07ee3.json
|
│ │ │ ├── d94bebfe941bb0acc30b633423eaee11690652709e5f8f28d157473d31a07ee3.json
|
||||||
│ │ │ ├── db68b8db5217e2295eb1195760d7352960627da044a4c4f8f6b89c67ad2f8f57.json
|
│ │ │ ├── db68b8db5217e2295eb1195760d7352960627da044a4c4f8f6b89c67ad2f8f57.json
|
||||||
|
│ │ │ ├── db794bbcafe219af962673ef26efb1ee1eacf3923e13113352f3eedfd3192d23.json
|
||||||
│ │ │ ├── dbbdad9d992922aff62ad3262a531716cfc32e2f15f600773b9a0cf10545058d.json
|
│ │ │ ├── dbbdad9d992922aff62ad3262a531716cfc32e2f15f600773b9a0cf10545058d.json
|
||||||
│ │ │ ├── dc0344515d1b3ffe8643560516b88418be0b306fba56101b8a630ed73d48db46.json
|
│ │ │ ├── dc0344515d1b3ffe8643560516b88418be0b306fba56101b8a630ed73d48db46.json
|
||||||
|
│ │ │ ├── dcad55d51bb4494a642af555d5bfe78c5b8ee3d11de1b3d8f4286d608980854b.json
|
||||||
|
│ │ │ ├── e08c4784f0f82403ff24a1676e3d75e11a208aadf8fe94a723461c9658afd1e0.json
|
||||||
│ │ │ ├── e16f78807ff202fa2c81867b321acfacede2d24cc785856eef0633d8d352796b.json
|
│ │ │ ├── e16f78807ff202fa2c81867b321acfacede2d24cc785856eef0633d8d352796b.json
|
||||||
│ │ │ ├── e1a7be59904a3dc503c5b12bddc7604c6783b02462c8ec76834c65c5610c3bda.json
|
│ │ │ ├── e1a7be59904a3dc503c5b12bddc7604c6783b02462c8ec76834c65c5610c3bda.json
|
||||||
|
│ │ │ ├── e2967fb2aed9420b2cdf823d95f971b7038323c97a6ed139ce63b7a1912381d1.json
|
||||||
|
│ │ │ ├── e2c62f6012dcae176d19340f6db8db3e94547ebbc4a21577b63caf35cc72ecfb.json
|
||||||
│ │ │ ├── e3ee8d9942c74a74be83ca00d5438e4a1e1cb5b23f55d95d308a42954e4757f0.json
|
│ │ │ ├── e3ee8d9942c74a74be83ca00d5438e4a1e1cb5b23f55d95d308a42954e4757f0.json
|
||||||
|
│ │ │ ├── e86dc6bb7703d4329623bd33ccbc5e5e6c6daebf8d36d84fd415d6f868c779b0.json
|
||||||
|
│ │ │ ├── e8e380264b41f789dddd25b7af5d0e3c517b957a14fe8ba2ce7bb8a529e005cf.json
|
||||||
│ │ │ ├── e9589ded446a5befb285a0a2e93a182ca9431c76661e68c9210ed252580dd7f0.json
|
│ │ │ ├── e9589ded446a5befb285a0a2e93a182ca9431c76661e68c9210ed252580dd7f0.json
|
||||||
│ │ │ ├── e9e14d581ed59799b2d83c67402e8555b5b59e7d49425950ec490422bd0307d9.json
|
│ │ │ ├── e9e14d581ed59799b2d83c67402e8555b5b59e7d49425950ec490422bd0307d9.json
|
||||||
|
│ │ │ ├── eaceb29a907e3e67bed5255cc1f8b8b3eab9d9946f4d4df2cc99070151285029.json
|
||||||
│ │ │ ├── ec07e022005d3513838eee7e6618ff060e25d914cab7745aa6172f22d4089497.json
|
│ │ │ ├── ec07e022005d3513838eee7e6618ff060e25d914cab7745aa6172f22d4089497.json
|
||||||
|
│ │ │ ├── eccad1f007b52643ce8d81576efb7d1969ecbff2e9db288989fd9a9c21fd7288.json
|
||||||
│ │ │ ├── ecd9c1dbfa0a72e253adb680ae7088eff24c26360e53a65862b274ea1281ceae.json
|
│ │ │ ├── ecd9c1dbfa0a72e253adb680ae7088eff24c26360e53a65862b274ea1281ceae.json
|
||||||
│ │ │ ├── ee450e6251e62d8972efb53bc35145c4a65050a7b6db2954b84bda42fca7b704.json
|
│ │ │ ├── ee450e6251e62d8972efb53bc35145c4a65050a7b6db2954b84bda42fca7b704.json
|
||||||
│ │ │ ├── ee557c6ed214858651cd3ebceb5e0fd00999354a4c54bb379f26c64b48b509ab.json
|
│ │ │ ├── ee557c6ed214858651cd3ebceb5e0fd00999354a4c54bb379f26c64b48b509ab.json
|
||||||
│ │ │ ├── efd113025c3fb3c0becaecd8090e8494b13d92ff3c49220d54665b3a8af586af.json
|
│ │ │ ├── efd113025c3fb3c0becaecd8090e8494b13d92ff3c49220d54665b3a8af586af.json
|
||||||
|
│ │ │ ├── f081ba494afddbf02c2599aedee9fc9d38ccdb159ade7f07ce3bdad3323a001e.json
|
||||||
│ │ │ ├── f1af209a02f2ec5c5a344e3fa7dd8ad7d443e0479e55f5465449c0a2eed314a1.json
|
│ │ │ ├── f1af209a02f2ec5c5a344e3fa7dd8ad7d443e0479e55f5465449c0a2eed314a1.json
|
||||||
│ │ │ ├── f3a6285dcf9eb39df500bc0b76b03b5941103e8b90c06ffaeab1b3129f18fedc.json
|
│ │ │ ├── f3a6285dcf9eb39df500bc0b76b03b5941103e8b90c06ffaeab1b3129f18fedc.json
|
||||||
|
│ │ │ ├── f3fe81220df98d0410b1505fa995df65427035dd552c85f1d80e35bb75a9aba1.json
|
||||||
|
│ │ │ ├── f48decaf1220666fbda1a5328b963dbd999caab9ebbb1d84459d4c556a6e6ba7.json
|
||||||
│ │ │ ├── f537b36e2ee33d1303c10e950a6910cc7fd954c7fce3220be2c95d75d70dfd40.json
|
│ │ │ ├── f537b36e2ee33d1303c10e950a6910cc7fd954c7fce3220be2c95d75d70dfd40.json
|
||||||
|
│ │ │ ├── f72f6f3fe9d226e4717e158dedd318fff0a1d52ece54f965311c2b4f862dcfee.json
|
||||||
│ │ │ ├── f76726d2898f2a3ba7168d3fb77c21087432eb9c200631b664ecb3b1837aaf17.json
|
│ │ │ ├── f76726d2898f2a3ba7168d3fb77c21087432eb9c200631b664ecb3b1837aaf17.json
|
||||||
|
│ │ │ ├── f92b94479afedf47039d8821a77facb55d9a862338e85876b80cac3b41c844f6.json
|
||||||
|
│ │ │ ├── fb6825c4e9ffba13d17a2da18a1e07aef792089a7cd24b31e7f4f32ee39287e8.json
|
||||||
|
│ │ │ ├── fbfc351c35c5272de5612b4740e380b06a24c37f8074b977e35f62ad85a6dcc0.json
|
||||||
│ │ │ ├── fc6ae63ae2639372c4ae4fbe9718feca814261dda099ba58bb780d14c69086cd.json
|
│ │ │ ├── fc6ae63ae2639372c4ae4fbe9718feca814261dda099ba58bb780d14c69086cd.json
|
||||||
│ │ │ ├── fd714e03c631694d30457ac529d05d8d131c183665e086246da607f752dea5eb.json
|
│ │ │ ├── fd714e03c631694d30457ac529d05d8d131c183665e086246da607f752dea5eb.json
|
||||||
|
│ │ │ ├── fe7f3f3c4b234398f7c0fb728456ac41c92c6e939634e147d2fdda12067df842.json
|
||||||
|
│ │ │ ├── fe8b7b639a9f98fd1b883b7dccf8490ee905a2a21df5671ca1838a173fa0f921.json
|
||||||
│ │ │ └── ff862e0ffad7bba911716072ae82f268240d82105ffefe78c7714a160c4c31ba.json
|
│ │ │ └── ff862e0ffad7bba911716072ae82f268240d82105ffefe78c7714a160c4c31ba.json
|
||||||
│ │ ├── semantic
|
│ │ ├── semantic
|
||||||
│ │ │ └── p5e80268fecd6
|
│ │ │ └── p5e80268fecd6
|
||||||
|
│ │ │ ├── 0be9cb2f924f20c8cd09e0361a90c8f0b4c8859120101bbe21eee930294ebdfd.json
|
||||||
│ │ │ ├── 0c4f87b180cd5b0576b197ff5cb0e9cc5dfd8d82d97d618036df00c7955fdbac.json
|
│ │ │ ├── 0c4f87b180cd5b0576b197ff5cb0e9cc5dfd8d82d97d618036df00c7955fdbac.json
|
||||||
|
│ │ │ ├── 14328edb08c286b631c2ef0ead8ac33b522157be84855de346e8c0688e30c965.json
|
||||||
│ │ │ ├── 14e03f8f9b4ebd7cf018188e2351ca859aa4e27d02d5dbfc7b1f8b932d282d16.json
|
│ │ │ ├── 14e03f8f9b4ebd7cf018188e2351ca859aa4e27d02d5dbfc7b1f8b932d282d16.json
|
||||||
|
│ │ │ ├── 212a2dc6dcc30c1b45f6cf5bfa9bf16386fa138bf382bbde438f8d6883769b29.json
|
||||||
│ │ │ ├── 2679b1fb1647af24f007334e6560733c957ca3cf3831eb02a9c8fb7a7ae44d62.json
|
│ │ │ ├── 2679b1fb1647af24f007334e6560733c957ca3cf3831eb02a9c8fb7a7ae44d62.json
|
||||||
│ │ │ ├── 296d532aced4658e74a296977efe4fd8a2ec3cf02a997ce3837f59ce36e61343.json
|
│ │ │ ├── 296d532aced4658e74a296977efe4fd8a2ec3cf02a997ce3837f59ce36e61343.json
|
||||||
│ │ │ ├── 2a13d2bb6f18a81d9511481df0edcdb1a8779e1ec03e919991693cae7e84c8e0.json
|
│ │ │ ├── 2a13d2bb6f18a81d9511481df0edcdb1a8779e1ec03e919991693cae7e84c8e0.json
|
||||||
|
│ │ │ ├── 2ac09852870126bb1792a5e9242b34d3f55365bfa6e3b09aa432461a6c72045e.json
|
||||||
│ │ │ ├── 2d3b8c0bb7eea6a50a24093abfc3457c0938079b0771b2568498060f6f3a4a74.json
|
│ │ │ ├── 2d3b8c0bb7eea6a50a24093abfc3457c0938079b0771b2568498060f6f3a4a74.json
|
||||||
│ │ │ ├── 40373bd2bcd76ed8ed27031299e541005d4c8c0c7df312a9312f390b173d7248.json
|
|
||||||
│ │ │ ├── 42a95a4a8f651dbebe297ce92beca376cb19eab99d6ef6b5d10a16d79d1842e4.json
|
│ │ │ ├── 42a95a4a8f651dbebe297ce92beca376cb19eab99d6ef6b5d10a16d79d1842e4.json
|
||||||
│ │ │ ├── 4f53e6b5fc48381f8afe6052371989c4df69347c84c2ec65a46b73cd1c464b5f.json
|
|
||||||
│ │ │ ├── 538cdc2fb71798c581f20d37d52a1484c2ef21691d2e01ee79edcf253ba06394.json
|
│ │ │ ├── 538cdc2fb71798c581f20d37d52a1484c2ef21691d2e01ee79edcf253ba06394.json
|
||||||
│ │ │ ├── 569db52f8b9229dc3cd5cbde156a4ac012ab711d32bdeb4175b60ba863ac8cd0.json
|
│ │ │ ├── 569db52f8b9229dc3cd5cbde156a4ac012ab711d32bdeb4175b60ba863ac8cd0.json
|
||||||
│ │ │ ├── 57dba5c2a37beb12ccc107807b7544f687d6d8ba6394c8977c3c59b433d2dab4.json
|
│ │ │ ├── 57dba5c2a37beb12ccc107807b7544f687d6d8ba6394c8977c3c59b433d2dab4.json
|
||||||
│ │ │ ├── 5ad5f87fc2eb132405e3aab9ef8a84e41f76c958fbcb188873c86abd4fbc12fd.json
|
│ │ │ ├── 5ad5f87fc2eb132405e3aab9ef8a84e41f76c958fbcb188873c86abd4fbc12fd.json
|
||||||
|
│ │ │ ├── 5c0f2ea11f5bdcc5272d6344c9015c20bf79f315d74a910d56bf7a2922c92315.json
|
||||||
|
│ │ │ ├── 5dbe87f1a5fa172581ed5abebee35b5d08ff488e798a334992a2756bff277cdc.json
|
||||||
│ │ │ ├── 6aaa93a769a41eb1d37a91608cd6f98b9252c2a5598bd859a130986429d1d53f.json
|
│ │ │ ├── 6aaa93a769a41eb1d37a91608cd6f98b9252c2a5598bd859a130986429d1d53f.json
|
||||||
│ │ │ ├── 7b504b4173cb8d683a7021270372c5c65f078ac3e3e1351601bedfec9c585632.json
|
|
||||||
│ │ │ ├── 7edbffbd4455d071df034927a69faaf76f8e8bbf6495921ce2b70eea1abe8e4f.json
|
│ │ │ ├── 7edbffbd4455d071df034927a69faaf76f8e8bbf6495921ce2b70eea1abe8e4f.json
|
||||||
|
│ │ │ ├── 82db37687728a8df60ccf231c06838b4a0074d22ad93e89d3f5e93d4de48132f.json
|
||||||
│ │ │ ├── 8a79c80fca0ed04957e7b4df749ab80b5d0f4ce3d0ae032a47d51d4a2fbfcd82.json
|
│ │ │ ├── 8a79c80fca0ed04957e7b4df749ab80b5d0f4ce3d0ae032a47d51d4a2fbfcd82.json
|
||||||
│ │ │ ├── 8f60bda631cbe7457886d1d574b2e3d2850034f469592a3e8c333f0623a2d3c6.json
|
│ │ │ ├── 8b6c538ab1a901fb71decc7f7f511c8c67a76d72f05f02b1bddc10f5026f9e4e.json
|
||||||
|
│ │ │ ├── 96f5469edd566b92d9d38423a27779142fbe2b77cfc9d85005d43ffc99ce3de1.json
|
||||||
│ │ │ ├── 9897fa1997a41e155ed8de922722238e1f684f5c77a7f56405c8ac7cd1aa376f.json
|
│ │ │ ├── 9897fa1997a41e155ed8de922722238e1f684f5c77a7f56405c8ac7cd1aa376f.json
|
||||||
│ │ │ ├── 9b674bec71dfe6e133af9fb4e35a421fa56cecd230544008ed83a44e6eaae3bf.json
|
│ │ │ ├── a3f3d3cf99dd02b4d8c41297d4634e120953f1650257c0c6a1888481aa4c5745.json
|
||||||
│ │ │ ├── aa41b77ce8563f4b4bee015be5ffa598f1c98d4f2854ec9a465f271e8c3f974b.json
|
│ │ │ ├── aa41b77ce8563f4b4bee015be5ffa598f1c98d4f2854ec9a465f271e8c3f974b.json
|
||||||
│ │ │ ├── b5f6ef338a857f51d581abe7c50f57f41757aa6900bf94f6ed8fc2ad442f6019.json
|
│ │ │ ├── b5f6ef338a857f51d581abe7c50f57f41757aa6900bf94f6ed8fc2ad442f6019.json
|
||||||
│ │ │ ├── be1200d7c9b2881ba54c7fa95daf5f3e2ce993619b974c7760a9d44a416e00c0.json
|
│ │ │ ├── be1200d7c9b2881ba54c7fa95daf5f3e2ce993619b974c7760a9d44a416e00c0.json
|
||||||
|
│ │ │ ├── c109dbf29a8233cec6f0e4e3c1256a94ecee99887756af2060e620aaa5979e4b.json
|
||||||
│ │ │ ├── c1d9e0aa5d7450242681eebd003d4f502744a3624112b10ab091011a50820ce3.json
|
│ │ │ ├── c1d9e0aa5d7450242681eebd003d4f502744a3624112b10ab091011a50820ce3.json
|
||||||
|
│ │ │ ├── c89f13bada10536a104ce57788562563a61c2682430e4273ca3195890d45c68c.json
|
||||||
│ │ │ ├── c97d49fed39b9aca8dea35a49c3e031a641420e4452e61d1ea2a912e899f6dd9.json
|
│ │ │ ├── c97d49fed39b9aca8dea35a49c3e031a641420e4452e61d1ea2a912e899f6dd9.json
|
||||||
│ │ │ ├── cacf6773249daafca21d21004106209d86aa215140de071c64363aff4d9e5431.json
|
│ │ │ ├── cacf6773249daafca21d21004106209d86aa215140de071c64363aff4d9e5431.json
|
||||||
│ │ │ ├── cfb57017bc764ee7e9011b52f1e44ad2f41b2670778af73232ecfc309b406a0d.json
|
│ │ │ ├── cfb57017bc764ee7e9011b52f1e44ad2f41b2670778af73232ecfc309b406a0d.json
|
||||||
│ │ │ ├── dc89fcddf8c6f20bc9f52ae15e5cc8b226320d2776dc8f27be16894e53165013.json
|
│ │ │ ├── dc89fcddf8c6f20bc9f52ae15e5cc8b226320d2776dc8f27be16894e53165013.json
|
||||||
│ │ │ ├── dd78862d0f7862653c702e86f7acfabfe33ec5d85d40ba764138c0d1784f7b06.json
|
│ │ │ ├── dd78862d0f7862653c702e86f7acfabfe33ec5d85d40ba764138c0d1784f7b06.json
|
||||||
│ │ │ ├── de67f63e3f49d4ee32811dd67676da33a1e584f671d396fca130f3681d54bd76.json
|
│ │ │ ├── de67f63e3f49d4ee32811dd67676da33a1e584f671d396fca130f3681d54bd76.json
|
||||||
│ │ │ ├── e0a52ba8204140f195e8d30847dda00d3de5439cf903c576b77fac1ec852b5fb.json
|
│ │ │ ├── e86dc6bb7703d4329623bd33ccbc5e5e6c6daebf8d36d84fd415d6f868c779b0.json
|
||||||
|
│ │ │ ├── e9ca16b55daa5d6bdf6b0257a4e08c35caeae32eaaeb5c0be11532b77ab658ea.json
|
||||||
|
│ │ │ ├── ea11661eb53bce4c7a43acb2d636de01d960acdad246c1f1372df351e25bac25.json
|
||||||
|
│ │ │ ├── eaceb29a907e3e67bed5255cc1f8b8b3eab9d9946f4d4df2cc99070151285029.json
|
||||||
│ │ │ ├── f71a50c524a31da468cf7c0c08d929d4d31bf17e77473db59b5830f4c583ce25.json
|
│ │ │ ├── f71a50c524a31da468cf7c0c08d929d4d31bf17e77473db59b5830f4c583ce25.json
|
||||||
│ │ │ ├── f7fdf480e35ed096b57529bfcafd8695aff9a98851050be1f00e4a736ad79719.json
|
│ │ │ ├── f7fdf480e35ed096b57529bfcafd8695aff9a98851050be1f00e4a736ad79719.json
|
||||||
│ │ │ └── fb1977d392a5009580c7a608fd403ac2ae96c72f6f2835ba6e49cf662dd0bf6d.json
|
│ │ │ └── fb1977d392a5009580c7a608fd403ac2ae96c72f6f2835ba6e49cf662dd0bf6d.json
|
||||||
@@ -484,12 +644,10 @@ khatibi
|
|||||||
│ │ │ ├── 2a13d2bb6f18a81d9511481df0edcdb1a8779e1ec03e919991693cae7e84c8e0.json
|
│ │ │ ├── 2a13d2bb6f18a81d9511481df0edcdb1a8779e1ec03e919991693cae7e84c8e0.json
|
||||||
│ │ │ ├── 2d3b8c0bb7eea6a50a24093abfc3457c0938079b0771b2568498060f6f3a4a74.json
|
│ │ │ ├── 2d3b8c0bb7eea6a50a24093abfc3457c0938079b0771b2568498060f6f3a4a74.json
|
||||||
│ │ │ ├── 32267f72ce9ef01cd2e804d43c6828549af8eb6f868c76e7d1619502a953f14d.json
|
│ │ │ ├── 32267f72ce9ef01cd2e804d43c6828549af8eb6f868c76e7d1619502a953f14d.json
|
||||||
│ │ │ ├── 40373bd2bcd76ed8ed27031299e541005d4c8c0c7df312a9312f390b173d7248.json
|
|
||||||
│ │ │ ├── 40a84c5465882009ab69d2874adbb9aaed373cd706d70c92e9e1f4c35640472e.json
|
│ │ │ ├── 40a84c5465882009ab69d2874adbb9aaed373cd706d70c92e9e1f4c35640472e.json
|
||||||
│ │ │ ├── 4280dd1094314da773fe73ba1ffb91f58f22eea26281bd5cb51f5e9a201c95f6.json
|
│ │ │ ├── 4280dd1094314da773fe73ba1ffb91f58f22eea26281bd5cb51f5e9a201c95f6.json
|
||||||
│ │ │ ├── 42a95a4a8f651dbebe297ce92beca376cb19eab99d6ef6b5d10a16d79d1842e4.json
|
│ │ │ ├── 42a95a4a8f651dbebe297ce92beca376cb19eab99d6ef6b5d10a16d79d1842e4.json
|
||||||
│ │ │ ├── 4b8f3b6c0ccb3b5494fb2af8c0f9892b6b198a8d503f53498bd1fe2548ef112a.json
|
│ │ │ ├── 4b8f3b6c0ccb3b5494fb2af8c0f9892b6b198a8d503f53498bd1fe2548ef112a.json
|
||||||
│ │ │ ├── 4f53e6b5fc48381f8afe6052371989c4df69347c84c2ec65a46b73cd1c464b5f.json
|
|
||||||
│ │ │ ├── 538cdc2fb71798c581f20d37d52a1484c2ef21691d2e01ee79edcf253ba06394.json
|
│ │ │ ├── 538cdc2fb71798c581f20d37d52a1484c2ef21691d2e01ee79edcf253ba06394.json
|
||||||
│ │ │ ├── 569db52f8b9229dc3cd5cbde156a4ac012ab711d32bdeb4175b60ba863ac8cd0.json
|
│ │ │ ├── 569db52f8b9229dc3cd5cbde156a4ac012ab711d32bdeb4175b60ba863ac8cd0.json
|
||||||
│ │ │ ├── 57dba5c2a37beb12ccc107807b7544f687d6d8ba6394c8977c3c59b433d2dab4.json
|
│ │ │ ├── 57dba5c2a37beb12ccc107807b7544f687d6d8ba6394c8977c3c59b433d2dab4.json
|
||||||
@@ -497,15 +655,12 @@ khatibi
|
|||||||
│ │ │ ├── 60002a01d6fc22147e2994d1a1a447d5e577c02aed2443789af20571ec2d2a19.json
|
│ │ │ ├── 60002a01d6fc22147e2994d1a1a447d5e577c02aed2443789af20571ec2d2a19.json
|
||||||
│ │ │ ├── 6aaa93a769a41eb1d37a91608cd6f98b9252c2a5598bd859a130986429d1d53f.json
|
│ │ │ ├── 6aaa93a769a41eb1d37a91608cd6f98b9252c2a5598bd859a130986429d1d53f.json
|
||||||
│ │ │ ├── 7101777f271bbc6bce9a2a4fd3bcf487bb62a846a2436bc8c70008bfb23ce33b.json
|
│ │ │ ├── 7101777f271bbc6bce9a2a4fd3bcf487bb62a846a2436bc8c70008bfb23ce33b.json
|
||||||
│ │ │ ├── 7b504b4173cb8d683a7021270372c5c65f078ac3e3e1351601bedfec9c585632.json
|
|
||||||
│ │ │ ├── 7db83b9e384d80a1584b152f9fbc781e0ae5a7524dac0a57ce98bb4d3432bfae.json
|
│ │ │ ├── 7db83b9e384d80a1584b152f9fbc781e0ae5a7524dac0a57ce98bb4d3432bfae.json
|
||||||
│ │ │ ├── 7edbffbd4455d071df034927a69faaf76f8e8bbf6495921ce2b70eea1abe8e4f.json
|
│ │ │ ├── 7edbffbd4455d071df034927a69faaf76f8e8bbf6495921ce2b70eea1abe8e4f.json
|
||||||
│ │ │ ├── 8a79c80fca0ed04957e7b4df749ab80b5d0f4ce3d0ae032a47d51d4a2fbfcd82.json
|
│ │ │ ├── 8a79c80fca0ed04957e7b4df749ab80b5d0f4ce3d0ae032a47d51d4a2fbfcd82.json
|
||||||
│ │ │ ├── 8f60bda631cbe7457886d1d574b2e3d2850034f469592a3e8c333f0623a2d3c6.json
|
|
||||||
│ │ │ ├── 908e0129cefe129d0b562243460fe20793c84876244cef2e99ca1becf8cf6a1f.json
|
│ │ │ ├── 908e0129cefe129d0b562243460fe20793c84876244cef2e99ca1becf8cf6a1f.json
|
||||||
│ │ │ ├── 92e1d82ed4571249c09163413ceae360dd4d7d03c162c554f3b5a47e94474cf1.json
|
│ │ │ ├── 92e1d82ed4571249c09163413ceae360dd4d7d03c162c554f3b5a47e94474cf1.json
|
||||||
│ │ │ ├── 9897fa1997a41e155ed8de922722238e1f684f5c77a7f56405c8ac7cd1aa376f.json
|
│ │ │ ├── 9897fa1997a41e155ed8de922722238e1f684f5c77a7f56405c8ac7cd1aa376f.json
|
||||||
│ │ │ ├── 9b674bec71dfe6e133af9fb4e35a421fa56cecd230544008ed83a44e6eaae3bf.json
|
|
||||||
│ │ │ ├── a3f1dac3e797de04ab83d263a12eda947904d8e5015b77363db2d29aba8b4928.json
|
│ │ │ ├── a3f1dac3e797de04ab83d263a12eda947904d8e5015b77363db2d29aba8b4928.json
|
||||||
│ │ │ ├── aa41b77ce8563f4b4bee015be5ffa598f1c98d4f2854ec9a465f271e8c3f974b.json
|
│ │ │ ├── aa41b77ce8563f4b4bee015be5ffa598f1c98d4f2854ec9a465f271e8c3f974b.json
|
||||||
│ │ │ ├── b205a6dda565ed51a38a6b75c509f41589425f0c2006aad838978f5dc6810b91.json
|
│ │ │ ├── b205a6dda565ed51a38a6b75c509f41589425f0c2006aad838978f5dc6810b91.json
|
||||||
@@ -522,7 +677,6 @@ khatibi
|
|||||||
│ │ │ ├── dc89fcddf8c6f20bc9f52ae15e5cc8b226320d2776dc8f27be16894e53165013.json
|
│ │ │ ├── dc89fcddf8c6f20bc9f52ae15e5cc8b226320d2776dc8f27be16894e53165013.json
|
||||||
│ │ │ ├── dd78862d0f7862653c702e86f7acfabfe33ec5d85d40ba764138c0d1784f7b06.json
|
│ │ │ ├── dd78862d0f7862653c702e86f7acfabfe33ec5d85d40ba764138c0d1784f7b06.json
|
||||||
│ │ │ ├── de67f63e3f49d4ee32811dd67676da33a1e584f671d396fca130f3681d54bd76.json
|
│ │ │ ├── de67f63e3f49d4ee32811dd67676da33a1e584f671d396fca130f3681d54bd76.json
|
||||||
│ │ │ ├── e0a52ba8204140f195e8d30847dda00d3de5439cf903c576b77fac1ec852b5fb.json
|
|
||||||
│ │ │ ├── e13320e9716b0be66b7cc677a64b44cf3127f7317f35cfa3b849dfbaa9c1f367.json
|
│ │ │ ├── e13320e9716b0be66b7cc677a64b44cf3127f7317f35cfa3b849dfbaa9c1f367.json
|
||||||
│ │ │ ├── e1c07127e4cef79fe0bfc63a0ab904e5d1b0d9c4be5278b553b6e48d39753b4d.json
|
│ │ │ ├── e1c07127e4cef79fe0bfc63a0ab904e5d1b0d9c4be5278b553b6e48d39753b4d.json
|
||||||
│ │ │ ├── f0478454ef37ecdc5128db97c18d0ffd92005c1d1db586b6750d985e30a25d76.json
|
│ │ │ ├── f0478454ef37ecdc5128db97c18d0ffd92005c1d1db586b6750d985e30a25d76.json
|
||||||
@@ -558,7 +712,9 @@ khatibi
|
|||||||
│ │ ├── http.py
|
│ │ ├── http.py
|
||||||
│ │ ├── normalize.py
|
│ │ ├── normalize.py
|
||||||
│ │ ├── providers.py
|
│ │ ├── providers.py
|
||||||
│ │ └── sms0098.py
|
│ │ ├── sms0098.py
|
||||||
|
│ │ ├── sms0098_codes.py
|
||||||
|
│ │ └── sms0098ws.py
|
||||||
│ ├── __init__.py
|
│ ├── __init__.py
|
||||||
│ ├── announcements.py
|
│ ├── announcements.py
|
||||||
│ └── voip.py
|
│ └── voip.py
|
||||||
@@ -581,16 +737,20 @@ khatibi
|
|||||||
│ ├── test_sms.py
|
│ ├── test_sms.py
|
||||||
│ ├── test_urls_contract.py
|
│ ├── test_urls_contract.py
|
||||||
│ └── test_view_smoke.py
|
│ └── test_view_smoke.py
|
||||||
|
├── .clinerules
|
||||||
├── .dockerignore
|
├── .dockerignore
|
||||||
├── .gitignore
|
├── .gitignore
|
||||||
├── .graphifyignore
|
├── .graphifyignore
|
||||||
|
├── 0098sms.md
|
||||||
├── AGENTS.md
|
├── AGENTS.md
|
||||||
├── check_finance.py
|
├── check_finance.py
|
||||||
|
├── CLAUDE.md
|
||||||
├── db.json
|
├── db.json
|
||||||
├── docker-compose.yml
|
├── docker-compose.yml
|
||||||
├── Dockerfile
|
├── Dockerfile
|
||||||
├── entrypoint.sh
|
├── entrypoint.sh
|
||||||
├── financial_audit_20260816_151225.csv
|
├── financial_audit_20260816_151225.csv
|
||||||
|
├── fix_customer_phones.md
|
||||||
├── GEMINI.md
|
├── GEMINI.md
|
||||||
├── ghost_hunter.py
|
├── ghost_hunter.py
|
||||||
├── khatibi.zip
|
├── khatibi.zip
|
||||||
@@ -598,6 +758,7 @@ khatibi
|
|||||||
├── manage.py
|
├── manage.py
|
||||||
├── production.env
|
├── production.env
|
||||||
├── project_tree.py
|
├── project_tree.py
|
||||||
|
├── recalc_last_bazdid.md
|
||||||
├── reconciliation_tool.py
|
├── reconciliation_tool.py
|
||||||
├── requirements.txt
|
├── requirements.txt
|
||||||
├── requirements2.txt
|
├── requirements2.txt
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
"""بازمحاسبهٔ ``Customer.last_bazdid_dt`` از روی اقدامهای «بازدیدِ تمامشده».
|
||||||
|
|
||||||
|
فیلد ``last_bazdid_dt`` «آخرین بازدید» مشتری است و صفحهٔ ``/panel/entezar/`` بر پایهٔ
|
||||||
|
آن مشتریانِ معوق (بیش از N ماه بازدید نشده) را لیست میکند. این مقدار باید برابر
|
||||||
|
«بزرگترین ``finishDateTime`` میان اقدامهای ``actionType='bazdid'`` و ``status='finish'``
|
||||||
|
هر مشتری» باشد — همان چیزی که ویوهای ``customerNewAction``/``customerEditAction``
|
||||||
|
مینویسند.
|
||||||
|
|
||||||
|
اگر رکوردی از راهی غیر از این ویوها وارد شود (اسکریپت، ایمپورت، ویرایش دستی دیتابیس)،
|
||||||
|
این فیلد عقب میماند و مشتریِ واقعاً بازدیدشده اشتباهاً «معوق» شمرده میشود. این
|
||||||
|
دستور آن واگرایی (drift) را از منبع حقیقت (جدول Action) ترمیم میکند.
|
||||||
|
|
||||||
|
python manage.py recalc_last_bazdid --dry-run # فقط گزارش، بدون هیچ تغییری
|
||||||
|
python manage.py recalc_last_bazdid # اعمال اصلاحات
|
||||||
|
|
||||||
|
مشخصات:
|
||||||
|
* idempotent — اجرای دوباره پس از یکبار اجرا هیچ تغییری ایجاد نمیکند.
|
||||||
|
* پیشفرض محافظهکارانه است: فقط مقدار را **بهروز** میکند و هیچوقت روی مقادیری که
|
||||||
|
اقدامِ پشتیبان ندارند دست نمیگذارد (مگر با ``--clear-orphans``).
|
||||||
|
* روی دیتابیس production هم قابل اجرا است.
|
||||||
|
"""
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.models import Max
|
||||||
|
|
||||||
|
from ...models import Action, Customer
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = (
|
||||||
|
"بازمحاسبهٔ Customer.last_bazdid_dt از MAX(finishDateTime) اقدامهای "
|
||||||
|
"بازدیدِ تمامشده (ترمیم واگرایی داده؛ idempotent)."
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
"--dry-run", action="store_true",
|
||||||
|
help="فقط گزارش کن که چه چیزی تغییر میکند؛ هیچ رکوردی ذخیره نمیشود.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--clear-orphans", action="store_true",
|
||||||
|
help="مقدارهایی که هیچ اقدامِ بازدیدِ تمامشدهای پشتشان نیست را هم NULL کن "
|
||||||
|
"(پیشفرض: دستنخورده بمانند). فقط اگر مطمئنی این ستون آلوده است.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--batch-size", type=int, default=500,
|
||||||
|
help="اندازهٔ دستهٔ خواندن مشتریان در هر گام (پیشفرض ۵۰۰).",
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
dry_run = options["dry_run"]
|
||||||
|
clear_orphans = options["clear_orphans"]
|
||||||
|
batch_size = max(1, options["batch_size"])
|
||||||
|
|
||||||
|
prefix = "[شبیهسازی] " if dry_run else ""
|
||||||
|
self.stdout.write(self.style.MIGRATE_HEADING(
|
||||||
|
f"{prefix}بازمحاسبهٔ آخرین بازدید مشتریان آغاز شد."
|
||||||
|
))
|
||||||
|
|
||||||
|
# منبع حقیقت: بزرگترین زمانِ پایانِ بازدید در اقدامهای تمامشدهٔ هر مشتری.
|
||||||
|
# (Max مقادیر NULL را نادیده میگیرد؛ مشتریِ بدون بازدید بهکل در نقشه نمیآید.)
|
||||||
|
latest_rows = (
|
||||||
|
Action.objects
|
||||||
|
.filter(actionType="bazdid", status="finish")
|
||||||
|
.exclude(customer_id__isnull=True)
|
||||||
|
.values("customer_id")
|
||||||
|
.annotate(mx=Max("finishDateTime"))
|
||||||
|
)
|
||||||
|
latest = {
|
||||||
|
row["customer_id"]: row["mx"]
|
||||||
|
for row in latest_rows.iterator(chunk_size=batch_size)
|
||||||
|
if row["mx"] is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
"scanned": 0, # کل مشتریان بررسیشده
|
||||||
|
"with_visit": 0, # مشتریان دارای بازدیدِ تمامشده
|
||||||
|
"updated": 0, # مقدار اصلاح/بهروز شد
|
||||||
|
"moved_forward": 0, # جدیدتر شد ⇒ از لیست معوقها خارج میشود
|
||||||
|
"moved_backward": 0, # عقبتر شد
|
||||||
|
"set_null": 0, # با --clear-orphans خالی شد
|
||||||
|
"already_ok": 0, # از قبل درست بود
|
||||||
|
"kept_orphan": 0, # مقدار داشت ولی اقدامِ پشتیبان نداشت (دستنخورده ماند)
|
||||||
|
"null_and_no_action": 0, # NULL و بدون اقدام (طبیعی؛ کاری لازم نیست)
|
||||||
|
}
|
||||||
|
|
||||||
|
pending = [] # (customer_id, target_value) برای اعمال گروهی
|
||||||
|
|
||||||
|
qs = Customer.objects.only("id", "last_bazdid_dt").order_by("id")
|
||||||
|
for customer in qs.iterator(chunk_size=batch_size):
|
||||||
|
stats["scanned"] += 1
|
||||||
|
mx = latest.get(customer.id)
|
||||||
|
current = customer.last_bazdid_dt
|
||||||
|
|
||||||
|
if mx is not None:
|
||||||
|
stats["with_visit"] += 1
|
||||||
|
if current == mx:
|
||||||
|
stats["already_ok"] += 1
|
||||||
|
continue
|
||||||
|
if current is None or current < mx:
|
||||||
|
stats["moved_forward"] += 1
|
||||||
|
else:
|
||||||
|
stats["moved_backward"] += 1
|
||||||
|
stats["updated"] += 1
|
||||||
|
pending.append((customer.id, mx))
|
||||||
|
else:
|
||||||
|
# هیچ بازدیدِ تمامشدهای پشتوانهٔ این مقدار نیست.
|
||||||
|
if current is None:
|
||||||
|
stats["null_and_no_action"] += 1
|
||||||
|
elif clear_orphans:
|
||||||
|
stats["set_null"] += 1
|
||||||
|
pending.append((customer.id, None))
|
||||||
|
else:
|
||||||
|
stats["kept_orphan"] += 1
|
||||||
|
|
||||||
|
if not dry_run and pending:
|
||||||
|
with transaction.atomic():
|
||||||
|
for customer_id, target in pending:
|
||||||
|
Customer.objects.filter(pk=customer_id).update(
|
||||||
|
last_bazdid_dt=target
|
||||||
|
)
|
||||||
|
|
||||||
|
self._report(stats, dry_run, clear_orphans)
|
||||||
|
|
||||||
|
def _report(self, stats, dry_run, clear_orphans):
|
||||||
|
style = self.style
|
||||||
|
label = "[شبیهسازی] " if dry_run else ""
|
||||||
|
self.stdout.write(style.MIGRATE_LABEL(f"\n{label}گزارش بازمحاسبهٔ آخرین بازدید:"))
|
||||||
|
self.stdout.write(f" بررسیشده (کل مشتریان): {stats['scanned']:,}")
|
||||||
|
self.stdout.write(f" دارای بازدیدِ تمامشده: {stats['with_visit']:,}")
|
||||||
|
self.stdout.write(f" از قبل درست: {stats['already_ok']:,}")
|
||||||
|
self.stdout.write(f" نیازمند اصلاح: {stats['updated']:,}")
|
||||||
|
self.stdout.write(f" → جدیدتر شد (خروج از معوق): {stats['moved_forward']:,}")
|
||||||
|
self.stdout.write(f" → عقبتر شد: {stats['moved_backward']:,}")
|
||||||
|
if clear_orphans:
|
||||||
|
self.stdout.write(f" → خالی شد (بدون اقدام): {stats['set_null']:,}")
|
||||||
|
else:
|
||||||
|
self.stdout.write(
|
||||||
|
f" بدون اقدام ولی دارای مقدار (دستنخورده): {stats['kept_orphan']:,}"
|
||||||
|
)
|
||||||
|
self.stdout.write(
|
||||||
|
f" NULL و بدون اقدام (طبیعی): {stats['null_and_no_action']:,}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
self.stdout.write(style.WARNING(
|
||||||
|
"\n[حالت شبیهسازی] هیچ رکوردی ذخیره نشد. برای اعمال، بدون --dry-run اجرا کن."
|
||||||
|
))
|
||||||
|
elif stats["updated"]:
|
||||||
|
self.stdout.write(style.SUCCESS(
|
||||||
|
f"\nانجام شد: {stats['updated']:,} رکورد بهروزرسانی شد."
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
self.stdout.write(style.SUCCESS(
|
||||||
|
"\nانجام شد: هیچ اصلاحی لازم نبود (داده از قبل درست است)."
|
||||||
|
))
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
"""کاهش انواع پیامک به همان ۷ نوع مصوب + همراستاسازی قالبها.
|
||||||
|
|
||||||
|
* سه نوع قدیمی (``cheque_due``، ``debt_due``، ``custom``) حذف میشوند:
|
||||||
|
قالبها و تنظیمات opt-out مربوط به آنها پاک میشود.
|
||||||
|
* دو نوع جدید اضافه میشود: ``visit_created`` (ثبت اقدام بازدید) و
|
||||||
|
``payment_thanks`` (تشکر از پرداخت).
|
||||||
|
* متن/زمانبندی نوعهای باقیمانده به معنای جدید همراستا میشود
|
||||||
|
(مثلاً «تأمین موجودی چک» = چند روز پس از سررسید، و نظرسنجی = بیدرنگ پس از فاکتور).
|
||||||
|
"""
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
SMS_TYPE_CHOICES = [
|
||||||
|
("welcome", "خوشآمدگویی (ایجاد مشتری)"),
|
||||||
|
("survey", "نظرسنجی پس از ثبت فاکتور"),
|
||||||
|
("cheque_registration", "یادآوری تأمین موجودی چک"),
|
||||||
|
("visit", "یادآوری بازدید خدمات دستگاه"),
|
||||||
|
("visit_created", "ثبت بازدید (اقدام از نوع بازدید)"),
|
||||||
|
("payment_thanks", "تشکر از پرداخت"),
|
||||||
|
("nesiye_due", "یادآوری سررسید نسیه"),
|
||||||
|
]
|
||||||
|
|
||||||
|
REMOVED_TYPES = ["cheque_due", "debt_due", "custom"]
|
||||||
|
|
||||||
|
# نوع → مقادیر پیشفرض (خالینماندن قالبهای جدید و همراستایی نوعهای تغییریافته).
|
||||||
|
DEFAULT_TEMPLATES = {
|
||||||
|
"welcome": {
|
||||||
|
"title": "خوشآمدگویی",
|
||||||
|
"text": "{name} عزیز، از خرید شما سپاسگزاریم. بهامید دیدار دوباره.",
|
||||||
|
"offset_days": 0,
|
||||||
|
"repeat_interval_days": None,
|
||||||
|
"repeat_interval_months": None,
|
||||||
|
},
|
||||||
|
"survey": {
|
||||||
|
"title": "نظرسنجی پس از فاکتور",
|
||||||
|
"text": "{name} عزیز، از خرید شما سپاسگزاریم. لطفاً با ثبت نظر خود در "
|
||||||
|
"نظرسنجی ما را همراهی کنید.",
|
||||||
|
"offset_days": 0,
|
||||||
|
"repeat_interval_days": None,
|
||||||
|
"repeat_interval_months": None,
|
||||||
|
},
|
||||||
|
"cheque_registration": {
|
||||||
|
"title": "یادآوری تأمین موجودی چک",
|
||||||
|
"text": "{name} عزیز، چک شما به مبلغ {amount_sep} ریال سررسید شده است. "
|
||||||
|
"لطفاً نسبت به تأمین موجودی حساب اقدام فرمایید.",
|
||||||
|
"offset_days": 1,
|
||||||
|
"repeat_interval_days": None,
|
||||||
|
"repeat_interval_months": None,
|
||||||
|
},
|
||||||
|
"visit": {
|
||||||
|
"title": "یادآوری بازدید خدمات دستگاه",
|
||||||
|
"text": "{name} عزیز، بیش از ۹ ماه از تعویض فیلتر دستگاه شما گذشته است. "
|
||||||
|
"لطفاً جهت هماهنگی برای ثبت بازدید رایگان با تلفن 09139741111 "
|
||||||
|
"تماس بگیرید.",
|
||||||
|
"offset_days": 0,
|
||||||
|
"repeat_interval_days": None,
|
||||||
|
"repeat_interval_months": 9,
|
||||||
|
},
|
||||||
|
"visit_created": {
|
||||||
|
"title": "ثبت بازدید",
|
||||||
|
"text": "{name} عزیز، بازدید شما برای تاریخ {date} ثبت شد. "
|
||||||
|
"از همکاری شما سپاسگزاریم.",
|
||||||
|
"offset_days": 0,
|
||||||
|
"repeat_interval_days": None,
|
||||||
|
"repeat_interval_months": None,
|
||||||
|
},
|
||||||
|
"payment_thanks": {
|
||||||
|
"title": "تشکر از پرداخت",
|
||||||
|
"text": "{name} عزیز، از پرداخت شما به مبلغ {amount_sep} ریال سپاسگزاریم.",
|
||||||
|
"offset_days": 0,
|
||||||
|
"repeat_interval_days": None,
|
||||||
|
"repeat_interval_months": None,
|
||||||
|
},
|
||||||
|
"nesiye_due": {
|
||||||
|
"title": "یادآوری سررسید نسیه",
|
||||||
|
"text": "{name} عزیز، سررسید بدهی شما به مبلغ {amount_sep} ریال نزدیک است "
|
||||||
|
"({date}). لطفاً نسبت به تسویه اقدام فرمایید.",
|
||||||
|
"offset_days": 0,
|
||||||
|
"repeat_interval_days": None,
|
||||||
|
"repeat_interval_months": None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def align_templates(apps, schema_editor):
|
||||||
|
SMSTemplate = apps.get_model("admin_panel", "SMSTemplate")
|
||||||
|
CustomerSMSSetting = apps.get_model("admin_panel", "CustomerSMSSetting")
|
||||||
|
|
||||||
|
# ۱. پاکسازی انواع حذفشده.
|
||||||
|
SMSTemplate.objects.filter(sms_type__in=REMOVED_TYPES).delete()
|
||||||
|
CustomerSMSSetting.objects.filter(sms_type__in=REMOVED_TYPES).delete()
|
||||||
|
|
||||||
|
# ۲. ایجاد/بهروزرسانی قالبهای ۷ نوع مجاز (متن پیشفرض جدید + زمانبندی).
|
||||||
|
for sms_type, data in DEFAULT_TEMPLATES.items():
|
||||||
|
_template, created = SMSTemplate.objects.get_or_create(
|
||||||
|
sms_type=sms_type,
|
||||||
|
defaults={
|
||||||
|
"title": data["title"],
|
||||||
|
"text": data["text"],
|
||||||
|
"is_enabled": True,
|
||||||
|
"offset_days": data["offset_days"],
|
||||||
|
"repeat_interval_days": data["repeat_interval_days"],
|
||||||
|
"repeat_interval_months": data["repeat_interval_months"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if not created:
|
||||||
|
SMSTemplate.objects.filter(sms_type=sms_type).update(
|
||||||
|
offset_days=data["offset_days"],
|
||||||
|
repeat_interval_days=data["repeat_interval_days"],
|
||||||
|
repeat_interval_months=data["repeat_interval_months"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# ۳. متن/عنوانِ نوعهای تغییریافته به معنای جدید همراستا میشود.
|
||||||
|
SMSTemplate.objects.filter(sms_type="visit").update(
|
||||||
|
title=DEFAULT_TEMPLATES["visit"]["title"],
|
||||||
|
text=DEFAULT_TEMPLATES["visit"]["text"],
|
||||||
|
)
|
||||||
|
SMSTemplate.objects.filter(sms_type="cheque_registration").update(
|
||||||
|
title=DEFAULT_TEMPLATES["cheque_registration"]["title"],
|
||||||
|
text=DEFAULT_TEMPLATES["cheque_registration"]["text"],
|
||||||
|
)
|
||||||
|
SMSTemplate.objects.filter(sms_type="survey").update(
|
||||||
|
title=DEFAULT_TEMPLATES["survey"]["title"],
|
||||||
|
text=DEFAULT_TEMPLATES["survey"]["text"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_removed_queue_types(apps, schema_editor):
|
||||||
|
"""نوعِ ردیفهای صفِ باقیمانده از انواع حذفشده را خالی میکند.
|
||||||
|
|
||||||
|
ردیف صف حذف نمیشود (متن و تاریخچهاش میماند) چون ممکن است هنوز ارسالنشده
|
||||||
|
باشد؛ فقط ``sms_type`` آن خالی میشود تا کدِ نوعِ نامعتبر در دیتابیس نماند.
|
||||||
|
"""
|
||||||
|
SMSQueue = apps.get_model("admin_panel", "SMSQueue")
|
||||||
|
SMSQueue.objects.filter(sms_type__in=REMOVED_TYPES).update(sms_type="")
|
||||||
|
|
||||||
|
|
||||||
|
def noop(apps, schema_editor):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("admin_panel", "0069_smstemplate_auto_send"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="smstemplate",
|
||||||
|
name="sms_type",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=SMS_TYPE_CHOICES, max_length=40, unique=True,
|
||||||
|
verbose_name="نوع پیامک",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="customersmssetting",
|
||||||
|
name="sms_type",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=SMS_TYPE_CHOICES, max_length=40, verbose_name="نوع پیامک",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="smsqueue",
|
||||||
|
name="sms_type",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, choices=SMS_TYPE_CHOICES, default="", max_length=40,
|
||||||
|
verbose_name="نوع",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(align_templates, noop),
|
||||||
|
migrations.RunPython(clear_removed_queue_types, noop),
|
||||||
|
]
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"""افزودن پرمیشن جداگانهٔ ``ManageSMSSettings`` برای صفحات تنظیمات/قالبهای پیامک.
|
||||||
|
|
||||||
|
پیش از این، همهٔ مسیرهای پیامک با ``ManageSMS`` کنترل میشد. از این پس صفحات
|
||||||
|
«تنظیمات سامانه» و «قالبها» با ``ManageSMSSettings`` جدا میشوند تا مدیر بتواند
|
||||||
|
دسترسی به آنها را فقط به کاربران مشخصی بدهد، بدون دادن کل اختیارات پیامک.
|
||||||
|
"""
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db.models import F
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_permission(apps, schema_editor):
|
||||||
|
"""رکورد Permission جدید را (اگر نبود) میسازد و نام نمایشیاش را اصلاح میکند."""
|
||||||
|
Permission = apps.get_model('auth', 'Permission')
|
||||||
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
|
|
||||||
|
content_type, _created = ContentType.objects.get_or_create(
|
||||||
|
app_label='admin_panel', model='smsproviderconfig',
|
||||||
|
)
|
||||||
|
Permission.objects.get_or_create(
|
||||||
|
content_type=content_type,
|
||||||
|
codename='ManageSMSSettings',
|
||||||
|
defaults={'name': 'ManageSMSSettings'},
|
||||||
|
)
|
||||||
|
# نام نمایشی هر دو پرمیشن پیامک را با قرارداد پروژه همراستا میکند.
|
||||||
|
Permission.objects.filter(
|
||||||
|
content_type=content_type, codename__in=['ManageSMS', 'ManageSMSSettings'],
|
||||||
|
).update(name=F('codename'))
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('admin_panel', '0070_sms_types_seven'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='smsproviderconfig',
|
||||||
|
options={
|
||||||
|
'permissions': [('ManageSMS', 'ManageSMS'),
|
||||||
|
('ManageSMSSettings', 'ManageSMSSettings')],
|
||||||
|
'verbose_name': 'تنظیمات سامانه پیامک',
|
||||||
|
'verbose_name_plural': 'تنظیمات سامانه پیامک',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RunPython(ensure_permission, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@@ -11,16 +11,20 @@ from django.db import models
|
|||||||
|
|
||||||
|
|
||||||
class SMSType(models.TextChoices):
|
class SMSType(models.TextChoices):
|
||||||
"""انواع پیامکهای سیستم."""
|
"""انواع پیامکهای سیستم (دقیقاً ۷ نوع، به ترتیب چرخهٔ کاری).
|
||||||
|
|
||||||
|
ترتیب تعریف = ترتیب نمایش در پنل. هر نوع یا از یک رویداد (view) یا از یک
|
||||||
|
اسکنر دورهای در صف قرار میگیرد؛ نگاشت آن در ``admin_panel/sms/triggers.py``
|
||||||
|
و ``admin_panel/sms/scanner.py`` است.
|
||||||
|
"""
|
||||||
|
|
||||||
WELCOME = "welcome", "خوشآمدگویی (ایجاد مشتری)"
|
WELCOME = "welcome", "خوشآمدگویی (ایجاد مشتری)"
|
||||||
CHEQUE_DUE = "cheque_due", "یادآوری سررسید چک"
|
SURVEY = "survey", "نظرسنجی پس از ثبت فاکتور"
|
||||||
NESIYE_DUE = "nesiye_due", "یادآوری سررسید نسیه"
|
|
||||||
CHEQUE_REGISTRATION = "cheque_registration", "یادآوری تأمین موجودی چک"
|
CHEQUE_REGISTRATION = "cheque_registration", "یادآوری تأمین موجودی چک"
|
||||||
DEBT_DUE = "debt_due", "یادآوری بدهی (سررسید وصول مطالبات)"
|
VISIT = "visit", "یادآوری بازدید خدمات دستگاه"
|
||||||
VISIT = "visit", "یادآوری بازدید خدمات"
|
VISIT_CREATED = "visit_created", "ثبت بازدید (اقدام از نوع بازدید)"
|
||||||
SURVEY = "survey", "نظرسنجی پس از فاکتور"
|
PAYMENT_THANKS = "payment_thanks", "تشکر از پرداخت"
|
||||||
CUSTOM = "custom", "پیام سفارشی"
|
NESIYE_DUE = "nesiye_due", "یادآوری سررسید نسیه"
|
||||||
|
|
||||||
|
|
||||||
class SMSProviderConfig(models.Model):
|
class SMSProviderConfig(models.Model):
|
||||||
@@ -33,6 +37,9 @@ class SMSProviderConfig(models.Model):
|
|||||||
# نام نمایشی مطابق قرارداد سایر پرمیشنهای پروژه (ViewStore/AddCustomer/...)
|
# نام نمایشی مطابق قرارداد سایر پرمیشنهای پروژه (ViewStore/AddCustomer/...)
|
||||||
# تا در فهرست پرمیشنهای Django Admin بهسادگی پیدا شود.
|
# تا در فهرست پرمیشنهای Django Admin بهسادگی پیدا شود.
|
||||||
("ManageSMS", "ManageSMS"),
|
("ManageSMS", "ManageSMS"),
|
||||||
|
# سطح دسترسی جداگانه برای صفحات «تنظیمات» و «قالبها»: مدیر میتواند فقط
|
||||||
|
# به کاربران مشخصی اجازهٔ دیدن/ویرایش اینها را بدهد (نه هر کاربر پیامک).
|
||||||
|
("ManageSMSSettings", "ManageSMSSettings"),
|
||||||
]
|
]
|
||||||
|
|
||||||
ProviderChoices = [
|
ProviderChoices = [
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ NOTICES = {
|
|||||||
"approved": ("success", "پیامک تأیید شد و برای ارسال در صف قرار گرفت."),
|
"approved": ("success", "پیامک تأیید شد و برای ارسال در صف قرار گرفت."),
|
||||||
"not_pending": ("warning", "این پیامک در وضعیت «در انتظار تأیید» نیست."),
|
"not_pending": ("warning", "این پیامک در وضعیت «در انتظار تأیید» نیست."),
|
||||||
"deleted": ("success", "پیامک از صف حذف شد."),
|
"deleted": ("success", "پیامک از صف حذف شد."),
|
||||||
|
"delete_done_blocked": (
|
||||||
|
"warning",
|
||||||
|
"این پیامک ارسال شده است و از صف حذف نمیشود؛ سابقهٔ آن در «گزارش ارسال» میماند.",
|
||||||
|
),
|
||||||
"bulk_approved_all": ("success", "{count} پیامک تأیید شد و برای ارسال در صف قرار گرفت."),
|
"bulk_approved_all": ("success", "{count} پیامک تأیید شد و برای ارسال در صف قرار گرفت."),
|
||||||
"bulk_approved": ("success", "{count} پیامک تأیید شد."),
|
"bulk_approved": ("success", "{count} پیامک تأیید شد."),
|
||||||
"bulk_deleted": ("success", "{count} پیامک از صف حذف شد."),
|
"bulk_deleted": ("success", "{count} پیامک از صف حذف شد."),
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
"""محاسبهٔ «تعداد پیامکِ محاسبهشده» (parts) و کدگذاری متن.
|
||||||
|
|
||||||
|
هر پیامک روی شبکهٔ موبایل بر اساس **کدگذاری** و **طول** به یک یا چند «بخش» (part)
|
||||||
|
شکسته میشود و اپراتور بر همان تعداد بخش هزینه میگیرد. این ماژول همان قاعدهٔ
|
||||||
|
استاندارد (3GPP/GSM 03.38) را پیاده میکند تا بتوان هزینهٔ ارسال را تحلیل کرد:
|
||||||
|
|
||||||
|
* **GSM-7** — برای متنهای انگلیسی/عددی. هر بخش ۱۶۰ کاراکتر؛ در حالت چندبخشی
|
||||||
|
(multipart) هر بخش ۱۵۳ کاراکتر (۷ کاراکتر برای هدر الحاق).
|
||||||
|
برخی کاراکترها (مثل `{}[]~\\|^€`) در جدول «الحاقی» هستند و **۲ کاراکتر** حساب
|
||||||
|
میشوند.
|
||||||
|
* **UCS-2** — اگر متن حتی **یک** کاراکتر خارج از جدول GSM-7 داشته باشد (فارسی،
|
||||||
|
عربی، ایموجی…)، **کل پیامک** با UCS-2 کدگذاری میشود: هر بخش ۷۰ کاراکتر؛ در
|
||||||
|
حالت چندبخشی هر بخش ۶۷ کاراکتر. ایموجیها (خارج از BMP) دو واحد کدِ UTF-16
|
||||||
|
حساب میشوند.
|
||||||
|
|
||||||
|
بنابراین یک متن فارسی حتی اگر کوتاه باشد، در «۷۰ کاراکتر» جا میشود و ممکن است
|
||||||
|
چند پیامک حساب شود؛ و یک متن ترکیبی فارسی+انگلیسی همیشه UCS-2 است.
|
||||||
|
"""
|
||||||
|
from math import ceil
|
||||||
|
|
||||||
|
ENCODING_GSM7 = "gsm7"
|
||||||
|
ENCODING_UCS2 = "ucs2"
|
||||||
|
|
||||||
|
# جدول کامل GSM 03.38 (بخش پایه). کاراکتر 0x1B عمداً وارد نشده چون نقش «پیشوند
|
||||||
|
# الحاق» دارد و کاراکتر مستقل قانونی نیست.
|
||||||
|
GSM7_BASIC = set(
|
||||||
|
"@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ"
|
||||||
|
" !\"#¤%&'()*+,-./0123456789:;<=>?"
|
||||||
|
"¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ"
|
||||||
|
"¿abcdefghijklmnopqrstuvwxyzäöñüà"
|
||||||
|
)
|
||||||
|
|
||||||
|
# کاراکترهای «جدول الحاقی»: در GSM-7 دو واحد حساب میشوند.
|
||||||
|
GSM7_EXTENDED = set("^{}\\[~]|€")
|
||||||
|
|
||||||
|
# (سقف تکبخشی، سقف هر بخش چندبخشی) برای هر کدگذاری.
|
||||||
|
LIMITS = {
|
||||||
|
ENCODING_GSM7: (160, 153),
|
||||||
|
ENCODING_UCS2: (70, 67),
|
||||||
|
}
|
||||||
|
|
||||||
|
ENCODING_LABELS = {
|
||||||
|
ENCODING_GSM7: "GSM-7 (انگلیسی/عدد)",
|
||||||
|
ENCODING_UCS2: "UCS-2 (فارسی/یونیکد)",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def is_gsm7(text):
|
||||||
|
"""آیا کل متن با جدول GSM-7 قابل نمایش است؟ (شرط کافی و لازم برای GSM-7)."""
|
||||||
|
return all(ch in GSM7_BASIC or ch in GSM7_EXTENDED for ch in text or "")
|
||||||
|
|
||||||
|
|
||||||
|
def detect_encoding(text):
|
||||||
|
"""کدگذاری متن: ``gsm7`` یا ``ucs2``. متن خالی = ``gsm7``."""
|
||||||
|
return ENCODING_GSM7 if is_gsm7(text) else ENCODING_UCS2
|
||||||
|
|
||||||
|
|
||||||
|
def encoded_length(text):
|
||||||
|
"""طول متن بر حسب «واحدهایی که اپراتور میشمارد».
|
||||||
|
|
||||||
|
* GSM-7: کاراکترهای الحاقی ۲ واحد.
|
||||||
|
* UCS-2: واحدهای کد UTF-16 (ایموجی = ۲).
|
||||||
|
"""
|
||||||
|
text = text or ""
|
||||||
|
if is_gsm7(text):
|
||||||
|
return sum(2 if ch in GSM7_EXTENDED else 1 for ch in text)
|
||||||
|
return len(text.encode("utf-16-le")) // 2
|
||||||
|
|
||||||
|
|
||||||
|
def parts_count(text):
|
||||||
|
"""تعداد بخشهایی که اپراتور برای این متن حساب میکند (۱ برای متن خالی = ۰)."""
|
||||||
|
text = text or ""
|
||||||
|
if not text:
|
||||||
|
return 0
|
||||||
|
single, multipart = LIMITS[detect_encoding(text)]
|
||||||
|
length = encoded_length(text)
|
||||||
|
if length <= single:
|
||||||
|
return 1
|
||||||
|
return ceil(length / multipart)
|
||||||
|
|
||||||
|
|
||||||
|
def describe(text):
|
||||||
|
"""خلاصهٔ محاسبه برای نمایش در گزارش (کدگذاری، طول، تعداد بخش)."""
|
||||||
|
encoding = detect_encoding(text)
|
||||||
|
return {
|
||||||
|
"encoding": encoding,
|
||||||
|
"encoding_label": ENCODING_LABELS[encoding],
|
||||||
|
"length": encoded_length(text),
|
||||||
|
"parts": parts_count(text),
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ from ..models import CustomerSMSSetting, SMSType
|
|||||||
|
|
||||||
def is_sms_enabled(customer, sms_type):
|
def is_sms_enabled(customer, sms_type):
|
||||||
"""اگر رکوردی با is_enabled=False وجود داشته باشد غیرفعال است؛ در غیر اینصورت فعال."""
|
"""اگر رکوردی با is_enabled=False وجود داشته باشد غیرفعال است؛ در غیر اینصورت فعال."""
|
||||||
if customer is None or sms_type == SMSType.CUSTOM:
|
if customer is None:
|
||||||
return True
|
return True
|
||||||
setting = CustomerSMSSetting.objects.filter(customer=customer, sms_type=sms_type).first()
|
setting = CustomerSMSSetting.objects.filter(customer=customer, sms_type=sms_type).first()
|
||||||
if setting is None:
|
if setting is None:
|
||||||
@@ -21,5 +21,4 @@ def customer_toggle_map(customer):
|
|||||||
return [
|
return [
|
||||||
{"code": code, "label": label, "enabled": overrides.get(code, True)}
|
{"code": code, "label": label, "enabled": overrides.get(code, True)}
|
||||||
for code, label in SMSType.choices
|
for code, label in SMSType.choices
|
||||||
if code != SMSType.CUSTOM
|
|
||||||
]
|
]
|
||||||
+27
-137
@@ -9,31 +9,29 @@
|
|||||||
در هر اجرا حداکثر ``backlog_max`` مورد قدیمی هم صف میشود.
|
در هر اجرا حداکثر ``backlog_max`` مورد قدیمی هم صف میشود.
|
||||||
* **سقف هر اجرا (``max_per_run``)**: سقف کل صفکردن هر اسکنر در یک اجرا.
|
* **سقف هر اجرا (``max_per_run``)**: سقف کل صفکردن هر اسکنر در یک اجرا.
|
||||||
|
|
||||||
|
اسکنرها فقط انواع «دورهای» را پوشش میدهند (تأمین موجودی چک، سررسید نسیه، بازدید
|
||||||
|
دورهای). انواع «رویدادی» (خوشآمدگویی، نظرسنجی، ثبت بازدید، تشکر از پرداخت) هنگام
|
||||||
|
وقوع رخداد توسط ویوها و از طریق ``admin_panel/sms/triggers.py`` صف میشوند.
|
||||||
|
|
||||||
هر اسکنر مستقل و مقاوم به خطاست؛ یک خطا بقیه را متوقف نمیکند. ``dedup_key`` از
|
هر اسکنر مستقل و مقاوم به خطاست؛ یک خطا بقیه را متوقف نمیکند. ``dedup_key`` از
|
||||||
ارسال تکراری جلوگیری میکند و تضمین میکند اجرای دوبارهٔ اسکنر رکورد جدیدی نسازد.
|
ارسال تکراری جلوگیری میکند و تضمین میکند اجرای دوبارهٔ اسکنر رکورد جدیدی نسازد.
|
||||||
|
|
||||||
یادآوریهای **دورهای** (بازدید و ثبت چک) کلیدشان شامل شمارهٔ دوره است. یعنی اگر
|
یادآوریهای **دورهای** (بازدید) کلیدشان شامل شمارهٔ دوره است. یعنی اگر مدیر یک مورد
|
||||||
مدیر یک مورد را از صف حذف کند، فقط همان دوره کنسل میشود و در دورهٔ بعد دوباره
|
را از صف حذف کند، فقط همان دوره کنسل میشود و در دورهٔ بعد دوباره ساخته میشود؛ برای
|
||||||
ساخته میشود؛ برای همیشه خاموش نمیماند.
|
همیشه خاموش نمیماند.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, time, timedelta
|
||||||
|
|
||||||
from django.db.models import Q, Sum
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from ..models import (Article, Customer, Factor, Sarresid, SMSProviderConfig,
|
from ..models import Customer, Sarresid, SMSProviderConfig, SMSTemplate, SMSType
|
||||||
SMSTemplate, SMSType)
|
|
||||||
from .enqueue import enqueue_sms
|
from .enqueue import enqueue_sms
|
||||||
from .timing import DEFAULT_VISIT_MONTHS
|
from .timing import DEFAULT_VISIT_MONTHS
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# پیشفرض فاصلهٔ تکرار «یادآوری ثبت چک» (روز) وقتی قالبی تنظیم نشده باشد.
|
|
||||||
CHEQUE_REG_INTERVAL_DEFAULT = 3
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _template(sms_type):
|
def _template(sms_type):
|
||||||
return SMSTemplate.objects.filter(sms_type=sms_type, is_enabled=True).first()
|
return SMSTemplate.objects.filter(sms_type=sms_type, is_enabled=True).first()
|
||||||
@@ -82,33 +80,39 @@ class _Budget:
|
|||||||
self.new_count += 1
|
self.new_count += 1
|
||||||
|
|
||||||
|
|
||||||
def scan_cheque_due(today):
|
def scan_cheque_funding(today):
|
||||||
template = _template(SMSType.CHEQUE_DUE)
|
"""یادآوری «تأمین موجودی چک» چند روز پس از سررسید چکِ پرداختنشده.
|
||||||
|
|
||||||
|
چند روزِ پس از سررسید از قالب همین نوع (قابل تنظیم از پنل) خوانده میشود و مبلغ
|
||||||
|
چک در متن قرار میگیرد. اگر چک پرداخت شود (``payStatus=true``) دیگر یادآوری
|
||||||
|
نمیگیرد؛ ``dedup_key`` نیز مانع ارسال تکراری در اجراهای بعدی میشود. اگر اسکنر
|
||||||
|
چند روز از دست برود، چکهای «معوقتر» هم واجد شرط میشوند تا یادآوری از دست نرود.
|
||||||
|
"""
|
||||||
|
template = _template(SMSType.CHEQUE_REGISTRATION)
|
||||||
if template is None:
|
if template is None:
|
||||||
return 0
|
return 0
|
||||||
budget = _Budget(SMSProviderConfig.load())
|
budget = _Budget(SMSProviderConfig.load())
|
||||||
offset = template.offset_days or 0
|
offset = template.offset_days or 0
|
||||||
|
target = today - timedelta(days=offset)
|
||||||
qs = Sarresid.objects.filter(
|
qs = Sarresid.objects.filter(
|
||||||
type="check", payStatus="false",
|
type="check", payStatus="false", sarresidDT__date__lte=target,
|
||||||
sarresidDT__date__gte=today,
|
|
||||||
sarresidDT__date__lte=today + timedelta(days=offset),
|
|
||||||
).select_related("customer").order_by("sarresidDT")
|
).select_related("customer").order_by("sarresidDT")
|
||||||
count = 0
|
count = 0
|
||||||
for s in qs:
|
for s in qs:
|
||||||
if not s.customer or not s.sarresidDT:
|
if not s.customer or not s.sarresidDT:
|
||||||
continue
|
continue
|
||||||
# پنجرهٔ یادآوری از «سررسید منهای offset» باز میشود.
|
# لحظهٔ باز شدن پنجرهٔ یادآوری = سررسید + offset روز.
|
||||||
trigger = _aware(s.sarresidDT) - timedelta(days=offset)
|
trigger = _aware(s.sarresidDT) + timedelta(days=offset)
|
||||||
if not budget.can_send(trigger):
|
if not budget.can_send(trigger):
|
||||||
continue
|
continue
|
||||||
days = (s.sarresidDT.date() - today).days
|
days = (today - s.sarresidDT.date()).days
|
||||||
made = enqueue_sms(
|
made = enqueue_sms(
|
||||||
customer=s.customer, sms_type=SMSType.CHEQUE_DUE,
|
customer=s.customer, sms_type=SMSType.CHEQUE_REGISTRATION,
|
||||||
context={
|
context={
|
||||||
"name": s.customer.name_family, "amount": s.price,
|
"name": s.customer.name_family, "amount": s.price,
|
||||||
"date": s.sarresidDT, "days": days, "check_row": s.check_row_number,
|
"date": s.sarresidDT, "days": days, "check_row": s.check_row_number,
|
||||||
},
|
},
|
||||||
dedup_key=f"cheque_due:{s.id}",
|
dedup_key=f"cheque_fund:{s.id}",
|
||||||
)
|
)
|
||||||
if made:
|
if made:
|
||||||
budget.record(trigger)
|
budget.record(trigger)
|
||||||
@@ -149,119 +153,6 @@ def scan_nesiye_due(today):
|
|||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
||||||
def scan_cheque_registration(today):
|
|
||||||
"""یادآوری «ثبت اطلاعات چک» برای چکهای پرداختنشدهٔ ثبتنشده.
|
|
||||||
|
|
||||||
هر ``interval`` روز یکبار تکرار میشود و بهمحض اینکه شمارهٔ صیادی چک ثبت شود
|
|
||||||
(یا چک پرداخت شود) متوقف میگردد. دورهٔ تکرار از پنل قابل تنظیم است.
|
|
||||||
"""
|
|
||||||
template = _template(SMSType.CHEQUE_REGISTRATION)
|
|
||||||
if template is None:
|
|
||||||
return 0
|
|
||||||
budget = _Budget(SMSProviderConfig.load())
|
|
||||||
interval = template.repeat_interval_days or CHEQUE_REG_INTERVAL_DEFAULT
|
|
||||||
qs = Sarresid.objects.filter(
|
|
||||||
type="check", payStatus="false", sarresidDT__date__gte=today,
|
|
||||||
).filter(
|
|
||||||
Q(check_sayyad_id="") | Q(check_sayyad_id__isnull=True),
|
|
||||||
).select_related("customer")
|
|
||||||
count = 0
|
|
||||||
for s in qs:
|
|
||||||
if not s.customer:
|
|
||||||
continue
|
|
||||||
anchor = s.createdDT or s.sarresidDT
|
|
||||||
if not anchor:
|
|
||||||
continue
|
|
||||||
days = (today - anchor.date()).days
|
|
||||||
if days <= 0 or days % interval != 0:
|
|
||||||
continue
|
|
||||||
cycle = days // interval
|
|
||||||
# لحظهٔ باز شدن همین دورهٔ تکرار (نه خودِ چک) ملاک خط شروع است.
|
|
||||||
trigger = _aware(anchor) + timedelta(days=days)
|
|
||||||
if not budget.can_send(trigger):
|
|
||||||
continue
|
|
||||||
made = enqueue_sms(
|
|
||||||
customer=s.customer, sms_type=SMSType.CHEQUE_REGISTRATION,
|
|
||||||
context={
|
|
||||||
"name": s.customer.name_family, "amount": s.price,
|
|
||||||
"date": s.sarresidDT, "days": (s.sarresidDT.date() - today).days
|
|
||||||
if s.sarresidDT else "",
|
|
||||||
"check_row": s.check_row_number,
|
|
||||||
},
|
|
||||||
dedup_key=f"cheque_reg:{s.id}:{cycle}",
|
|
||||||
)
|
|
||||||
if made:
|
|
||||||
budget.record(trigger)
|
|
||||||
count += made
|
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
def scan_debt_due(today):
|
|
||||||
"""یادآوری بدهی برای ماندهدارها روی سررسید وصول مطالبات."""
|
|
||||||
template = _template(SMSType.DEBT_DUE)
|
|
||||||
if template is None:
|
|
||||||
return 0
|
|
||||||
budget = _Budget(SMSProviderConfig.load())
|
|
||||||
offset = template.offset_days or 0
|
|
||||||
target = today - timedelta(days=offset)
|
|
||||||
qs = Sarresid.objects.filter(
|
|
||||||
payStatus="false", sarresidDT__date=target,
|
|
||||||
).select_related("customer").order_by("sarresidDT")
|
|
||||||
count = 0
|
|
||||||
seen = set()
|
|
||||||
for s in qs:
|
|
||||||
if not s.customer or s.customer_id in seen or not s.sarresidDT:
|
|
||||||
continue
|
|
||||||
trigger = _aware(s.sarresidDT)
|
|
||||||
if not budget.can_send(trigger):
|
|
||||||
continue
|
|
||||||
totals = Article.objects.filter(customer=s.customer).aggregate(
|
|
||||||
d=Sum("debit"), c=Sum("credit"),
|
|
||||||
)
|
|
||||||
net = (totals["d"] or 0) - (totals["c"] or 0)
|
|
||||||
if net <= 0:
|
|
||||||
continue
|
|
||||||
made = enqueue_sms(
|
|
||||||
customer=s.customer, sms_type=SMSType.DEBT_DUE,
|
|
||||||
context={"name": s.customer.name_family, "amount": net},
|
|
||||||
dedup_key=f"debt_due:{s.id}",
|
|
||||||
)
|
|
||||||
if made:
|
|
||||||
seen.add(s.customer_id)
|
|
||||||
budget.record(trigger)
|
|
||||||
count += made
|
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
def scan_survey(today):
|
|
||||||
"""نظرسنجی چند روز پس از ایجاد فاکتور."""
|
|
||||||
template = _template(SMSType.SURVEY)
|
|
||||||
if template is None:
|
|
||||||
return 0
|
|
||||||
budget = _Budget(SMSProviderConfig.load())
|
|
||||||
offset = template.offset_days or 3
|
|
||||||
target = today - timedelta(days=offset)
|
|
||||||
qs = Factor.objects.filter(
|
|
||||||
is_active=True, create_date__date=target,
|
|
||||||
).exclude(pay_status="cancell").select_related("customer").order_by("create_date")
|
|
||||||
count = 0
|
|
||||||
for f in qs:
|
|
||||||
if not f.customer or not f.create_date:
|
|
||||||
continue
|
|
||||||
trigger = _aware(f.create_date) + timedelta(days=offset)
|
|
||||||
if not budget.can_send(trigger):
|
|
||||||
continue
|
|
||||||
made = enqueue_sms(
|
|
||||||
customer=f.customer, sms_type=SMSType.SURVEY,
|
|
||||||
context={"name": f.customer.name_family, "amount": f.amount},
|
|
||||||
dedup_key=f"survey:{f.id}",
|
|
||||||
)
|
|
||||||
if made:
|
|
||||||
budget.record(trigger)
|
|
||||||
count += made
|
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
def scan_visit(today):
|
def scan_visit(today):
|
||||||
"""یادآوری بازدید برای مشتریان عقبافتاده از بازدید (منطق صفحهٔ «انتظار»).
|
"""یادآوری بازدید برای مشتریان عقبافتاده از بازدید (منطق صفحهٔ «انتظار»).
|
||||||
|
|
||||||
@@ -313,12 +204,11 @@ def scan_visit(today):
|
|||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
# فقط همان ۷ نوع مجاز؛ انواع رویدادی (خوشآمدگویی، نظرسنجی، ثبت بازدید، تشکر از
|
||||||
|
# پرداخت) در ``admin_panel/sms/triggers.py`` توسط ویوها صف میشوند، نه اینجا.
|
||||||
SCANNERS = [
|
SCANNERS = [
|
||||||
("cheque_due", scan_cheque_due),
|
("cheque_registration", scan_cheque_funding),
|
||||||
("nesiye_due", scan_nesiye_due),
|
("nesiye_due", scan_nesiye_due),
|
||||||
("cheque_registration", scan_cheque_registration),
|
|
||||||
("debt_due", scan_debt_due),
|
|
||||||
("survey", scan_survey),
|
|
||||||
("visit", scan_visit),
|
("visit", scan_visit),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+24
-34
@@ -9,8 +9,7 @@ from ..models import SMSType
|
|||||||
|
|
||||||
NONE = "none"
|
NONE = "none"
|
||||||
BEFORE_DUE = "before_due"
|
BEFORE_DUE = "before_due"
|
||||||
AFTER_CREATION = "after_creation"
|
AFTER_DUE = "after_due"
|
||||||
RECURRING = "recurring"
|
|
||||||
PERIOD_MONTHS = "period_months"
|
PERIOD_MONTHS = "period_months"
|
||||||
|
|
||||||
# پیشفرض دورهٔ بازدید (ماه) وقتی مدیر مقداری در قالب نگذاشته باشد.
|
# پیشفرض دورهٔ بازدید (ماه) وقتی مدیر مقداری در قالب نگذاشته باشد.
|
||||||
@@ -22,39 +21,18 @@ TIMING = {
|
|||||||
"field": None,
|
"field": None,
|
||||||
"note": "بهمحض ثبت مشتری و بدون تأخیر ارسال میشود؛ تنظیم زمانی ندارد.",
|
"note": "بهمحض ثبت مشتری و بدون تأخیر ارسال میشود؛ تنظیم زمانی ندارد.",
|
||||||
},
|
},
|
||||||
SMSType.CHEQUE_DUE: {
|
|
||||||
"kind": BEFORE_DUE,
|
|
||||||
"field": "offset_days",
|
|
||||||
"label": "چند روز قبل از سررسید چک",
|
|
||||||
"default": 2,
|
|
||||||
"note": "پنجرهٔ یادآوری از «سررسید − این تعداد روز» باز میشود و تا سررسید ادامه دارد.",
|
|
||||||
},
|
|
||||||
SMSType.NESIYE_DUE: {
|
|
||||||
"kind": BEFORE_DUE,
|
|
||||||
"field": "offset_days",
|
|
||||||
"label": "چند روز قبل از سررسید نسیه",
|
|
||||||
"default": 2,
|
|
||||||
"note": "پنجرهٔ یادآوری از «سررسید − این تعداد روز» باز میشود و تا سررسید ادامه دارد.",
|
|
||||||
},
|
|
||||||
SMSType.CHEQUE_REGISTRATION: {
|
|
||||||
"kind": RECURRING,
|
|
||||||
"field": "repeat_interval_days",
|
|
||||||
"label": "هر چند روز یکبار",
|
|
||||||
"default": 3,
|
|
||||||
"note": "تا زمانی که اطلاعات ثبت چک (شمارهٔ صیادی) وارد نشده و چک پرداخت نشده، "
|
|
||||||
"با این فاصله تکرار میشود.",
|
|
||||||
},
|
|
||||||
SMSType.SURVEY: {
|
SMSType.SURVEY: {
|
||||||
"kind": AFTER_CREATION,
|
|
||||||
"field": "offset_days",
|
|
||||||
"label": "چند روز بعد از صدور فاکتور",
|
|
||||||
"default": 3,
|
|
||||||
"note": "این تعداد روز پس از ثبت فاکتور ارسال میشود.",
|
|
||||||
},
|
|
||||||
SMSType.DEBT_DUE: {
|
|
||||||
"kind": NONE,
|
"kind": NONE,
|
||||||
"field": None,
|
"field": None,
|
||||||
"note": "روی سررسیدِ وصول مطالبات و فقط برای مشتریانی که مانده بدهکارند ارسال میشود.",
|
"note": "بیدرنگ پس از ثبت فاکتور ارسال میشود؛ تنظیم زمانی ندارد.",
|
||||||
|
},
|
||||||
|
SMSType.CHEQUE_REGISTRATION: {
|
||||||
|
"kind": AFTER_DUE,
|
||||||
|
"field": "offset_days",
|
||||||
|
"label": "چند روز پس از سررسید چک",
|
||||||
|
"default": 1,
|
||||||
|
"note": "این تعداد روز پس از سررسید چکِ پرداختنشده، پیامک یادآوری "
|
||||||
|
"تأمین موجودی با مبلغ چک ارسال میشود.",
|
||||||
},
|
},
|
||||||
SMSType.VISIT: {
|
SMSType.VISIT: {
|
||||||
"kind": PERIOD_MONTHS,
|
"kind": PERIOD_MONTHS,
|
||||||
@@ -63,10 +41,22 @@ TIMING = {
|
|||||||
"default": DEFAULT_VISIT_MONTHS,
|
"default": DEFAULT_VISIT_MONTHS,
|
||||||
"note": "برای مشتریانی که از این مدت بازدید نشدهاند ارسال میشود.",
|
"note": "برای مشتریانی که از این مدت بازدید نشدهاند ارسال میشود.",
|
||||||
},
|
},
|
||||||
SMSType.CUSTOM: {
|
SMSType.VISIT_CREATED: {
|
||||||
"kind": NONE,
|
"kind": NONE,
|
||||||
"field": None,
|
"field": None,
|
||||||
"note": "فقط بهصورت دستی از صفحهٔ مشتری ارسال میشود؛ تنظیم زمانی ندارد.",
|
"note": "بهمحض ثبت «اقدام از نوع بازدید» ارسال میشود؛ تنظیم زمانی ندارد.",
|
||||||
|
},
|
||||||
|
SMSType.PAYMENT_THANKS: {
|
||||||
|
"kind": NONE,
|
||||||
|
"field": None,
|
||||||
|
"note": "بیدرنگ پس از ثبت پرداخت (سند وصول/بستانکار) ارسال میشود؛ تنظیم زمانی ندارد.",
|
||||||
|
},
|
||||||
|
SMSType.NESIYE_DUE: {
|
||||||
|
"kind": BEFORE_DUE,
|
||||||
|
"field": "offset_days",
|
||||||
|
"label": "چند روز قبل از سررسید نسیه",
|
||||||
|
"default": 0,
|
||||||
|
"note": "پنجرهٔ یادآوری از «سررسید − این تعداد روز» باز میشود و تا سررسید ادامه دارد.",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
"""تریگرهای رویدادی پیامک (لایهٔ سرویس، بدون signal).
|
||||||
|
|
||||||
|
چهار نوع پیامک «رویدادی» هستند و لحظهٔ وقوعشان یک اکشن پنل است، نه یک سررسید
|
||||||
|
تاریخی؛ پس بهجای اسکنر دورهای، ویوها بعد از انجام کارِ اصلی این توابع را صدا
|
||||||
|
میزنند:
|
||||||
|
|
||||||
|
* :func:`notify_welcome` — ایجاد مشتری.
|
||||||
|
* :func:`notify_factor_survey` — ثبت فاکتور (نظرسنجی).
|
||||||
|
* :func:`notify_visit_scheduled` — ثبت «اقدام از نوع بازدید».
|
||||||
|
* :func:`notify_payment_thanks` — ثبت پرداخت (سند بستانکار / وصول).
|
||||||
|
|
||||||
|
قرارداد پروژه: استفاده از signal ممنوع است؛ همهچیز صریح از دلِ ویو صدا زده میشود.
|
||||||
|
این توابع هیچ استثنایی به فراخوان پرتاب نمیکنند (``enqueue_sms`` خودش مقاوم است) تا
|
||||||
|
مسیر اصلی هرگز بهخاطر پیامک مختل نشود.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ..models import SMSType
|
||||||
|
from .enqueue import enqueue_sms
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# روشهای «غیرپرداخت»: سند بستانکاری که بهخاطر تخفیف صادر شده، پرداختِ واقعی نیست
|
||||||
|
# و نباید پیامک «تشکر از پرداخت» بگیرد.
|
||||||
|
NON_PAYMENT_METHODS = {"تخفیف"}
|
||||||
|
|
||||||
|
|
||||||
|
def notify_welcome(customer, *, created_by=None):
|
||||||
|
"""پیامک خوشآمدگویی در لحظهٔ ساخت مشتری."""
|
||||||
|
if customer is None or customer.pk is None:
|
||||||
|
return 0
|
||||||
|
return enqueue_sms(
|
||||||
|
customer=customer, sms_type=SMSType.WELCOME,
|
||||||
|
context={"name": customer.name_family},
|
||||||
|
dedup_key=f"welcome:{customer.pk}",
|
||||||
|
created_by=created_by,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def notify_factor_survey(customer, factor, *, created_by=None):
|
||||||
|
"""پیامک نظرسنجی، بیدرنگ پس از ثبت فاکتور."""
|
||||||
|
if customer is None or factor is None or factor.pk is None:
|
||||||
|
return 0
|
||||||
|
return enqueue_sms(
|
||||||
|
customer=customer, sms_type=SMSType.SURVEY,
|
||||||
|
context={"name": customer.name_family, "amount": factor.amount},
|
||||||
|
dedup_key=f"survey:{factor.pk}",
|
||||||
|
created_by=created_by,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def notify_visit_scheduled(customer, action, *, created_by=None):
|
||||||
|
"""پیامک «بازدید شما برای تاریخ … ثبت شد» پس از ایجاد اقدام بازدید."""
|
||||||
|
if customer is None or action is None or action.pk is None:
|
||||||
|
return 0
|
||||||
|
return enqueue_sms(
|
||||||
|
customer=customer, sms_type=SMSType.VISIT_CREATED,
|
||||||
|
context={"name": customer.name_family, "date": action.actionDateTime},
|
||||||
|
dedup_key=f"visit_created:{action.pk}",
|
||||||
|
created_by=created_by,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def notify_payment_thanks(customer, amount, *, article_id=None, method=None,
|
||||||
|
created_by=None):
|
||||||
|
"""پیامک تشکر از پرداخت؛ پس از ثبت سند پرداخت (بستانکار/وصول).
|
||||||
|
|
||||||
|
``article_id`` برای یکتاسازی استفاده میشود تا یک پرداخت هرگز دو پیامک نگیرد.
|
||||||
|
``method`` نحوهٔ پرداخت است؛ اگر «تخفیف» باشد (که پرداختِ واقعی نیست) هیچ پیامکی
|
||||||
|
ساخته نمیشود.
|
||||||
|
"""
|
||||||
|
if customer is None:
|
||||||
|
return 0
|
||||||
|
if method and str(method).strip() in NON_PAYMENT_METHODS:
|
||||||
|
return 0
|
||||||
|
dedup_key = f"payment_thanks:{article_id}" if article_id is not None else None
|
||||||
|
return enqueue_sms(
|
||||||
|
customer=customer, sms_type=SMSType.PAYMENT_THANKS,
|
||||||
|
context={"name": customer.name_family, "amount": amount},
|
||||||
|
dedup_key=dedup_key,
|
||||||
|
created_by=created_by,
|
||||||
|
)
|
||||||
@@ -130,3 +130,4 @@ input[disabled]{
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
border-left: 1px solid #b7b7b7;
|
border-left: 1px solid #b7b7b7;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
{% load custom_tags %}
|
||||||
|
{% comment %}
|
||||||
|
کارت پروفایل مشتری — ستون سمت راست صفحهٔ مشتری.
|
||||||
|
تکمنبع (DRY): هم customerBase.html و هم customerBaseNew.html این را include میکنند
|
||||||
|
تا ساختار، استایل و اسکریپت ویرایش/شمارهها در همهٔ صفحات مشتری یکسان بماند.
|
||||||
|
شناسههای موردنیاز جاوااسکریپت:
|
||||||
|
#formcsEdit, #enEdit, #editCs, #cancelEdit, #del_customer, #showUserDT, #userDT
|
||||||
|
{% endcomment %}
|
||||||
|
<form action="{% url 'admin_panel:customerEdit' customer.id %}" id="formcsEdit" class="has-background-white brad5" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="buttons m-0 level p-2 is-hidden-tablet">
|
||||||
|
<p class="level-item">{{ customer.name_family }}</p>
|
||||||
|
<span id="showUserDT" class="button level-item">مشاهده مشخصات</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="userDT" class="is-hidden-mobile">
|
||||||
|
|
||||||
|
<div class="field column is-12-desktop is-12-mobile m-0">
|
||||||
|
<label class="label">نام ونام خانوادگی</label>
|
||||||
|
<div class="control">
|
||||||
|
<input required disabled value="{{ customer.name_family }}" name="an_name" class="input an_name" type="text" placeholder="نام نام خانوادگی">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field column is-12-desktop is-12-mobile m-0">
|
||||||
|
<label class="label">آدرس</label>
|
||||||
|
<div class="control">
|
||||||
|
<input disabled name="an_address" value="{{ customer.address }}" class="input an_address" type="text" placeholder="آدرس کامل">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include "./_customer_phones.html" with phones_editable=1 %}
|
||||||
|
|
||||||
|
<div class="field column is-12-desktop is-12-mobile m-0">
|
||||||
|
<label class="label">تاریخ نصب</label>
|
||||||
|
<div class="control">
|
||||||
|
<input disabled name="installDate" value="{{ customer.installDate | toJalali }}" id="persianDateP" class="input ss02 installDate" type="text" placeholder="مثل: 1400/06/01">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field column is-12-desktop is-12-mobile m-0">
|
||||||
|
<label class="label">کدشناسایی</label>
|
||||||
|
<div class="control">
|
||||||
|
<input disabled value="{{ customer.idCode }}" name="idnCode" class="input idnCode ss02" type="text" placeholder="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field column is-12-desktop is-12-mobile m-0">
|
||||||
|
<label class="label">توضیحات</label>
|
||||||
|
<div class="control">
|
||||||
|
<textarea disabled name="notes" class="textarea ss02 an_notes" rows="4" placeholder="یادداشت آزاد">{{ customer.notes }}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field column is-12-desktop is-12-mobile m-0">
|
||||||
|
<label class="label">لغو بازدید</label>
|
||||||
|
<div class="control">
|
||||||
|
<select name="laghv_bazdid" style="width: 100%" class="select" id="LaghvBazdid">
|
||||||
|
<option value="false" {% if not customer.laghv_bazdid %}selected{% endif %}>خیر</option>
|
||||||
|
<option value="true" {% if customer.laghv_bazdid %}selected{% endif %}>بله</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field column is-12-desktop is-12-mobile m-0">
|
||||||
|
{% if perms.admin_panel.DelCustomer %}
|
||||||
|
<span class="button is-danger" id="del_customer">حذف مشتری</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="button is-success" id="enEdit">ویرایش</span>
|
||||||
|
<span class="button is-light is-hidden" id="cancelEdit">انصراف</span>
|
||||||
|
<input class="button is-danger is-hidden" id="editCs" value="ثبت" type="submit">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// اسکریپت مشترک کارت پروفایل مشتری: ویرایش/انصراف، افزودن و حذف شماره،
|
||||||
|
// باز/بستن در موبایل و حذف مشتری. (یکبار bind میشود.)
|
||||||
|
(function () {
|
||||||
|
if (window.__customerInfoCardInit) { return; }
|
||||||
|
window.__customerInfoCardInit = true;
|
||||||
|
|
||||||
|
// --- ورود به حالت ویرایش ---
|
||||||
|
$(document).on('click', '#enEdit', function () {
|
||||||
|
$('#formcsEdit').find('input, textarea, select').prop('disabled', false);
|
||||||
|
$('#formcsEdit').find('.remove-phone-btn, .add-phone-btn').prop('disabled', false);
|
||||||
|
$('#enEdit').addClass('is-hidden');
|
||||||
|
$('#cancelEdit').removeClass('is-hidden');
|
||||||
|
$('#editCs').removeClass('is-hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- انصراف: بارگذاری مجدد؛ تغییرات ذخیرهنشده دور ریخته میشود ---
|
||||||
|
$(document).on('click', '#cancelEdit', function () {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- افزودن شماره (در حالت ویرایش) ---
|
||||||
|
$(document).on('click', '.add-phone-btn', function () {
|
||||||
|
var phoneRow = `
|
||||||
|
<div class="field has-addons mt-2 ss02">
|
||||||
|
<div class="control" style="width: 30%;">
|
||||||
|
<input name="phone_titles" class="input" type="text" placeholder="عنوان">
|
||||||
|
</div>
|
||||||
|
<div class="control is-expanded">
|
||||||
|
<input name="phones" class="input an_phone" type="text" placeholder="شمارهٔ تماس جدید">
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<button type="button" class="button is-danger remove-phone-btn">-</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
$('#dynamic-phones-edit').append(phoneRow);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- حذف شماره (حداقل یک شماره باید بماند) ---
|
||||||
|
$(document).on('click', '.remove-phone-btn', function () {
|
||||||
|
if ($('#dynamic-phones-edit .field.has-addons').length > 1) {
|
||||||
|
$(this).closest('.field.has-addons').remove();
|
||||||
|
} else {
|
||||||
|
window.alert('حداقل یک شمارهٔ تماس باید وجود داشته باشد.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- باز/بستن مشخصات در موبایل ---
|
||||||
|
$(document).on('click', '#showUserDT', function () {
|
||||||
|
var $btn = $(this);
|
||||||
|
if ($btn.text().trim() === 'مشاهده مشخصات') {
|
||||||
|
$btn.text('مخفی کردن');
|
||||||
|
$('#userDT').removeClass('is-hidden-mobile');
|
||||||
|
} else {
|
||||||
|
$btn.text('مشاهده مشخصات');
|
||||||
|
$('#userDT').addClass('is-hidden-mobile');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- حذف مشتری ---
|
||||||
|
$(document).on('click', '#del_customer', function () {
|
||||||
|
if (window.confirm('با حذف مشتری، تمام سوابق، فاکتورها و اطلاعات از سیستم حذف میشود، مطمئن هستید؟')) {
|
||||||
|
window.location.href = '/panel/customer/{{ customer.id }}/del';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -19,6 +19,16 @@
|
|||||||
📞
|
📞
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
{% if perms.admin_panel.ManageSMS and entry.is_mobile %}
|
||||||
|
<div class="control">
|
||||||
|
<button type="button"
|
||||||
|
class="button sms-primary-toggle {% if entry.is_primary %}is-warning{% else %}is-light{% endif %}"
|
||||||
|
data-url="{% url 'admin_panel:sms_primary_toggle' customer.id %}"
|
||||||
|
data-phone="{{ entry.raw }}"
|
||||||
|
data-primary="{% if entry.is_primary %}true{% else %}false{% endif %}"
|
||||||
|
title="{% if entry.is_primary %}برداشتن از شمارههای اصلی پیامک{% else %}تعیین بهعنوان شمارهٔ اصلی پیامک{% endif %}">{% if entry.is_primary %}★ اصلی{% else %}☆ اصلی{% endif %}</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if phones_editable and perms.admin_panel.ViewStore %}
|
{% if phones_editable and perms.admin_panel.ViewStore %}
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button type="button" disabled class="button is-danger remove-phone-btn">-</button>
|
<button type="button" disabled class="button is-danger remove-phone-btn">-</button>
|
||||||
@@ -35,3 +45,50 @@
|
|||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if perms.admin_panel.ManageSMS %}
|
||||||
|
<script>
|
||||||
|
// تعیین شمارهٔ اصلی پیامک از همین ستون (بدون فرم تودرتو) — با درخواست async و
|
||||||
|
// بارگذاری مجدد صفحه تا وضعیت شمارهها و رنگ دکمهها بهدرستی بهروز شوند.
|
||||||
|
(function () {
|
||||||
|
if (window.__smsPrimaryToggleInit) { return; }
|
||||||
|
window.__smsPrimaryToggleInit = true;
|
||||||
|
|
||||||
|
function csrfToken() {
|
||||||
|
var match = document.cookie.match(/(^|;\s*)csrftoken=([^;]+)/);
|
||||||
|
return match ? decodeURIComponent(match[2]) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', function (event) {
|
||||||
|
var btn = event.target.closest ? event.target.closest('.sms-primary-toggle') : null;
|
||||||
|
if (!btn || btn.disabled) { return; }
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
var body = new URLSearchParams();
|
||||||
|
body.append('phone', btn.getAttribute('data-phone') || '');
|
||||||
|
body.append('is_primary', btn.getAttribute('data-primary') === 'true' ? 'false' : 'true');
|
||||||
|
|
||||||
|
btn.classList.add('is-loading');
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
fetch(btn.getAttribute('data-url'), {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': csrfToken(),
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||||
|
},
|
||||||
|
body: body.toString()
|
||||||
|
}).then(function (response) {
|
||||||
|
if (!response.ok) { throw new Error('HTTP ' + response.status); }
|
||||||
|
window.location.reload();
|
||||||
|
}).catch(function () {
|
||||||
|
btn.classList.remove('is-loading');
|
||||||
|
btn.disabled = false;
|
||||||
|
window.alert('خطا در تغییر شمارهٔ اصلی. لطفاً دوباره تلاش کنید.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
@@ -3,37 +3,7 @@
|
|||||||
<h2 class="title is-5">پیامک</h2>
|
<h2 class="title is-5">پیامک</h2>
|
||||||
|
|
||||||
<div class="columns is-multiline">
|
<div class="columns is-multiline">
|
||||||
<div class="column is-6-desktop is-12-mobile">
|
<div class="column is-12">
|
||||||
<h3 class="subtitle is-6">شمارههای اصلی (گیرنده پیامک)</h3>
|
|
||||||
{# همهٔ شمارهها از هر منبعی که ذخیره شدهاند نمایش داده میشوند (جدول شمارهها یا فیلدهای قدیمی) #}
|
|
||||||
{% for entry in sms_phone_entries %}
|
|
||||||
<form method="post" action="{% url 'admin_panel:sms_primary_toggle' customer.id %}" class="field has-addons">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="phone" value="{{ entry.raw }}">
|
|
||||||
<input type="hidden" name="is_primary" value="{% if entry.is_primary %}false{% else %}true{% endif %}">
|
|
||||||
<div class="control is-expanded">
|
|
||||||
<input class="input" type="text"
|
|
||||||
value="{{ entry.title }} — {{ entry.display }}{% if not entry.is_mobile %} (غیرقابل ارسال پیامک){% endif %}"
|
|
||||||
disabled>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
{% if entry.is_mobile %}
|
|
||||||
<button class="button {% if entry.is_primary %}is-success{% else %}is-light{% endif %}" type="submit">
|
|
||||||
{% if entry.is_primary %}اصلی ✓{% else %}تعیین بهعنوان اصلی{% endif %}
|
|
||||||
</button>
|
|
||||||
{% else %}
|
|
||||||
<button class="button is-light" type="button" disabled>غیرموبایل</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% empty %}
|
|
||||||
<p class="help">شمارهای برای این مشتری ثبت نشده است.</p>
|
|
||||||
{% endfor %}
|
|
||||||
<p class="help">اگر شمارهٔ اصلی تعیین نشده باشد، شمارهٔ اولِ معتبر بهصورت خودکار استفاده میشود.</p>
|
|
||||||
<p class="help">گیرندگان فعلی: {% for r in sms_recipients %}<span dir="ltr">{{ r }}</span>{% if not forloop.last %}، {% endif %}{% empty %}—{% endfor %}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="column is-6-desktop is-12-mobile">
|
|
||||||
<h3 class="subtitle is-6">فعال/غیرفعال پیامکها برای این مشتری</h3>
|
<h3 class="subtitle is-6">فعال/غیرفعال پیامکها برای این مشتری</h3>
|
||||||
{% for item in sms_toggles %}
|
{% for item in sms_toggles %}
|
||||||
<form method="post" action="{% url 'admin_panel:sms_customer_toggle' customer.id %}" class="field has-addons">
|
<form method="post" action="{% url 'admin_panel:sms_customer_toggle' customer.id %}" class="field has-addons">
|
||||||
@@ -58,7 +28,7 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<textarea class="textarea" name="text" rows="2" placeholder="متن پیامک... (خالی بگذارید تا متن قالب «پیام سفارشی» استفاده شود)"></textarea>
|
<textarea class="textarea" name="text" rows="2" placeholder="متن پیامک..."></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="help">میتوانید از متغیرهایی مثل {name} (نام مشتری) استفاده کنید.</p>
|
<p class="help">میتوانید از متغیرهایی مثل {name} (نام مشتری) استفاده کنید.</p>
|
||||||
|
|||||||
@@ -120,19 +120,26 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% if perms.admin_panel.ManageSMS %}
|
{% if perms.admin_panel.ManageSMS or perms.admin_panel.ManageSMSSettings %}
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
<a class="navbar-link">پیامک</a>
|
<a class="navbar-link">پیامک</a>
|
||||||
<div class="navbar-dropdown">
|
<div class="navbar-dropdown">
|
||||||
|
{% if perms.admin_panel.ManageSMSSettings %}
|
||||||
<a href="{% url 'admin_panel:sms_settings' %}" style="padding-right: 10px;" class="navbar-item">
|
<a href="{% url 'admin_panel:sms_settings' %}" style="padding-right: 10px;" class="navbar-item">
|
||||||
تنظیمات سامانه
|
تنظیمات سامانه
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'admin_panel:sms_templates' %}" style="padding-right: 10px;" class="navbar-item">
|
<a href="{% url 'admin_panel:sms_templates' %}" style="padding-right: 10px;" class="navbar-item">
|
||||||
قالبها
|
قالبها
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.admin_panel.ManageSMS %}
|
||||||
<a href="{% url 'admin_panel:sms_queue' %}" style="padding-right: 10px;" class="navbar-item">
|
<a href="{% url 'admin_panel:sms_queue' %}" style="padding-right: 10px;" class="navbar-item">
|
||||||
صف ارسال
|
صف ارسال
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{% url 'admin_panel:sms_report' %}" style="padding-right: 10px;" class="navbar-item">
|
||||||
|
گزارش ارسال
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -6,55 +6,7 @@
|
|||||||
<div class="">
|
<div class="">
|
||||||
<div class="columns m-0">
|
<div class="columns m-0">
|
||||||
<div class="column is-12-mobile is-4-desktop">
|
<div class="column is-12-mobile is-4-desktop">
|
||||||
<form action="{% url 'admin_panel:customerEdit' customer.id %}" id="formcsEdit" class="has-background-white brad5" method="post">
|
{% include "./_customer_info.html" %}
|
||||||
{% csrf_token %}
|
|
||||||
<div class="buttons m-0 level p-2 is-hidden-tablet">
|
|
||||||
<p class="level-item">محمد جواد فصاحت</p>
|
|
||||||
<span id="showUserDT" class="button level-item">مشاهده مشخصات</span>
|
|
||||||
</div>
|
|
||||||
<div id="userDT" class="is-hidden-mobile">
|
|
||||||
<div class="field column is-12-desktop is-12-mobile m-0">
|
|
||||||
<label class="label">نام ونام خانوادگی</label>
|
|
||||||
<div class="control">
|
|
||||||
<input disabled value="{{ customer.name_family }}" name="an_name" class="input an_name" type="text" placeholder="نام نام خانوادگی">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field column is-12-desktop is-12-mobile m-0">
|
|
||||||
<label class="label">آدرس</label>
|
|
||||||
<div class="control">
|
|
||||||
<input disabled name="an_address" value="{{ customer.address }}" class="input an_address" type="text" placeholder="آدرس کامل">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% include "./_customer_phones.html" %}
|
|
||||||
|
|
||||||
|
|
||||||
<div class="field column is-12-desktop is-12-mobile m-0">
|
|
||||||
<label class="label">تاریخ نصب</label>
|
|
||||||
<div class="control">
|
|
||||||
<input disabled name="installDate" value="{{ customer.installDate | toJalali }}" id="persianDateP" class="input installDate" type="text" placeholder="مثل: 1400/06/01">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field column is-12-desktop is-12-mobile m-0">
|
|
||||||
<label class="label">کدشناسایی</label>
|
|
||||||
<div class="control">
|
|
||||||
<input disabled value="{{ customer.idCode }}" name="idnCode" class="input idnCode" type="text" placeholder="">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field column is-12-desktop is-12-mobile m-0">
|
|
||||||
{% if perms.admin_panel.DelCustomer %}
|
|
||||||
<span class="button is-danger" id="del_customer">حذف مشتری</span>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<span class="button is-success" id="enEdit">ویرایش</span>
|
|
||||||
<input class="button is-danger is-hidden" id="editCs" value="ثبت" type="submit">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-12-mobile is-8-desktop">
|
<div class="column is-12-mobile is-8-desktop">
|
||||||
@@ -229,11 +181,6 @@
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#del_customer").click(function(){
|
|
||||||
if(confirm("با حذف مشتری، تمام سوابق، فاکتورها و اطلاعات از سیستم حذف میشود، مطمئن هستید؟")){
|
|
||||||
window.location.href = "/panel/customer/{{ customer.id }}/del";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".closeNewIteam").click(function(){
|
$(".closeNewIteam").click(function(){
|
||||||
$("#addNewIteam").removeClass("is-active");
|
$("#addNewIteam").removeClass("is-active");
|
||||||
@@ -318,22 +265,6 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
var test = 0
|
var test = 0
|
||||||
|
|
||||||
$("#enEdit").click(function(){
|
|
||||||
$("#formcsEdit").find("input").prop('disabled', false);
|
|
||||||
$("#enEdit").addClass("is-hidden");
|
|
||||||
$("#editCs").removeClass("is-hidden");
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#showUserDT").click(function(){
|
|
||||||
if( $(this).html()=='مشاهده مشخصات' ){
|
|
||||||
$(this).html('مخفی کردن');
|
|
||||||
$("#userDT").removeClass("is-hidden-mobile");
|
|
||||||
}else{
|
|
||||||
$(this).html('مشاهده مشخصات');
|
|
||||||
$("#userDT").addClass("is-hidden-mobile");
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
$(".closeSearch").click(function(){
|
$(".closeSearch").click(function(){
|
||||||
$(".SearchBox").removeClass("is-active")
|
$(".SearchBox").removeClass("is-active")
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,76 +18,7 @@
|
|||||||
<div class="">
|
<div class="">
|
||||||
<div class="columns m-0">
|
<div class="columns m-0">
|
||||||
<div class="column is-12-mobile is-4-desktop">
|
<div class="column is-12-mobile is-4-desktop">
|
||||||
<form action="{% url 'admin_panel:customerEdit' customer.id %}" id="formcsEdit" class="has-background-white brad5" method="post">
|
{% include "./_customer_info.html" %}
|
||||||
{% csrf_token %}
|
|
||||||
<div class="buttons m-0 level p-2 is-hidden-tablet">
|
|
||||||
<p class="level-item">
|
|
||||||
{{ customer.name_family }}
|
|
||||||
</p>
|
|
||||||
<span id="showUserDT" class="button level-item">مشاهده مشخصات</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="userDT" class="is-hidden-mobile">
|
|
||||||
<div class="field column is-12-desktop is-12-mobile m-0">
|
|
||||||
<label class="label">نام ونام خانوادگی</label>
|
|
||||||
<div class="control">
|
|
||||||
<input required disabled value="{{ customer.name_family }}" name="an_name" class="input an_name" type="text" placeholder="نام نام خانوادگی">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field column is-12-desktop is-12-mobile m-0">
|
|
||||||
<label class="label">آدرس</label>
|
|
||||||
<div class="control">
|
|
||||||
<input disabled name="an_address" value="{{ customer.address }}" class="input an_address" type="text" placeholder="آدرس کامل">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% include "./_customer_phones.html" with phones_editable=1 %}
|
|
||||||
|
|
||||||
|
|
||||||
<div class="field column is-12-desktop is-12-mobile m-0">
|
|
||||||
<label class="label">تاریخ نصب</label>
|
|
||||||
<div class="control">
|
|
||||||
<input disabled name="installDate" value="{{ customer.installDate | toJalali }}" id="persianDateP" class="input ss02 installDate" type="text" placeholder="مثل: 1400/06/01">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field column is-12-desktop is-12-mobile m-0">
|
|
||||||
<label class="label">کدشناسایی</label>
|
|
||||||
<div class="control">
|
|
||||||
<input disabled value="{{ customer.idCode }}" name="idnCode" class="input idnCode ss02 " type="text" placeholder="">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field column is-12-desktop is-12-mobile m-0">
|
|
||||||
<label class="label">توضیحات</label>
|
|
||||||
<div class="control">
|
|
||||||
<textarea disabled name="notes" class="textarea ss02 an_notes" rows="4" placeholder="یادداشت آزاد">{{ customer.notes }}</textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field column is-12-desktop is-12-mobile m-0">
|
|
||||||
<label class="label">لغو بازدید</label>
|
|
||||||
<div class="control">
|
|
||||||
<select name="laghv_bazdid" style="width: 100%" class="select" id="LaghvBazdid">
|
|
||||||
<option value="false">خیر</option>
|
|
||||||
<option value="true">بله</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field column is-12-desktop is-12-mobile m-0">
|
|
||||||
{% if perms.admin_panel.DelCustomer %}
|
|
||||||
<span class="button is-danger" id="del_customer">حذف مشتری</span>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<span class="button is-success" id="enEdit">ویرایش</span>
|
|
||||||
<input class="button is-danger is-hidden" id="editCs" value="ثبت" type="submit">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-12-mobile is-8-desktop">
|
<div class="column is-12-mobile is-8-desktop">
|
||||||
@@ -102,6 +33,7 @@
|
|||||||
<li id="newFactorOpen" class="{% if url_name == 'customerNew' %}is-active{% else %} {% endif %} "><a href="{% url 'admin_panel:customerNew' customer.id %}">ایجاد فاکتور</a></li>
|
<li id="newFactorOpen" class="{% if url_name == 'customerNew' %}is-active{% else %} {% endif %} "><a href="{% url 'admin_panel:customerNew' customer.id %}">ایجاد فاکتور</a></li>
|
||||||
<li id="FactorHaOpen" class="{% if url_name == 'customerFactors' %}is-active{% else %} {% endif %} "><a href="{% url 'admin_panel:customerFactors' customer.id %}">فاکتورها </a></li>
|
<li id="FactorHaOpen" class="{% if url_name == 'customerFactors' %}is-active{% else %} {% endif %} "><a href="{% url 'admin_panel:customerFactors' customer.id %}">فاکتورها </a></li>
|
||||||
<li class=""><a href="{% url 'admin_panel:customerAction' customer.id %}">بازاریابی</a></li>
|
<li class=""><a href="{% url 'admin_panel:customerAction' customer.id %}">بازاریابی</a></li>
|
||||||
|
{% if perms.admin_panel.ManageSMS %}<li class="{% if url_name == 'customerSmsHistory' %}is-active{% else %} {% endif %} "><a href="{% url 'admin_panel:customerSmsHistory' customer.id %}">سوابق پیامک</a></li>{% endif %}
|
||||||
|
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
@@ -473,11 +405,6 @@
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#del_customer").click(function(){
|
|
||||||
if(confirm("با حذف مشتری، تمام سوابق، فاکتورها و اطلاعات از سیستم حذف میشود، مطمئن هستید؟")){
|
|
||||||
window.location.href = "/panel/customer/{{ customer.id }}/del";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".closeNewIteam").click(function(){
|
$(".closeNewIteam").click(function(){
|
||||||
$("#addNewIteam").removeClass("is-active");
|
$("#addNewIteam").removeClass("is-active");
|
||||||
@@ -603,50 +530,6 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
var test = 0;
|
var test = 0;
|
||||||
|
|
||||||
$("#enEdit").click(function(){
|
|
||||||
$("#formcsEdit").find("input, textarea").prop('disabled', false);
|
|
||||||
// فعال کردن دکمههای کنترل تلفن
|
|
||||||
$("#formcsEdit").find("button.remove-phone-btn, button.add-phone-btn").prop('disabled', false);
|
|
||||||
|
|
||||||
$("#enEdit").addClass("is-hidden");
|
|
||||||
$("#editCs").removeClass("is-hidden");
|
|
||||||
});
|
|
||||||
|
|
||||||
// توابع داینامیک فرم ویرایش
|
|
||||||
$(document).on('click', '.add-phone-btn', function() {
|
|
||||||
var phoneRow = `
|
|
||||||
<div class="field has-addons mt-2 ss02">
|
|
||||||
<div class="control" style="width: 35%;">
|
|
||||||
<input name="phone_titles" class="input" type="text" placeholder="عنوان">
|
|
||||||
</div>
|
|
||||||
<div class="control is-expanded">
|
|
||||||
<input name="phones" class="input an_phone" type="text" placeholder="شماره تماس جدید">
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<button type="button" class="button is-danger remove-phone-btn">-</button>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
$("#dynamic-phones-edit").append(phoneRow);
|
|
||||||
});
|
|
||||||
|
|
||||||
$(document).on('click', '.remove-phone-btn', function() {
|
|
||||||
if ($("#dynamic-phones-edit .has-addons").length > 1) {
|
|
||||||
$(this).closest('.field.has-addons').remove();
|
|
||||||
} else {
|
|
||||||
alert("حداقل یک شماره تماس باید وجود داشته باشد.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#showUserDT").click(function(){
|
|
||||||
if( $(this).html()=='مشاهده مشخصات' ){
|
|
||||||
$(this).html('مخفی کردن');
|
|
||||||
$("#userDT").removeClass("is-hidden-mobile");
|
|
||||||
}else{
|
|
||||||
$(this).html('مشاهده مشخصات');
|
|
||||||
$("#userDT").addClass("is-hidden-mobile");
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
$(".closeSearch").click(function(){
|
$(".closeSearch").click(function(){
|
||||||
$(".SearchBox").removeClass("is-active")
|
$(".SearchBox").removeClass("is-active")
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
{% extends "./customerBaseNew.html" %}
|
||||||
|
{% load custom_tags %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block BodyCustomer1 %}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* جدول سوابق پیامک مشتری */
|
||||||
|
.smshist-msg {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
max-width: 34rem;
|
||||||
|
line-height: 1.9;
|
||||||
|
}
|
||||||
|
.smshist-meta { font-size: .78rem; line-height: 1.7; }
|
||||||
|
.smshist-badge { white-space: nowrap; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="customerSmsHistory" class="column is-12-mobile is-12-desktop" dir="rtl">
|
||||||
|
<h1 class="has-text-centered has-text-weight-bold mb-1">سوابق پیامک مشتری</h1>
|
||||||
|
<p class="has-text-centered has-text-grey smshist-meta mb-3">
|
||||||
|
همهٔ پیامکهای ارسالشده برای شمارههای این مشتری — جمعاً
|
||||||
|
<strong>{{ total_count }}</strong> پیامک.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form method="get" class="field is-grouped is-grouped-multiline mb-3">
|
||||||
|
<div class="control">
|
||||||
|
<div class="select">
|
||||||
|
<select name="status">
|
||||||
|
<option value="">همهٔ وضعیتهای ارسال</option>
|
||||||
|
{% for value, label in status_choices %}
|
||||||
|
<option value="{{ value }}" {% if status == value %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<div class="select">
|
||||||
|
<select name="delivery">
|
||||||
|
<option value="">همهٔ وضعیتهای تحویل</option>
|
||||||
|
{% for value, label in delivery_choices %}
|
||||||
|
<option value="{{ value }}" {% if delivery == value %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control"><button class="button is-link" type="submit">فیلتر</button></div>
|
||||||
|
{% if status or delivery %}
|
||||||
|
<div class="control">
|
||||||
|
<a class="button is-light" href="{% url 'admin_panel:customerSmsHistory' customer.id %}">حذف فیلتر</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if status or delivery %}
|
||||||
|
<p class="mb-2 smshist-meta">نتایج فیلترشده: <strong>{{ filtered_count }}</strong> مورد</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="table is-fullwidth is-narrow is-hoverable is-striped is-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>شناسه</th>
|
||||||
|
<th style="min-width: 7rem;">نوع پیامک</th>
|
||||||
|
<th style="min-width: 8rem;">شماره گیرنده</th>
|
||||||
|
<th style="min-width: 18rem;">متن پیامک</th>
|
||||||
|
<th>وضعیت ارسال</th>
|
||||||
|
<th style="min-width: 8rem;">وضعیت تحویل</th>
|
||||||
|
<th>تاریخ ایجاد</th>
|
||||||
|
<th>تاریخ ارسال</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in items %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ item.id }}</td>
|
||||||
|
<td>{{ item.get_sms_type_display|default:'—' }}</td>
|
||||||
|
<td dir="ltr">{{ item.phone_number }}</td>
|
||||||
|
<td class="smshist-msg">{{ item.message }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="tag smshist-badge
|
||||||
|
{% if item.status == 'sent' %}is-success
|
||||||
|
{% elif item.status == 'failed' %}is-danger
|
||||||
|
{% elif item.status == 'pending' %}is-warning
|
||||||
|
{% elif item.status == 'approved' %}is-info
|
||||||
|
{% else %}is-light{% endif %}">
|
||||||
|
{{ item.get_status_display }}
|
||||||
|
</span>
|
||||||
|
{% if item.last_error %}
|
||||||
|
<div class="has-text-danger smshist-meta" title="{{ item.last_error }}">
|
||||||
|
خطا: {{ item.last_error|truncatechars:80 }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="tag smshist-badge
|
||||||
|
{% if item.delivery_status == 'delivered' %}is-success
|
||||||
|
{% elif item.delivery_status == 'not_delivered' %}is-danger
|
||||||
|
{% elif item.delivery_status == 'expired' %}is-danger
|
||||||
|
{% elif item.delivery_status == 'pending' %}is-warning
|
||||||
|
{% else %}is-light{% endif %}">
|
||||||
|
{{ item.get_delivery_status_display|default:'بدون پیگیری' }}
|
||||||
|
</span>
|
||||||
|
{% if item.delivery_checked_at %}
|
||||||
|
<div class="has-text-grey smshist-meta">استعلام: {{ item.delivery_checked_at|toJalaliDT }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="smshist-meta">{{ item.created_at|toJalaliDT }}</td>
|
||||||
|
<td class="smshist-meta">{{ item.sent_at|toJalaliDT|default:'—' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="8" class="has-text-centered">پیامکی برای این مشتری ثبت نشده است.</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if items.has_other_pages %}
|
||||||
|
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
|
||||||
|
{% if items.has_previous %}
|
||||||
|
<a class="pagination-previous" href="?page={{ items.previous_page_number }}{% if status %}&status={{ status }}{% endif %}{% if delivery %}&delivery={{ delivery }}{% endif %}">قبلی</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="pagination-previous" disabled>قبلی</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if items.has_next %}
|
||||||
|
<a class="pagination-next" href="?page={{ items.next_page_number }}{% if status %}&status={{ status }}{% endif %}{% if delivery %}&delivery={{ delivery }}{% endif %}">بعدی</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="pagination-next" disabled>بعدی</a>
|
||||||
|
{% endif %}
|
||||||
|
<ul class="pagination-list">
|
||||||
|
<li><span class="pagination-link">صفحهٔ {{ items.number }} از {{ items.paginator.num_pages }}</span></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -12,6 +12,9 @@
|
|||||||
حذف یک مورد، فقط همان نوبت را کنسل میکند. یادآوریهای دورهای (بازدید و ثبت
|
حذف یک مورد، فقط همان نوبت را کنسل میکند. یادآوریهای دورهای (بازدید و ثبت
|
||||||
اطلاعات چک) در <strong>دورهٔ بعدی</strong> دوباره ساخته میشوند؛ پس با حذف یک
|
اطلاعات چک) در <strong>دورهٔ بعدی</strong> دوباره ساخته میشوند؛ پس با حذف یک
|
||||||
مورد، پیامک برای آن مشتری برای همیشه قطع نمیشود.
|
مورد، پیامک برای آن مشتری برای همیشه قطع نمیشود.
|
||||||
|
<br>
|
||||||
|
پیامکهای <strong>ارسالشده</strong> از این صف بیرون میروند؛ فهرست و سابقهٔ
|
||||||
|
کاملشان در <a href="{% url 'admin_panel:sms_report' %}">گزارش ارسال</a> است.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="columns is-multiline mb-2" dir="rtl">
|
<div class="columns is-multiline mb-2" dir="rtl">
|
||||||
@@ -21,6 +24,13 @@
|
|||||||
<p class="title is-5 mb-0">{{ total_count }}</p>
|
<p class="title is-5 mb-0">{{ total_count }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<a href="{% url 'admin_panel:sms_report' %}" class="box py-3 px-4 has-text-centered"
|
||||||
|
title="مشاهدهٔ سوابق ارسالشدهها در گزارش ارسال">
|
||||||
|
<p class="heading mb-0">ارسالشده (در گزارش)</p>
|
||||||
|
<p class="title is-5 mb-0 has-text-success">{{ sent_count }}</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
{% for code, label, count in status_stats %}
|
{% for code, label, count in status_stats %}
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
<a href="?status={{ code }}" class="box py-3 px-4 has-text-centered"
|
<a href="?status={{ code }}" class="box py-3 px-4 has-text-centered"
|
||||||
@@ -71,11 +81,12 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="notification is-danger is-light is-flex is-align-items-center is-justify-content-space-between mb-3">
|
<div class="notification is-danger is-light is-flex is-align-items-center is-justify-content-space-between mb-3">
|
||||||
<span>خالی کردن کل صف: همهٔ پیامکها (در هر وضعیتی) حذف میشوند و دیگر بازنمیگردند.</span>
|
<span>خالی کردن صف: همهٔ موارد باقیمانده (در انتظار تأیید، تأییدشده، در حال پردازش و ناموفق)
|
||||||
|
حذف میشوند و دیگر بازنمیگردند. پیامکهای ارسالشده دستنخورده میمانند (در گزارش ارسال).</span>
|
||||||
<form method="post" action="{% url 'admin_panel:sms_queue_empty' %}"
|
<form method="post" action="{% url 'admin_panel:sms_queue_empty' %}"
|
||||||
onsubmit="return confirm('کل صف پیامک خالی شود؟ این پیامکها دیگر هرگز ارسال یا بازسازی نمیشوند.');">
|
onsubmit="return confirm('صف پیامک خالی شود؟ این پیامکها دیگر هرگز ارسال یا بازسازی نمیشوند.');">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="button is-danger" type="submit">خالی کردن کل صف</button>
|
<button class="button is-danger" type="submit">خالی کردن صف</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -119,11 +130,17 @@
|
|||||||
<button class="button is-small is-warning" type="submit">ارسال مجدد</button>
|
<button class="button is-small is-warning" type="submit">ارسال مجدد</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if item.status != 'sent' %}
|
||||||
<form method="post" action="{% url 'admin_panel:sms_queue_delete' item.id %}"
|
<form method="post" action="{% url 'admin_panel:sms_queue_delete' item.id %}"
|
||||||
onclick="return confirm('این پیامک از صف حذف شود؟');">
|
onclick="return confirm('این پیامک از صف حذف شود؟');">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="button is-small is-danger" type="submit">حذف</button>
|
<button class="button is-small is-danger" type="submit">حذف</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<a class="button is-small is-light"
|
||||||
|
href="{% url 'admin_panel:sms_report' %}?q={{ item.phone_number }}"
|
||||||
|
title="این پیامک ارسال شده است؛ سابقهٔ آن در گزارش ارسال است">در گزارش</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
{% extends "./base.html" %}
|
||||||
|
{% load custom_tags %}
|
||||||
|
|
||||||
|
{% block bodyPage %}
|
||||||
|
<div class="container mt-5" dir="rtl">
|
||||||
|
<h1 class="title is-4">گزارش ارسال پیامک</h1>
|
||||||
|
<p class="has-text-grey mb-3">
|
||||||
|
پیامکهای <strong>تأییدشده و ارسالشده</strong> (موارد «در انتظار تأیید» و «لغوشده»
|
||||||
|
در این گزارش نمیآیند) بههمراه «تعداد پیامکی که اپراتور محاسبه کرده»
|
||||||
|
(بر اساس کدگذاری GSM-7 برای انگلیسی/عدد و UCS-2 برای فارسی/یونیکد) — برای تحلیل هزینه.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{# --- کارتهای خلاصه (روی کل نتیجهٔ فیلترشده) --- #}
|
||||||
|
<div class="columns is-multiline mb-2">
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<div class="box py-3 px-4 has-text-centered">
|
||||||
|
<p class="heading mb-0">تعداد پیامک (رکورد)</p>
|
||||||
|
<p class="title is-5 mb-0">{{ total_records }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<div class="box py-3 px-4 has-text-centered">
|
||||||
|
<p class="heading mb-0">مجموع پیامک محاسبهشده</p>
|
||||||
|
<p class="title is-5 mb-0 has-text-danger">{{ total_parts }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<div class="box py-3 px-4 has-text-centered">
|
||||||
|
<p class="heading mb-0">GSM-7 / UCS-2</p>
|
||||||
|
<p class="title is-6 mb-0">{{ gsm7_parts }} / {{ ucs2_parts }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% for code, label, count in status_stats %}
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<a href="?status={{ code }}" class="box py-3 px-4 has-text-centered"
|
||||||
|
{% if status == code %}style="border:2px solid #3273dc"{% endif %}>
|
||||||
|
<p class="heading mb-0">{{ label }}</p>
|
||||||
|
<p class="title is-5 mb-0">{{ count }}</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# --- فرم فیلتر --- #}
|
||||||
|
<form method="get" class="box">
|
||||||
|
<div class="columns is-multiline is-variable is-2">
|
||||||
|
<div class="column is-2">
|
||||||
|
<label class="label is-small">از تاریخ (شمسی)</label>
|
||||||
|
<input class="input" type="text" id="dateFromP" name="date_from"
|
||||||
|
value="{{ date_from }}" placeholder="1404/01/01" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="column is-2">
|
||||||
|
<label class="label is-small">تا تاریخ (شمسی)</label>
|
||||||
|
<input class="input" type="text" id="dateToP" name="date_to"
|
||||||
|
value="{{ date_to }}" placeholder="1404/12/29" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="column is-2">
|
||||||
|
<label class="label is-small">وضعیت ارسال</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select name="status">
|
||||||
|
<option value="">همه</option>
|
||||||
|
{% for value, label in status_choices %}
|
||||||
|
<option value="{{ value }}" {% if status == value %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-2">
|
||||||
|
<label class="label is-small">وضعیت تحویل</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select name="delivery">
|
||||||
|
<option value="">همه</option>
|
||||||
|
{% for value, label in delivery_choices %}
|
||||||
|
<option value="{{ value }}" {% if delivery == value %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-2">
|
||||||
|
<label class="label is-small">نوع پیامک</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select name="sms_type">
|
||||||
|
<option value="">همه</option>
|
||||||
|
{% for value, label in type_choices %}
|
||||||
|
<option value="{{ value }}" {% if sms_type == value %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-2">
|
||||||
|
<label class="label is-small">جستجو (متن/شماره/نام مشتری)</label>
|
||||||
|
<input class="input" type="text" name="q" value="{{ query }}" placeholder="جستجو…">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="button is-link" type="submit">اعمال فیلتر</button>
|
||||||
|
{% if has_filter %}
|
||||||
|
<a class="button is-light" href="{% url 'admin_panel:sms_report' %}">حذف فیلترها</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{# --- جدول --- #}
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="table is-fullwidth is-narrow is-hoverable is-striped is-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>شناسه</th>
|
||||||
|
<th style="min-width: 16rem;">متن دقیق پیامک</th>
|
||||||
|
<th style="min-width: 8rem;">شماره گیرنده</th>
|
||||||
|
<th>نوع پیامک</th>
|
||||||
|
<th style="min-width: 8rem;">مشتری</th>
|
||||||
|
<th>سوابق</th>
|
||||||
|
<th>تعداد پیامک</th>
|
||||||
|
<th>وضعیت ارسال</th>
|
||||||
|
<th>تحویل</th>
|
||||||
|
<th style="min-width: 8rem;">تاریخ ایجاد</th>
|
||||||
|
<th style="min-width: 8rem;">تاریخ ارسال</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for row in rows %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ row.item.id }}</td>
|
||||||
|
<td class="smsrep-msg">{{ row.item.message }}</td>
|
||||||
|
<td dir="ltr">{{ row.item.phone_number }}</td>
|
||||||
|
<td>{{ row.item.get_sms_type_display|default:'دستی/سفارشی' }}</td>
|
||||||
|
<td>
|
||||||
|
{% if row.item.customer_id %}
|
||||||
|
<a href="{% url 'admin_panel:customer' row.item.customer_id %}">{{ row.item.customer.name_family }}</a>
|
||||||
|
{% else %}—{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if row.item.customer_id %}
|
||||||
|
<a class="button is-small is-light"
|
||||||
|
href="{% url 'admin_panel:customerSmsHistory' row.item.customer_id %}"
|
||||||
|
title="مشاهدهٔ سوابق پیامک این مشتری">سوابق پیامک</a>
|
||||||
|
{% else %}—{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="has-text-centered">
|
||||||
|
<span class="tag {% if row.parts > 1 %}is-danger{% else %}is-success{% endif %} is-light"
|
||||||
|
title="{{ row.encoding }} · {{ row.length }} واحد">{{ row.parts }}</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ row.item.get_status_display }}</td>
|
||||||
|
<td>{{ row.item.get_delivery_status_display|default:'—' }}</td>
|
||||||
|
<td>{{ row.item.created_at|toJalaliDT }}</td>
|
||||||
|
<td>{{ row.item.sent_at|toJalaliDT|default:'—' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="11" class="has-text-centered">پیامکی با این شرایط یافت نشد.</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if page.has_other_pages %}
|
||||||
|
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
|
||||||
|
{% if page.has_previous %}
|
||||||
|
<a class="pagination-previous" href="?{{ base_query }}&page={{ page.previous_page_number }}">قبلی</a>
|
||||||
|
{% else %}<a class="pagination-previous" disabled>قبلی</a>{% endif %}
|
||||||
|
{% if page.has_next %}
|
||||||
|
<a class="pagination-next" href="?{{ base_query }}&page={{ page.next_page_number }}">بعدی</a>
|
||||||
|
{% else %}<a class="pagination-next" disabled>بعدی</a>{% endif %}
|
||||||
|
<ul class="pagination-list">
|
||||||
|
<li><span class="pagination-link">صفحهٔ {{ page.number }} از {{ page.paginator.num_pages }}</span></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block cssEnd %}
|
||||||
|
<style>
|
||||||
|
/* متن پیامک با حفظ شکست خطوط و شکستن متنهای طولانی/بدونفاصله */
|
||||||
|
.smsrep-msg { white-space: pre-wrap; overflow-wrap: anywhere; max-width: 26rem; line-height: 1.9; }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block jsEnd %}
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
if (typeof kamaDatepicker !== 'function') { return; }
|
||||||
|
var opts = { buttonsColor: "red", forceFarsiDigits: true, sync: true };
|
||||||
|
['dateFromP', 'dateToP'].forEach(function (id) {
|
||||||
|
if (document.getElementById(id)) { kamaDatepicker(id, opts); }
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -7,6 +7,13 @@
|
|||||||
<div class="box">
|
<div class="box">
|
||||||
<form method="post" action="{% url 'admin_panel:sms_settings' %}">
|
<form method="post" action="{% url 'admin_panel:sms_settings' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
<h2 class="title is-5">اتصال به سامانه پیامک</h2>
|
||||||
|
<p class="help mb-3">
|
||||||
|
این بخش مشخص میکند پیامکها <strong>از چه مسیری و با چه حساب کاربری</strong> به
|
||||||
|
اپراتور سپرده میشوند. تا وقتی «وضعیت ارسال» روشن نباشد، هیچکدام از سرویسهای
|
||||||
|
پسزمینه (worker) پیامکی نمیفرستند.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="columns is-multiline">
|
<div class="columns is-multiline">
|
||||||
<div class="field column is-4-desktop is-12-mobile">
|
<div class="field column is-4-desktop is-12-mobile">
|
||||||
<label class="label">سرویسدهنده</label>
|
<label class="label">سرویسدهنده</label>
|
||||||
@@ -19,6 +26,19 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="help">
|
||||||
|
<strong>مسیر فنی ارسال را تعیین میکند</strong> و روی بقیهٔ فیلدها اثر دارد:
|
||||||
|
<br>• <span dir="ltr">0098SMS (لینکی HTTP)</span>: سادهترین حالت؛ درخواست به
|
||||||
|
آدرس <span dir="ltr">api_url</span> زده میشود. <strong>شناسهٔ پیامک برنمیگردد</strong>،
|
||||||
|
پس استعلام «تحویل به گوشی» ممکن نیست.
|
||||||
|
<br>• <span dir="ltr">0098SMS (وبسرویس)</span>: ارسال SOAP به
|
||||||
|
<span dir="ltr">webservice_url</span>؛ <strong>شناسهٔ پیامک (smsid)</strong> برمیگردد و
|
||||||
|
میتوان وضعیت واقعی تحویل را استعلام کرد.
|
||||||
|
<br>• <span dir="ltr">HTTP عمومی</span>: برای سرویسهای دلخواه؛ با
|
||||||
|
<span dir="ltr">api_token</span> احراز هویت میشود.
|
||||||
|
<br>• <span dir="ltr">Console</span>: فقط برای تست/دیباگ؛ پیامک واقعی نمیرود و
|
||||||
|
متن در لاگ چاپ میشود.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field column is-4-desktop is-12-mobile">
|
<div class="field column is-4-desktop is-12-mobile">
|
||||||
@@ -29,6 +49,11 @@
|
|||||||
فعال باشد
|
فعال باشد
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="help">
|
||||||
|
<strong>کلید اصلی خاموش/روشن ارسال.</strong> اگر تیک نخورد، سرویس worker هیچ
|
||||||
|
پیامکی نمیفرستد (ردیفها در صف میمانند). اسکنر و صفکردن مستقل از این است،
|
||||||
|
پس میتوانید پیامکها را از قبل در صف بچینید و بعد ارسال را روشن کنید.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field column is-4-desktop is-12-mobile">
|
<div class="field column is-4-desktop is-12-mobile">
|
||||||
@@ -36,6 +61,13 @@
|
|||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="number" name="timeout_seconds" value="{{ config.timeout_seconds }}">
|
<input class="input" type="number" name="timeout_seconds" value="{{ config.timeout_seconds }}">
|
||||||
</div>
|
</div>
|
||||||
|
<p class="help">
|
||||||
|
<strong>حداکثر زمان انتظار worker برای پاسخ سرویسدهنده</strong> در هر تلاش.
|
||||||
|
اگر پاسخ دیرتر بیاید، آن تلاش «ناموفق» ثبت میشود و بر اساس سیاست تلاش مجدد
|
||||||
|
دوباره در نوبت قرار میگیرد. مقدار کم (مثل ۵) ممکن است در سرویس کند، ارسالهای
|
||||||
|
سالم را هم ناموفق کند؛ مقدار زیاد (مثل ۱۲۰) در قطعی سرویس، worker را بیکار
|
||||||
|
نگه میدارد. پیشفرض ۳۰ ثانیه متعادل است.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field column is-12-mobile">
|
<div class="field column is-12-mobile">
|
||||||
@@ -44,9 +76,14 @@
|
|||||||
<input class="input" type="text" name="api_url" value="{{ config.api_url }}" dir="ltr">
|
<input class="input" type="text" name="api_url" value="{{ config.api_url }}" dir="ltr">
|
||||||
</div>
|
</div>
|
||||||
<p class="help">
|
<p class="help">
|
||||||
برای 0098SMS همین آدرس لینکی ارسال است
|
<strong>آدرس درخواست ارسال برای providerهای «لینکی HTTP» و «HTTP عمومی».</strong>
|
||||||
|
<br>• برای 0098SMS لینکی همین آدرس است
|
||||||
(<span dir="ltr">http://www.0098sms.com/sendsmslink.aspx</span>)؛ این سرویس
|
(<span dir="ltr">http://www.0098sms.com/sendsmslink.aspx</span>)؛ این سرویس
|
||||||
WSDL/SOAP ندارد و آدرس WSDL باعث خطای «Invalid XML content» میشود.
|
WSDL/SOAP ندارد و اگر آدرس WSDL بگذارید خطای «Invalid XML content» میگیرید.
|
||||||
|
<br>• برای provider وبسرویس، این فیلد استفاده <strong>نمیشود</strong> (آن یکی
|
||||||
|
<span dir="ltr">webservice_url</span> را میخواند).
|
||||||
|
<br>• اگر آدرس اشتباه یا در دسترس نباشد، پیامکها «ناموفق» میشوند و در صف تلاش
|
||||||
|
مجدد میخورند.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -56,8 +93,9 @@
|
|||||||
<input class="input" type="text" name="webservice_url" value="{{ config.webservice_url }}" dir="ltr">
|
<input class="input" type="text" name="webservice_url" value="{{ config.webservice_url }}" dir="ltr">
|
||||||
</div>
|
</div>
|
||||||
<p class="help">
|
<p class="help">
|
||||||
ارسال با شناسهٔ پیامک (<span dir="ltr">smsid</span>) و استعلام تحویل به گوشی
|
<strong>فقط وقتی provider = «وبسرویس» معنا دارد.</strong> هم ارسال با شناسهٔ
|
||||||
از همین آدرس انجام میشود. پیشفرض:
|
پیامک (<span dir="ltr">smsid</span>) و هم استعلام تحویل به گوشی از همین آدرس
|
||||||
|
انجام میشود. با providerهای دیگر این فیلد نادیده گرفته میشود. پیشفرض:
|
||||||
<span dir="ltr">https://webservice.0098sms.com/service.asmx</span>
|
<span dir="ltr">https://webservice.0098sms.com/service.asmx</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,7 +105,11 @@
|
|||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="text" name="panel_no" value="{{ config.panel_no }}" dir="ltr">
|
<input class="input" type="text" name="panel_no" value="{{ config.panel_no }}" dir="ltr">
|
||||||
</div>
|
</div>
|
||||||
<p class="help">اگر خالی بماند از «شماره خط فرستنده» استفاده میشود.</p>
|
<p class="help">
|
||||||
|
<strong>شمارهٔ اختصاصی حساب شما در وبسرویس</strong> (فقط provider وبسرویس).
|
||||||
|
اگر خالی بماند، سیستم از «شماره خط فرستنده» استفاده میکند. مقدار اشتباه باعث
|
||||||
|
رد شدن ارسال با خطای احراز هویت میشود.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field column is-4-desktop is-12-mobile">
|
<div class="field column is-4-desktop is-12-mobile">
|
||||||
@@ -75,6 +117,10 @@
|
|||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="text" name="username" value="{{ config.username }}" dir="ltr">
|
<input class="input" type="text" name="username" value="{{ config.username }}" dir="ltr">
|
||||||
</div>
|
</div>
|
||||||
|
<p class="help">
|
||||||
|
<strong>نام کاربری حساب سامانهٔ پیامک</strong> (در همهٔ providerها بهجز Console
|
||||||
|
لازم است). اگر اشتباه باشد، ارسالها رد میشوند و در «خطا» صف ثبت میگردد.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field column is-4-desktop is-12-mobile">
|
<div class="field column is-4-desktop is-12-mobile">
|
||||||
@@ -82,6 +128,11 @@
|
|||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="password" name="password" placeholder="{% if config.password %}(بدون تغییر){% endif %}" dir="ltr">
|
<input class="input" type="password" name="password" placeholder="{% if config.password %}(بدون تغییر){% endif %}" dir="ltr">
|
||||||
</div>
|
</div>
|
||||||
|
<p class="help">
|
||||||
|
<strong>رمز حساب سامانهٔ پیامک.</strong> برای امنیت، مقدار ذخیرهشده بازخوانی
|
||||||
|
نمیشود؛ این کادر را <strong>خالی بگذارید</strong> تا رمز قبلی بماند و فقط وقتی
|
||||||
|
رمز عوض شده، مقدار جدید را وارد کنید.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field column is-4-desktop is-12-mobile">
|
<div class="field column is-4-desktop is-12-mobile">
|
||||||
@@ -89,6 +140,12 @@
|
|||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="text" name="sender" value="{{ config.sender }}" dir="ltr">
|
<input class="input" type="text" name="sender" value="{{ config.sender }}" dir="ltr">
|
||||||
</div>
|
</div>
|
||||||
|
<p class="help">
|
||||||
|
<strong>شماره یا عنوان خطی که پیامک با آن به دست مشتری میرسد.</strong> باید
|
||||||
|
جزو خطوط مجاز حساب شما باشد؛ در غیر این صورت اپراتور ارسال را رد میکند.
|
||||||
|
همچنین در provider وبسرویس، اگر «شمارهٔ پنل» خالی باشد همین مقدار جای آن
|
||||||
|
استفاده میشود.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field column is-12-mobile">
|
<div class="field column is-12-mobile">
|
||||||
@@ -96,15 +153,23 @@
|
|||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="password" name="api_token" placeholder="{% if config.api_token %}(بدون تغییر){% endif %}" dir="ltr">
|
<input class="input" type="password" name="api_token" placeholder="{% if config.api_token %}(بدون تغییر){% endif %}" dir="ltr">
|
||||||
</div>
|
</div>
|
||||||
|
<p class="help">
|
||||||
|
<strong>کلید/توکن احراز هویت providerهای «HTTP عمومی».</strong> برای 0098SMS
|
||||||
|
لازم نیست. مثل رمز، خالی گذاشتن آن یعنی «بدون تغییر».
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<h2 class="title is-5">کنترل جریان ارسال</h2>
|
<h2 class="title is-5">کنترل جریان ارسال</h2>
|
||||||
<p class="help mb-3">
|
<p class="help mb-3">
|
||||||
برای جلوگیری از سیل پیامک: تنها مواردی ارسال میشوند که پنجرهٔ یادآوریشان از
|
این بخش تعیین میکند اسکنر دورهای <strong>چه چیزی و چقدر</strong> در هر اجرا در صف
|
||||||
«خط شروع» به بعد باز شود. موارد قدیمیتر تنها با روشنکردن «اجازهٔ موارد قدیمی»
|
بگذارد. اسکنر هر چند دقیقه یکبار اجرا میشود و بر اساس «سررسیدها» تصمیم میگیرد
|
||||||
و در سقف تعیینشده ارسال میشوند. بین هر پیامک هم فاصلهٔ زمانی رعایت میشود.
|
کدام مشتری باید یادآوری بگیرد. منطق بودجه به این شکل است:
|
||||||
|
<strong>سقف هر اجرا</strong> ← تعیین میکند کلاً چند مورد صف شود؛
|
||||||
|
<strong>خط شروع</strong> ← موارد قدیمیتر از آن را کنار میگذارد؛
|
||||||
|
<strong>اجازهٔ موارد قدیمی + سقف آن</strong> ← تصمیم میگیرد از آن موارد کنارگذاشتهشده
|
||||||
|
چند تا (اگر لازم بود) ارسال شود.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="columns is-multiline">
|
<div class="columns is-multiline">
|
||||||
@@ -116,9 +181,15 @@
|
|||||||
placeholder="مثلاً 1403/07/18 — خالی = همین حالا" dir="ltr">
|
placeholder="مثلاً 1403/07/18 — خالی = همین حالا" dir="ltr">
|
||||||
</div>
|
</div>
|
||||||
<p class="help">
|
<p class="help">
|
||||||
فقط یادآوریهایی صف میشوند که «پنجرهٔ» آنها از این تاریخ به بعد باز شده باشد.
|
<strong>خط جداکنندهٔ «موارد جدید» از «موارد قدیمی».</strong>
|
||||||
مشتریان قدیمیتری که پنجرهشان پیش از این تاریخ باز شده، وارد صف نمیشوند.
|
هر مورد (چک، نسیه، بازدید) یک «لحظهٔ باز شدن پنجرهٔ یادآوری» دارد. اگر این
|
||||||
قالب: <span dir="ltr">YYYY/MM/DD</span> (شمسی) — خالی = همین حالا.
|
لحظه <strong>از این تاریخ به بعد</strong> باشد، مورد «جدید» است و آزادانه صف
|
||||||
|
میشود؛ اگر <strong>قبل از</strong> آن باشد، «قدیمی» (backlog) حساب میشود.
|
||||||
|
<br>• هدف اصلی: در نخستین راهاندازی، هزاران مشتری قدیمیِ واجد شرط یکباره پیامک
|
||||||
|
نگیرند.
|
||||||
|
<br>• قالب: <span dir="ltr">YYYY/MM/DD</span> شمسی — خالی = همین حالا.
|
||||||
|
<br>• عوضکردن آن به گذشته، مشتریان قدیمیتر را «جدید» میکند و ممکن است سیل پیامک
|
||||||
|
بسازد؛ با احتیاط تغییر دهید.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -127,6 +198,19 @@
|
|||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="number" min="0" name="max_per_run" value="{{ config.max_per_run }}">
|
<input class="input" type="number" min="0" name="max_per_run" value="{{ config.max_per_run }}">
|
||||||
</div>
|
</div>
|
||||||
|
<p class="help">
|
||||||
|
<strong>سقف کل صفکردن در یک اجرا — بهازای هر اسکنر جداگانه.</strong>
|
||||||
|
<br>• «هر اسکنر» یعنی این عدد برای هر نوع یادآوری مستقل اعمال میشود
|
||||||
|
(تأمین موجودی چک، سررسید نسیه، بازدید دورهای)؛ پس اگر ۱۰۰ باشد و هر سه اسکنر
|
||||||
|
واجد شرط باشند، در یک اجرا حداکثر ۱۰۰ + ۱۰۰ + ۱۰۰ = ۳۰۰ ردیف صف میشود.
|
||||||
|
<br>• شمارش از مجموع «جدیدها + موارد قدیمی» است، نه فقط جدیدها.
|
||||||
|
<br>• <strong>۰ = بینهایت</strong> (هیچ سقفی اعمال نمیشود).
|
||||||
|
<br>• چون اسکنر دورهای اجرا میشود، سقف باعث «دستهدسته» شدن ارسالهای انبوه
|
||||||
|
میشود (بقیه در اجراهای بعدی میآیند) و از ریختن یکبارهٔ صدها پیامک روی ارسال
|
||||||
|
جلوگیری میکند.
|
||||||
|
<br>• نکته: این سقفها روی «صفکردن» اثر دارند، نه سرعت ارسال واقعی؛ سرعت ارسال
|
||||||
|
به «فاصلهٔ بین هر پیامک» بستگی دارد.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field column is-3-desktop is-6-mobile">
|
<div class="field column is-3-desktop is-6-mobile">
|
||||||
@@ -134,6 +218,11 @@
|
|||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="number" min="0" name="send_interval_seconds" value="{{ config.send_interval_seconds }}">
|
<input class="input" type="number" min="0" name="send_interval_seconds" value="{{ config.send_interval_seconds }}">
|
||||||
</div>
|
</div>
|
||||||
|
<p class="help">
|
||||||
|
<strong>مکث سرویس worker پس از هر ارسال</strong> تا اپراتور زیر بار زیاد نرود.
|
||||||
|
مثال: با مقدار ۳، ارسال ۱۰۰ پیامک حدود ۵ دقیقه طول میکشد. مقدار ۰ یعنی بدون
|
||||||
|
مکث (سریع ولی پرخطر — ممکن است سرویسدهنده محدودتان کند).
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field column is-3-desktop is-6-mobile">
|
<div class="field column is-3-desktop is-6-mobile">
|
||||||
@@ -144,6 +233,12 @@
|
|||||||
فعال باشد
|
فعال باشد
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="help">
|
||||||
|
<strong>دربِ موارد «قدیمیتر از تاریخ شروع دوره».</strong> بهصورت پیشفرض بسته
|
||||||
|
است. تا وقتی بسته باشد، موارد قدیمی <strong>هرگز</strong> صف نمیشوند (حتی اگر
|
||||||
|
سقف باقی باشد). اگر لازم بود این موارد هم پردازش شوند، آن را روشن کنید و مقدار
|
||||||
|
«حداکثر موارد قدیمی در هر اجرا» را تعیین کنید.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field column is-3-desktop is-6-mobile">
|
<div class="field column is-3-desktop is-6-mobile">
|
||||||
@@ -151,7 +246,15 @@
|
|||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="number" min="0" name="backlog_max" value="{{ config.backlog_max }}">
|
<input class="input" type="number" min="0" name="backlog_max" value="{{ config.backlog_max }}">
|
||||||
</div>
|
</div>
|
||||||
<p class="help">صفر = هیچ مورد قدیمی ارسال نمیشود.</p>
|
<p class="help">
|
||||||
|
<strong>سقف ارسال موارد قدیمی در هر اجرا، بهازای هر اسکنر.</strong>
|
||||||
|
<br>• فقط وقتی «اجازهٔ ارسال موارد قدیمی» روشن باشد اثر دارد.
|
||||||
|
<br>• صفر = هیچ مورد قدیمی ارسال نمیشود (حتی با روشنبودن کلید).
|
||||||
|
<br>• این سقف <strong>در کنار</strong> «حداکثر پیامک هر اسکنر» اعمال میشود؛ پس
|
||||||
|
ممکن است ابتدا سقف کل (max_per_run) پر شود.
|
||||||
|
<br>• برای پاککردن تدریجی انبار قدیمی، عدد کوچکی مثل ۵۰ بگذارید تا در هر اجرا
|
||||||
|
دستهای ارسال شود.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ urlpatterns = [
|
|||||||
path('pardakhtNesiye/<int:cs_id>', views.pardakhtNesiye, name='pardakhtNesiye'),
|
path('pardakhtNesiye/<int:cs_id>', views.pardakhtNesiye, name='pardakhtNesiye'),
|
||||||
|
|
||||||
# === مدیریت پیامک ===
|
# === مدیریت پیامک ===
|
||||||
|
path('customer/<int:c_id>/sms/', views.customer_sms_history, name='customerSmsHistory'),
|
||||||
path('sms/settings', views.sms_settings, name='sms_settings'),
|
path('sms/settings', views.sms_settings, name='sms_settings'),
|
||||||
path('sms/templates', views.sms_templates, name='sms_templates'),
|
path('sms/templates', views.sms_templates, name='sms_templates'),
|
||||||
path('sms/template/<int:t_id>/edit', views.sms_template_edit, name='sms_template_edit'),
|
path('sms/template/<int:t_id>/edit', views.sms_template_edit, name='sms_template_edit'),
|
||||||
@@ -88,6 +89,7 @@ urlpatterns = [
|
|||||||
path('sms/customer/<int:c_id>/primary', views.sms_primary_toggle, name='sms_primary_toggle'),
|
path('sms/customer/<int:c_id>/primary', views.sms_primary_toggle, name='sms_primary_toggle'),
|
||||||
path('sms/customer/<int:c_id>/send', views.sms_send_custom, name='sms_send_custom'),
|
path('sms/customer/<int:c_id>/send', views.sms_send_custom, name='sms_send_custom'),
|
||||||
path('sms/queue', views.sms_queue, name='sms_queue'),
|
path('sms/queue', views.sms_queue, name='sms_queue'),
|
||||||
|
path('sms/report', views.sms_report, name='sms_report'),
|
||||||
path('sms/queue/bulk', views.sms_queue_bulk, name='sms_queue_bulk'),
|
path('sms/queue/bulk', views.sms_queue_bulk, name='sms_queue_bulk'),
|
||||||
path('sms/queue/empty', views.sms_queue_empty, name='sms_queue_empty'),
|
path('sms/queue/empty', views.sms_queue_empty, name='sms_queue_empty'),
|
||||||
path('sms/queue/<int:q_id>/approve', views.sms_queue_approve, name='sms_queue_approve'),
|
path('sms/queue/<int:q_id>/approve', views.sms_queue_approve, name='sms_queue_approve'),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from django.contrib.auth.models import User
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
|
||||||
from ..models import Action, Customer
|
from ..models import Action, Customer
|
||||||
|
from ..sms.triggers import notify_visit_scheduled
|
||||||
|
|
||||||
|
|
||||||
@login_required(login_url='/panel/p_login/')
|
@login_required(login_url='/panel/p_login/')
|
||||||
@@ -91,6 +92,10 @@ def customerNewAction(request ,c_id):
|
|||||||
customer_obj.last_bazdid_dt = newAction.finishDateTime
|
customer_obj.last_bazdid_dt = newAction.finishDateTime
|
||||||
customer_obj.save()
|
customer_obj.save()
|
||||||
|
|
||||||
|
# پیامک «بازدید شما برای تاریخ … ثبت شد» فقط برای اقدام از نوع بازدید.
|
||||||
|
if newAction.actionType == 'bazdid':
|
||||||
|
notify_visit_scheduled(customer_obj, newAction, created_by=request.user)
|
||||||
|
|
||||||
return redirect('/panel/customer/'+str(c_id)+'/action')
|
return redirect('/panel/customer/'+str(c_id)+'/action')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,10 @@ from django.contrib.auth.decorators import login_required, permission_required
|
|||||||
from persiantools.jdatetime import JalaliDate
|
from persiantools.jdatetime import JalaliDate
|
||||||
|
|
||||||
from ..models import (Customer, PhoneNumber, ProductCategory, Product, Factor,
|
from ..models import (Customer, PhoneNumber, ProductCategory, Product, Factor,
|
||||||
FactorIteam, Article, Sarresid, Action, CacheTable, SMSType)
|
FactorIteam, Article, Sarresid, Action, CacheTable)
|
||||||
from ..sms.enqueue import enqueue_sms
|
from ..sms.phones import (normalize_primary_values,
|
||||||
from ..sms.phones import (customer_phone_entries, normalize_primary_values,
|
|
||||||
save_primary_normalized)
|
save_primary_normalized)
|
||||||
from ..sms.recipients import resolve_recipients
|
from ..sms.triggers import notify_payment_thanks, notify_welcome
|
||||||
from ..sms.rules import customer_toggle_map
|
from ..sms.rules import customer_toggle_map
|
||||||
from ._helpers import logger, _parse_jalali_date, _parse_phone_rows
|
from ._helpers import logger, _parse_jalali_date, _parse_phone_rows
|
||||||
|
|
||||||
@@ -122,15 +121,10 @@ def customerNew2(request ,c_id):
|
|||||||
#return JsonResponse(all_prd,safe = False)
|
#return JsonResponse(all_prd,safe = False)
|
||||||
dateNow = datetime.datetime.now().date
|
dateNow = datetime.datetime.now().date
|
||||||
sms_toggles = customer_toggle_map(customer)
|
sms_toggles = customer_toggle_map(customer)
|
||||||
# همهٔ شمارهها از هر منبعی که ذخیره شدهاند (PhoneNumber و phone1/2/3 قدیمی).
|
|
||||||
sms_phone_entries = customer_phone_entries(customer)
|
|
||||||
sms_effective = {n for n in resolve_recipients(customer)}
|
|
||||||
data = {
|
data = {
|
||||||
'customer': customer, 'categorys': categorys, 'dateNow': dateNow,
|
'customer': customer, 'categorys': categorys, 'dateNow': dateNow,
|
||||||
'factorha': factorha, 'mandehesabCs': mandehesabCs,
|
'factorha': factorha, 'mandehesabCs': mandehesabCs,
|
||||||
'sms_toggles': sms_toggles, 'sms_phone_entries': sms_phone_entries,
|
'sms_toggles': sms_toggles,
|
||||||
'sms_effective': sms_effective,
|
|
||||||
'sms_recipients': sorted(sms_effective),
|
|
||||||
}
|
}
|
||||||
return render(request, 'admin_panel/customerNew2.html', data)
|
return render(request, 'admin_panel/customerNew2.html', data)
|
||||||
|
|
||||||
@@ -271,12 +265,7 @@ def customersAdd(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# پیامک خوشآمدگویی (در صورت فعال بودن قالب و تنظیم مشتری)
|
# پیامک خوشآمدگویی (در صورت فعال بودن قالب و تنظیم مشتری)
|
||||||
enqueue_sms(
|
notify_welcome(newCs, created_by=request.user)
|
||||||
customer=newCs, sms_type=SMSType.WELCOME,
|
|
||||||
context={'name': newCs.name_family},
|
|
||||||
dedup_key=f'welcome:{newCs.id}',
|
|
||||||
created_by=request.user,
|
|
||||||
)
|
|
||||||
|
|
||||||
return redirect('/panel/customers/')
|
return redirect('/panel/customers/')
|
||||||
|
|
||||||
@@ -494,4 +483,9 @@ def customerHesabdariAdd(request ,c_id):
|
|||||||
newArticl.user = user_op
|
newArticl.user = user_op
|
||||||
newArticl.nahvePardakht = nahvePardakhat
|
newArticl.nahvePardakht = nahvePardakhat
|
||||||
newArticl.save()
|
newArticl.save()
|
||||||
|
# سند بستانکار (credit>0 و debit=0) یعنی مشتری پول پرداخت کرده ⇒ پیامک تشکر.
|
||||||
|
if type == 'credit':
|
||||||
|
notify_payment_thanks(newArticl.customer, newArticl.credit,
|
||||||
|
article_id=newArticl.id, method=nahvePardakhat,
|
||||||
|
created_by=user_op)
|
||||||
return redirect('/panel/customer/'+str(c_id)+'/hesabdari')
|
return redirect('/panel/customer/'+str(c_id)+'/hesabdari')
|
||||||
@@ -12,6 +12,7 @@ from persiantools import digits
|
|||||||
|
|
||||||
from ..models import (Customer, StoreRoom, Factor, FactorIteam, Product,
|
from ..models import (Customer, StoreRoom, Factor, FactorIteam, Product,
|
||||||
ProductInStore, Article, Sarresid)
|
ProductInStore, Article, Sarresid)
|
||||||
|
from ..sms.triggers import notify_factor_survey, notify_payment_thanks
|
||||||
from ._helpers import logger, _safe_int, _parse_jalali_date, _parse_timestamp, InventoryException
|
from ._helpers import logger, _safe_int, _parse_jalali_date, _parse_timestamp, InventoryException
|
||||||
|
|
||||||
|
|
||||||
@@ -196,6 +197,8 @@ def newFactor2(request, c_id):
|
|||||||
if not str(item.get('garanty_Esh', '')).strip()
|
if not str(item.get('garanty_Esh', '')).strip()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
new_factor = None
|
||||||
|
payment_articles = []
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
|
|
||||||
@@ -231,7 +234,7 @@ def newFactor2(request, c_id):
|
|||||||
|
|
||||||
if key in payment_map:
|
if key in payment_map:
|
||||||
nahve_pardakht = payment_map[key]
|
nahve_pardakht = payment_map[key]
|
||||||
Article.objects.create(
|
payment_articles.append((Article.objects.create(
|
||||||
debit=0,
|
debit=0,
|
||||||
credit=amount,
|
credit=amount,
|
||||||
customer=customer_obj,
|
customer=customer_obj,
|
||||||
@@ -240,7 +243,7 @@ def newFactor2(request, c_id):
|
|||||||
nahvePardakht=nahve_pardakht,
|
nahvePardakht=nahve_pardakht,
|
||||||
payStatus="true",
|
payStatus="true",
|
||||||
user=user_op
|
user=user_op
|
||||||
)
|
), nahve_pardakht))
|
||||||
|
|
||||||
elif key in ['l_nesiye', 'l_checkPrice']:
|
elif key in ['l_nesiye', 'l_checkPrice']:
|
||||||
is_nesiye = (key == 'l_nesiye')
|
is_nesiye = (key == 'l_nesiye')
|
||||||
@@ -359,6 +362,16 @@ def newFactor2(request, c_id):
|
|||||||
logger.error(f"Error creating factor for customer {c_id}: {str(e)}", exc_info=True)
|
logger.error(f"Error creating factor for customer {c_id}: {str(e)}", exc_info=True)
|
||||||
return HttpResponse("خطایی غیرمنتظره در سرور رخ داد. اطلاعات ذخیره نشد.", status=500)
|
return HttpResponse("خطایی غیرمنتظره در سرور رخ داد. اطلاعات ذخیره نشد.", status=500)
|
||||||
|
|
||||||
|
# تریگرهای پیامک پس از تأیید تراکنش (خارج از atomic تا در صورت rollback ساخته نشوند).
|
||||||
|
if new_factor is not None:
|
||||||
|
# نظرسنجی بیواسطه پس از ثبت فاکتور.
|
||||||
|
notify_factor_survey(customer_obj, new_factor, created_by=user_op)
|
||||||
|
# تشکر برای هر پرداخت نقدی/کارتیِ ثبتشده روی همین فاکتور (تخفیف مستثناست).
|
||||||
|
for art, method in payment_articles:
|
||||||
|
notify_payment_thanks(customer_obj, art.credit,
|
||||||
|
article_id=art.id, method=method,
|
||||||
|
created_by=user_op)
|
||||||
|
|
||||||
return redirect(f'/panel/customer/{c_id}/factors/')
|
return redirect(f'/panel/customer/{c_id}/factors/')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from persiantools.jdatetime import JalaliDate
|
|||||||
|
|
||||||
from ..models import (Article, Sarresid, Customer, Factor, ProductInStore,
|
from ..models import (Article, Sarresid, Customer, Factor, ProductInStore,
|
||||||
StoreRoom, Action)
|
StoreRoom, Action)
|
||||||
|
from ..sms.triggers import notify_payment_thanks
|
||||||
from ._helpers import logger, _safe_int
|
from ._helpers import logger, _safe_int
|
||||||
|
|
||||||
|
|
||||||
@@ -713,6 +714,11 @@ def passCheck(request, articl_id):
|
|||||||
sarresidINC.desc = desc
|
sarresidINC.desc = desc
|
||||||
sarresidINC.save()
|
sarresidINC.save()
|
||||||
|
|
||||||
|
# سند وصول (credit>0 و debit=0) یعنی مشتری پول داده ⇒ پیامک تشکر از پرداخت.
|
||||||
|
notify_payment_thanks(articleHesabDari2.customer, articleHesabDari2.credit,
|
||||||
|
article_id=articleHesabDari2.id,
|
||||||
|
method=articleHesabDari2.nahvePardakht, created_by=user_op)
|
||||||
|
|
||||||
return HttpResponse("ok")
|
return HttpResponse("ok")
|
||||||
#return redirect(request.META.get('HTTP_REFERER'))
|
#return redirect(request.META.get('HTTP_REFERER'))
|
||||||
|
|
||||||
@@ -916,6 +922,11 @@ def pardakhtNesiye(request, cs_id):
|
|||||||
articleHesabDariObj.user = user_op
|
articleHesabDariObj.user = user_op
|
||||||
articleHesabDariObj.save()
|
articleHesabDariObj.save()
|
||||||
|
|
||||||
|
# پرداخت نسیه (بستانکار) ⇒ پیامک تشکر از پرداخت (تخفیف مستثناست).
|
||||||
|
notify_payment_thanks(customerObj, articleHesabDariObj.credit,
|
||||||
|
article_id=articleHesabDariObj.id,
|
||||||
|
method=nahvePardakht, created_by=user_op)
|
||||||
|
|
||||||
return redirect(request.META.get('HTTP_REFERER'))
|
return redirect(request.META.get('HTTP_REFERER'))
|
||||||
|
|
||||||
except:
|
except:
|
||||||
|
|||||||
+217
-30
@@ -1,11 +1,11 @@
|
|||||||
"""ویوهای مدیریت پیامک در پنل (نیازمند پرمیشن ``admin_panel.ManageSMS``)."""
|
"""ویوهای مدیریت پیامک در پنل (نیازمند پرمیشن ``admin_panel.ManageSMS``)."""
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, time
|
from datetime import datetime, time, timedelta
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from django.contrib.auth.decorators import login_required, permission_required
|
from django.contrib.auth.decorators import login_required, permission_required
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.db.models import Count
|
from django.db.models import Count, Q
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -13,12 +13,34 @@ from django.utils import timezone
|
|||||||
from ..models import (Customer, SMSDedup, SMSProviderConfig, SMSQueue,
|
from ..models import (Customer, SMSDedup, SMSProviderConfig, SMSQueue,
|
||||||
SMSTemplate, SMSType)
|
SMSTemplate, SMSType)
|
||||||
from ..sms.enqueue import enqueue_sms
|
from ..sms.enqueue import enqueue_sms
|
||||||
|
from ..sms.parts import ENCODING_LABELS, describe, parts_count
|
||||||
from ..sms.phones import (customer_phone_entries, save_primary_normalized,
|
from ..sms.phones import (customer_phone_entries, save_primary_normalized,
|
||||||
stored_primary_normalized)
|
stored_primary_normalized)
|
||||||
from ..sms.render import format_jalali, render_template
|
from ..sms.render import format_jalali, render_template
|
||||||
from ..sms.timing import apply_timing, current_timing_value, timing_for
|
from ..sms.timing import apply_timing, current_timing_value, timing_for
|
||||||
|
|
||||||
_manage_required = permission_required("admin_panel.ManageSMS", raise_exception=True)
|
_manage_required = permission_required("admin_panel.ManageSMS", raise_exception=True)
|
||||||
|
# صفحات «تنظیمات سامانه» و «قالبها» یک سطح دسترسی جداگانه دارند تا مدیر بتواند
|
||||||
|
# فقط به کاربران موردنظرش اجازهٔ دیدن/ویرایش بدهد (نه هر کاربر دارای ManageSMS).
|
||||||
|
_settings_required = permission_required(
|
||||||
|
"admin_panel.ManageSMSSettings", raise_exception=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# گزارش هزینه فقط روی پیامکهایی است که از مرحلهٔ «تأیید مدیر» گذشته و واقعاً به
|
||||||
|
# سامانه/اپراتور سپرده شدهاند. موارد «در انتظار تأیید» (هنوز ارسال نشده) و
|
||||||
|
# «لغوشده» (هرگز ارسال نمیشود) نباید در تحلیل هزینه بیایند.
|
||||||
|
SENT_STATUSES = [
|
||||||
|
SMSQueue.Status.APPROVED,
|
||||||
|
SMSQueue.Status.PROCESSING,
|
||||||
|
SMSQueue.Status.SENT,
|
||||||
|
SMSQueue.Status.FAILED,
|
||||||
|
]
|
||||||
|
|
||||||
|
# وضعیتهای «پایانی» که دیگر کاری روی آنها لازم نیست. صفحهٔ «صف ارسال» فقط مواردِ
|
||||||
|
# نیازمند اقدام را نشان میدهد؛ پیامکِ ارسالشده از صف بیرون میرود و سابقهاش در
|
||||||
|
# صفحهٔ «گزارش ارسال» (/panel/sms/report) دیده میشود. این کار جلوی حذفِ ناخواستهٔ
|
||||||
|
# تاریخچه از روی صف را میگیرد و صف را به فهرستِ کاریِ تمیز تبدیل میکند.
|
||||||
|
DONE_STATUSES = [SMSQueue.Status.SENT]
|
||||||
|
|
||||||
|
|
||||||
def _notice(url, code, **extra):
|
def _notice(url, code, **extra):
|
||||||
@@ -47,14 +69,13 @@ TEMPLATE_VARIABLES = [
|
|||||||
|
|
||||||
# فقط برای نمایش: آیکن و توضیح کوتاه «چه زمانی ارسال میشود» هر نوع (لایهٔ UI، نه دامنه).
|
# فقط برای نمایش: آیکن و توضیح کوتاه «چه زمانی ارسال میشود» هر نوع (لایهٔ UI، نه دامنه).
|
||||||
SMS_TYPE_UI = {
|
SMS_TYPE_UI = {
|
||||||
SMSType.WELCOME: {"icon": "👋", "when": "بیدرنگ پس از ثبت مشتری"},
|
SMSType.WELCOME: {"icon": "👋", "when": "بیدرنگ پس از ثبت مشتری"},
|
||||||
SMSType.CHEQUE_DUE: {"icon": "🧾", "when": "پیش از سررسید چک"},
|
SMSType.SURVEY: {"icon": "⭐", "when": "بیدرنگ پس از ثبت فاکتور"},
|
||||||
|
SMSType.CHEQUE_REGISTRATION: {"icon": "🧾", "when": "چند روز پس از سررسید چک"},
|
||||||
|
SMSType.VISIT: {"icon": "🔧", "when": "پس از گذشت دورهٔ بازدید دستگاه"},
|
||||||
|
SMSType.VISIT_CREATED: {"icon": "📅", "when": "پس از ثبت اقدام بازدید"},
|
||||||
|
SMSType.PAYMENT_THANKS: {"icon": "🙏", "when": "پس از ثبت پرداخت"},
|
||||||
SMSType.NESIYE_DUE: {"icon": "💳", "when": "پیش از سررسید نسیه"},
|
SMSType.NESIYE_DUE: {"icon": "💳", "when": "پیش از سررسید نسیه"},
|
||||||
SMSType.CHEQUE_REGISTRATION: {"icon": "⏳", "when": "تا ثبتشدن اطلاعات چک"},
|
|
||||||
SMSType.DEBT_DUE: {"icon": "💰", "when": "روز سررسید وصول مطالبات"},
|
|
||||||
SMSType.SURVEY: {"icon": "⭐", "when": "پس از صدور فاکتور"},
|
|
||||||
SMSType.VISIT: {"icon": "🔧", "when": "پس از گذشت دورهٔ بازدید"},
|
|
||||||
SMSType.CUSTOM: {"icon": "✍️", "when": "ارسال دستی از صفحهٔ مشتری"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# نمونهٔ داده برای «پیشنمایش زندهٔ» متن در پنل (هیچ پیامکی با این مقادیر ارسال نمیشود).
|
# نمونهٔ داده برای «پیشنمایش زندهٔ» متن در پنل (هیچ پیامکی با این مقادیر ارسال نمیشود).
|
||||||
@@ -70,7 +91,7 @@ SAMPLE_CONTEXT = {
|
|||||||
|
|
||||||
|
|
||||||
@login_required(login_url='/panel/p_login/')
|
@login_required(login_url='/panel/p_login/')
|
||||||
@_manage_required
|
@_settings_required
|
||||||
def sms_settings(request):
|
def sms_settings(request):
|
||||||
config = SMSProviderConfig.load()
|
config = SMSProviderConfig.load()
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
@@ -82,7 +103,7 @@ def sms_settings(request):
|
|||||||
config.test_message = test_text
|
config.test_message = test_text
|
||||||
config.save(update_fields=["test_message"])
|
config.save(update_fields=["test_message"])
|
||||||
enqueue_sms(
|
enqueue_sms(
|
||||||
customer=None, sms_type=SMSType.CUSTOM,
|
customer=None, sms_type="",
|
||||||
message=test_text or config.test_message,
|
message=test_text or config.test_message,
|
||||||
recipients=resolve_recipients_test(phone), created_by=request.user,
|
recipients=resolve_recipients_test(phone), created_by=request.user,
|
||||||
approved=True, # ارسال دستیِ مدیر؛ نیاز به بازبینی صف ندارد.
|
approved=True, # ارسال دستیِ مدیر؛ نیاز به بازبینی صف ندارد.
|
||||||
@@ -192,7 +213,7 @@ def _sample_context():
|
|||||||
|
|
||||||
|
|
||||||
@login_required(login_url='/panel/p_login/')
|
@login_required(login_url='/panel/p_login/')
|
||||||
@_manage_required
|
@_settings_required
|
||||||
def sms_templates(request):
|
def sms_templates(request):
|
||||||
raw_sample, text_sample = _sample_context()
|
raw_sample, text_sample = _sample_context()
|
||||||
rows = []
|
rows = []
|
||||||
@@ -236,7 +257,7 @@ def _templates_redirect(anchor="", notice="", **extra):
|
|||||||
|
|
||||||
|
|
||||||
@login_required(login_url='/panel/p_login/')
|
@login_required(login_url='/panel/p_login/')
|
||||||
@_manage_required
|
@_settings_required
|
||||||
def sms_template_edit(request, t_id):
|
def sms_template_edit(request, t_id):
|
||||||
"""ویرایش یک قالب. فیلد زمانی بر اساس نوع پیامک انتخاب و اعتبارسنجی میشود.
|
"""ویرایش یک قالب. فیلد زمانی بر اساس نوع پیامک انتخاب و اعتبارسنجی میشود.
|
||||||
|
|
||||||
@@ -321,22 +342,17 @@ def sms_primary_toggle(request, c_id):
|
|||||||
def sms_send_custom(request, c_id):
|
def sms_send_custom(request, c_id):
|
||||||
"""ارسال پیامک با متن سفارشی برای همین مشتری.
|
"""ارسال پیامک با متن سفارشی برای همین مشتری.
|
||||||
|
|
||||||
اگر کادر متن خالی بماند، متن قالب «پیام سفارشی» استفاده میشود. متغیرهایی مثل
|
متن باید توسط مدیر نوشته شود (قالب پیشفرضی وجود ندارد)؛ متغیرهایی مثل
|
||||||
``{name}`` در متن واردشده هم رندر میشوند.
|
``{name}`` در متن واردشده هم رندر میشوند.
|
||||||
"""
|
"""
|
||||||
customer = get_object_or_404(Customer, id=c_id)
|
customer = get_object_or_404(Customer, id=c_id)
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
text = (request.POST.get("text", "") or "").strip()
|
text = (request.POST.get("text", "") or "").strip()
|
||||||
if not text:
|
|
||||||
template = SMSTemplate.objects.filter(
|
|
||||||
sms_type=SMSType.CUSTOM, is_enabled=True,
|
|
||||||
).first()
|
|
||||||
text = (template.text if template else "").strip()
|
|
||||||
rendered = render_template(text, {"name": customer.name_family}).strip()
|
rendered = render_template(text, {"name": customer.name_family}).strip()
|
||||||
if not rendered:
|
if not rendered:
|
||||||
return _notice(_customer_url(c_id), "custom_empty")
|
return _notice(_customer_url(c_id), "custom_empty")
|
||||||
created = enqueue_sms(
|
created = enqueue_sms(
|
||||||
customer=customer, sms_type=SMSType.CUSTOM, message=rendered,
|
customer=customer, sms_type="", message=rendered,
|
||||||
created_by=request.user,
|
created_by=request.user,
|
||||||
approved=True, # متن را خودِ مدیر نوشته و ارسال را خواسته است.
|
approved=True, # متن را خودِ مدیر نوشته و ارسال را خواسته است.
|
||||||
)
|
)
|
||||||
@@ -348,9 +364,19 @@ def sms_send_custom(request, c_id):
|
|||||||
@login_required(login_url='/panel/p_login/')
|
@login_required(login_url='/panel/p_login/')
|
||||||
@_manage_required
|
@_manage_required
|
||||||
def sms_queue(request):
|
def sms_queue(request):
|
||||||
|
"""صف ارسال: فقط مواردی که هنوز «کارِ باقیمانده» دارند.
|
||||||
|
|
||||||
|
پیامکهای ارسالشده (``sent``) از این فهرست بیرون میمانند چون کارشان تمام است و
|
||||||
|
سابقهشان در «گزارش ارسال» دیده میشود؛ ماندنشان در صف فقط باعث میشد دکمهٔ
|
||||||
|
«حذف» روی تاریخچه ظاهر شود. بقیهٔ وضعیتها (در انتظار تأیید، تأییدشده، در حال
|
||||||
|
پردازش، ناموفق) در صف میمانند چون هنوز نیاز به اقدام دارند.
|
||||||
|
"""
|
||||||
status = request.GET.get("status", "")
|
status = request.GET.get("status", "")
|
||||||
delivery = request.GET.get("delivery", "")
|
delivery = request.GET.get("delivery", "")
|
||||||
qs = SMSQueue.objects.all().select_related("customer").order_by("-created_at")
|
qs = (
|
||||||
|
SMSQueue.objects.exclude(status__in=DONE_STATUSES)
|
||||||
|
.select_related("customer").order_by("-created_at")
|
||||||
|
)
|
||||||
if status:
|
if status:
|
||||||
qs = qs.filter(status=status)
|
qs = qs.filter(status=status)
|
||||||
if delivery:
|
if delivery:
|
||||||
@@ -364,24 +390,177 @@ def sms_queue(request):
|
|||||||
counts = dict(
|
counts = dict(
|
||||||
SMSQueue.objects.order_by().values_list("status").annotate(n=Count("id"))
|
SMSQueue.objects.order_by().values_list("status").annotate(n=Count("id"))
|
||||||
)
|
)
|
||||||
|
# «کل صف» = موارد باقیمانده (بدون ارسالشدهها).
|
||||||
|
queue_total = sum(n for code, n in counts.items() if code not in DONE_STATUSES)
|
||||||
data = {
|
data = {
|
||||||
"queue": page,
|
"queue": page,
|
||||||
"status": status,
|
"status": status,
|
||||||
"delivery": delivery,
|
"delivery": delivery,
|
||||||
"status_choices": SMSQueue.Status.choices,
|
# گزینههای فیلتر وضعیت: فقط وضعیتهای درون صف (ارسالشده حذف میشود).
|
||||||
|
"status_choices": [
|
||||||
|
(code, label) for code, label in SMSQueue.Status.choices
|
||||||
|
if code not in DONE_STATUSES
|
||||||
|
],
|
||||||
"delivery_choices": SMSQueue.DeliveryStatus.choices,
|
"delivery_choices": SMSQueue.DeliveryStatus.choices,
|
||||||
"pending_count": counts.get(SMSQueue.Status.PENDING, 0),
|
"pending_count": counts.get(SMSQueue.Status.PENDING, 0),
|
||||||
"total_count": sum(counts.values()),
|
"total_count": queue_total,
|
||||||
|
# تعداد ارسالشدهها جداگانه نمایش داده میشود (برای رفتن به گزارش).
|
||||||
|
"sent_count": counts.get(SMSQueue.Status.SENT, 0),
|
||||||
"filtered_count": paginator.count,
|
"filtered_count": paginator.count,
|
||||||
# (کد وضعیت، برچسب، تعداد) — به ترتیب تعریف Status.
|
# (کد وضعیت، برچسب، تعداد) — فقط وضعیتهای درون صف.
|
||||||
"status_stats": [
|
"status_stats": [
|
||||||
(code, label, counts.get(code, 0))
|
(code, label, counts.get(code, 0))
|
||||||
for code, label in SMSQueue.Status.choices
|
for code, label in SMSQueue.Status.choices
|
||||||
|
if code not in DONE_STATUSES
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
return render(request, "admin_panel/sms_queue.html", data)
|
return render(request, "admin_panel/sms_queue.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required(login_url='/panel/p_login/')
|
||||||
|
@_manage_required
|
||||||
|
def customer_sms_history(request, c_id):
|
||||||
|
"""سوابق پیامکهای یک مشتری روی صفحهٔ خود مشتری.
|
||||||
|
|
||||||
|
همهٔ پیامکهایی که برای شمارههای این مشتری در صف قرار گرفته/ارسال شدهاند
|
||||||
|
(هر وضعیتی) با متن دقیق، تاریخ شمسی و وضعیت تحویل (دلیوری) نمایش داده میشوند و
|
||||||
|
فهرست صفحهبندی میشود. با ``?status=`` و ``?delivery=`` میتوان فیلتر کرد.
|
||||||
|
"""
|
||||||
|
customer = get_object_or_404(Customer, id=c_id)
|
||||||
|
status = request.GET.get("status", "")
|
||||||
|
delivery = request.GET.get("delivery", "")
|
||||||
|
qs = (
|
||||||
|
SMSQueue.objects.filter(customer=customer).order_by("-created_at")
|
||||||
|
)
|
||||||
|
if status:
|
||||||
|
qs = qs.filter(status=status)
|
||||||
|
if delivery:
|
||||||
|
qs = qs.filter(delivery_status=delivery)
|
||||||
|
paginator = Paginator(qs, 20)
|
||||||
|
page = paginator.get_page(request.GET.get("page"))
|
||||||
|
data = {
|
||||||
|
"customer": customer,
|
||||||
|
"items": page,
|
||||||
|
"status": status,
|
||||||
|
"delivery": delivery,
|
||||||
|
"status_choices": SMSQueue.Status.choices,
|
||||||
|
"delivery_choices": SMSQueue.DeliveryStatus.choices,
|
||||||
|
"total_count": SMSQueue.objects.filter(customer=customer).count(),
|
||||||
|
"filtered_count": paginator.count,
|
||||||
|
}
|
||||||
|
return render(request, "admin_panel/customerSmsHistory.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required(login_url='/panel/p_login/')
|
||||||
|
@_manage_required
|
||||||
|
def sms_report(request):
|
||||||
|
"""گزارش پیامکهای ارسالشده برای تحلیل هزینه.
|
||||||
|
|
||||||
|
امکان فیلتر بر اساس **بازهٔ تاریخ شمسی** (از/تا)، وضعیت ارسال، وضعیت تحویل و
|
||||||
|
نوع پیامک، بههمراه جستجوی متنی روی متن پیامک/شماره/نام مشتری.
|
||||||
|
|
||||||
|
برای هر ردیف، «تعداد پیامکی که اپراتور محاسبه کرده» (parts) بر اساس کدگذاری
|
||||||
|
واقعی متن (GSM-7 برای انگلیسی/عدد، UCS-2 برای فارسی/یونیکد) محاسبه و نمایش
|
||||||
|
داده میشود؛ جمع کل بخشها هم برای برآورد هزینه گزارش میشود.
|
||||||
|
"""
|
||||||
|
date_from_raw = (request.GET.get("date_from") or "").strip()
|
||||||
|
date_to_raw = (request.GET.get("date_to") or "").strip()
|
||||||
|
status = request.GET.get("status", "")
|
||||||
|
delivery = request.GET.get("delivery", "")
|
||||||
|
sms_type = request.GET.get("sms_type", "")
|
||||||
|
query = (request.GET.get("q") or "").strip()
|
||||||
|
|
||||||
|
qs = (
|
||||||
|
SMSQueue.objects.select_related("customer")
|
||||||
|
.filter(status__in=SENT_STATUSES)
|
||||||
|
.order_by("-created_at")
|
||||||
|
)
|
||||||
|
|
||||||
|
# بازهٔ تاریخ شمسی روی «زمان ایجاد» (ثبت در سیستم) اعمال میشود. بازهٔ «تا» تا
|
||||||
|
# پایان همان روز در نظر گرفته میشود تا پیامکهای همان روز هم بیایند.
|
||||||
|
parsed_from = parse_jalali_date(date_from_raw) if date_from_raw else None
|
||||||
|
parsed_to = parse_jalali_date(date_to_raw) if date_to_raw else None
|
||||||
|
if parsed_from is not None:
|
||||||
|
qs = qs.filter(created_at__gte=parsed_from)
|
||||||
|
if parsed_to is not None:
|
||||||
|
qs = qs.filter(created_at__lt=parsed_to + timedelta(days=1))
|
||||||
|
|
||||||
|
if status:
|
||||||
|
qs = qs.filter(status=status)
|
||||||
|
if delivery:
|
||||||
|
qs = qs.filter(delivery_status=delivery)
|
||||||
|
if sms_type:
|
||||||
|
qs = qs.filter(sms_type=sms_type)
|
||||||
|
if query:
|
||||||
|
qs = qs.filter(
|
||||||
|
Q(message__icontains=query)
|
||||||
|
| Q(phone_number__icontains=query)
|
||||||
|
| Q(customer__name_family__icontains=query)
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- آمار کلی روی کل نتیجهٔ فیلترشده (نه فقط صفحهٔ جاری) ---
|
||||||
|
status_counts = dict(
|
||||||
|
qs.order_by().values_list("status").annotate(n=Count("id"))
|
||||||
|
)
|
||||||
|
total_records = sum(status_counts.values())
|
||||||
|
# جمع بخشها (parts) باید در پایتون حساب شود چون به کدگذاری هر متن بستگی دارد.
|
||||||
|
total_parts = 0
|
||||||
|
gsm7_parts = ucs2_parts = 0
|
||||||
|
for message in qs.values_list("message", flat=True).iterator(chunk_size=500):
|
||||||
|
info = describe(message)
|
||||||
|
total_parts += info["parts"]
|
||||||
|
if info["encoding"] == "gsm7":
|
||||||
|
gsm7_parts += info["parts"]
|
||||||
|
else:
|
||||||
|
ucs2_parts += info["parts"]
|
||||||
|
|
||||||
|
paginator = Paginator(qs, 50)
|
||||||
|
page = paginator.get_page(request.GET.get("page"))
|
||||||
|
|
||||||
|
# برای هر ردیف صفحه، محاسبهٔ کدگذاری/طول/بخش (نمایشی).
|
||||||
|
rows = []
|
||||||
|
for item in page:
|
||||||
|
info = describe(item.message)
|
||||||
|
rows.append({"item": item, "parts": info["parts"],
|
||||||
|
"encoding": info["encoding_label"], "length": info["length"]})
|
||||||
|
|
||||||
|
# برای لینک صفحهبندی که همهٔ فیلترها را حفظ کند (بدون خودِ page).
|
||||||
|
params = request.GET.copy()
|
||||||
|
params.pop("page", None)
|
||||||
|
base_query = params.urlencode()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"rows": rows,
|
||||||
|
"page": page,
|
||||||
|
"status": status,
|
||||||
|
"delivery": delivery,
|
||||||
|
"sms_type": sms_type,
|
||||||
|
"query": query,
|
||||||
|
"date_from": date_from_raw,
|
||||||
|
"date_to": date_to_raw,
|
||||||
|
"status_choices": [
|
||||||
|
(code, label) for code, label in SMSQueue.Status.choices
|
||||||
|
if code in SENT_STATUSES
|
||||||
|
],
|
||||||
|
"delivery_choices": SMSQueue.DeliveryStatus.choices,
|
||||||
|
"type_choices": SMSType.choices,
|
||||||
|
"total_records": total_records,
|
||||||
|
"total_parts": total_parts,
|
||||||
|
"gsm7_parts": gsm7_parts,
|
||||||
|
"ucs2_parts": ucs2_parts,
|
||||||
|
"encoding_labels": ENCODING_LABELS,
|
||||||
|
"status_stats": [
|
||||||
|
(code, label, status_counts.get(code, 0))
|
||||||
|
for code, label in SMSQueue.Status.choices
|
||||||
|
if code in SENT_STATUSES
|
||||||
|
],
|
||||||
|
"filtered": paginator.count,
|
||||||
|
"base_query": base_query,
|
||||||
|
"has_filter": any([date_from_raw, date_to_raw, status, delivery, sms_type, query]),
|
||||||
|
}
|
||||||
|
return render(request, "admin_panel/sms_report.html", data)
|
||||||
|
|
||||||
|
|
||||||
def _approve(queryset):
|
def _approve(queryset):
|
||||||
"""پیامکهای «در انتظار تأیید» را تأیید و آمادهٔ ارسال میکند."""
|
"""پیامکهای «در انتظار تأیید» را تأیید و آمادهٔ ارسال میکند."""
|
||||||
return queryset.filter(status=SMSQueue.Status.PENDING).update(
|
return queryset.filter(status=SMSQueue.Status.PENDING).update(
|
||||||
@@ -403,9 +582,15 @@ def sms_queue_approve(request, q_id):
|
|||||||
@login_required(login_url='/panel/p_login/')
|
@login_required(login_url='/panel/p_login/')
|
||||||
@_manage_required
|
@_manage_required
|
||||||
def sms_queue_delete(request, q_id):
|
def sms_queue_delete(request, q_id):
|
||||||
"""حذف یک پیامک از صف (مثلاً پس از بازبینی و رد آن)."""
|
"""حذف یک پیامک از صف (مثلاً پس از بازبینی و رد آن).
|
||||||
|
|
||||||
|
پیامکِ ارسالشده حذف نمیشود؛ سابقهٔ آن در «گزارش ارسال» میماند و حذفش یعنی
|
||||||
|
پاککردنِ تاریخچه. برای چنین موردی فقط پیام هشدار برمیگردد.
|
||||||
|
"""
|
||||||
item = get_object_or_404(SMSQueue, id=q_id)
|
item = get_object_or_404(SMSQueue, id=q_id)
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
if item.status in DONE_STATUSES:
|
||||||
|
return _notice(reverse("admin_panel:sms_queue"), "delete_done_blocked")
|
||||||
item.delete()
|
item.delete()
|
||||||
return _notice(reverse("admin_panel:sms_queue"), "deleted")
|
return _notice(reverse("admin_panel:sms_queue"), "deleted")
|
||||||
return redirect("admin_panel:sms_queue")
|
return redirect("admin_panel:sms_queue")
|
||||||
@@ -427,6 +612,8 @@ def sms_queue_bulk(request):
|
|||||||
count = _approve(selected)
|
count = _approve(selected)
|
||||||
code, extra = "bulk_approved", {"count": count}
|
code, extra = "bulk_approved", {"count": count}
|
||||||
elif action == "delete_selected":
|
elif action == "delete_selected":
|
||||||
|
# تاریخچهٔ ارسالشدهها از طریق صف حذف نمیشود.
|
||||||
|
selected = selected.exclude(status__in=DONE_STATUSES)
|
||||||
count = selected.count()
|
count = selected.count()
|
||||||
selected.delete()
|
selected.delete()
|
||||||
code, extra = "bulk_deleted", {"count": count}
|
code, extra = "bulk_deleted", {"count": count}
|
||||||
@@ -439,14 +626,14 @@ def sms_queue_bulk(request):
|
|||||||
@login_required(login_url='/panel/p_login/')
|
@login_required(login_url='/panel/p_login/')
|
||||||
@_manage_required
|
@_manage_required
|
||||||
def sms_queue_empty(request):
|
def sms_queue_empty(request):
|
||||||
"""خالی کردن کل صف — هر وضعیتی، بدون بازگشت.
|
"""خالی کردن صف — فقط موارد باقیمانده (بدون بازگشت).
|
||||||
|
|
||||||
پیش از حذف، کلید یکتاسازیِ هر رکورد در دفتر ``SMSDedup`` تضمین میشود تا اسکنر
|
پیامکهای ارسالشده جزو صف نیستند و حذف نمیشوند؛ تاریخچهشان در «گزارش ارسال»
|
||||||
در اجرای بعدی همان پیامکها را از نو نسازد. رکوردهایی که کلید ندارند (مثلاً
|
لازم است. پیش از حذف، کلید یکتاسازیِ هر رکورد در دفتر ``SMSDedup`` تضمین میشود
|
||||||
پیامک دستی) کلید مشتق از خودشان را میگیرند.
|
تا اسکنر در اجرای بعدی همان پیامکها را از نو نسازد.
|
||||||
"""
|
"""
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
rows = SMSQueue.objects.all()
|
rows = SMSQueue.objects.exclude(status__in=DONE_STATUSES)
|
||||||
keys = [k for k in rows.values_list("dedup_key", flat=True) if k]
|
keys = [k for k in rows.values_list("dedup_key", flat=True) if k]
|
||||||
SMSDedup.objects.bulk_create(
|
SMSDedup.objects.bulk_create(
|
||||||
[SMSDedup(key=k) for k in keys], ignore_conflicts=True,
|
[SMSDedup(key=k) for k in keys], ignore_conflicts=True,
|
||||||
|
|||||||
+1871
-692
File diff suppressed because it is too large.
Load diff
+169
-138
@@ -1,142 +1,173 @@
|
|||||||
{
|
{
|
||||||
"0": "views/store.py",
|
"0": "views/sms.py",
|
||||||
"1": "project_tree.py",
|
"1": "run_all",
|
||||||
"2": "sqlite_db_backup.py",
|
"2": "django_db",
|
||||||
"3": "django_db",
|
"3": "views/store.py",
|
||||||
"4": "Customer ID Backup/New Factor Template",
|
"4": "ModelsPackageContractTests",
|
||||||
"5": "views/customers.py",
|
"5": "PhoneCleanupTests",
|
||||||
"6": "cheque_inquiry.py",
|
"6": ".load",
|
||||||
"7": "admin_panel/templates/admin_panel/baseHesabdari.html",
|
"7": "models/sms.py",
|
||||||
"8": "django_db_models_deletion",
|
"8": "logging",
|
||||||
"9": "0051_auto_20260705_1356.py",
|
"9": "views/customers.py",
|
||||||
"10": "admin_panel/storeBK1.html",
|
"10": "django_db_models_deletion",
|
||||||
"11": "ViewSmokeTests",
|
"11": "make_customer",
|
||||||
"12": "admin_panel/templates/admin_panel/customerAction.html",
|
"12": "customer_phone_entries",
|
||||||
"13": "User",
|
"13": "EventTriggerTests",
|
||||||
"14": "run_all",
|
"14": "views/finance.py",
|
||||||
"15": "admin_panel/admin.py",
|
"15": "SMSResult",
|
||||||
"16": "stock_in",
|
"16": "sms0098ws.py",
|
||||||
"17": "admin_panel/templates/admin_panel/bedehi.html",
|
"17": "Admin Panel Customization",
|
||||||
"18": "hesabdariCheckPassFail.html",
|
"18": "User",
|
||||||
"19": "Reference guide",
|
"19": "django_conf",
|
||||||
"20": "normalize_mobile",
|
"20": "test_customers.py",
|
||||||
"21": "graphify",
|
"21": "phones.py",
|
||||||
"22": "hesabdariResidNashode.html",
|
"22": "Login Template Forms",
|
||||||
"23": "0017_factoriteam_garantyesh.py",
|
"23": "cheque_inquiry.py",
|
||||||
"24": "scanner.py",
|
"24": "factories.py",
|
||||||
"25": "0002_auto_20220114_1616.py",
|
"25": "Rip'n Fast. Fewer Tokens. Better Code.",
|
||||||
"26": "SMSResult",
|
"26": "views/factors.py",
|
||||||
"27": "Rip'n Fast. Fewer Tokens. Better Code.",
|
"27": "to_local",
|
||||||
"28": "models/sms.py",
|
"28": "make_user",
|
||||||
"29": "admin_panel/templates/admin_panel/baseStore.html",
|
"29": "QueueReviewTests",
|
||||||
"30": "views/finance.py",
|
"30": "NoticeMechanismTests",
|
||||||
"31": "Customer Modals UI",
|
"31": "enqueue.py",
|
||||||
"32": "views/factors.py",
|
"32": "enqueue_sms",
|
||||||
"33": "0034_alter_article_sarresid.py",
|
"33": "PanelTestCase",
|
||||||
"34": "views/sms.py",
|
"34": "ProductInStore",
|
||||||
"35": "0035_alter_article_sarresid.py",
|
"35": "admin_panel/templates/admin_panel/baseHesabdari.html",
|
||||||
"36": "django_db_models",
|
"36": "Reference guide",
|
||||||
"37": "0010_auto_20220122_1546.py",
|
"37": "Factor Creation UI",
|
||||||
"38": "0040_customer_laghv_bazdid.py",
|
"38": "admin_panel/templates/admin_panel/bedehi.html",
|
||||||
"39": "test_sms.py",
|
"39": "project_tree.py",
|
||||||
"40": "0048_alter_productinstore_options.py",
|
"40": "CustomerSmsHistoryTests",
|
||||||
"41": "0032_article_paystatus.py",
|
"41": "TemplatesPageUITests",
|
||||||
"42": "0033_article_nahvepardakht.py",
|
"42": "SMS Integration Docs",
|
||||||
"43": "make_customer",
|
"43": "timing_for",
|
||||||
"44": "sms0098.py",
|
"44": "django_shortcuts",
|
||||||
"45": "0044_sarresid_desc.py",
|
"45": "stock_in",
|
||||||
"46": "models/__init__.py",
|
"46": "admin_panel/templates/admin_panel/customerAction.html",
|
||||||
"47": "make_user",
|
"47": "parts_count",
|
||||||
"48": "GEMINI.md",
|
"48": "write_tree",
|
||||||
"49": "admin/migrations/__init__.py",
|
"49": "CustomerInfoCardUITests",
|
||||||
"50": "Customer Action Include",
|
"50": "Customer",
|
||||||
"51": "New Customer Base Template",
|
"51": "Phone Cleanup Guide",
|
||||||
"52": "Customer Feedback Partial",
|
"52": "SmsReportTests",
|
||||||
"53": "Customer Factor Detail",
|
"53": "manage.py",
|
||||||
"54": "Customer Accounting Template",
|
"54": "Agent Operating Guidelines",
|
||||||
"55": "Customer Factor List",
|
"55": "SMSDeliveryCommandTests",
|
||||||
"56": "Customer Feedback Page",
|
"56": "BehinSMSProvider",
|
||||||
"57": "commands/__init__.py",
|
"57": "test_sms.py",
|
||||||
"58": "management/__init__.py",
|
"58": "Financial Data Objects",
|
||||||
"59": "admin_panel/migrations/__init__.py",
|
"59": "Hybrid Migration Command",
|
||||||
"60": "Action List Template",
|
"60": "بازمحاسبهٔ «آخرین بازدید» مشتریان",
|
||||||
"61": "base.html",
|
"61": "Resource Cleanup Handler",
|
||||||
"62": "Customer New Factor",
|
"62": "SMS Timing Migration",
|
||||||
"63": "Customer New Factor V2",
|
"63": "Articl Model",
|
||||||
"64": "Customer Satisfaction List",
|
"64": "customerIDFactorBackup.html",
|
||||||
"65": "Customer Satisfaction Partial",
|
"65": "test_factors.py",
|
||||||
"66": "Customer Check/Debt Management",
|
"66": "os",
|
||||||
"67": "admin_panel/templates/admin_panel/customerBaseNew.html",
|
"67": "Graphify Tooling Docs",
|
||||||
"68": "Customer List",
|
"68": "django_urls",
|
||||||
"69": "admin_panel/templates/admin_panel/entezarBK.html",
|
"69": "0063_alter_smsqueue_status.py",
|
||||||
"70": "Financial Dashboard",
|
"70": "SMS Template Seeding",
|
||||||
"71": "Accounting Main View",
|
"71": "admin_panel/entezar.html",
|
||||||
"72": "admin_panel/templates/admin_panel/index.html",
|
"72": "customerNewBackup.html",
|
||||||
"73": "New Inventory Operation",
|
"73": "hesabdariResidNashode.html",
|
||||||
"74": "Inventory Operations List",
|
"74": "0070_sms_types_seven.py",
|
||||||
"75": "Operations Backup Template",
|
"75": "Endpoint Smoke Tests",
|
||||||
"76": "Financial Report Template",
|
"76": "Timezone Migration History",
|
||||||
"77": "Debt Collection Dashboard",
|
"77": "models/__init__.py",
|
||||||
"78": "Inventory Movement Report",
|
"78": "UrlContractTests",
|
||||||
"79": "Customer Satisfaction Dashboard",
|
"79": "Brand Scrubbing Migration",
|
||||||
"80": "Check and Credit Report Template",
|
"80": "SMS Dedup Migration",
|
||||||
"81": "Satisfaction List Template",
|
"81": "Installation Guide",
|
||||||
"82": "Store Inventory Template",
|
"82": "Project Honesty Contract",
|
||||||
"83": "Store Transfers Template",
|
"83": "Flow Control Migration",
|
||||||
"84": "Stores List Template",
|
"84": "Permission Label Migration",
|
||||||
"85": "toCurrency",
|
"85": "stop.sh",
|
||||||
"86": "templatetags/__init__.py",
|
"86": "Output Format Specs",
|
||||||
"87": "URL: newStore",
|
"87": "Performance Benchmarks",
|
||||||
"88": "URL: process_transfer_action",
|
"88": "SMS Integration Overview",
|
||||||
"89": "URL: rezayatEdit",
|
"89": "System Cache Table",
|
||||||
"90": "khatibi/__init__.py",
|
"90": "Customer Modal UI",
|
||||||
"91": "Extended Requirements File",
|
"91": "SentQueueProtectionTests",
|
||||||
"92": "Requirements File",
|
"92": "Infrastructure Services",
|
||||||
"93": "django_shortcuts",
|
"93": "Agent Setup Guide",
|
||||||
"94": "resolve_recipients",
|
"94": "First Use Guide",
|
||||||
"95": "factories.py",
|
"95": "0056_auto_20260731_1824.py",
|
||||||
"96": "write_tree",
|
"96": "0009_remove_storeroom_user.py",
|
||||||
"97": "TemplatesPageUITests",
|
"97": "PhoneEntry",
|
||||||
"98": "reconciliation_tool.py",
|
"98": "reconciliation_tool.py",
|
||||||
"99": "ModelsPackageContractTests",
|
"99": "announcements.py",
|
||||||
"100": "django_conf",
|
"100": "pathlib",
|
||||||
"101": "Command",
|
"101": "Command",
|
||||||
"102": "UrlContractTests",
|
"102": "0064_sms_webservice_delivery.py",
|
||||||
"103": "customerIDFactorBackup.html",
|
"103": "0040_customer_laghv_bazdid.py",
|
||||||
"104": "system.py",
|
"104": "0068_auto_20261010_1305.py",
|
||||||
"105": "BehinSMSProvider",
|
"105": "0069_smstemplate_auto_send.py",
|
||||||
"106": "test_customers.py",
|
"106": "0047_alter_productinstore_movement_type.py",
|
||||||
"107": "tests/__init__.py",
|
"107": "Command",
|
||||||
"108": "Command",
|
"108": "SMS Domain Logic",
|
||||||
"109": "Command",
|
"109": "Customer Communication Partials",
|
||||||
"110": "0061_sms_timing_and_primary.py",
|
"110": "SMS Template Management",
|
||||||
"111": "bale/providers.py",
|
"111": "Documentation and Graphing",
|
||||||
"112": "os",
|
"112": "Admin Migration Package",
|
||||||
"113": "Command",
|
"113": "bale/providers.py",
|
||||||
"114": "0058_seed_sms_templates.py",
|
"114": "Customer Action Templates",
|
||||||
"115": "test_factors.py",
|
"115": "Customer Feedback Components",
|
||||||
"116": "Customer",
|
"116": "Customer Invoice Details",
|
||||||
"117": "customerNewBackup.html",
|
"117": "Accounting Document Templates",
|
||||||
"118": "0059_sms_flow_control.py",
|
"118": "Customer Invoice List",
|
||||||
"119": "0060_sms_permission_label.py",
|
"119": "Customer Feedback Interface",
|
||||||
"120": "datetime",
|
"120": "Custom Management Commands",
|
||||||
"121": "0008_storeroom_user.py",
|
"121": "Django Management Package",
|
||||||
"122": "0013_alter_factor_options.py",
|
"122": "Admin Panel Migrations",
|
||||||
"123": "0018_alter_factoriteam_product.py",
|
"123": "Action List Interface",
|
||||||
"124": "0020_auto_20220126_1532.py",
|
"124": "Base HTML Layout",
|
||||||
"125": "0022_article_typepy.py",
|
"125": "New Invoice Creation",
|
||||||
"126": "0025_action_actiontype.py",
|
"126": "Customer Satisfaction Records",
|
||||||
"127": "0028_action_grade.py",
|
"127": "Satisfaction UI Components",
|
||||||
"128": "0030_auto_20220414_1047.py",
|
"128": "Debt and Credit Management",
|
||||||
"129": "0036_sarresid.py",
|
"129": "Customer Base Layout",
|
||||||
"130": "0037_sarresid_sarresiddt.py",
|
"130": "Waiting List Template",
|
||||||
"131": "0039_customer_last_bazdid_dt.py",
|
"131": "Financial Dashboard",
|
||||||
"132": "0041_auto_20240216_1607.py",
|
"132": "Accounting Main View",
|
||||||
"133": "0047_alter_productinstore_movement_type.py",
|
"133": "Admin Dashboard Index",
|
||||||
"134": "0049_alter_productinstore_options.py",
|
"134": "Inventory Entry Form",
|
||||||
"135": "0057_sms.py",
|
"135": "Inventory Operations History",
|
||||||
"136": "admin_panel/sms/__init__.py",
|
"136": "Operations Backup View",
|
||||||
"137": "bale/__init__.py",
|
"137": "Financial Reporting",
|
||||||
"138": "integrations/__init__.py",
|
"138": "Debt Collection Interface",
|
||||||
"139": "integrations/sms/__init__.py"
|
"139": "Inventory Movement Analytics",
|
||||||
|
"140": "Satisfaction Analytics Dashboard",
|
||||||
|
"141": "Credit Status Reports",
|
||||||
|
"142": "Satisfaction List View",
|
||||||
|
"143": "SMS Queue Management",
|
||||||
|
"144": "Store Inventory View",
|
||||||
|
"145": "Store Transfer Interface",
|
||||||
|
"146": "Store Directory Template",
|
||||||
|
"147": "Currency Formatting Utility",
|
||||||
|
"148": "Django Template Tags",
|
||||||
|
"149": "Store Creation Endpoint",
|
||||||
|
"150": "Transfer Processing Endpoint",
|
||||||
|
"151": "Satisfaction Edit Endpoint",
|
||||||
|
"152": "Customer Deletion Endpoint",
|
||||||
|
"154": "Django Permissions System",
|
||||||
|
"155": "Static Asset Management",
|
||||||
|
"156": "Data Cleanup Documentation",
|
||||||
|
"157": "Bale Integration Package",
|
||||||
|
"158": "External Integrations",
|
||||||
|
"159": "SMS Integration Module",
|
||||||
|
"160": "Khatibi Custom Module",
|
||||||
|
"161": "Project Dependencies",
|
||||||
|
"162": "Test Suite Package",
|
||||||
|
"164": "QueueStatsTests",
|
||||||
|
"166": "sms_settings",
|
||||||
|
"167": "Command",
|
||||||
|
"168": "0071_sms_settings_permission.py",
|
||||||
|
"169": "index",
|
||||||
|
"172": "0017_factoriteam_garantyesh.py",
|
||||||
|
"173": "0028_action_grade.py",
|
||||||
|
"174": "0034_alter_article_sarresid.py",
|
||||||
|
"175": "0039_customer_last_bazdid_dt.py"
|
||||||
}
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
{"0": "5ff8a72726a083db", "1": "219b01f43ec5f76d", "2": "edab4be6f8bf260c", "3": "70018840272ab80e", "4": "d36df3c85181f806", "5": "9eb1daeea9dda6c2", "6": "076144c85b2925b4", "7": "0928f3aa4e597117", "8": "0e080dc901d1f670", "9": "a00d0684d05c01a4", "10": "9701f1591d8fda6e", "11": "5290f7a7558a9c3e", "12": "e6a8282720eded0c", "13": "05119fe8608a0cbd", "14": "903ab35120e04b54", "15": "91b75fb079f9c02c", "16": "c12f70f2136b1a97", "17": "06959e26b0aedfb2", "18": "90146abe85d9419f", "19": "21f888d34b160f31", "20": "bc287e5355564fe6", "21": "0f632fc57e9feeb0", "22": "af89b22ae0662d6d", "23": "a800ecffc01fc9d5", "24": "955593b6e631a7a7", "25": "a19c211702401a5b", "26": "e72a0ec031902754", "27": "c9e7bf6ac46f263c", "28": "1a67e40fab1eeadd", "29": "3f3a4eb6e6b2f4d6", "30": "389c17cd386d28f2", "31": "03f8896e9b60ca96", "32": "40771cc7fa5ee930", "33": "c02bc9595eaedd36", "34": "d76e40111efda56a", "35": "7714d721fe893ef5", "36": "466b12915e08a4f0", "37": "2f1621907db6f86f", "38": "ef8221c5fdeee7f8", "39": "a890d0d5da72d758", "40": "bf60a8731033e467", "41": "d5c0f5f6714c9991", "42": "9948fb8b548535ed", "43": "61882165e32527ce", "44": "f1310e6117296d59", "45": "90e57f26f669372d", "46": "95f91e2eaf52002c", "47": "32e693b104a691c2", "48": "66ed76491a412a72", "49": "e67668b62ee6c62c", "50": "68aee39cd24db354", "51": "09b7297ea62919f2", "52": "054401d27ffaa4b2", "53": "dace81c1995fb321", "54": "19e301aeace95bb2", "55": "d6fd0cec4634a0b0", "56": "0230d355a6dabb54", "57": "5cebbb5528321795", "58": "536fe28d24c361f5", "59": "b08bdc2712cb0f12", "60": "6ce8444f504022cc", "61": "8021f52559633383", "62": "d8e34a3b3eb43ee0", "63": "056b8d6b5072289b", "64": "171c34c74a95481b", "65": "73e9eca6f2ca1c9b", "66": "dcc2230ae751c29a", "67": "2a617b932f5d92b2", "68": "2f5417a69eb1d15e", "69": "d55cb638532fdfeb", "70": "06cae4389208269f", "71": "e9c0a7be255e9b26", "72": "455b4ca39e55ecb0", "73": "8105c55538c34081", "74": "74304670b54c018f", "75": "a27f876eabb044b2", "76": "73f74dd7a40776d6", "77": "29d433d164b99f89", "78": "3f44aa48aa05d597", "79": "ca8a7efba0a3e579", "80": "5cc770af9d184d77", "81": "2d31fa723128b164", "82": "9d814c2520329846", "83": "f97d1a5c3a066598", "84": "14f84a6e4cd311d6", "85": "3a592d27b1f4ccc9", "86": "0bf6f9d0aeab4fd8", "87": "8b491bb3ff0649dd", "88": "638812cee15d1b9b", "89": "3dd16d68928a479c", "90": "72dad91d66d471ce", "91": "353a9d09eaae47ea", "92": "2d29260874184e4a", "93": "00d4d9fc79267971", "94": "8fedb8424b4f7d54", "95": "c7ce06d5e4e87224", "96": "9d46bbe4641a8543", "97": "68b25e60cbe5368c", "98": "8f5a6aa62c618b5f", "99": "33819123dbb4d454", "100": "e8d32595704b722a", "101": "e4fcae6cb365fba9", "102": "32ed40052fe2c3f8", "103": "8ff365aee1c1a767", "104": "c5743041b2101132", "105": "891ec2c014046f05", "106": "0c4707ef57c917a2", "107": "43bbf35b96423d46", "108": "88f880da5a61f9dc", "109": "dbfbf8557361f5ea", "110": "6e90b41fbd82c265", "111": "118bde95524551e0", "112": "b85898490162caed", "113": "d55977ec0e3ae213", "114": "df7ea8ce3fa52be3", "115": "460dadaff123e841", "116": "4faac8c35fa8b703", "117": "aea8fc3ff351253f", "118": "bda9be183e0f933e", "119": "63526d01888c4f22", "120": "329e844b7c9edf2d", "121": "37d2f4398ee065c5", "122": "49438bf9f08f11ff", "123": "b9687fc2d4953aed", "124": "6288b9f165b037f4", "125": "f97753a176d20dae", "126": "9cc7a507cfecd8fb", "127": "edb619bf5ac0fc49", "128": "8cb6b3db3aadb973", "129": "c9638aef2c5c82d8", "130": "c6b3f6e0a15a6a0b", "131": "52502a4c1ef75ce6", "132": "8d20e5c1bce8916d", "133": "b5ce3fce9192f8b8", "134": "6cef5bc37e710e62", "135": "0098e532f53735f2", "136": "b738c5aefe939cff", "137": "a9bf69b3d550abef", "138": "b039e1c69c8fb7b9", "139": "60029bf0948fc02a"}
|
{"0": "41d4c54b2ed9be31", "1": "b3e9db47e46c8950", "2": "c23f6f22b3532a57", "3": "44c42f7cbafe0d72", "4": "a480b4d6afdb4d1b", "5": "a8d6a179fdf0eb6f", "6": "3bd1354c22863397", "7": "88c19438cd72b3dd", "8": "15ca8060fedfc22d", "9": "274a49fd42c1a00e", "10": "58991daa10bb40c9", "11": "892dbef1deee47a9", "12": "f30060e6c2359aa4", "13": "96ec15fbcfe2d27a", "14": "05fc19ac0daa7b6c", "15": "7fe7625f0e064fdb", "16": "60b1fcd46cba604e", "17": "9eec9953ee3ca67f", "18": "dd5464972fce7e01", "19": "1f155e80fa7aa135", "20": "5c5477821216359a", "21": "13d087c92ddf57ef", "22": "9701f1591d8fda6e", "23": "7cd46ce4c8fb3ca4", "24": "d0b49ec6ae8ee7a4", "25": "75866dc8db907971", "26": "c578cf70cb6202fa", "27": "5c3fd3713dd960e8", "28": "1f2e16b5709d3936", "29": "1e9c1fa6fabde0d3", "30": "627dae19704f40f5", "31": "a5bd616f03cdbec3", "32": "ba49af147f38b69e", "33": "509eeabd8a23e031", "34": "f5248661c2005b5a", "35": "e091859c108d6145", "36": "4e9567703b727de7", "37": "d36df3c85181f806", "38": "b9fa892721272dc1", "39": "219b01f43ec5f76d", "40": "89444f5d51189c99", "41": "8c2e4ed6cd161bf3", "42": "bbd8a9066e48dada", "43": "781d0380d9314204", "44": "159d7e82897e35fc", "45": "0827f8a8bc343f88", "46": "a2f2be073f6f5bb9", "47": "8fd5a6f3cd5970b9", "48": "9d46bbe4641a8543", "49": "7bc001888a685a2a", "50": "dc8e4b85d8dd63ea", "51": "afc6d855b9fc1b76", "52": "11d769da5f6dd93e", "53": "3ec6277e63307488", "54": "34401beb9f60ebfd", "55": "054037b18f80b80c", "56": "330644cc56a0e06c", "57": "a3022e2d8c68315c", "58": "90146abe85d9419f", "59": "88f880da5a61f9dc", "60": "69bb788e4cabbf39", "61": "dbfbf8557361f5ea", "62": "6e90b41fbd82c265", "63": "bf2566d91998d2e4", "64": "8ff365aee1c1a767", "65": "120dc3cc659bbc4a", "66": "41c8f6c33d904f01", "67": "0f632fc57e9feeb0", "68": "2db67510f080898c", "69": "84208a7b8507da66", "70": "df7ea8ce3fa52be3", "71": "7db54a3d7ae0017c", "72": "aea8fc3ff351253f", "73": "af89b22ae0662d6d", "74": "a534e43d03671a7c", "75": "5290f7a7558a9c3e", "76": "a19c211702401a5b", "77": "7f4cbca34cc36f61", "78": "0f4a56d55e3925dc", "79": "da683f642f8c61ac", "80": "40313433c93b7054", "81": "81af0a28b68936d9", "82": "25287261c3d1c44b", "83": "bda9be183e0f933e", "84": "63526d01888c4f22", "85": "827420b645a55301", "86": "694a38d63caf7217", "87": "f220eaa4fac26e3e", "88": "d78c8e21a45613e9", "89": "c5743041b2101132", "90": "03f8896e9b60ca96", "91": "a06651e191a4aa22", "92": "4bfa64a1cd8c6a47", "93": "057eae5a523d2004", "94": "5de74655cbc56e06", "95": "cfbcf8cef78c5757", "96": "a1a1f3c1cdff72cf", "97": "72fca8ce97e3eeee", "98": "2ae07d5b9790c230", "99": "4793efd518dcb021", "100": "1b94d53a14e88aa3", "101": "35aa0fc2a0626f70", "102": "cb48fd8d430ac66e", "103": "ef8221c5fdeee7f8", "104": "9144923ab31b52a3", "105": "a2eea51406c7d4fc", "106": "b5ce3fce9192f8b8", "107": "7508d3e84c588617", "108": "b738c5aefe939cff", "109": "d2001d7d283134ac", "110": "6b8e6100893a6b6a", "111": "66ed76491a412a72", "112": "e67668b62ee6c62c", "113": "7b932cfef527d130", "114": "68aee39cd24db354", "115": "054401d27ffaa4b2", "116": "dace81c1995fb321", "117": "19e301aeace95bb2", "118": "d6fd0cec4634a0b0", "119": "0230d355a6dabb54", "120": "5cebbb5528321795", "121": "536fe28d24c361f5", "122": "b08bdc2712cb0f12", "123": "6ce8444f504022cc", "124": "8021f52559633383", "125": "d8e34a3b3eb43ee0", "126": "171c34c74a95481b", "127": "73e9eca6f2ca1c9b", "128": "dcc2230ae751c29a", "129": "2a617b932f5d92b2", "130": "d55cb638532fdfeb", "131": "06cae4389208269f", "132": "e9c0a7be255e9b26", "133": "455b4ca39e55ecb0", "134": "8105c55538c34081", "135": "74304670b54c018f", "136": "a27f876eabb044b2", "137": "73f74dd7a40776d6", "138": "29d433d164b99f89", "139": "3f44aa48aa05d597", "140": "ca8a7efba0a3e579", "141": "5cc770af9d184d77", "142": "2d31fa723128b164", "143": "be720fc0071f0c3d", "144": "9d814c2520329846", "145": "f97d1a5c3a066598", "146": "14f84a6e4cd311d6", "147": "3a592d27b1f4ccc9", "148": "0bf6f9d0aeab4fd8", "149": "8b491bb3ff0649dd", "150": "638812cee15d1b9b", "151": "3dd16d68928a479c", "152": "03a0ee43e18f7934", "154": "435331945d6aad17", "155": "968e1a135dc062b7", "156": "d1a987a548e55a76", "157": "a9bf69b3d550abef", "158": "b039e1c69c8fb7b9", "159": "60029bf0948fc02a", "160": "72dad91d66d471ce", "161": "353a9d09eaae47ea", "162": "43bbf35b96423d46", "164": "825ba1cf2cfa5500", "166": "9fee6d982ee873a6", "167": "ce80aac5f4c439da", "168": "1abe2e7b0bcbeca9", "169": "ce5f237abed01c9e", "172": "a800ecffc01fc9d5", "173": "edb619bf5ac0fc49", "174": "c02bc9595eaedd36", "175": "52502a4c1ef75ce6"}
|
||||||
@@ -1 +1 @@
|
|||||||
{"output_tokens": 126792}
|
{"output_tokens": 201}
|
||||||
+449
-311
@@ -1,183 +1,213 @@
|
|||||||
# Graph Report - khatibi (2026-10-10)
|
# Graph Report - khatibi (2026-10-11)
|
||||||
|
|
||||||
## Corpus Check
|
## Corpus Check
|
||||||
- 160 files · ~127,728 words
|
- 182 files · ~145,325 words
|
||||||
- Verdict: corpus is large enough that graph structure adds value.
|
- Verdict: corpus is large enough that graph structure adds value.
|
||||||
- Unclassified: 7 file(s) not represented in the graph (top: (none) 4, .env 2, .csv 1)
|
- Unclassified: 8 file(s) not represented in the graph (top: (none) 5, .env 2, .csv 1)
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
- 1280 nodes · 2237 edges · 140 communities (47 shown, 93 thin omitted)
|
- 1684 nodes · 3008 edges · 171 communities (74 shown, 97 thin omitted)
|
||||||
- Extraction: 94% EXTRACTED · 6% INFERRED · 0% AMBIGUOUS · INFERRED: 131 edges (avg confidence: 0.87)
|
- Extraction: 95% EXTRACTED · 5% INFERRED · 0% AMBIGUOUS · INFERRED: 151 edges (avg confidence: 0.89)
|
||||||
- Token cost: 0 input · 0 output
|
- Token cost: 0 input · 0 output
|
||||||
|
|
||||||
## Graph Freshness
|
## Graph Freshness
|
||||||
- Built from commit: `250cdbab`
|
- Built from commit: `1dea3509`
|
||||||
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
||||||
- Run `graphify update .` after code changes (no API cost).
|
- Run `graphify update .` after code changes (no API cost).
|
||||||
|
|
||||||
## Community Hubs (Navigation)
|
## Community Hubs (Navigation)
|
||||||
- views/store.py
|
|
||||||
- project_tree.py
|
|
||||||
- django_db
|
|
||||||
- Customer ID Backup/New Factor Template
|
|
||||||
- views/customers.py
|
|
||||||
- cheque_inquiry.py
|
|
||||||
- admin_panel/templates/admin_panel/baseHesabdari.html
|
|
||||||
- django_db_models_deletion
|
|
||||||
- 0051_auto_20260705_1356.py
|
|
||||||
- admin_panel/storeBK1.html
|
|
||||||
- ViewSmokeTests
|
|
||||||
- admin_panel/templates/admin_panel/customerAction.html
|
|
||||||
- User
|
|
||||||
- run_all
|
|
||||||
- admin_panel/admin.py
|
|
||||||
- stock_in
|
|
||||||
- admin_panel/templates/admin_panel/bedehi.html
|
|
||||||
- hesabdariCheckPassFail.html
|
|
||||||
- Reference guide
|
|
||||||
- normalize_mobile
|
|
||||||
- graphify
|
|
||||||
- hesabdariResidNashode.html
|
|
||||||
- 0017_factoriteam_garantyesh.py
|
|
||||||
- scanner.py
|
|
||||||
- 0002_auto_20220114_1616.py
|
|
||||||
- SMSResult
|
|
||||||
- Rip'n Fast. Fewer Tokens. Better Code.
|
|
||||||
- models/sms.py
|
|
||||||
- admin_panel/templates/admin_panel/baseStore.html
|
|
||||||
- views/finance.py
|
|
||||||
- Customer Modals UI
|
|
||||||
- views/factors.py
|
|
||||||
- 0034_alter_article_sarresid.py
|
|
||||||
- views/sms.py
|
- views/sms.py
|
||||||
- 0035_alter_article_sarresid.py
|
- run_all
|
||||||
- django_db_models
|
- django_db
|
||||||
- 0010_auto_20220122_1546.py
|
- views/store.py
|
||||||
- 0040_customer_laghv_bazdid.py
|
- ModelsPackageContractTests
|
||||||
- test_sms.py
|
- PhoneCleanupTests
|
||||||
- 0048_alter_productinstore_options.py
|
- .load
|
||||||
- 0032_article_paystatus.py
|
- models/sms.py
|
||||||
- 0033_article_nahvepardakht.py
|
- logging
|
||||||
|
- views/customers.py
|
||||||
|
- django_db_models_deletion
|
||||||
- make_customer
|
- make_customer
|
||||||
- sms0098.py
|
- customer_phone_entries
|
||||||
- 0044_sarresid_desc.py
|
- EventTriggerTests
|
||||||
- models/__init__.py
|
- views/finance.py
|
||||||
|
- SMSResult
|
||||||
|
- sms0098ws.py
|
||||||
|
- Admin Panel Customization
|
||||||
|
- User
|
||||||
|
- django_conf
|
||||||
|
- test_customers.py
|
||||||
|
- phones.py
|
||||||
|
- Login Template Forms
|
||||||
|
- cheque_inquiry.py
|
||||||
|
- factories.py
|
||||||
|
- Rip'n Fast. Fewer Tokens. Better Code.
|
||||||
|
- views/factors.py
|
||||||
|
- to_local
|
||||||
- make_user
|
- make_user
|
||||||
- GEMINI.md
|
- QueueReviewTests
|
||||||
- Customer Action Include
|
- NoticeMechanismTests
|
||||||
- New Customer Base Template
|
- enqueue.py
|
||||||
- Customer Feedback Partial
|
- enqueue_sms
|
||||||
- Customer Factor Detail
|
- PanelTestCase
|
||||||
- Customer Accounting Template
|
- ProductInStore
|
||||||
- Customer Factor List
|
- admin_panel/templates/admin_panel/baseHesabdari.html
|
||||||
- Customer Feedback Page
|
- Reference guide
|
||||||
- Action List Template
|
- Factor Creation UI
|
||||||
- base.html
|
- admin_panel/templates/admin_panel/bedehi.html
|
||||||
- Customer New Factor
|
- project_tree.py
|
||||||
- Customer New Factor V2
|
- CustomerSmsHistoryTests
|
||||||
- Customer Satisfaction List
|
- TemplatesPageUITests
|
||||||
- Customer Satisfaction Partial
|
- SMS Integration Docs
|
||||||
- Customer Check/Debt Management
|
- timing_for
|
||||||
- admin_panel/templates/admin_panel/customerBaseNew.html
|
- django_shortcuts
|
||||||
- Customer List
|
- stock_in
|
||||||
|
- admin_panel/templates/admin_panel/customerAction.html
|
||||||
|
- parts_count
|
||||||
|
- write_tree
|
||||||
|
- CustomerInfoCardUITests
|
||||||
|
- Customer
|
||||||
|
- Phone Cleanup Guide
|
||||||
|
- SmsReportTests
|
||||||
|
- manage.py
|
||||||
|
- Agent Operating Guidelines
|
||||||
|
- SMSDeliveryCommandTests
|
||||||
|
- BehinSMSProvider
|
||||||
|
- test_sms.py
|
||||||
|
- Financial Data Objects
|
||||||
|
- Hybrid Migration Command
|
||||||
|
- بازمحاسبهٔ «آخرین بازدید» مشتریان
|
||||||
|
- Resource Cleanup Handler
|
||||||
|
- SMS Timing Migration
|
||||||
|
- Articl Model
|
||||||
|
- customerIDFactorBackup.html
|
||||||
|
- test_factors.py
|
||||||
|
- os
|
||||||
|
- Graphify Tooling Docs
|
||||||
|
- 0063_alter_smsqueue_status.py
|
||||||
|
- SMS Template Seeding
|
||||||
|
- admin_panel/entezar.html
|
||||||
|
- customerNewBackup.html
|
||||||
|
- hesabdariResidNashode.html
|
||||||
|
- 0070_sms_types_seven.py
|
||||||
|
- Endpoint Smoke Tests
|
||||||
|
- Timezone Migration History
|
||||||
|
- models/__init__.py
|
||||||
|
- UrlContractTests
|
||||||
|
- Brand Scrubbing Migration
|
||||||
|
- SMS Dedup Migration
|
||||||
|
- Installation Guide
|
||||||
|
- Project Honesty Contract
|
||||||
|
- Flow Control Migration
|
||||||
|
- Permission Label Migration
|
||||||
|
- stop.sh
|
||||||
|
- Output Format Specs
|
||||||
|
- Performance Benchmarks
|
||||||
|
- SMS Integration Overview
|
||||||
|
- System Cache Table
|
||||||
|
- Customer Modal UI
|
||||||
|
- SentQueueProtectionTests
|
||||||
|
- Infrastructure Services
|
||||||
|
- Agent Setup Guide
|
||||||
|
- First Use Guide
|
||||||
|
- 0056_auto_20260731_1824.py
|
||||||
|
- 0009_remove_storeroom_user.py
|
||||||
|
- PhoneEntry
|
||||||
|
- reconciliation_tool.py
|
||||||
|
- announcements.py
|
||||||
|
- Command
|
||||||
|
- 0064_sms_webservice_delivery.py
|
||||||
|
- 0040_customer_laghv_bazdid.py
|
||||||
|
- 0068_auto_20261010_1305.py
|
||||||
|
- 0069_smstemplate_auto_send.py
|
||||||
|
- 0047_alter_productinstore_movement_type.py
|
||||||
|
- Command
|
||||||
|
- Customer Communication Partials
|
||||||
|
- SMS Template Management
|
||||||
|
- Documentation and Graphing
|
||||||
|
- bale/providers.py
|
||||||
|
- Customer Action Templates
|
||||||
|
- Customer Feedback Components
|
||||||
|
- Customer Invoice Details
|
||||||
|
- Accounting Document Templates
|
||||||
|
- Customer Invoice List
|
||||||
|
- Customer Feedback Interface
|
||||||
|
- Action List Interface
|
||||||
|
- Base HTML Layout
|
||||||
|
- New Invoice Creation
|
||||||
|
- Customer Satisfaction Records
|
||||||
|
- Satisfaction UI Components
|
||||||
|
- Debt and Credit Management
|
||||||
|
- Customer Base Layout
|
||||||
- Financial Dashboard
|
- Financial Dashboard
|
||||||
- Accounting Main View
|
- Accounting Main View
|
||||||
- New Inventory Operation
|
- Inventory Entry Form
|
||||||
- Inventory Operations List
|
- Inventory Operations History
|
||||||
- Operations Backup Template
|
- Operations Backup View
|
||||||
- Financial Report Template
|
- Financial Reporting
|
||||||
- Debt Collection Dashboard
|
- Debt Collection Interface
|
||||||
- Inventory Movement Report
|
- Inventory Movement Analytics
|
||||||
- Customer Satisfaction Dashboard
|
- Satisfaction Analytics Dashboard
|
||||||
- Check and Credit Report Template
|
- Credit Status Reports
|
||||||
- Satisfaction List Template
|
- Satisfaction List View
|
||||||
- Store Inventory Template
|
- SMS Queue Management
|
||||||
- Store Transfers Template
|
- Store Inventory View
|
||||||
- Stores List Template
|
- Store Transfer Interface
|
||||||
- toCurrency
|
- Store Directory Template
|
||||||
- Extended Requirements File
|
- Currency Formatting Utility
|
||||||
- Requirements File
|
- Data Cleanup Documentation
|
||||||
- django_shortcuts
|
- Project Dependencies
|
||||||
- resolve_recipients
|
- QueueStatsTests
|
||||||
- factories.py
|
- sms_settings
|
||||||
- write_tree
|
|
||||||
- TemplatesPageUITests
|
|
||||||
- reconciliation_tool.py
|
|
||||||
- ModelsPackageContractTests
|
|
||||||
- django_conf
|
|
||||||
- Command
|
- Command
|
||||||
- UrlContractTests
|
- 0071_sms_settings_permission.py
|
||||||
- customerIDFactorBackup.html
|
- index
|
||||||
- system.py
|
- 0017_factoriteam_garantyesh.py
|
||||||
- BehinSMSProvider
|
|
||||||
- test_customers.py
|
|
||||||
- Command
|
|
||||||
- Command
|
|
||||||
- 0061_sms_timing_and_primary.py
|
|
||||||
- bale/providers.py
|
|
||||||
- Command
|
|
||||||
- 0058_seed_sms_templates.py
|
|
||||||
- Customer
|
|
||||||
- customerNewBackup.html
|
|
||||||
- 0059_sms_flow_control.py
|
|
||||||
- 0060_sms_permission_label.py
|
|
||||||
- 0008_storeroom_user.py
|
|
||||||
- 0013_alter_factor_options.py
|
|
||||||
- 0018_alter_factoriteam_product.py
|
|
||||||
- 0020_auto_20220126_1532.py
|
|
||||||
- 0022_article_typepy.py
|
|
||||||
- 0025_action_actiontype.py
|
|
||||||
- 0028_action_grade.py
|
- 0028_action_grade.py
|
||||||
- 0030_auto_20220414_1047.py
|
- 0034_alter_article_sarresid.py
|
||||||
- 0036_sarresid.py
|
|
||||||
- 0037_sarresid_sarresiddt.py
|
|
||||||
- 0039_customer_last_bazdid_dt.py
|
- 0039_customer_last_bazdid_dt.py
|
||||||
- 0041_auto_20240216_1607.py
|
|
||||||
- 0047_alter_productinstore_movement_type.py
|
|
||||||
- 0049_alter_productinstore_options.py
|
|
||||||
- 0057_sms.py
|
|
||||||
|
|
||||||
## God Nodes (most connected - your core abstractions)
|
## God Nodes (most connected - your core abstractions)
|
||||||
1. `make_customer()` - 37 edges
|
1. `make_customer()` - 70 edges
|
||||||
2. `make_user()` - 24 edges
|
2. `make_user()` - 28 edges
|
||||||
3. `Rip'n Fast. Fewer Tokens. Better Code.` - 23 edges
|
3. `run_all()` - 24 edges
|
||||||
4. `User` - 22 edges
|
4. `enqueue_sms()` - 23 edges
|
||||||
5. `create_transfer_request()` - 22 edges
|
5. `SMSResult` - 23 edges
|
||||||
6. `enqueue_sms()` - 21 edges
|
6. `Rip'n Fast. Fewer Tokens. Better Code.` - 23 edges
|
||||||
7. `SMSResult` - 20 edges
|
7. `User` - 22 edges
|
||||||
8. `normalize_mobile()` - 20 edges
|
8. `create_transfer_request()` - 22 edges
|
||||||
9. `SMSProvider` - 19 edges
|
9. `normalize_mobile()` - 22 edges
|
||||||
10. `Reference guide` - 18 edges
|
10. `SMSProvider` - 21 edges
|
||||||
|
|
||||||
## Surprising Connections (you probably didn't know these)
|
## Surprising Connections (you probably didn't know these)
|
||||||
|
- `نکتههای مهم` --references--> `Action` [INFERRED]
|
||||||
|
recalc_last_bazdid.md → admin_panel/models/actions.py
|
||||||
|
- `SMS Templates UI` --semantically_similar_to--> `SMS Templates UI Screenshot` [EXTRACTED] [semantically similar]
|
||||||
|
admin_panel/templates/admin_panel/sms_templates.html → screencapture-localhost-8000-panel-sms-templates-2026-10-09-23_01_18.png
|
||||||
- `check_financial_discrepancy()` --uses--> `Customer` [INFERRED]
|
- `check_financial_discrepancy()` --uses--> `Customer` [INFERRED]
|
||||||
check_finance.py → admin/models.py
|
check_finance.py → admin/models.py
|
||||||
- `run_360_audit()` --uses--> `Customer` [INFERRED]
|
- `run_360_audit()` --uses--> `Customer` [INFERRED]
|
||||||
ghost_hunter.py → admin/models.py
|
ghost_hunter.py → admin/models.py
|
||||||
- ``nest=` is a max, so it cannot tell a long function from a tangled one` --references--> `index()` [INFERRED]
|
- ``nest=` is a max, so it cannot tell a long function from a tangled one` --references--> `index()` [INFERRED]
|
||||||
ripwire - README.md → admin/views.py
|
ripwire - README.md → admin/views.py
|
||||||
- `VisitQueue` --uses--> `User` [INFERRED]
|
|
||||||
admin_panel/models/actions.py → admin/models.py
|
|
||||||
- `StoreTransferRequest` --uses--> `User` [INFERRED]
|
|
||||||
admin_panel/models/store.py → admin/models.py
|
|
||||||
|
|
||||||
## Import Cycles
|
## Import Cycles
|
||||||
- None detected.
|
- None detected.
|
||||||
|
|
||||||
## Hyperedges (group relationships)
|
## Hyperedges (group relationships)
|
||||||
- **Customer Satisfaction & Collection** — admin_panel_templates_admin_panel_report_rezayat, admin_panel_templates_admin_panel_report_debtors_no_vosol, admin_panel_customer_customernazarinc [EXTRACTED 0.90]
|
- **Customer Satisfaction & Collection** — admin_panel_templates_admin_panel_report_rezayat, admin_panel_templates_admin_panel_report_debtors_no_vosol, admin_panel_customer_customernazarinc [EXTRACTED 0.90]
|
||||||
- **Customer Management Flow** — admin_panel_customer_base_new, admin_panel_customer_action_inc, admin_panel_customer_id_factor, admin_panel_customer_hesabdari [EXTRACTED 1.00]
|
- **Customer Management Flow** — admin_panel_customer_action_inc, admin_panel_customer_id_factor, admin_panel_customer_hesabdari [EXTRACTED 1.00]
|
||||||
- **Financial & Debt Management** — admin_panel_templates_admin_panel_financial_dashboard, admin_panel_templates_admin_panel_hesabdari [EXTRACTED 1.00]
|
- **Financial & Debt Management** — admin_panel_templates_admin_panel_financial_dashboard, admin_panel_templates_admin_panel_hesabdari [EXTRACTED 1.00]
|
||||||
- **Graphify Output Artifacts** — graphify-out_graph_json, graphify-out_wiki_index_md, graphify-out_graph_report_md [EXTRACTED 1.00]
|
- **Graphify Output Artifacts** — graphify-out_graph_json, graphify-out_wiki_index_md, graphify-out_graph_report_md [EXTRACTED 1.00]
|
||||||
- **Graphify Subcommands** — graphify_query, graphify_path, graphify_explain [EXTRACTED 1.00]
|
- **Graphify Subcommands** — graphify_query, graphify_path, graphify_explain [EXTRACTED 1.00]
|
||||||
- **Inventory Management System** — admin_panel_templates_admin_panel_store, admin_panel_templates_admin_panel_store_transfers, admin_panel_templates_admin_panel_report_inventory_movements, admin_panel_templates_admin_panel_operationsbk [EXTRACTED 1.00]
|
- **Inventory Management System** — admin_panel_templates_admin_panel_store, admin_panel_templates_admin_panel_store_transfers, admin_panel_templates_admin_panel_report_inventory_movements, admin_panel_templates_admin_panel_operationsbk [EXTRACTED 1.00]
|
||||||
|
- **SMS Management System** — admin_panel_templates_admin_panel_sms_settings_html, admin_panel_templates_admin_panel_sms_templates_html, admin_panel_templates_admin_panel_sms_queue_html, sms_worker, sms_delivery [EXTRACTED 1.00]
|
||||||
- **Bedehi Report Navigation** — django_urls_admin_panel_bedehi, django_urls_admin_panel_bedehikhadamat, django_urls_admin_panel_bedehinasb [EXTRACTED]
|
- **Bedehi Report Navigation** — django_urls_admin_panel_bedehi, django_urls_admin_panel_bedehikhadamat, django_urls_admin_panel_bedehinasb [EXTRACTED]
|
||||||
- **Login Form** — admin_panel_templates_admin_panel_login, django_url_admin_panel_p_login, django_csrf_token, form_field_username, form_field_password [EXTRACTED]
|
- **Login Form** — admin_panel_templates_admin_panel_login, django_url_admin_panel_p_login, django_csrf_token, form_field_username, form_field_password [EXTRACTED]
|
||||||
- **New Store Form** — admin_panel_templates_admin_panel_storebk1, django_url_admin_panel_newstore, django_csrf_token, form_field_storename [EXTRACTED]
|
- **New Store Form** — admin_panel_templates_admin_panel_storebk1, django_url_admin_panel_newstore, django_csrf_token, form_field_storename [EXTRACTED]
|
||||||
- **Store Inventory Display** — admin_panel_templates_admin_panel_storebk1, template_variable_productsum, product_property_product_name, product_property_mojodi [EXTRACTED]
|
- **Store Inventory Display** — admin_panel_templates_admin_panel_storebk1, template_variable_productsum, product_property_product_name, product_property_mojodi [EXTRACTED]
|
||||||
- **Customer List Page** — admin_panel_templates_admin_panel_entezar_html, concept_customer_data, concept_url_admin_panel_customers [INFERRED 0.80]
|
- **Customer List Page** — admin_panel_templates_admin_panel_entezar_html, concept_customer_data, concept_url_admin_panel_customers [INFERRED 0.80]
|
||||||
- **Customer Lifecycle Management** — admin_panel_templates_admin_panel_customers [INFERRED 0.85]
|
- **Customer Lifecycle Management** — admin_panel_templates_admin_panel_customers [INFERRED 0.85]
|
||||||
- **Customer Interaction Lifecycle** — admin_panel_templates_admin_panel_customers, admin_panel_templates_admin_panel_customerNew, admin_panel_templates_admin_panel_customerRezayat [INFERRED 0.85]
|
|
||||||
- **New Factor Creation Flow** — admin_panel_templates_admin_panel_customernewbackup_html, admin_panel_urls_newfactor, concept_client_side_form_logic, concept_customer, concept_category [INFERRED 0.85]
|
- **New Factor Creation Flow** — admin_panel_templates_admin_panel_customernewbackup_html, admin_panel_urls_newfactor, concept_client_side_form_logic, concept_customer, concept_category [INFERRED 0.85]
|
||||||
- **Customer Management UI Flow** — admin_panel_templates_admin_panel_customersbk_html, admin_panel_templates_admin_panel_customersbk_html_createcs, admin_panel_templates_admin_panel_customersbk_html_openrow, concept_customer_modal_ui [INFERRED 0.90]
|
- **Customer Management UI Flow** — admin_panel_templates_admin_panel_customersbk_html, admin_panel_templates_admin_panel_customersbk_html_createcs, admin_panel_templates_admin_panel_customersbk_html_openrow, concept_customer_modal_ui [INFERRED 0.90]
|
||||||
- **Financial & Debt Management Flow** — admin_panel_templates_admin_panel_customerSarresid, admin_panel_templates_admin_panel_hesabdari, admin_panel_templates_admin_panel_financial_dashboard [INFERRED 0.90]
|
- **Financial & Debt Management Flow** — admin_panel_templates_admin_panel_customerSarresid, admin_panel_templates_admin_panel_hesabdari, admin_panel_templates_admin_panel_financial_dashboard [INFERRED 0.90]
|
||||||
@@ -192,223 +222,331 @@
|
|||||||
- **New Factor Creation Flow** — admin_panel_templates_admin_panel_customeridbackup_html, django_url_admin_panel_productsjson, api_endpoint_panel_productmojodi, admin_panel_templates_admin_panel_customeridbackup_html_orderlist, admin_panel_templates_admin_panel_customeridbackup_html_buildorderlist, django_url_admin_panel_newfactor [INFERRED]
|
- **New Factor Creation Flow** — admin_panel_templates_admin_panel_customeridbackup_html, django_url_admin_panel_productsjson, api_endpoint_panel_productmojodi, admin_panel_templates_admin_panel_customeridbackup_html_orderlist, admin_panel_templates_admin_panel_customeridbackup_html_buildorderlist, django_url_admin_panel_newfactor [INFERRED]
|
||||||
- **Store and Inventory Management Flow** — admin_panel_templates_admin_panel_basestore, concept_url_admin_panel_stores, concept_url_admin_panel_productadd, concept_url_admin_panel_operations, concept_url_admin_panel_store_transfers [INFERRED]
|
- **Store and Inventory Management Flow** — admin_panel_templates_admin_panel_basestore, concept_url_admin_panel_stores, concept_url_admin_panel_productadd, concept_url_admin_panel_operations, concept_url_admin_panel_store_transfers [INFERRED]
|
||||||
|
|
||||||
## Communities (140 total, 93 thin omitted)
|
## Communities (171 total, 97 thin omitted)
|
||||||
|
|
||||||
### Community 0 - "views/store.py"
|
### Community 0 - "views/sms.py"
|
||||||
Cohesion: 0.07
|
Cohesion: 0.14
|
||||||
Nodes (26): StoreTransferRequest, Meta, Status, approve_transfer_request(), cancel_transfer_request(), _check_source_store_permission(), _check_target_store_permission(), create_transfer_request() (+18 more)
|
Nodes (14): _approve(), customer_sms_history(), _customer_url(), _notice(), sms_customer_toggle(), sms_primary_toggle(), sms_queue(), sms_queue_approve() (+6 more)
|
||||||
|
|
||||||
### Community 1 - "project_tree.py"
|
### Community 1 - "run_all"
|
||||||
Cohesion: 0.21
|
|
||||||
Nodes (6): is_excluded_file(), main(), non_negative_integer(), parse_args(), sorted_entries(), warn()
|
|
||||||
|
|
||||||
### Community 3 - "django_db"
|
|
||||||
Cohesion: 0.07
|
|
||||||
Nodes (14): Migration, Migration, Migration, Migration, Migration, Migration, Migration, Migration (+6 more)
|
|
||||||
|
|
||||||
### Community 4 - "Customer ID Backup/New Factor Template"
|
|
||||||
Cohesion: 0.15
|
|
||||||
Nodes (4): Customer ID Backup/New Factor Template, all_products (JS variable), buildOrderList (JS function), orderList (JS variable)
|
|
||||||
|
|
||||||
### Community 5 - "views/customers.py"
|
|
||||||
Cohesion: 0.13
|
|
||||||
Nodes (16): customer_toggle_map(), customer(), customerDel(), customerEdit(), customerFactor(), customerFactors(), customerHesabdari(), customerHesabdariAdd() (+8 more)
|
|
||||||
|
|
||||||
### Community 6 - "cheque_inquiry.py"
|
|
||||||
Cohesion: 0.17
|
|
||||||
Nodes (6): api_check_sabt_inquiry(), api_check_status_inquiry(), parse_cheque_result_html(), run_bpi_cheque_inquiry_logic(), run_cbi_sayad_inquiry_logic(), solve_base64_captcha()
|
|
||||||
|
|
||||||
### Community 7 - "admin_panel/templates/admin_panel/baseHesabdari.html"
|
|
||||||
Cohesion: 0.13
|
|
||||||
Nodes (14): URL: admin_panel:ActionListUserToday, URL: admin_panel:bedehi, URL: admin_panel:entezar, URL: admin_panel:hesabdari, URL: admin_panel:hesabdari_residnashone, URL: admin_panel:index, URL: admin_panel:p_logout, URL: admin_panel:rezayat (+6 more)
|
|
||||||
|
|
||||||
### Community 8 - "django_db_models_deletion"
|
|
||||||
Cohesion: 0.06
|
Cohesion: 0.06
|
||||||
Nodes (15): Migration, Migration, Migration, Migration, Migration, Migration, Migration, Migration (+7 more)
|
Nodes (9): Command, run_all(), apply_timing(), activate_sms(), AutoSendTests, ColdStartTests, ConsoleEncodingTests, ScannerTests (+1 more)
|
||||||
|
|
||||||
### Community 10 - "admin_panel/storeBK1.html"
|
### Community 2 - "django_db"
|
||||||
|
Cohesion: 0.04
|
||||||
|
Nodes (24): Migration, Migration, Migration, Migration, Migration, Migration, Migration, Migration (+16 more)
|
||||||
|
|
||||||
|
### Community 3 - "views/store.py"
|
||||||
|
Cohesion: 0.07
|
||||||
|
Nodes (26): Product, Meta, StoreTransferRequest, Meta, Status, approve_transfer_request(), cancel_transfer_request(), _check_source_store_permission() (+18 more)
|
||||||
|
|
||||||
|
### Community 4 - "ModelsPackageContractTests"
|
||||||
|
Cohesion: 0.07
|
||||||
|
Nodes (5): AdminConfig, AdminPanelConfig, make_std_streams_unicode_safe(), configure_sqlite_connection(), ModelsPackageContractTests
|
||||||
|
|
||||||
|
### Community 5 - "PhoneCleanupTests"
|
||||||
|
Cohesion: 0.09
|
||||||
|
Nodes (9): Command, Migration, scrub_legacy_phones(), _append_note(), clean_all(), clean_customer(), clean_legacy_cell(), _numeric_tokens() (+1 more)
|
||||||
|
|
||||||
|
### Community 6 - ".load"
|
||||||
|
Cohesion: 0.06
|
||||||
|
Nodes (9): 12. Supported languages and formats, Languages, HttpProviderRobustnessTests, RuleTests, SettingsPanelTests, TestMessageSettingsTests, WorkerTests, ZeroZeroNinetyEightLinkTests (+1 more)
|
||||||
|
|
||||||
|
### Community 7 - "models/sms.py"
|
||||||
|
Cohesion: 0.06
|
||||||
|
Nodes (15): CustomerSMSSetting, Meta, SMSDedup, Meta, SMSLog, Meta, SMSProviderConfig, Meta (+7 more)
|
||||||
|
|
||||||
|
### Community 8 - "logging"
|
||||||
|
Cohesion: 0.09
|
||||||
|
Nodes (3): get_provider_class(), get_sms_provider(), reset_sms_provider()
|
||||||
|
|
||||||
|
### Community 9 - "views/customers.py"
|
||||||
|
Cohesion: 0.09
|
||||||
|
Nodes (18): normalize_primary_values(), customer_toggle_map(), notify_welcome(), customer(), customerDel(), customerEdit(), customerFactor(), customerFactors() (+10 more)
|
||||||
|
|
||||||
|
### Community 10 - "django_db_models_deletion"
|
||||||
|
Cohesion: 0.08
|
||||||
|
Nodes (12): Migration, Migration, Migration, Migration, Migration, Migration, Migration, Migration (+4 more)
|
||||||
|
|
||||||
|
### Community 11 - "make_customer"
|
||||||
|
Cohesion: 0.14
|
||||||
|
Nodes (3): make_customer(), EmptyQueueTests, PrimaryNumberPanelTests
|
||||||
|
|
||||||
|
### Community 12 - "customer_phone_entries"
|
||||||
|
Cohesion: 0.13
|
||||||
|
Nodes (5): customer_phone_entries(), _iter_sources(), resolve_recipients(), mark_primary(), RecipientTests
|
||||||
|
|
||||||
|
### Community 14 - "views/finance.py"
|
||||||
|
Cohesion: 0.18
|
||||||
|
Nodes (19): Article, Meta, notify_payment_thanks(), addSarresid(), bedehi(), bedehiKhadamat(), bedehiNasb(), financial_dashboard() (+11 more)
|
||||||
|
|
||||||
|
### Community 15 - "SMSResult"
|
||||||
|
Cohesion: 0.13
|
||||||
|
Nodes (7): SMSProvider, SMSResult, webotp_message(), console_write(), ConsoleSMSProvider, HTTPSMSProvider, _FailingProvider
|
||||||
|
|
||||||
|
### Community 16 - "sms0098ws.py"
|
||||||
|
Cohesion: 0.10
|
||||||
|
Nodes (7): classify_delivery(), DeliveryCode, describe_send_code(), _as_text(), ZeroZeroNinetyEightWebServiceProvider, place_voip_call(), VoipCallResult
|
||||||
|
|
||||||
|
### Community 17 - "Admin Panel Customization"
|
||||||
|
Cohesion: 0.12
|
||||||
|
Nodes (10): CustomerSMSSettingAdmin, SMSLogAdmin, SMSProviderConfigAdmin, SMSProviderConfigForm, Meta, SMSQueueAdmin, SMSTemplateAdmin, StoreRoomAdmin (+2 more)
|
||||||
|
|
||||||
|
### Community 18 - "User"
|
||||||
|
Cohesion: 0.16
|
||||||
|
Nodes (17): User, Action, VisitQueue, UserProfile, notify_visit_scheduled(), ActionList(), ActionListAll(), ActionListToday() (+9 more)
|
||||||
|
|
||||||
|
### Community 19 - "django_conf"
|
||||||
|
Cohesion: 0.09
|
||||||
|
Nodes (11): Migration, Migration, Migration, Migration, Migration, Migration, Migration, Migration (+3 more)
|
||||||
|
|
||||||
|
### Community 20 - "test_customers.py"
|
||||||
|
Cohesion: 0.11
|
||||||
|
Nodes (4): login_superuser(), CustomerCrudTests, CustomerDeleteGuardTests, InactiveCustomerHiddenTests
|
||||||
|
|
||||||
|
### Community 21 - "phones.py"
|
||||||
|
Cohesion: 0.18
|
||||||
|
Nodes (6): primary_phones(), resolved_recipients(), save_primary_normalized(), stored_primary_normalized(), usable_entries(), primary_phones()
|
||||||
|
|
||||||
|
### Community 22 - "Login Template Forms"
|
||||||
Cohesion: 0.11
|
Cohesion: 0.11
|
||||||
Nodes (17): Django CSRF Token, Django Custom Tags, Django Static Files, Django URL: admin_panel:newStore, Django URL: admin_panel:operations, Django URL: admin_panel:p_login, Django URL: admin_panel:store, Form Field: password (+9 more)
|
Nodes (17): Django CSRF Token, Django Custom Tags, Django Static Files, Django URL: admin_panel:newStore, Django URL: admin_panel:operations, Django URL: admin_panel:p_login, Django URL: admin_panel:store, Form Field: password (+9 more)
|
||||||
|
|
||||||
### Community 12 - "admin_panel/templates/admin_panel/customerAction.html"
|
### Community 23 - "cheque_inquiry.py"
|
||||||
Cohesion: 0.13
|
Cohesion: 0.19
|
||||||
Nodes (13): admin_panel/templates/admin_panel/customerActionInc.html, admin_panel:customerNew, admin_panel:customerNewAction URL, Action Data Model, Articl Model, Customer Model, DataTables Library, jQuery (+5 more)
|
Nodes (6): api_check_sabt_inquiry(), api_check_status_inquiry(), parse_cheque_result_html(), run_bpi_cheque_inquiry_logic(), run_cbi_sayad_inquiry_logic(), solve_base64_captcha()
|
||||||
|
|
||||||
### Community 13 - "User"
|
### Community 24 - "factories.py"
|
||||||
Cohesion: 0.07
|
|
||||||
Nodes (30): User, Action, VisitQueue, Factor, Meta, FactorIteam, Product, Meta (+22 more)
|
|
||||||
|
|
||||||
### Community 14 - "run_all"
|
|
||||||
Cohesion: 0.05
|
|
||||||
Nodes (9): run_all(), activate_sms(), ColdStartTests, ConsoleEncodingTests, HttpProviderRobustnessTests, NormalizeTests, ScannerTests, WorkerTests (+1 more)
|
|
||||||
|
|
||||||
### Community 15 - "admin_panel/admin.py"
|
|
||||||
Cohesion: 0.09
|
|
||||||
Nodes (10): CustomerSMSSettingAdmin, SMSLogAdmin, SMSProviderConfigAdmin, SMSProviderConfigForm, Meta, SMSQueueAdmin, SMSTemplateAdmin, StoreRoomAdmin (+2 more)
|
|
||||||
|
|
||||||
### Community 16 - "stock_in"
|
|
||||||
Cohesion: 0.11
|
|
||||||
Nodes (11): Customer, Factor, FactorIteam, Product, ProductInStore, StoreRoom, VisitQueue, Command (+3 more)
|
|
||||||
|
|
||||||
### Community 17 - "admin_panel/templates/admin_panel/bedehi.html"
|
|
||||||
Cohesion: 0.12
|
|
||||||
Nodes (11): admin_panel:pardakhtNesiye, /panel/passCheck/, all_sarresid, URL: admin_panel:newFactor, mandehesabKP, admin_panel.ViewReportMali, toJalali, admin_panel:bedehi (+3 more)
|
|
||||||
|
|
||||||
### Community 18 - "hesabdariCheckPassFail.html"
|
|
||||||
Cohesion: 0.20
|
|
||||||
Nodes (9): Customer Data Object, Factor Data Object, Sarresid Data Object, admin_panel:customerNew URL, admin_panel:pardakhtNesiye URL, /panel/passCheck/ URL, admin_panel.ViewReportMali Permission, jQuery JS Library (+1 more)
|
|
||||||
|
|
||||||
### Community 19 - "Reference guide"
|
|
||||||
Cohesion: 0.07
|
|
||||||
Nodes (27): 10. Quality evaluation, 11. Agent integration, 12. Supported languages and formats, 13. Performance, 14. Known limits, 15. Documentation, 16. Improvement, 17. License (+19 more)
|
|
||||||
|
|
||||||
### Community 20 - "normalize_mobile"
|
|
||||||
Cohesion: 0.09
|
|
||||||
Nodes (12): customer_phone_entries(), _iter_sources(), normalize_primary_values(), PhoneEntry, primary_phones(), resolved_recipients(), save_primary_normalized(), stored_primary_normalized() (+4 more)
|
|
||||||
|
|
||||||
### Community 21 - "graphify"
|
|
||||||
Cohesion: 0.22
|
|
||||||
Nodes (8): Copilot Chat, graphify, graphify-out/graph.json, graphify-out/GRAPH_REPORT.md, graphify-out/wiki/index.md, graphify explain, graphify path, graphify query
|
|
||||||
|
|
||||||
### Community 22 - "hesabdariResidNashode.html"
|
|
||||||
Cohesion: 0.47
|
|
||||||
Nodes (5): jQuery, pDatepicker JS Library, custom_tags, toCurrency, toJalali
|
|
||||||
|
|
||||||
### Community 24 - "scanner.py"
|
|
||||||
Cohesion: 0.13
|
|
||||||
Nodes (12): enqueue_sms(), _aware(), _Budget, scan_cheque_due(), scan_cheque_registration(), scan_debt_due(), scan_nesiye_due(), scan_survey() (+4 more)
|
|
||||||
|
|
||||||
### Community 25 - "0002_auto_20220114_1616.py"
|
|
||||||
Cohesion: 0.29
|
|
||||||
Nodes (3): Migration, Migration, Migration
|
|
||||||
|
|
||||||
### Community 26 - "SMSResult"
|
|
||||||
Cohesion: 0.13
|
|
||||||
Nodes (8): SMSDeliveryResult, SMSProvider, SMSResult, webotp_message(), console_write(), ConsoleSMSProvider, HTTPSMSProvider, _FailingProvider
|
|
||||||
|
|
||||||
### Community 27 - "Rip'n Fast. Fewer Tokens. Better Code."
|
|
||||||
Cohesion: 0.05
|
|
||||||
Nodes (42): index(), 1. Codebase Navigation & Analysis (Ripwire vs. Graphify), 2. Dynamic Help, Self-Correction & Staleness, 3. Git Operations & Safety, 4. Development Server Lifecycle, 5. Environment, Frontend & PWA Safety, 6. End-to-End Browser Tests (Playwright), Project Operating & Agent Guidelines (+34 more)
|
|
||||||
|
|
||||||
### Community 28 - "models/sms.py"
|
|
||||||
Cohesion: 0.08
|
|
||||||
Nodes (12): CustomerSMSSetting, Meta, SMSLog, Meta, SMSProviderConfig, Meta, SMSQueue, Meta (+4 more)
|
|
||||||
|
|
||||||
### Community 29 - "admin_panel/templates/admin_panel/baseStore.html"
|
|
||||||
Cohesion: 0.13
|
|
||||||
Nodes (12): admin_panel/base.html, Customer Data Structure, Django Custom Tags: custom_tags, kamaDatepicker Library, URL: admin_panel:customers, URL: admin_panel:operations, URL: admin_panel:productAdd, URL: admin_panel:store (+4 more)
|
|
||||||
|
|
||||||
### Community 30 - "views/finance.py"
|
|
||||||
Cohesion: 0.20
|
|
||||||
Nodes (18): Article, Meta, addSarresid(), bedehi(), bedehiKhadamat(), bedehiNasb(), financial_dashboard(), hesabdari() (+10 more)
|
|
||||||
|
|
||||||
### Community 31 - "Customer Modals UI"
|
|
||||||
Cohesion: 0.67
|
|
||||||
Nodes (3): createCS click handler, openRow click handler, Customer Modals UI
|
|
||||||
|
|
||||||
### Community 32 - "views/factors.py"
|
|
||||||
Cohesion: 0.16
|
|
||||||
Nodes (6): FactorDel(), newFactor2(), InventoryException, _parse_jalali_date(), _parse_timestamp(), _safe_int()
|
|
||||||
|
|
||||||
### Community 34 - "views/sms.py"
|
|
||||||
Cohesion: 0.17
|
|
||||||
Nodes (12): parse_scan_since(), resolve_recipients_test(), _sample_context(), sms_customer_toggle(), sms_primary_toggle(), sms_queue(), sms_queue_retry(), sms_send_custom() (+4 more)
|
|
||||||
|
|
||||||
### Community 36 - "django_db_models"
|
|
||||||
Cohesion: 0.15
|
|
||||||
Nodes (3): get_provider_class(), get_sms_provider(), reset_sms_provider()
|
|
||||||
|
|
||||||
### Community 39 - "test_sms.py"
|
|
||||||
Cohesion: 0.14
|
|
||||||
Nodes (4): apply_timing(), current_timing_value(), timing_for(), TimingsTests
|
|
||||||
|
|
||||||
### Community 43 - "make_customer"
|
|
||||||
Cohesion: 0.15
|
|
||||||
Nodes (6): PhoneNumber, make_customer(), PanelTestCase, PrimaryNumberPanelTests, RuleTests, WelcomeTriggerTests
|
|
||||||
|
|
||||||
### Community 44 - "sms0098.py"
|
|
||||||
Cohesion: 0.15
|
|
||||||
Nodes (5): _clean(), to_local(), _configured_codes(), _response_code(), ZeroZeroNinetyEightSMSProvider
|
|
||||||
|
|
||||||
### Community 46 - "models/__init__.py"
|
|
||||||
Cohesion: 0.15
|
|
||||||
Nodes (5): format_amount(), format_jalali(), render_template(), _SafeDict, is_sms_enabled()
|
|
||||||
|
|
||||||
### Community 93 - "django_shortcuts"
|
|
||||||
Cohesion: 0.13
|
|
||||||
Nodes (3): Migration, ArticleType, Sarresid
|
|
||||||
|
|
||||||
### Community 94 - "resolve_recipients"
|
|
||||||
Cohesion: 0.20
|
|
||||||
Nodes (3): resolve_recipients(), mark_primary(), RecipientTests
|
|
||||||
|
|
||||||
### Community 95 - "factories.py"
|
|
||||||
Cohesion: 0.27
|
Cohesion: 0.27
|
||||||
Nodes (5): ProductCategory, make_category(), make_product(), make_store(), _next()
|
Nodes (5): ProductCategory, make_category(), make_product(), make_store(), _next()
|
||||||
|
|
||||||
### Community 96 - "write_tree"
|
### Community 25 - "Rip'n Fast. Fewer Tokens. Better Code."
|
||||||
|
Cohesion: 0.11
|
||||||
|
Nodes (18): Against the leading graph-database code-context MCP server, Better Code: It automates the review judgments nobody has time to make — every lens from published research, Documentation, Graph-Ranked Retrieval: It finds the right files more often than the alternatives, If it works on your codebase, tell us what it got wrong, Improve it with your agent, Licence, Quickstart (+10 more)
|
||||||
|
|
||||||
|
### Community 26 - "views/factors.py"
|
||||||
|
Cohesion: 0.19
|
||||||
|
Nodes (6): notify_factor_survey(), newFactor2(), InventoryException, _parse_jalali_date(), _parse_timestamp(), _safe_int()
|
||||||
|
|
||||||
|
### Community 27 - "to_local"
|
||||||
|
Cohesion: 0.17
|
||||||
|
Nodes (4): to_local(), _configured_codes(), _response_code(), ZeroZeroNinetyEightSMSProvider
|
||||||
|
|
||||||
|
### Community 30 - "NoticeMechanismTests"
|
||||||
|
Cohesion: 0.17
|
||||||
|
Nodes (3): dashBoxData(), resolve(), NoticeMechanismTests
|
||||||
|
|
||||||
|
### Community 31 - "enqueue.py"
|
||||||
|
Cohesion: 0.15
|
||||||
|
Nodes (6): format_amount(), format_jalali(), render_template(), _SafeDict, is_sms_enabled(), _sample_context()
|
||||||
|
|
||||||
|
### Community 32 - "enqueue_sms"
|
||||||
|
Cohesion: 0.21
|
||||||
|
Nodes (7): enqueue_sms(), _aware(), _Budget, scan_cheque_funding(), scan_nesiye_due(), scan_visit(), _template()
|
||||||
|
|
||||||
|
### Community 33 - "PanelTestCase"
|
||||||
|
Cohesion: 0.17
|
||||||
|
Nodes (5): PhoneNumber, DeletedStaysDeletedTests, EnqueueIdempotencyTests, PanelTestCase, WelcomeTriggerTests
|
||||||
|
|
||||||
|
### Community 34 - "ProductInStore"
|
||||||
|
Cohesion: 0.14
|
||||||
|
Nodes (8): Factor, Meta, FactorIteam, ProductInStore, Meta, MovementType, FactorDel(), newFactor()
|
||||||
|
|
||||||
|
### Community 35 - "admin_panel/templates/admin_panel/baseHesabdari.html"
|
||||||
|
Cohesion: 0.07
|
||||||
|
Nodes (27): Base Template, Customer Base Template, New Customer Base Template, Create Factor View, Customer List View, Django Custom Tags: custom_tags, URL: admin_panel:ActionListUserToday, URL: admin_panel:bedehi (+19 more)
|
||||||
|
|
||||||
|
### Community 36 - "Reference guide"
|
||||||
|
Cohesion: 0.14
|
||||||
|
Nodes (14): 10. Quality evaluation, 11. Agent integration, 13. Performance, 14. Known limits, 15. Documentation, 16. Improvement, 17. License, 1. Purpose and function (+6 more)
|
||||||
|
|
||||||
|
### Community 37 - "Factor Creation UI"
|
||||||
|
Cohesion: 0.15
|
||||||
|
Nodes (4): Customer ID Backup/New Factor Template, all_products (JS variable), buildOrderList (JS function), orderList (JS variable)
|
||||||
|
|
||||||
|
### Community 38 - "admin_panel/templates/admin_panel/bedehi.html"
|
||||||
|
Cohesion: 0.16
|
||||||
|
Nodes (10): admin_panel:pardakhtNesiye, /panel/passCheck/, all_sarresid, mandehesabKP, admin_panel.ViewReportMali, toJalali, admin_panel:bedehi, admin_panel:bedehiKhadamat (+2 more)
|
||||||
|
|
||||||
|
### Community 39 - "project_tree.py"
|
||||||
|
Cohesion: 0.21
|
||||||
|
Nodes (6): is_excluded_file(), main(), non_negative_integer(), parse_args(), sorted_entries(), warn()
|
||||||
|
|
||||||
|
### Community 40 - "CustomerSmsHistoryTests"
|
||||||
|
Cohesion: 0.15
|
||||||
|
Nodes (3): customer_phones(), toJalaliDT(), CustomerSmsHistoryTests
|
||||||
|
|
||||||
|
### Community 42 - "SMS Integration Docs"
|
||||||
|
Cohesion: 0.15
|
||||||
|
Nodes (12): **جدول کدهای خطای لینک ارتباطی**, **جدول کدهای خطای وبسرویس و وضعیتهای دلیوری**, **راهنمای استفاده از Web Service**, **راهنمای استفاده از لینک ارتباطی (URL GET)**, **راهنمای استفاده از لینک ارتباطی و وبسرویس سامانه پیامک (0098vip.com و 0098sms.com)**, **فهرست مطالب**, **مقدمات اتصال**, **نمونه کدهای پیادهسازی** (+4 more)
|
||||||
|
|
||||||
|
### Community 43 - "timing_for"
|
||||||
|
Cohesion: 0.12
|
||||||
|
Nodes (6): current_timing_value(), timing_for(), sms_template_edit(), sms_templates(), _templates_redirect(), TimingsTests
|
||||||
|
|
||||||
|
### Community 45 - "stock_in"
|
||||||
|
Cohesion: 0.25
|
||||||
|
Nodes (6): Product, ProductInStore, StoreRoom, current_stock(), stock_in(), ProductInStoreMathTests
|
||||||
|
|
||||||
|
### Community 46 - "admin_panel/templates/admin_panel/customerAction.html"
|
||||||
|
Cohesion: 0.16
|
||||||
|
Nodes (10): admin_panel/templates/admin_panel/customerActionInc.html, admin_panel:customerNew, admin_panel:customerNewAction URL, Action Data Model, DataTables Library, jQuery, Django 'url' template tag, Bulma CSS (+2 more)
|
||||||
|
|
||||||
|
### Community 47 - "parts_count"
|
||||||
|
Cohesion: 0.16
|
||||||
|
Nodes (6): describe(), detect_encoding(), encoded_length(), is_gsm7(), parts_count(), SmsPartsTests
|
||||||
|
|
||||||
|
### Community 48 - "write_tree"
|
||||||
Cohesion: 0.21
|
Cohesion: 0.21
|
||||||
Nodes (6): Counts, format_header(), generate_snapshot(), normalized_path(), write_tree(), visit()
|
Nodes (6): Counts, format_header(), generate_snapshot(), normalized_path(), write_tree(), visit()
|
||||||
|
|
||||||
### Community 98 - "reconciliation_tool.py"
|
### Community 50 - "Customer"
|
||||||
Cohesion: 0.19
|
Cohesion: 0.16
|
||||||
Nodes (4): remove_query_param(), check_financial_discrepancy(), run_360_audit(), run_audit()
|
Nodes (6): Customer, Factor, FactorIteam, VisitQueue, Command, NewFactor2ViewTests
|
||||||
|
|
||||||
### Community 99 - "ModelsPackageContractTests"
|
### Community 51 - "Phone Cleanup Guide"
|
||||||
Cohesion: 0.05
|
Cohesion: 0.17
|
||||||
Nodes (15): AdminConfig, AdminPanelConfig, make_std_streams_unicode_safe(), configure_sqlite_connection(), entrypoint.sh script, main(), run.sh script, start_service() (+7 more)
|
Nodes (11): اجرا روی سرور اصلی (production), اجرای دستور, اصلاح دستی شمارههای قدیمی مشتریان, راه جایگزین: مهاجرت (migration), قاعدهٔ پاکسازی, مشاهدهٔ نتیجه, نکتهها, چرا؟ (+3 more)
|
||||||
|
|
||||||
### Community 100 - "django_conf"
|
### Community 53 - "manage.py"
|
||||||
Cohesion: 0.22
|
Cohesion: 0.22
|
||||||
Nodes (4): AnnouncementRejected, place_announcement_call(), place_voip_call(), VoipCallResult
|
Nodes (5): entrypoint.sh script, main(), run.sh script, start_service(), usage()
|
||||||
|
|
||||||
### Community 103 - "customerIDFactorBackup.html"
|
### Community 54 - "Agent Operating Guidelines"
|
||||||
Cohesion: 0.18
|
Cohesion: 0.18
|
||||||
Nodes (8): Client-side script in customerIDFactorBackup.html, URL: admin_panel:customerEdit, Customer Data, Factor Data Model, Product Data Model, Product Category Data Model, User Data Model, URL: /panel/productMojodi/
|
Nodes (8): 1. Codebase Navigation & Analysis (Ripwire vs. Graphify), 2. Dynamic Help, Self-Correction & Staleness, 3. Git Operations & Safety, 4. Development Server Lifecycle, 5. Environment, Frontend & PWA Safety, 6. End-to-End Browser Tests (Playwright), Project Operating & Agent Guidelines, Routing Gate & Pipeline
|
||||||
|
|
||||||
### Community 110 - "0061_sms_timing_and_primary.py"
|
### Community 57 - "test_sms.py"
|
||||||
|
Cohesion: 0.17
|
||||||
|
Nodes (4): to_display(), _clean(), normalize_mobile(), NormalizeTests
|
||||||
|
|
||||||
|
### Community 58 - "Financial Data Objects"
|
||||||
|
Cohesion: 0.20
|
||||||
|
Nodes (9): Customer Data Object, Factor Data Object, Sarresid Data Object, admin_panel:customerNew URL, admin_panel:pardakhtNesiye URL, /panel/passCheck/ URL, admin_panel.ViewReportMali Permission, jQuery JS Library (+1 more)
|
||||||
|
|
||||||
|
### Community 60 - "بازمحاسبهٔ «آخرین بازدید» مشتریان"
|
||||||
|
Cohesion: 0.18
|
||||||
|
Nodes (10): اجرا روی production, بازمحاسبهٔ «آخرین بازدید» مشتریان, بررسی صحت داده (SQL، پیش از اجرا), دستور, دقیقاً چه میکند؟, نمونهٔ خروجی, نکتههای مهم, گزینهها (+2 more)
|
||||||
|
|
||||||
|
### Community 62 - "SMS Timing Migration"
|
||||||
Cohesion: 0.22
|
Cohesion: 0.22
|
||||||
Nodes (4): backfill_primary_phones(), Migration, _normalize(), normalize_template_timing()
|
Nodes (4): backfill_primary_phones(), Migration, _normalize(), normalize_template_timing()
|
||||||
|
|
||||||
### Community 111 - "bale/providers.py"
|
### Community 63 - "Articl Model"
|
||||||
Cohesion: 0.31
|
Cohesion: 0.67
|
||||||
Nodes (4): BaleBotProvider, ConsoleBaleProvider, get_bale_provider(), NotificationResult
|
Nodes (3): Articl Model, Customer Model, articls
|
||||||
|
|
||||||
### Community 116 - "Customer"
|
### Community 64 - "customerIDFactorBackup.html"
|
||||||
Cohesion: 0.29
|
Cohesion: 0.18
|
||||||
Nodes (3): Customer, Meta, PhoneNumber
|
Nodes (8): Client-side script in customerIDFactorBackup.html, URL: admin_panel:customerEdit, Customer Data, Factor Data Model, Product Data Model, Product Category Data Model, User Data Model, URL: /panel/productMojodi/
|
||||||
|
|
||||||
### Community 117 - "customerNewBackup.html"
|
### Community 67 - "Graphify Tooling Docs"
|
||||||
|
Cohesion: 0.22
|
||||||
|
Nodes (8): Copilot Chat, graphify, graphify-out/graph.json, graphify-out/GRAPH_REPORT.md, graphify-out/wiki/index.md, graphify explain, graphify path, graphify query
|
||||||
|
|
||||||
|
### Community 71 - "admin_panel/entezar.html"
|
||||||
|
Cohesion: 0.40
|
||||||
|
Nodes (4): Customer Data Structure, kamaDatepicker Library, custom_tags, toJalali filter
|
||||||
|
|
||||||
|
### Community 72 - "customerNewBackup.html"
|
||||||
Cohesion: 0.29
|
Cohesion: 0.29
|
||||||
Nodes (6): admin_panel:newFactor URL, Category Data, Client-side Form Logic, CSRF Token, custom_tags, static
|
Nodes (6): admin_panel:newFactor URL, Category Data, Client-side Form Logic, CSRF Token, custom_tags, static
|
||||||
|
|
||||||
|
### Community 73 - "hesabdariResidNashode.html"
|
||||||
|
Cohesion: 0.47
|
||||||
|
Nodes (5): jQuery, pDatepicker JS Library, custom_tags, toCurrency, toJalali
|
||||||
|
|
||||||
|
### Community 76 - "Timezone Migration History"
|
||||||
|
Cohesion: 0.29
|
||||||
|
Nodes (3): Migration, Migration, Migration
|
||||||
|
|
||||||
|
### Community 77 - "models/__init__.py"
|
||||||
|
Cohesion: 0.08
|
||||||
|
Nodes (9): Customer, Meta, PhoneNumber, ProductCategory, ProductStockHistory, Meta, StoreRoom, Meta (+1 more)
|
||||||
|
|
||||||
|
### Community 81 - "Installation Guide"
|
||||||
|
Cohesion: 0.40
|
||||||
|
Nodes (5): 3.1 Install a prebuilt binary, 3.2 Build from source, 3.3 Install the agent skills, 3.4 Register the MCP server (optional), 3. Installation
|
||||||
|
|
||||||
|
### Community 82 - "Project Honesty Contract"
|
||||||
|
Cohesion: 0.40
|
||||||
|
Nodes (5): In the numbers, In the output, In the tests, The honesty contract, What it misses, and what to run next
|
||||||
|
|
||||||
|
### Community 85 - "stop.sh"
|
||||||
|
Cohesion: 0.73
|
||||||
|
Nodes (5): is_our_process(), say(), stop.sh script, stop_pid(), windows_cleanup()
|
||||||
|
|
||||||
|
### Community 86 - "Output Format Specs"
|
||||||
|
Cohesion: 0.50
|
||||||
|
Nodes (4): 6.1 Output example, 6.2 Exit codes, 6.3 JSON output, 6. Output format
|
||||||
|
|
||||||
|
### Community 87 - "Performance Benchmarks"
|
||||||
|
Cohesion: 0.50
|
||||||
|
Nodes (4): Against other tools, Measured, What it saves you, in tokens, Where its own cycles go — hardware counters, per scope
|
||||||
|
|
||||||
|
### Community 88 - "SMS Integration Overview"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): SMS Gateway Integration Guide, SMS Settings UI, Python Dependencies
|
||||||
|
|
||||||
|
### Community 90 - "Customer Modal UI"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): createCS click handler, openRow click handler, Customer Modals UI
|
||||||
|
|
||||||
|
### Community 92 - "Infrastructure Services"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): Docker Infrastructure, SMS Delivery Tracker, SMS Worker Service
|
||||||
|
|
||||||
|
### Community 93 - "Agent Setup Guide"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): 1. (Optional) Register the MCP server, 2. Install the skills, Set it up in your coding agent
|
||||||
|
|
||||||
|
### Community 94 - "First Use Guide"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): 4.1 Make the first map, 4.2 Use the common commands, 4. First use
|
||||||
|
|
||||||
|
### Community 98 - "reconciliation_tool.py"
|
||||||
|
Cohesion: 0.24
|
||||||
|
Nodes (3): remove_query_param(), run_360_audit(), run_audit()
|
||||||
|
|
||||||
|
### Community 113 - "bale/providers.py"
|
||||||
|
Cohesion: 0.36
|
||||||
|
Nodes (4): BaleBotProvider, ConsoleBaleProvider, get_bale_provider(), NotificationResult
|
||||||
|
|
||||||
|
### Community 166 - "sms_settings"
|
||||||
|
Cohesion: 0.33
|
||||||
|
Nodes (4): parse_jalali_date(), parse_scan_since(), resolve_recipients_test(), sms_settings()
|
||||||
|
|
||||||
|
### Community 169 - "index"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (3): index(), `nest=` is a max, so it cannot tell a long function from a tangled one, The quality panel
|
||||||
|
|
||||||
## Ambiguous Edges - Review These
|
## Ambiguous Edges - Review These
|
||||||
- `admin_panel/base.html` → `admin_panel/templates/admin_panel/baseHesabdari.html` [AMBIGUOUS]
|
- `admin_panel/templates/admin_panel/baseHesabdari.html` → `Base Template` [AMBIGUOUS]
|
||||||
admin_panel/templates/admin_panel/baseHesabdari.html · relation: conceptually_related_to
|
admin_panel/templates/admin_panel/baseHesabdari.html · relation: conceptually_related_to
|
||||||
|
|
||||||
## Knowledge Gaps
|
## Knowledge Gaps
|
||||||
- **110 isolated node(s):** `DataTables Library`, `Customer Data Object`, `Factor Data Object`, `admin_panel:pardakhtNesiye URL`, `/panel/passCheck/ URL` (+105 more)
|
- **118 isolated node(s):** `base.html`, `toCurrency`, `DataTables Library`, `Customer Data Object`, `Factor Data Object` (+113 more)
|
||||||
These have ≤1 connection - possible missing edges or undocumented components. (Counts symbols only; 575 node(s) total have ≤1 connection when file, concept and rationale nodes are included.)
|
These have ≤1 connection - possible missing edges or undocumented components. (Counts symbols only; 738 node(s) total have ≤1 connection when file, concept and rationale nodes are included.)
|
||||||
- **93 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
- **97 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||||
|
|
||||||
## Suggested Questions
|
## Suggested Questions
|
||||||
_Questions this graph is uniquely positioned to answer:_
|
_Questions this graph is uniquely positioned to answer:_
|
||||||
|
|
||||||
- **Why does `SMSProviderConfig` connect `models/sms.py` to `scanner.py`?**
|
- **Why does `Rip'n Fast. Fewer Tokens. Better Code.` connect `Rip'n Fast. Fewer Tokens. Better Code.` to `Reference guide`, `.load`, `index`, `Project Honesty Contract`, `Agent Operating Guidelines`, `Performance Benchmarks`, `Agent Setup Guide`?**
|
||||||
_High betweenness centrality (0.031) - this node is a cross-community bridge._
|
_High betweenness centrality (0.046) - this node is a cross-community bridge._
|
||||||
- **Are the 2 inferred relationships involving `make_customer()` (e.g. with `Customer` and `PhoneNumber`) actually correct?**
|
- **Are the 2 inferred relationships involving `make_customer()` (e.g. with `Customer` and `PhoneNumber`) actually correct?**
|
||||||
_`make_customer()` has 2 INFERRED edges - model-reasoned connections that need verification._
|
_`make_customer()` has 2 INFERRED edges - model-reasoned connections that need verification._
|
||||||
- **What connects `DataTables Library`, `Customer Data Object`, `Factor Data Object` to the rest of the system?**
|
- **What connects `base.html`, `toCurrency`, `DataTables Library` to the rest of the system?**
|
||||||
_110 weakly-connected nodes found - possible documentation gaps or missing edges._
|
_118 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||||
- **Should `views/store.py` be split into smaller, more focused modules?**
|
- **Should `views/sms.py` be split into smaller, more focused modules?**
|
||||||
_Cohesion score 0.06540825285338016 - nodes in this community are weakly interconnected._
|
_Cohesion score 0.1425287356321839 - nodes in this community are weakly interconnected._
|
||||||
- **Why does `Languages` connect `Rip'n Fast. Fewer Tokens. Better Code.` to `scanner.py`?**
|
- **Why does `Languages` connect `.load` to `Rip'n Fast. Fewer Tokens. Better Code.`?**
|
||||||
_High betweenness centrality (0.026) - this node is a cross-community bridge._
|
_High betweenness centrality (0.044) - this node is a cross-community bridge._
|
||||||
- **Should `django_db` be split into smaller, more focused modules?**
|
- **Should `run_all` be split into smaller, more focused modules?**
|
||||||
_Cohesion score 0.06896551724137931 - nodes in this community are weakly interconnected._
|
_Cohesion score 0.05552617662612375 - nodes in this community are weakly interconnected._
|
||||||
|
|
||||||
### Low-confidence Hints
|
### Low-confidence Hints
|
||||||
_AMBIGUOUS edges — the extractor was unsure. Verify before acting on these._
|
_AMBIGUOUS edges — the extractor was unsure. Verify before acting on these._
|
||||||
|
|
||||||
- **What is the exact relationship between `admin_panel/base.html` and `admin_panel/templates/admin_panel/baseHesabdari.html`?**
|
- **What is the exact relationship between `admin_panel/templates/admin_panel/baseHesabdari.html` and `Base Template`?**
|
||||||
_Edge tagged AMBIGUOUS (relation: conceptually_related_to) - confidence is low._
|
_Edge tagged AMBIGUOUS (relation: conceptually_related_to) - confidence is low._
|
||||||
@@ -63,12 +63,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="legend"></div>
|
<div id="legend"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="stats">1280 nodes · 2237 edges · 140 communities</div>
|
<div id="stats">1684 nodes · 3008 edges · 171 communities</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
const RAW_NODES = [{"id": "admin_panel_models_store_storetransferrequest", "label": "StoreTransferRequest", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 13.8, "font": {"size": 0, "color": "#ffffff"}, "title": "StoreTransferRequest", "community": 0, "community_name": "views/store.py", "source_file": "admin_panel/models/store.py", "file_type": "code", "degree": 10}, {"id": "admin_panel_models_store_storetransferrequest_meta", "label": "Meta", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "Meta", "community": 0, "community_name": "views/store.py", "source_file": "admin_panel/models/store.py", "file_type": "code", "degree": 1}, {"id": "admin_panel_models_store_storetransferrequest_status", "label": "Status", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "Status", "community": 0, "community_name": "views/store.py", "source_file": "admin_panel/models/store.py", "file_type": "code", "degree": 1}, {"id": "tests_test_inventory_productinstoremathtests", "label": "ProductInStoreMathTests", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 13.4, "font": {"size": 0, "color": "#ffffff"}, "title": "ProductInStoreMathTests", "community": 0, "community_name": "views/store.py", "source_file": "tests/test_inventory.py", "file_type": "code", "degree": 9}, {"id": "tests_test_inventory_transferworkflowtests", "label": "TransferWorkflowTests", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 14.9, "font": {"size": 12, "color": "#ffffff"}, "title": "TransferWorkflowTests", "community": 0, "community_name": "views/store.py", "source_file": "tests/test_inventory.py", "file_type": "code", "degree": 13}, {"id": "integrations_announcements_announcementrejected", "label": "AnnouncementRejected", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 11.5, "font": {"size": 0, "color": "#ffffff"}, "title": "AnnouncementRejected", "community": 100, "community_name": "django_conf", "source_file": "integrations/announcements.py", "file_type": "code", "degree": 4}, {"id": "integrations_voip_voipcallresult", "label": "VoipCallResult", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.8, "font": {"size": 0, "color": "#ffffff"}, "title": "VoipCallResult", "community": 100, "community_name": "django_conf", "source_file": "integrations/voip.py", "file_type": "code", "degree": 2}, {"id": "admin_panel_management_commands_sms_worker_command", "label": "Command", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 13.8, "font": {"size": 0, "color": "#ffffff"}, "title": "Command", "community": 101, "community_name": "Command", "source_file": "admin_panel/management/commands/sms_worker.py", "file_type": "code", "degree": 10}, {"id": "tests_test_urls_contract_urlcontracttests", "label": "UrlContractTests", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 12.3, "font": {"size": 0, "color": "#ffffff"}, "title": "UrlContractTests", "community": 102, "community_name": "UrlContractTests", "source_file": "tests/test_urls_contract.py", "file_type": "code", "degree": 6}, {"id": "admin_panel_models_system_cachetable", "label": "CacheTable", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.8, "font": {"size": 0, "color": "#ffffff"}, "title": "CacheTable", "community": 104, "community_name": "system.py", "source_file": "admin_panel/models/system.py", "file_type": "code", "degree": 2}, {"id": "integrations_sms_behin_behinsmsprovider", "label": "BehinSMSProvider", "color": {"background": "#EDC948", "border": "#EDC948", "highlight": {"background": "#ffffff", "border": "#EDC948"}}, "size": 13.8, "font": {"size": 0, "color": "#ffffff"}, "title": "BehinSMSProvider", "community": 105, "community_name": "BehinSMSProvider", "source_file": "integrations/sms/behin.py", "file_type": "code", "degree": 10}, {"id": "tests_test_customers_customercrudtests", "label": "CustomerCrudTests", "color": {"background": "#B07AA1", "border": "#B07AA1", "highlight": {"background": "#ffffff", "border": "#B07AA1"}}, "size": 12.7, "font": {"size": 0, "color": "#ffffff"}, "title": "CustomerCrudTests", "community": 106, "community_name": "test_customers.py", "source_file": "tests/test_customers.py", "file_type": "code", "degreLine truncated
|
const RAW_NODES = [{"id": "admin_panel_management_commands_sms_scan_command", "label": "Command", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 12.4, "font": {"size": 0, "color": "#ffffff"}, "title": "Command", "community": 1, "community_name": "run_all", "source_file": "admin_panel/management/commands/sms_scan.py", "file_type": "code", "degree": 7}, {"id": "tests_test_sms_autosendtests", "label": "AutoSendTests", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 14.0, "font": {"size": 0, "color": "#ffffff"}, "title": "AutoSendTests", "community": 1, "community_name": "run_all", "source_file": "tests/test_sms.py", "file_type": "code", "degree": 12}, {"id": "tests_test_sms_coldstarttests", "label": "ColdStartTests", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 14.0, "font": {"size": 0, "color": "#ffffff"}, "title": "ColdStartTests", "community": 1, "community_name": "run_all", "source_file": "tests/test_sms.py", "file_type": "code", "degree": 12}, {"id": "tests_test_sms_consoleencodingtests", "label": "ConsoleEncodingTests", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 13.4, "font": {"size": 0, "color": "#ffffff"}, "title": "ConsoleEncodingTests", "community": 1, "community_name": "run_all", "source_file": "tests/test_sms.py", "file_type": "code", "degree": 10}, {"id": "tests_test_sms_scannertests", "label": "ScannerTests", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 15.1, "font": {"size": 12, "color": "#ffffff"}, "title": "ScannerTests", "community": 1, "community_name": "run_all", "source_file": "tests/test_sms.py", "file_type": "code", "degree": 15}, {"id": "admin_panel_migrations_0001_initial_migration", "label": "Migration", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "Migration", "community": 10, "community_name": "django_db_models_deletion", "source_file": "admin_panel/migrations/0001_initial.py", "file_type": "code", "degree": 1}, {"id": "admin_panel_migrations_0007_productinstore_factor_migration", "label": "Migration", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "Migration", "community": 10, "community_name": "django_db_models_deletion", "source_file": "admin_panel/migrations/0007_productinstore_factor.py", "file_type": "code", "degree": 1}, {"id": "admin_panel_migrations_0008_storeroom_user_migration", "label": "Migration", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "Migration", "community": 10, "community_name": "django_db_models_deletion", "source_file": "admin_panel/migrations/0008_storeroom_user.py", "file_type": "code", "degree": 1}, {"id": "admin_panel_migrations_0018_alter_factoriteam_product_migration", "label": "Migration", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "Migration", "community": 10, "community_name": "django_db_models_deletion", "source_file": "admin_panel/migrations/0018_alter_factoriteam_product.py", "file_type": "code", "degree": 1}, {"id": "admin_panel_migrations_0019_article_articletype_migration", "label": "Migration", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "Migration", "community": 10, "community_name": "django_db_models_deletion", "source_file": "admin_panel/migrations/0019_article_articletype.py", "file_type": "code", "degree": 1}, {"id": "admin_panel_migrations_0036_sarresid_migration", "label": "Migration", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "Migration", "community": 10, "community_name": "django_db_models_deletion", "source_file": "admin_panel/migrations/0036_sarresid.py", "file_type": "code", "degree": 1}, {"id": "admin_panel_migrations_0038_auto_20240213_1735_migration", "label": "Migration", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "Migration", "community": 10, "community_name": "django_db_models_deletLine truncated
|
||||||
const RAW_EDGES = [{"from": "admin_panel_services_approve_transfer_request", "to": "admin_panel_models_store_storetransferrequest", "label": "references", "title": "references [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_services_cancel_transfer_request", "to": "admin_panel_models_store_storetransferrequest", "label": "references", "title": "references [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_services_create_transfer_request", "to": "admin_panel_models_store_storetransferrequest", "label": "references", "title": "references [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_services_reject_transfer_request", "to": "admin_panel_models_store_storetransferrequest", "label": "references", "title": "references [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_models_store", "to": "admin_panel_models_store_storetransferrequest", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_models_store_storetransferrequest", "to": "admin_panel_models_store_storetransferrequest_meta", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_models_store_storetransferrequest", "to": "admin_panel_models_store_storetransferrequest_status", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_models_store_storetransferrequest", "to": "admin_panel_models_store_storetransferrequest_str", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_models_store_storetransferrequest", "to": "admin_panel_models_store_storetransferrequest_clean", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_models_store_storetransferrequest", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "tests_test_inventory", "to": "tests_test_inventory_productinstoremathtests", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_inventory_productinstoremathtests", "to": "tests_test_inventory_py_testcase", "label": "inherits", "title": "inherits [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_inventory_productinstoremathtests", "to": "tests_test_inventory_productinstoremathtests_setup", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_inventory_productinstoremathtests", "to": "tests_test_inventory_productinstoremathtests_test_initial_in_records_history_and_stock", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_inventory_productinstoremathtests", "to": "tests_test_inventory_productinstoremathtests_test_multiple_movements_accumulate_correctly", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_inventory_productinstoremathtests", "to": "tests_test_inventory_productinstoremathtests_test_editing_a_row_updates_stock_and_history", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_inventory_productinstoremathtests", "to": "tests_test_inventory_productinstoremathtests_test_deleting_a_row_creates_history_and_reduces_stock", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_inventory_productinstoremathtests", "to": "tests_test_inventory_productinstoremathtests_test_garbage_numeric_values_are_coerced_to_zero", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_inventory_productinstoremathtests", "to": "admin_models_productinstore", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "tests_test_inventory", "to": "tests_test_inventory_transferworkflowtests", "label": "contains", "tLine truncated
|
const RAW_EDGES = [{"from": "admin_panel_management_commands_sms_scan", "to": "admin_panel_management_commands_sms_scan_command", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_management_commands_sms_scan_command", "to": "admin_panel_management_commands_sms_scan_py_basecommand", "label": "inherits", "title": "inherits [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_management_commands_sms_scan_command", "to": "admin_panel_management_commands_sms_scan_command_init", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_management_commands_sms_scan_command", "to": "admin_panel_management_commands_sms_scan_command_add_arguments", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_management_commands_sms_scan_command", "to": "admin_panel_management_commands_sms_scan_command_handle", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_management_commands_sms_scan_command", "to": "admin_panel_management_commands_sms_scan_command_handle_signal", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_management_commands_sms_scan_command", "to": "admin_panel_management_commands_sms_scan_command_emit", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_sms", "to": "tests_test_sms_autosendtests", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_sms_autosendtests", "to": "tests_test_sms_paneltestcase", "label": "inherits", "title": "inherits [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_sms_autosendtests", "to": "tests_test_sms_autosendtests_overdue_visit_customer", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_sms_autosendtests", "to": "tests_test_sms_autosendtests_test_default_is_pending_so_manager_must_approve", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_sms_autosendtests", "to": "tests_test_sms_autosendtests_test_auto_send_enqueues_approved_without_manager", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_sms_autosendtests", "to": "tests_test_sms_autosendtests_test_auto_send_does_not_bypass_opt_out_or_disabled", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_sms_autosendtests", "to": "tests_test_sms_autosendtests_test_welcome_auto_send_skips_approval_on_customer_creation", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_sms_autosendtests", "to": "tests_test_sms_autosendtests_test_template_edit_persists_auto_send", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_sms_autosendtests", "to": "tests_test_sms_autosendtests_test_templates_page_shows_auto_send_switch", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_sms_rationale_1069", "to": "tests_test_sms_autosendtests", "label": "rationale_for", "title": "rationale_for [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_sms_autosendtests", "to": "admin_models_phonenumber", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "tests_test_sms_autosendtests", "to": "admin_models_customer", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "tests_test_sms", "to": "tests_test_sms_coldstarttests", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_sms_coldstarttests", "to": "tests_test_sms_py_testcase", "label": "inherits", "Line truncated
|
||||||
const LEGEND = [{"cid": 0, "color": "#4E79A7", "label": "views/store.py", "count": 68}, {"cid": 1, "color": "#F28E2B", "label": "project_tree.py", "count": 14}, {"cid": 2, "color": "#E15759", "label": "sqlite_db_backup.py", "count": 11}, {"cid": 3, "color": "#76B7B2", "label": "django_db", "count": 29}, {"cid": 4, "color": "#59A14F", "label": "Customer ID Backup/New Factor Template", "count": 14}, {"cid": 5, "color": "#EDC948", "label": "views/customers.py", "count": 26}, {"cid": 6, "color": "#B07AA1", "label": "cheque_inquiry.py", "count": 21}, {"cid": 7, "color": "#FF9DA7", "label": "admin_panel/templates/admin_panel/baseHesabdari.html", "count": 15}, {"cid": 8, "color": "#9C755F", "label": "django_db_models_deletion", "count": 31}, {"cid": 9, "color": "#BAB0AC", "label": "0051_auto_20260705_1356.py", "count": 2}, {"cid": 10, "color": "#4E79A7", "label": "admin_panel/storeBK1.html", "count": 19}, {"cid": 11, "color": "#F28E2B", "label": "ViewSmokeTests", "count": 8}, {"cid": 12, "color": "#E15759", "label": "admin_panel/templates/admin_panel/customerAction.html", "count": 17}, {"cid": 13, "color": "#76B7B2", "label": "User", "count": 48}, {"cid": 14, "color": "#59A14F", "label": "run_all", "count": 64}, {"cid": 15, "color": "#EDC948", "label": "admin_panel/admin.py", "count": 28}, {"cid": 16, "color": "#B07AA1", "label": "stock_in", "count": 24}, {"cid": 17, "color": "#FF9DA7", "label": "admin_panel/templates/admin_panel/bedehi.html", "count": 20}, {"cid": 18, "color": "#9C755F", "label": "hesabdariCheckPassFail.html", "count": 10}, {"cid": 19, "color": "#BAB0AC", "label": "Reference guide", "count": 27}, {"cid": 20, "color": "#4E79A7", "label": "normalize_mobile", "count": 34}, {"cid": 21, "color": "#F28E2B", "label": "graphify", "count": 9}, {"cid": 22, "color": "#E15759", "label": "hesabdariResidNashode.html", "count": 6}, {"cid": 23, "color": "#76B7B2", "label": "0017_factoriteam_garantyesh.py", "count": 2}, {"cid": 24, "color": "#59A14F", "label": "scanner.py", "count": 32}, {"cid": 25, "color": "#EDC948", "label": "0002_auto_20220114_1616.py", "count": 7}, {"cid": 26, "color": "#B07AA1", "label": "SMSResult", "count": 30}, {"cid": 27, "color": "#FF9DA7", "label": "Rip'n Fast. Fewer Tokens. Better Code.", "count": 44}, {"cid": 28, "color": "#9C755F", "label": "models/sms.py", "count": 26}, {"cid": 29, "color": "#BAB0AC", "label": "admin_panel/templates/admin_panel/baseStore.html", "count": 15}, {"cid": 30, "color": "#4E79A7", "label": "views/finance.py", "count": 24}, {"cid": 31, "color": "#F28E2B", "label": "Customer Modals UI", "count": 3}, {"cid": 32, "color": "#E15759", "label": "views/factors.py", "count": 22}, {"cid": 33, "color": "#76B7B2", "label": "0034_alter_article_sarresid.py", "count": 2}, {"cid": 34, "color": "#59A14F", "label": "views/sms.py", "count": 22}, {"cid": 35, "color": "#EDC948", "label": "0035_alter_article_sarresid.py", "count": 2}, {"cid": 36, "color": "#B07AA1", "label": "django_db_models", "count": 20}, {"cid": 37, "color": "#FF9DA7", "label": "0010_auto_20220122_1546.py", "count": 2}, {"cid": 38, "color": "#9C755F", "label": "0040_customer_laghv_bazdid.py", "count": 2}, {"cid": 39, "color": "#BAB0AC", "label": "test_sms.py", "count": 20}, {"cid": 40, "color": "#4E79A7", "label": "0048_alter_productinstore_options.py", "count": 2}, {"cid": 41, "color": "#F28E2B", "label": "0032_article_paystatus.py", "count": 2}, {"cid": 42, "color": "#E15759", "label": "0033_article_nahvepardakht.py", "count": 2}, {"cid": 43, "color": "#76B7B2", "label": "make_customer", "count": 19}, {"cid": 44, "color": "#59A14F", "label": "sms0098.py", "count": 19}, {"cid": 45, "color": "#EDC948", "label": "0044_sarresid_desc.py", "count": 2}, {"cid": 46, "color": "#B07AA1", "label": "models/__init__.py", "count": 18}, {"cid": 47, "color": "#FF9DA7", "label": "make_user", "count": 17}, {"cid": 48, "color": "#9C755F", "label": "GEMINI.md", "count": 2}, {"cid": 49, "color": "#BAB0AC", "label": "admin/migrations/__init__.py", "count": 1}, {"cid": 50, "color": "#4E79A7", "label": "Customer Action Include", "count": 1}, {"cid": 51, "color": "#F28E2B", "label": "New Customer Base Template", "count": 1}, {"cid": 52, "color": "#E15759", "label": "Customer Feedback Partial", "count": 1}, {"cid": 53, "color": "#76B7B2", "label": "Customer Factor Detail", "count": 1}, {"cid": 54, "color": "#59A14F", "label": "Customer Accounting Template", "count": 1}, {"cid": 55, "color": "#EDC948", "label": "Customer Factor List", "count": 1}, {"cid": 56, "color": "#B07AA1", "label": "Customer Feedback Page", "count": 1}, {"cid": 57, "color": "#FF9DA7", "label": "commands/__init__.py", "count": 1}, {"cid": 58, "color": "#9C755F", "label": "management/__init__.py", "count": 1}, {"cid": 59, "color": "#BAB0AC", "label": "admin_panel/migrations/__init__.py", "count": 1}, {"cid": 60, "color": "#4E79A7", "label": "Action List Template", "count": 1}, {"cid": 61, "color": "#F28E2B", "label": "base.html", "count": 1}, {"cid": 62,Line truncated
|
const LEGEND = [{"cid": 0, "color": "#4E79A7", "label": "views/sms.py", "count": 30}, {"cid": 1, "color": "#F28E2B", "label": "run_all", "count": 62}, {"cid": 2, "color": "#E15759", "label": "django_db", "count": 49}, {"cid": 3, "color": "#76B7B2", "label": "views/store.py", "count": 63}, {"cid": 4, "color": "#59A14F", "label": "ModelsPackageContractTests", "count": 28}, {"cid": 5, "color": "#EDC948", "label": "PhoneCleanupTests", "count": 33}, {"cid": 6, "color": "#B07AA1", "label": ".load", "count": 61}, {"cid": 7, "color": "#FF9DA7", "label": "models/sms.py", "count": 32}, {"cid": 8, "color": "#9C755F", "label": "logging", "count": 34}, {"cid": 9, "color": "#BAB0AC", "label": "views/customers.py", "count": 34}, {"cid": 10, "color": "#4E79A7", "label": "django_db_models_deletion", "count": 25}, {"cid": 11, "color": "#F28E2B", "label": "make_customer", "count": 20}, {"cid": 12, "color": "#E15759", "label": "customer_phone_entries", "count": 23}, {"cid": 13, "color": "#76B7B2", "label": "EventTriggerTests", "count": 22}, {"cid": 14, "color": "#59A14F", "label": "views/finance.py", "count": 26}, {"cid": 15, "color": "#EDC948", "label": "SMSResult", "count": 31}, {"cid": 16, "color": "#B07AA1", "label": "sms0098ws.py", "count": 29}, {"cid": 17, "color": "#FF9DA7", "label": "Admin Panel Customization", "count": 22}, {"cid": 18, "color": "#9C755F", "label": "User", "count": 25}, {"cid": 19, "color": "#BAB0AC", "label": "django_conf", "count": 23}, {"cid": 20, "color": "#4E79A7", "label": "test_customers.py", "count": 21}, {"cid": 21, "color": "#F28E2B", "label": "phones.py", "count": 14}, {"cid": 22, "color": "#E15759", "label": "Login Template Forms", "count": 19}, {"cid": 23, "color": "#76B7B2", "label": "cheque_inquiry.py", "count": 19}, {"cid": 24, "color": "#59A14F", "label": "factories.py", "count": 14}, {"cid": 25, "color": "#EDC948", "label": "Rip'n Fast. Fewer Tokens. Better Code.", "count": 18}, {"cid": 26, "color": "#B07AA1", "label": "views/factors.py", "count": 20}, {"cid": 27, "color": "#FF9DA7", "label": "to_local", "count": 16}, {"cid": 28, "color": "#9C755F", "label": "make_user", "count": 23}, {"cid": 29, "color": "#BAB0AC", "label": "QueueReviewTests", "count": 9}, {"cid": 30, "color": "#4E79A7", "label": "NoticeMechanismTests", "count": 16}, {"cid": 31, "color": "#F28E2B", "label": "enqueue.py", "count": 17}, {"cid": 32, "color": "#E15759", "label": "enqueue_sms", "count": 18}, {"cid": 33, "color": "#76B7B2", "label": "PanelTestCase", "count": 13}, {"cid": 34, "color": "#59A14F", "label": "ProductInStore", "count": 16}, {"cid": 35, "color": "#EDC948", "label": "admin_panel/templates/admin_panel/baseHesabdari.html", "count": 31}, {"cid": 36, "color": "#B07AA1", "label": "Reference guide", "count": 14}, {"cid": 37, "color": "#FF9DA7", "label": "Factor Creation UI", "count": 14}, {"cid": 38, "color": "#9C755F", "label": "admin_panel/templates/admin_panel/bedehi.html", "count": 14}, {"cid": 39, "color": "#BAB0AC", "label": "project_tree.py", "count": 14}, {"cid": 40, "color": "#4E79A7", "label": "CustomerSmsHistoryTests", "count": 24}, {"cid": 41, "color": "#F28E2B", "label": "TemplatesPageUITests", "count": 14}, {"cid": 42, "color": "#E15759", "label": "SMS Integration Docs", "count": 13}, {"cid": 43, "color": "#76B7B2", "label": "timing_for", "count": 20}, {"cid": 44, "color": "#59A14F", "label": "django_shortcuts", "count": 12}, {"cid": 45, "color": "#EDC948", "label": "stock_in", "count": 14}, {"cid": 46, "color": "#B07AA1", "label": "admin_panel/templates/admin_panel/customerAction.html", "count": 14}, {"cid": 47, "color": "#FF9DA7", "label": "parts_count", "count": 22}, {"cid": 48, "color": "#9C755F", "label": "write_tree", "count": 14}, {"cid": 49, "color": "#BAB0AC", "label": "CustomerInfoCardUITests", "count": 12}, {"cid": 50, "color": "#4E79A7", "label": "Customer", "count": 14}, {"cid": 51, "color": "#F28E2B", "label": "Phone Cleanup Guide", "count": 12}, {"cid": 52, "color": "#E15759", "label": "SmsReportTests", "count": 17}, {"cid": 53, "color": "#76B7B2", "label": "manage.py", "count": 11}, {"cid": 54, "color": "#59A14F", "label": "Agent Operating Guidelines", "count": 11}, {"cid": 55, "color": "#EDC948", "label": "SMSDeliveryCommandTests", "count": 10}, {"cid": 56, "color": "#B07AA1", "label": "BehinSMSProvider", "count": 8}, {"cid": 57, "color": "#FF9DA7", "label": "test_sms.py", "count": 16}, {"cid": 58, "color": "#9C755F", "label": "Financial Data Objects", "count": 10}, {"cid": 59, "color": "#BAB0AC", "label": "Hybrid Migration Command", "count": 10}, {"cid": 60, "color": "#4E79A7", "label": "\u0628\u0627\u0632\u0645\u062d\u0627\u0633\u0628\u0647\u0654 \u00ab\u0622\u062e\u0631\u06cc\u0646 \u0628\u0627\u0632\u062f\u06cc\u062f\u00bb \u0645\u0634\u062a\u0631\u06cc\u0627\u0646", "count": 11}, {"cid": 61, "color": "#F28E2B", "label": "Resource Cleanup Handler", "count": 10}, {"cid": 62, "color": "#E15759", "label": "SMS Timing Migration", "count": 10}, {"cid": 63, "color": "#Line truncated
|
||||||
|
|
||||||
// HTML-escape helper — prevents XSS when injecting graph data into innerHTML
|
// HTML-escape helper — prevents XSS when injecting graph data into innerHTML
|
||||||
function esc(s) {
|
function esc(s) {
|
||||||
@@ -293,7 +293,7 @@ LEGEND.forEach(c => {
|
|||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
// Render hyperedges as shaded regions
|
// Render hyperedges as shaded regions
|
||||||
const hyperedges = [{"id": "customer_retention_flow", "label": "Customer Satisfaction & Collection", "nodes": ["admin_panel_templates_admin_panel_report_rezayat", "admin_panel_templates_admin_panel_report_debtors_no_vosol", "admin_panel_customer_customernazarinc"], "relation": "participate_in", "confidence": "EXTRACTED", "confidence_score": 0.9}, {"id": "customer_management_flow", "label": "Customer Management Flow", "nodes": ["admin_panel_customer_base_new", "admin_panel_customer_action_inc", "admin_panel_customer_id_factor", "admin_panel_customer_hesabdari"], "relation": "participate_in", "confidence": "EXTRACTED", "confidence_score": 1.0}, {"id": "financial_management_flow", "label": "Financial & Debt Management", "nodes": ["admin_panel_templates_admin_panel_financial_dashboard", "admin_panel_templates_admin_panel_hesabdari"], "relation": "participate_in", "confidence": "EXTRACTED", "confidence_score": 1.0}, {"id": "graphify_output_artifacts", "label": "Graphify Output Artifacts", "nodes": ["graphify-out_graph_json", "graphify-out_wiki_index_md", "graphify-out_graph_report_md"], "relation": "form", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": ".github/copilot-instructions.md"}, {"id": "graphify_subcommands", "label": "Graphify Subcommands", "nodes": ["graphify_query", "graphify_path", "graphify_explain"], "relation": "form", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": ".github/copilot-instructions.md"}, {"id": "inventory_management_flow", "label": "Inventory Management System", "nodes": ["admin_panel_templates_admin_panel_store", "admin_panel_templates_admin_panel_store_transfers", "admin_panel_templates_admin_panel_report_inventory_movements", "admin_panel_templates_admin_panel_operationsbk"], "relation": "participate_in", "confidence": "EXTRACTED", "confidence_score": 1.0}, {"id": "bedehi_report_navigation", "label": "Bedehi Report Navigation", "nodes": ["django_urls_admin_panel_bedehi", "django_urls_admin_panel_bedehikhadamat", "django_urls_admin_panel_bedehinasb"], "relation": "participate_in", "confidence": "EXTRACTED", "source_file": "admin_panel/templates/admin_panel/bedehi.html"}, {"id": "login_form", "label": "Login Form", "nodes": ["admin_panel_templates_admin_panel_login", "django_url_admin_panel_p_login", "django_csrf_token", "form_field_username", "form_field_password"], "relation": "form", "confidence": "EXTRACTED", "source_file": "admin_panel/templates/admin_panel/login.html"}, {"id": "new_store_form", "label": "New Store Form", "nodes": ["admin_panel_templates_admin_panel_storebk1", "django_url_admin_panel_newstore", "django_csrf_token", "form_field_storename"], "relation": "form", "confidence": "EXTRACTED", "source_file": "admin_panel/templates/admin_panel/storeBK1.html"}, {"id": "store_inventory_display", "label": "Store Inventory Display", "nodes": ["admin_panel_templates_admin_panel_storebk1", "template_variable_productsum", "product_property_product_name", "product_property_mojodi"], "relation": "participate_in", "confidence": "EXTRACTED", "source_file": "admin_panel/templates/admin_panel/storeBK1.html"}, {"id": "customer_list_page", "label": "Customer List Page", "nodes": ["admin_panel_templates_admin_panel_entezar_html", "concept_customer_data", "concept_url_admin_panel_customers"], "relation": "form", "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "admin_panel/templates/admin_panel/entezar.html"}, {"id": "customer_interaction_flow", "label": "Customer Lifecycle Management", "nodes": ["admin_panel_templates_admin_panel_customers"], "relation": "participate_in", "confidence": "INFERRED", "confidence_score": 0.85}, {"id": "customer_lifecycle_management", "label": "Customer Interaction Lifecycle", "nodes": ["admin_panel_templates_admin_panel_customers", "admin_panel_templates_admin_panel_customerNew", "admin_panel_templates_admin_panel_customerRezayat"], "relation": "form", "confidence": "INFERRED", "confidence_score": 0.85, "source_file": "admin_panel/templates/admin_panel/customers.html"}, {"id": "new_factor_creation_flow", "label": "New Factor Creation Flow", "nodes": ["admin_panel_templates_admin_panel_customernewbackup_html", "admin_panel_urls_newfactor", "concept_client_side_form_logic", "concept_customer", "concept_category"], "relation": "participate_in", "confidence": "INFERRED", "confidence_score": 0.85, "source_file": "admin_panel/templates/admin_panel/customerNewBackup.html"}, {"id": "customer_management_ui_flow", "label": "Customer Management UI Flow", "nodes": ["admin_panel_templates_admin_panel_customersbk_html", "admin_panel_templates_admin_panel_customersbk_html_createcs", "admin_panel_templates_admin_panel_customersbk_html_openrow", "concept_customer_modal_ui"], "relation": "participate_in", "confidence": "INFERRED", "confidence_score": 0.9, "source_file": "admin_panel/templates/admin_panel/customersBK.html"}, {"id": "financial_management_flow", "label": "Financial & Debt Management Flow", "nodes": ["adLine truncated
|
const hyperedges = [{"id": "customer_retention_flow", "label": "Customer Satisfaction & Collection", "nodes": ["admin_panel_templates_admin_panel_report_rezayat", "admin_panel_templates_admin_panel_report_debtors_no_vosol", "admin_panel_customer_customernazarinc"], "relation": "participate_in", "confidence": "EXTRACTED", "confidence_score": 0.9}, {"id": "customer_management_flow", "label": "Customer Management Flow", "nodes": ["admin_panel_customer_action_inc", "admin_panel_customer_id_factor", "admin_panel_customer_hesabdari"], "relation": "participate_in", "confidence": "EXTRACTED", "confidence_score": 1.0}, {"id": "financial_management_flow", "label": "Financial & Debt Management", "nodes": ["admin_panel_templates_admin_panel_financial_dashboard", "admin_panel_templates_admin_panel_hesabdari"], "relation": "participate_in", "confidence": "EXTRACTED", "confidence_score": 1.0}, {"id": "graphify_output_artifacts", "label": "Graphify Output Artifacts", "nodes": ["graphify-out_graph_json", "graphify-out_wiki_index_md", "graphify-out_graph_report_md"], "relation": "form", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": ".github/copilot-instructions.md"}, {"id": "graphify_subcommands", "label": "Graphify Subcommands", "nodes": ["graphify_query", "graphify_path", "graphify_explain"], "relation": "form", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": ".github/copilot-instructions.md"}, {"id": "inventory_management_flow", "label": "Inventory Management System", "nodes": ["admin_panel_templates_admin_panel_store", "admin_panel_templates_admin_panel_store_transfers", "admin_panel_templates_admin_panel_report_inventory_movements", "admin_panel_templates_admin_panel_operationsbk"], "relation": "participate_in", "confidence": "EXTRACTED", "confidence_score": 1.0}, {"id": "sms_management_system", "label": "SMS Management System", "nodes": ["admin_panel_templates_admin_panel_sms_settings_html", "admin_panel_templates_admin_panel_sms_templates_html", "admin_panel_templates_admin_panel_sms_queue_html", "sms_worker", "sms_delivery"], "relation": "participate_in", "confidence": "EXTRACTED", "confidence_score": 1.0}, {"id": "bedehi_report_navigation", "label": "Bedehi Report Navigation", "nodes": ["django_urls_admin_panel_bedehi", "django_urls_admin_panel_bedehikhadamat", "django_urls_admin_panel_bedehinasb"], "relation": "participate_in", "confidence": "EXTRACTED", "source_file": "admin_panel/templates/admin_panel/bedehi.html"}, {"id": "login_form", "label": "Login Form", "nodes": ["admin_panel_templates_admin_panel_login", "django_url_admin_panel_p_login", "django_csrf_token", "form_field_username", "form_field_password"], "relation": "form", "confidence": "EXTRACTED", "source_file": "admin_panel/templates/admin_panel/login.html"}, {"id": "new_store_form", "label": "New Store Form", "nodes": ["admin_panel_templates_admin_panel_storebk1", "django_url_admin_panel_newstore", "django_csrf_token", "form_field_storename"], "relation": "form", "confidence": "EXTRACTED", "source_file": "admin_panel/templates/admin_panel/storeBK1.html"}, {"id": "store_inventory_display", "label": "Store Inventory Display", "nodes": ["admin_panel_templates_admin_panel_storebk1", "template_variable_productsum", "product_property_product_name", "product_property_mojodi"], "relation": "participate_in", "confidence": "EXTRACTED", "source_file": "admin_panel/templates/admin_panel/storeBK1.html"}, {"id": "customer_list_page", "label": "Customer List Page", "nodes": ["admin_panel_templates_admin_panel_entezar_html", "concept_customer_data", "concept_url_admin_panel_customers"], "relation": "form", "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "admin_panel/templates/admin_panel/entezar.html"}, {"id": "customer_interaction_flow", "label": "Customer Lifecycle Management", "nodes": ["admin_panel_templates_admin_panel_customers"], "relation": "participate_in", "confidence": "INFERRED", "confidence_score": 0.85}, {"id": "new_factor_creation_flow", "label": "New Factor Creation Flow", "nodes": ["admin_panel_templates_admin_panel_customernewbackup_html", "admin_panel_urls_newfactor", "concept_client_side_form_logic", "concept_customer", "concept_category"], "relation": "participate_in", "confidence": "INFERRED", "confidence_score": 0.85, "source_file": "admin_panel/templates/admin_panel/customerNewBackup.html"}, {"id": "customer_management_ui_flow", "label": "Customer Management UI Flow", "nodes": ["admin_panel_templates_admin_panel_customersbk_html", "admin_panel_templates_admin_panel_customersbk_html_createcs", "admin_panel_templates_admin_panel_customersbk_html_openrow", "concept_customer_modal_ui"], "relation": "participate_in", "confidence": "INFERRED", "confidence_score": 0.9, "source_file": "admin_panel/templates/admin_panel/customersBK.html"}, {"id": "financial_management_flow", "label": "Financial & Debt Management Flow", "nodes": ["admin_panel_templates_admin_panel_customerSarresid", "admin_panelLine truncated
|
||||||
// afterDrawing passes ctx already transformed to network coordinate space.
|
// afterDrawing passes ctx already transformed to network coordinate space.
|
||||||
// Draw node positions raw — no manual pan/zoom/DPR math needed.
|
// Draw node positions raw — no manual pan/zoom/DPR math needed.
|
||||||
|
|
||||||
|
|||||||
+27452
-13833
File diff suppressed because it is too large.
Load diff
+342
-186
@@ -66,22 +66,22 @@
|
|||||||
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
|
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
|
||||||
},
|
},
|
||||||
"admin_panel/admin.py": {
|
"admin_panel/admin.py": {
|
||||||
"mtime": 1791567672.622296,
|
"mtime": 1791627073.834386,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.2978444,
|
||||||
"ast_hash": "79e7d7e10f67c3ba818373d384f4ad17",
|
"ast_hash": "bf3715ab483bd7eda55aa56913ae347e",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "bf3715ab483bd7eda55aa56913ae347e"
|
||||||
},
|
},
|
||||||
"admin_panel/apps.py": {
|
"admin_panel/apps.py": {
|
||||||
"mtime": 1791570183.2597606,
|
"mtime": 1791570183.2597606,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.2978444,
|
||||||
"ast_hash": "d0b73aeac890b520b7d1f8796b59ae21",
|
"ast_hash": "d0b73aeac890b520b7d1f8796b59ae21",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "d0b73aeac890b520b7d1f8796b59ae21"
|
||||||
},
|
},
|
||||||
"admin_panel/context_processor.py": {
|
"admin_panel/context_processor.py": {
|
||||||
"mtime": 1785506207.060478,
|
"mtime": 1791618283.1369228,
|
||||||
"seen": 1791552045.91651,
|
"seen": 1791702801.2978444,
|
||||||
"ast_hash": "a9bc61278ecd5950347b24fd8438583b",
|
"ast_hash": "8d915c82c3a2690e90d3367fd34cf383",
|
||||||
"semantic_hash": "a9bc61278ecd5950347b24fd8438583b"
|
"semantic_hash": "8d915c82c3a2690e90d3367fd34cf383"
|
||||||
},
|
},
|
||||||
"admin_panel/management/__init__.py": {
|
"admin_panel/management/__init__.py": {
|
||||||
"mtime": 1782242229.7079034,
|
"mtime": 1782242229.7079034,
|
||||||
@@ -468,10 +468,10 @@
|
|||||||
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
|
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
|
||||||
},
|
},
|
||||||
"admin_panel/templatetags/custom_tags.py": {
|
"admin_panel/templatetags/custom_tags.py": {
|
||||||
"mtime": 1783251836.4148617,
|
"mtime": 1791641617.9080276,
|
||||||
"seen": 1791552045.91651,
|
"seen": 1791702801.3156137,
|
||||||
"ast_hash": "8d8c826e168cec5bb68a21c8457380ba",
|
"ast_hash": "849cd0b0dc695c9b16de2f5af51d1166",
|
||||||
"semantic_hash": "8d8c826e168cec5bb68a21c8457380ba"
|
"semantic_hash": "849cd0b0dc695c9b16de2f5af51d1166"
|
||||||
},
|
},
|
||||||
"admin_panel/templatetags/remove_q_p.py": {
|
"admin_panel/templatetags/remove_q_p.py": {
|
||||||
"mtime": 1683307703.0,
|
"mtime": 1683307703.0,
|
||||||
@@ -486,9 +486,9 @@
|
|||||||
"semantic_hash": "e08f0582500f6562bf0a931ef9503b39"
|
"semantic_hash": "e08f0582500f6562bf0a931ef9503b39"
|
||||||
},
|
},
|
||||||
"admin_panel/urls.py": {
|
"admin_panel/urls.py": {
|
||||||
"mtime": 1791564580.742213,
|
"mtime": 1791717588.9114156,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791720362.5829337,
|
||||||
"ast_hash": "c17170a3be54ce7a173bf4d215864538",
|
"ast_hash": "5b14236a71f471fee84008cb051566b9",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"check_finance.py": {
|
"check_finance.py": {
|
||||||
@@ -499,9 +499,9 @@
|
|||||||
},
|
},
|
||||||
"entrypoint.sh": {
|
"entrypoint.sh": {
|
||||||
"mtime": 1791564734.2804396,
|
"mtime": 1791564734.2804396,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3178282,
|
||||||
"ast_hash": "5efb2100271785fa69a864bc6a89edb8",
|
"ast_hash": "5efb2100271785fa69a864bc6a89edb8",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "5efb2100271785fa69a864bc6a89edb8"
|
||||||
},
|
},
|
||||||
"ghost_hunter.py": {
|
"ghost_hunter.py": {
|
||||||
"mtime": 1786881510.522436,
|
"mtime": 1786881510.522436,
|
||||||
@@ -529,9 +529,9 @@
|
|||||||
},
|
},
|
||||||
"khatibi/settings.py": {
|
"khatibi/settings.py": {
|
||||||
"mtime": 1791564345.9580202,
|
"mtime": 1791564345.9580202,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3183815,
|
||||||
"ast_hash": "b3f5a8c5895df16b918fc132147d8859",
|
"ast_hash": "b3f5a8c5895df16b918fc132147d8859",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "b3f5a8c5895df16b918fc132147d8859"
|
||||||
},
|
},
|
||||||
"khatibi/urls.py": {
|
"khatibi/urls.py": {
|
||||||
"mtime": 1650778147.0,
|
"mtime": 1650778147.0,
|
||||||
@@ -576,9 +576,9 @@
|
|||||||
"semantic_hash": "4ba55e04b37e585d96f0d628c695d228"
|
"semantic_hash": "4ba55e04b37e585d96f0d628c695d228"
|
||||||
},
|
},
|
||||||
"admin_panel/templates/admin_panel/base.html": {
|
"admin_panel/templates/admin_panel/base.html": {
|
||||||
"mtime": 1791564697.854277,
|
"mtime": 1791719559.991003,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791720362.5829337,
|
||||||
"ast_hash": "d6fd88c25741fd4b8173ade946f7c26c",
|
"ast_hash": "2a59c1f5b13d05f75c5dd56b20c228ce",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"admin_panel/templates/admin_panel/baseHesabdari.html": {
|
"admin_panel/templates/admin_panel/baseHesabdari.html": {
|
||||||
@@ -618,15 +618,15 @@
|
|||||||
"semantic_hash": "3232cbf82efe94504f69735791ca5cf4"
|
"semantic_hash": "3232cbf82efe94504f69735791ca5cf4"
|
||||||
},
|
},
|
||||||
"admin_panel/templates/admin_panel/customerBase.html": {
|
"admin_panel/templates/admin_panel/customerBase.html": {
|
||||||
"mtime": 1650778147.0,
|
"mtime": 1791712775.7233438,
|
||||||
"seen": 1791553187.9024515,
|
"seen": 1791714098.633808,
|
||||||
"ast_hash": "2724fa49985d8c8fa76ddf5c31b64054",
|
"ast_hash": "2fe76f54223135513e31b95e40fe5891",
|
||||||
"semantic_hash": "2724fa49985d8c8fa76ddf5c31b64054"
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"admin_panel/templates/admin_panel/customerBaseNew.html": {
|
"admin_panel/templates/admin_panel/customerBaseNew.html": {
|
||||||
"mtime": 1791567422.453766,
|
"mtime": 1791712756.7746606,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791714098.633808,
|
||||||
"ast_hash": "2059fbc03889c7641f43a77f2ca66974",
|
"ast_hash": "488e036cf11d96e273c6dcd04544598e",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"admin_panel/templates/admin_panel/customerFactor.html": {
|
"admin_panel/templates/admin_panel/customerFactor.html": {
|
||||||
@@ -679,9 +679,9 @@
|
|||||||
},
|
},
|
||||||
"admin_panel/templates/admin_panel/customerNew2.html": {
|
"admin_panel/templates/admin_panel/customerNew2.html": {
|
||||||
"mtime": 1791564687.384954,
|
"mtime": 1791564687.384954,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702836.3239095,
|
||||||
"ast_hash": "5ebe9f703b4f229f55a6658064b609ef",
|
"ast_hash": "5ebe9f703b4f229f55a6658064b609ef",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "5ebe9f703b4f229f55a6658064b609ef"
|
||||||
},
|
},
|
||||||
"admin_panel/templates/admin_panel/customerNewBackup.html": {
|
"admin_panel/templates/admin_panel/customerNewBackup.html": {
|
||||||
"mtime": 1650778147.0,
|
"mtime": 1650778147.0,
|
||||||
@@ -720,10 +720,10 @@
|
|||||||
"semantic_hash": "1cb1e5086a0f291b86437f03af835407"
|
"semantic_hash": "1cb1e5086a0f291b86437f03af835407"
|
||||||
},
|
},
|
||||||
"admin_panel/templates/admin_panel/customers.html": {
|
"admin_panel/templates/admin_panel/customers.html": {
|
||||||
"mtime": 1791564720.7973502,
|
"mtime": 1791626647.6043096,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702836.3239095,
|
||||||
"ast_hash": "fb4b36a0b24edde3fae32542d675449e",
|
"ast_hash": "4364f20bb55d32a42d903202afdf736d",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "4364f20bb55d32a42d903202afdf736d"
|
||||||
},
|
},
|
||||||
"admin_panel/templates/admin_panel/customersBK.html": {
|
"admin_panel/templates/admin_panel/customersBK.html": {
|
||||||
"mtime": 1650778147.0,
|
"mtime": 1650778147.0,
|
||||||
@@ -865,9 +865,9 @@
|
|||||||
},
|
},
|
||||||
"requirements.txt": {
|
"requirements.txt": {
|
||||||
"mtime": 1791564743.1777852,
|
"mtime": 1791564743.1777852,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3208632,
|
||||||
"ast_hash": "41f5e0e5a503cf2eddb6bafb3aff3214",
|
"ast_hash": "41f5e0e5a503cf2eddb6bafb3aff3214",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "41f5e0e5a503cf2eddb6bafb3aff3214"
|
||||||
},
|
},
|
||||||
"requirements2.txt": {
|
"requirements2.txt": {
|
||||||
"mtime": 1654964543.0,
|
"mtime": 1654964543.0,
|
||||||
@@ -877,9 +877,9 @@
|
|||||||
},
|
},
|
||||||
"admin_panel/models/__init__.py": {
|
"admin_panel/models/__init__.py": {
|
||||||
"mtime": 1791564332.991965,
|
"mtime": 1791564332.991965,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.313386,
|
||||||
"ast_hash": "7cd6b8258d07f9d1a4f668a95b1c9c58",
|
"ast_hash": "7cd6b8258d07f9d1a4f668a95b1c9c58",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "7cd6b8258d07f9d1a4f668a95b1c9c58"
|
||||||
},
|
},
|
||||||
"admin_panel/models/actions.py": {
|
"admin_panel/models/actions.py": {
|
||||||
"mtime": 1791557666.0745652,
|
"mtime": 1791557666.0745652,
|
||||||
@@ -888,10 +888,10 @@
|
|||||||
"semantic_hash": "8213389e796cad243cdb758fc78076a5"
|
"semantic_hash": "8213389e796cad243cdb758fc78076a5"
|
||||||
},
|
},
|
||||||
"admin_panel/models/customers.py": {
|
"admin_panel/models/customers.py": {
|
||||||
"mtime": 1791567220.2646446,
|
"mtime": 1791621708.4566169,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.313386,
|
||||||
"ast_hash": "c9623b3b8f8b1e4b03f2113919ef3608",
|
"ast_hash": "b70fa9395bb5cacb452b84cf0a794efe",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "b70fa9395bb5cacb452b84cf0a794efe"
|
||||||
},
|
},
|
||||||
"admin_panel/models/factors.py": {
|
"admin_panel/models/factors.py": {
|
||||||
"mtime": 1791557664.8430376,
|
"mtime": 1791557664.8430376,
|
||||||
@@ -931,21 +931,21 @@
|
|||||||
},
|
},
|
||||||
"admin_panel/views/__init__.py": {
|
"admin_panel/views/__init__.py": {
|
||||||
"mtime": 1791564503.1344602,
|
"mtime": 1791564503.1344602,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3156137,
|
||||||
"ast_hash": "88b45aececacd04d8f8faadaa295524b",
|
"ast_hash": "88b45aececacd04d8f8faadaa295524b",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "88b45aececacd04d8f8faadaa295524b"
|
||||||
},
|
},
|
||||||
"admin_panel/views/_helpers.py": {
|
"admin_panel/views/_helpers.py": {
|
||||||
"mtime": 1791568674.700356,
|
"mtime": 1791568674.700356,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3156137,
|
||||||
"ast_hash": "4412599acacb9e21ffa7a2d2748d9484",
|
"ast_hash": "4412599acacb9e21ffa7a2d2748d9484",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "4412599acacb9e21ffa7a2d2748d9484"
|
||||||
},
|
},
|
||||||
"admin_panel/views/actions.py": {
|
"admin_panel/views/actions.py": {
|
||||||
"mtime": 1791557931.7421222,
|
"mtime": 1791708825.7957535,
|
||||||
"seen": 1791560671.8564537,
|
"seen": 1791710591.5772536,
|
||||||
"ast_hash": "4a96fd10db79b691fa606e5d521d776b",
|
"ast_hash": "e0f571ab730d0c1de7a17c8e908f161d",
|
||||||
"semantic_hash": "4a96fd10db79b691fa606e5d521d776b"
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"admin_panel/views/auth.py": {
|
"admin_panel/views/auth.py": {
|
||||||
"mtime": 1791557771.327603,
|
"mtime": 1791557771.327603,
|
||||||
@@ -960,22 +960,22 @@
|
|||||||
"semantic_hash": "364f4190f934261a082c189d8f44d29c"
|
"semantic_hash": "364f4190f934261a082c189d8f44d29c"
|
||||||
},
|
},
|
||||||
"admin_panel/views/customers.py": {
|
"admin_panel/views/customers.py": {
|
||||||
"mtime": 1791568689.7329805,
|
"mtime": 1791711853.5219991,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791714098.633808,
|
||||||
"ast_hash": "10f28fbc7076400ec0074172cc0471ea",
|
"ast_hash": "43945c8ffdbde356447635b1203b9566",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"admin_panel/views/factors.py": {
|
"admin_panel/views/factors.py": {
|
||||||
"mtime": 1791557824.3959832,
|
"mtime": 1791710085.8869143,
|
||||||
"seen": 1791560671.8564537,
|
"seen": 1791710591.5772536,
|
||||||
"ast_hash": "4edb8f009937a4d52ae4355c15950797",
|
"ast_hash": "d47aa24b93c787acf8f385e0765bdbeb",
|
||||||
"semantic_hash": "4edb8f009937a4d52ae4355c15950797"
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"admin_panel/views/finance.py": {
|
"admin_panel/views/finance.py": {
|
||||||
"mtime": 1791557914.4702559,
|
"mtime": 1791713964.8948607,
|
||||||
"seen": 1791560671.8564537,
|
"seen": 1791714098.633808,
|
||||||
"ast_hash": "6d192f725bec56cac50f8ab1a3f7529a",
|
"ast_hash": "d199e23a302ef9eee93c9abf459bc511",
|
||||||
"semantic_hash": "6d192f725bec56cac50f8ab1a3f7529a"
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"admin_panel/views/store.py": {
|
"admin_panel/views/store.py": {
|
||||||
"mtime": 1791557842.9065113,
|
"mtime": 1791557842.9065113,
|
||||||
@@ -996,16 +996,16 @@
|
|||||||
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
|
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
|
||||||
},
|
},
|
||||||
"tests/factories.py": {
|
"tests/factories.py": {
|
||||||
"mtime": 1791558395.0672476,
|
"mtime": 1791615046.5393274,
|
||||||
"seen": 1791560671.8564537,
|
"seen": 1791702801.3202996,
|
||||||
"ast_hash": "50fd151eba01ea60af0eca0412c6769c",
|
"ast_hash": "44b675e6955ee776b91f3f3bbdbb1fc0",
|
||||||
"semantic_hash": "50fd151eba01ea60af0eca0412c6769c"
|
"semantic_hash": "44b675e6955ee776b91f3f3bbdbb1fc0"
|
||||||
},
|
},
|
||||||
"tests/test_customers.py": {
|
"tests/test_customers.py": {
|
||||||
"mtime": 1791558470.3088942,
|
"mtime": 1791713632.3402467,
|
||||||
"seen": 1791560671.8564537,
|
"seen": 1791714098.633808,
|
||||||
"ast_hash": "59649733efa1edadc23835a27f68bbba",
|
"ast_hash": "cf9df1225c3c9d7b101ab3b2e2db8f76",
|
||||||
"semantic_hash": "59649733efa1edadc23835a27f68bbba"
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"tests/test_factors.py": {
|
"tests/test_factors.py": {
|
||||||
"mtime": 1791558462.1194556,
|
"mtime": 1791558462.1194556,
|
||||||
@@ -1026,15 +1026,15 @@
|
|||||||
"semantic_hash": "55662ec48c0ef4b683a87b94808b91b0"
|
"semantic_hash": "55662ec48c0ef4b683a87b94808b91b0"
|
||||||
},
|
},
|
||||||
"tests/test_urls_contract.py": {
|
"tests/test_urls_contract.py": {
|
||||||
"mtime": 1791564827.3614242,
|
"mtime": 1791718031.9609113,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791720362.5829337,
|
||||||
"ast_hash": "c500c49133546f55ce37a11fe374a5dc",
|
"ast_hash": "e02361bf950d2549018c1b19fad366bb",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"tests/test_view_smoke.py": {
|
"tests/test_view_smoke.py": {
|
||||||
"mtime": 1791567549.952807,
|
"mtime": 1791718041.2553785,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791720362.5829337,
|
||||||
"ast_hash": "1f375dcb28dd39eb1d7ca203cac2b8d9",
|
"ast_hash": "527f0f3fcffdddd54cf8db80ed21ce2b",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"AGENTS.md": {
|
"AGENTS.md": {
|
||||||
@@ -1051,254 +1051,410 @@
|
|||||||
},
|
},
|
||||||
"admin_panel/management/commands/sms_scan.py": {
|
"admin_panel/management/commands/sms_scan.py": {
|
||||||
"mtime": 1791564935.0555663,
|
"mtime": 1791564935.0555663,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.2988422,
|
||||||
"ast_hash": "ed3caa947399bcd9d8d7785c0400c81e",
|
"ast_hash": "ed3caa947399bcd9d8d7785c0400c81e",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "ed3caa947399bcd9d8d7785c0400c81e"
|
||||||
},
|
},
|
||||||
"admin_panel/management/commands/sms_worker.py": {
|
"admin_panel/management/commands/sms_worker.py": {
|
||||||
"mtime": 1791570227.772429,
|
"mtime": 1791617008.8882565,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.2988422,
|
||||||
"ast_hash": "b632f000bc231f91c876a57819e94d4c",
|
"ast_hash": "fe7d671672a25ef84c428955640bb136",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "fe7d671672a25ef84c428955640bb136"
|
||||||
},
|
},
|
||||||
"admin_panel/migrations/0057_sms.py": {
|
"admin_panel/migrations/0057_sms.py": {
|
||||||
"mtime": 1791564769.8150086,
|
"mtime": 1791564769.8150086,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3128505,
|
||||||
"ast_hash": "48769309cf799b598f1511a37865c665",
|
"ast_hash": "48769309cf799b598f1511a37865c665",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "48769309cf799b598f1511a37865c665"
|
||||||
},
|
},
|
||||||
"admin_panel/migrations/0058_seed_sms_templates.py": {
|
"admin_panel/migrations/0058_seed_sms_templates.py": {
|
||||||
"mtime": 1791564787.7929118,
|
"mtime": 1791618477.6292322,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3128505,
|
||||||
"ast_hash": "8e42b8ea8b81ab4b3662cb70075e055b",
|
"ast_hash": "644789822fa1fab80f29f0ef995b95aa",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "644789822fa1fab80f29f0ef995b95aa"
|
||||||
},
|
},
|
||||||
"admin_panel/migrations/0059_sms_flow_control.py": {
|
"admin_panel/migrations/0059_sms_flow_control.py": {
|
||||||
"mtime": 1791565703.6785793,
|
"mtime": 1791565703.6785793,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3128505,
|
||||||
"ast_hash": "d44f0ff09baf2be5dfee52a09abb2326",
|
"ast_hash": "d44f0ff09baf2be5dfee52a09abb2326",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "d44f0ff09baf2be5dfee52a09abb2326"
|
||||||
},
|
},
|
||||||
"admin_panel/migrations/0060_sms_permission_label.py": {
|
"admin_panel/migrations/0060_sms_permission_label.py": {
|
||||||
"mtime": 1791566229.439728,
|
"mtime": 1791566229.439728,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3128505,
|
||||||
"ast_hash": "6c11511c7b690a0fcb7ca5fae64c512c",
|
"ast_hash": "6c11511c7b690a0fcb7ca5fae64c512c",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "6c11511c7b690a0fcb7ca5fae64c512c"
|
||||||
},
|
},
|
||||||
"admin_panel/migrations/0061_sms_timing_and_primary.py": {
|
"admin_panel/migrations/0061_sms_timing_and_primary.py": {
|
||||||
"mtime": 1791567453.636376,
|
"mtime": 1791567453.636376,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3128505,
|
||||||
"ast_hash": "f389c45783e25be977e01db9175b96cd",
|
"ast_hash": "f389c45783e25be977e01db9175b96cd",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "f389c45783e25be977e01db9175b96cd"
|
||||||
},
|
},
|
||||||
"admin_panel/migrations/0062_sms_link_provider.py": {
|
"admin_panel/migrations/0062_sms_link_provider.py": {
|
||||||
"mtime": 1791571832.394663,
|
"mtime": 1791571832.394663,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3128505,
|
||||||
"ast_hash": "be04ce55506d075f0afe266cadeda08e",
|
"ast_hash": "be04ce55506d075f0afe266cadeda08e",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "be04ce55506d075f0afe266cadeda08e"
|
||||||
},
|
},
|
||||||
"admin_panel/models/sms.py": {
|
"admin_panel/models/sms.py": {
|
||||||
"mtime": 1791571800.3552737,
|
"mtime": 1791719506.0225618,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791720362.5829337,
|
||||||
"ast_hash": "e8e1edef3b27bd220a755b3519f168aa",
|
"ast_hash": "8e4bb3e917888a16e675f3f139207b1b",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"admin_panel/signals.py": {
|
"admin_panel/signals.py": {
|
||||||
"mtime": 1791564587.418286,
|
"mtime": 1791564587.418286,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3150773,
|
||||||
"ast_hash": "2eace8c40bf29514679f1113c3d28fdb",
|
"ast_hash": "2eace8c40bf29514679f1113c3d28fdb",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "2eace8c40bf29514679f1113c3d28fdb"
|
||||||
},
|
},
|
||||||
"admin_panel/sms/__init__.py": {
|
"admin_panel/sms/__init__.py": {
|
||||||
"mtime": 1791564397.2313416,
|
"mtime": 1791564397.2313416,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3150773,
|
||||||
"ast_hash": "22f7602ea6f852c9a1cf8a71a40c7f5c",
|
"ast_hash": "22f7602ea6f852c9a1cf8a71a40c7f5c",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "22f7602ea6f852c9a1cf8a71a40c7f5c"
|
||||||
},
|
},
|
||||||
"admin_panel/sms/enqueue.py": {
|
"admin_panel/sms/enqueue.py": {
|
||||||
"mtime": 1791564422.1932628,
|
"mtime": 1791626998.7211082,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3150773,
|
||||||
"ast_hash": "f3b793473bbf7f4d0f699966fc778610",
|
"ast_hash": "7d00e585643244a5e694427047972c74",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "7d00e585643244a5e694427047972c74"
|
||||||
},
|
},
|
||||||
"admin_panel/sms/phones.py": {
|
"admin_panel/sms/phones.py": {
|
||||||
"mtime": 1791567752.8740702,
|
"mtime": 1791567752.8740702,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3150773,
|
||||||
"ast_hash": "e7a2f0d3b2ccfbc3e37351f134c0a3c8",
|
"ast_hash": "e7a2f0d3b2ccfbc3e37351f134c0a3c8",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "e7a2f0d3b2ccfbc3e37351f134c0a3c8"
|
||||||
},
|
},
|
||||||
"admin_panel/sms/recipients.py": {
|
"admin_panel/sms/recipients.py": {
|
||||||
"mtime": 1791567203.9483874,
|
"mtime": 1791567203.9483874,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3150773,
|
||||||
"ast_hash": "19bd0d12c9932809424ddcc58d9d5ccd",
|
"ast_hash": "19bd0d12c9932809424ddcc58d9d5ccd",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "19bd0d12c9932809424ddcc58d9d5ccd"
|
||||||
},
|
},
|
||||||
"admin_panel/sms/render.py": {
|
"admin_panel/sms/render.py": {
|
||||||
"mtime": 1791564403.1354272,
|
"mtime": 1791564403.1354272,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3150773,
|
||||||
"ast_hash": "9b82ac87329178259ffa6929b1f0bd94",
|
"ast_hash": "9b82ac87329178259ffa6929b1f0bd94",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "9b82ac87329178259ffa6929b1f0bd94"
|
||||||
},
|
},
|
||||||
"admin_panel/sms/rules.py": {
|
"admin_panel/sms/rules.py": {
|
||||||
"mtime": 1791564642.983444,
|
"mtime": 1791708897.995327,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791710591.5772536,
|
||||||
"ast_hash": "fc8fcea92189cc20de4dc722f309150f",
|
"ast_hash": "ec604f627bcc4534ae16da8503f64085",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"admin_panel/sms/scanner.py": {
|
"admin_panel/sms/scanner.py": {
|
||||||
"mtime": 1791567251.4876964,
|
"mtime": 1791708785.8918655,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791710591.5772536,
|
||||||
"ast_hash": "24a6409edcdd755c353092a56d12af15",
|
"ast_hash": "e9d675f9673e33de929cd9c06fe7de41",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"admin_panel/sms/timing.py": {
|
"admin_panel/sms/timing.py": {
|
||||||
"mtime": 1791567400.39615,
|
"mtime": 1791708745.8135014,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791710591.5772536,
|
||||||
"ast_hash": "675024399edacf343357c3481471fa19",
|
"ast_hash": "4819c52781b07b14c5c19f66a89bea9c",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"admin_panel/views/sms.py": {
|
"admin_panel/views/sms.py": {
|
||||||
"mtime": 1791568027.236827,
|
"mtime": 1791720558.729439,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791721256.78409,
|
||||||
"ast_hash": "dc0f5c6b03f46965c52d60a70661a882",
|
"ast_hash": "c0041541c95e34dd74b4d5439a701f73",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"integrations/__init__.py": {
|
"integrations/__init__.py": {
|
||||||
"mtime": 1788591142.756229,
|
"mtime": 1788591142.756229,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3178282,
|
||||||
"ast_hash": "d41d8cd98f00b204e9800998ecf8427e",
|
"ast_hash": "d41d8cd98f00b204e9800998ecf8427e",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
|
||||||
},
|
},
|
||||||
"integrations/announcements.py": {
|
"integrations/announcements.py": {
|
||||||
"mtime": 1790610911.1591492,
|
"mtime": 1790610911.1591492,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3178282,
|
||||||
"ast_hash": "4addf10259bf16babf4b6ad98fe822bb",
|
"ast_hash": "4addf10259bf16babf4b6ad98fe822bb",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "4addf10259bf16babf4b6ad98fe822bb"
|
||||||
},
|
},
|
||||||
"integrations/bale/__init__.py": {
|
"integrations/bale/__init__.py": {
|
||||||
"mtime": 1788597944.5804,
|
"mtime": 1788597944.5804,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3178282,
|
||||||
"ast_hash": "d41d8cd98f00b204e9800998ecf8427e",
|
"ast_hash": "d41d8cd98f00b204e9800998ecf8427e",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
|
||||||
},
|
},
|
||||||
"integrations/bale/providers.py": {
|
"integrations/bale/providers.py": {
|
||||||
"mtime": 1791366465.5345488,
|
"mtime": 1791366465.5345488,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3178282,
|
||||||
"ast_hash": "f2ccd57a80e8e97b1ab8be7325b1f27a",
|
"ast_hash": "f2ccd57a80e8e97b1ab8be7325b1f27a",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "f2ccd57a80e8e97b1ab8be7325b1f27a"
|
||||||
},
|
},
|
||||||
"integrations/sms/__init__.py": {
|
"integrations/sms/__init__.py": {
|
||||||
"mtime": 1788591146.1142309,
|
"mtime": 1788591146.1142309,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3178282,
|
||||||
"ast_hash": "d41d8cd98f00b204e9800998ecf8427e",
|
"ast_hash": "d41d8cd98f00b204e9800998ecf8427e",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
|
||||||
},
|
},
|
||||||
"integrations/sms/base.py": {
|
"integrations/sms/base.py": {
|
||||||
"mtime": 1789362040.0196111,
|
"mtime": 1789362040.0196111,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3178282,
|
||||||
"ast_hash": "03c0d180df50cb521d7120ee6f76108c",
|
"ast_hash": "03c0d180df50cb521d7120ee6f76108c",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "03c0d180df50cb521d7120ee6f76108c"
|
||||||
},
|
},
|
||||||
"integrations/sms/behin.py": {
|
"integrations/sms/behin.py": {
|
||||||
"mtime": 1788695832.9056084,
|
"mtime": 1788695832.9056084,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3178282,
|
||||||
"ast_hash": "e2028ce13ec6cdc7f603aa44bac7e7fd",
|
"ast_hash": "e2028ce13ec6cdc7f603aa44bac7e7fd",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "e2028ce13ec6cdc7f603aa44bac7e7fd"
|
||||||
},
|
},
|
||||||
"integrations/sms/console.py": {
|
"integrations/sms/console.py": {
|
||||||
"mtime": 1791570192.9150548,
|
"mtime": 1791570192.9150548,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3178282,
|
||||||
"ast_hash": "4120946c1d52e58f0ec2fa110c2442a6",
|
"ast_hash": "4120946c1d52e58f0ec2fa110c2442a6",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "4120946c1d52e58f0ec2fa110c2442a6"
|
||||||
},
|
},
|
||||||
"integrations/sms/factory.py": {
|
"integrations/sms/factory.py": {
|
||||||
"mtime": 1791564358.8557389,
|
"mtime": 1791616908.4313548,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3178282,
|
||||||
"ast_hash": "92d89fa13a0e75ecdc5de5181c5cb13b",
|
"ast_hash": "d02f691930a144885ef755f5229e8299",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "d02f691930a144885ef755f5229e8299"
|
||||||
},
|
},
|
||||||
"integrations/sms/http.py": {
|
"integrations/sms/http.py": {
|
||||||
"mtime": 1791570455.5151258,
|
"mtime": 1791570455.5151258,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3178282,
|
||||||
"ast_hash": "5dbf2f727c47d6bcc8ae067134205317",
|
"ast_hash": "5dbf2f727c47d6bcc8ae067134205317",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "5dbf2f727c47d6bcc8ae067134205317"
|
||||||
},
|
},
|
||||||
"integrations/sms/normalize.py": {
|
"integrations/sms/normalize.py": {
|
||||||
"mtime": 1791564352.0481617,
|
"mtime": 1791564352.0481617,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3178282,
|
||||||
"ast_hash": "e022b15a9a5e16f7cb9b3f7c8aba9eb0",
|
"ast_hash": "e022b15a9a5e16f7cb9b3f7c8aba9eb0",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "e022b15a9a5e16f7cb9b3f7c8aba9eb0"
|
||||||
},
|
},
|
||||||
"integrations/sms/providers.py": {
|
"integrations/sms/providers.py": {
|
||||||
"mtime": 1791571810.6925046,
|
"mtime": 1791616909.5377069,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3178282,
|
||||||
"ast_hash": "0ad971198d05e572b2feab738cf3d315",
|
"ast_hash": "7f0d619716ffdb880cff970e58b81fa6",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "7f0d619716ffdb880cff970e58b81fa6"
|
||||||
},
|
},
|
||||||
"integrations/sms/sms0098.py": {
|
"integrations/sms/sms0098.py": {
|
||||||
"mtime": 1791573482.2129214,
|
"mtime": 1791573482.2129214,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3178282,
|
||||||
"ast_hash": "66945d0a7a930c90522b6ab18a83c950",
|
"ast_hash": "66945d0a7a930c90522b6ab18a83c950",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "66945d0a7a930c90522b6ab18a83c950"
|
||||||
},
|
},
|
||||||
"integrations/voip.py": {
|
"integrations/voip.py": {
|
||||||
"mtime": 1790611362.4278078,
|
"mtime": 1790611362.4278078,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3178282,
|
||||||
"ast_hash": "3d171dd1f3023ad1372d8fc1a1be0c7c",
|
"ast_hash": "3d171dd1f3023ad1372d8fc1a1be0c7c",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "3d171dd1f3023ad1372d8fc1a1be0c7c"
|
||||||
},
|
},
|
||||||
"run.sh": {
|
"run.sh": {
|
||||||
"mtime": 1791570234.3635445,
|
"mtime": 1791617114.3584476,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3199909,
|
||||||
"ast_hash": "a2b5cef99cc254f1f7c811bdf27d01fe",
|
"ast_hash": "776fa3896434b36ecc788f36f0b9a598",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "776fa3896434b36ecc788f36f0b9a598"
|
||||||
},
|
},
|
||||||
"stop.sh": {
|
"stop.sh": {
|
||||||
"mtime": 1791569764.8847363,
|
"mtime": 1791569764.8847363,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3199909,
|
||||||
"ast_hash": "d181c7e91c9fc824e45ee36afe852884",
|
"ast_hash": "d181c7e91c9fc824e45ee36afe852884",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "d181c7e91c9fc824e45ee36afe852884"
|
||||||
},
|
},
|
||||||
"tests/test_sms.py": {
|
"tests/test_sms.py": {
|
||||||
"mtime": 1791571899.7188718,
|
"mtime": 1791720635.36276,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791721256.78409,
|
||||||
"ast_hash": "e09952b55a7aa687607ee5de505e0992",
|
"ast_hash": "797136590c528a8a52e6884b355122d2",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"admin_panel/templates/admin_panel/_customer_sms.html": {
|
"admin_panel/templates/admin_panel/_customer_sms.html": {
|
||||||
"mtime": 1791567700.7075107,
|
"mtime": 1791711840.1595013,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791714098.633808,
|
||||||
"ast_hash": "7a044490275116dc8f47bdfc29410938",
|
"ast_hash": "a310d9aa207143cbe4f705da3f9d5253",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"admin_panel/templates/admin_panel/sms_queue.html": {
|
"admin_panel/templates/admin_panel/sms_queue.html": {
|
||||||
"mtime": 1791564635.2349718,
|
"mtime": 1791720613.7829041,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791721256.7850897,
|
||||||
"ast_hash": "e89844164967411d20f2e74f4aaaa501",
|
"ast_hash": "e6c1ae1d848bc0c7e5ed4984850c7876",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"admin_panel/templates/admin_panel/sms_settings.html": {
|
"admin_panel/templates/admin_panel/sms_settings.html": {
|
||||||
"mtime": 1791571805.9115343,
|
"mtime": 1791719660.6053514,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791720362.5829337,
|
||||||
"ast_hash": "89e5910fc3a560dca5b8253047b4aa6c",
|
"ast_hash": "4f8933e5687929857d93448098e6806e",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"admin_panel/templates/admin_panel/sms_templates.html": {
|
"admin_panel/templates/admin_panel/sms_templates.html": {
|
||||||
"mtime": 1791568503.831997,
|
"mtime": 1791627052.1153367,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3208632,
|
||||||
"ast_hash": "43820b9951c1a89e17388293f3383302",
|
"ast_hash": "46a8ec7859956045631cfa8433461236",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "46a8ec7859956045631cfa8433461236"
|
||||||
},
|
},
|
||||||
"docker-compose.yml": {
|
"docker-compose.yml": {
|
||||||
"mtime": 1791564755.02377,
|
"mtime": 1791636506.4167986,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3208632,
|
||||||
"ast_hash": "45bf00ab0fc7ce59c6fb247c42358e0d",
|
"ast_hash": "0b75a943cb3429658925f450e26d3e7d",
|
||||||
"semantic_hash": ""
|
"semantic_hash": "0b75a943cb3429658925f450e26d3e7d"
|
||||||
},
|
},
|
||||||
"screencapture-localhost-8000-panel-sms-templates-2026-10-09-23_01_18.png": {
|
"screencapture-localhost-8000-panel-sms-templates-2026-10-09-23_01_18.png": {
|
||||||
"mtime": 1791574286.9688969,
|
"mtime": 1791574286.9688969,
|
||||||
"seen": 1791614104.8488438,
|
"seen": 1791702801.3208632,
|
||||||
"ast_hash": "5b1f0abdf5d40f951d02ddbab7167125",
|
"ast_hash": "5b1f0abdf5d40f951d02ddbab7167125",
|
||||||
|
"semantic_hash": "5b1f0abdf5d40f951d02ddbab7167125"
|
||||||
|
},
|
||||||
|
"admin_panel/management/commands/fix_customer_phones.py": {
|
||||||
|
"mtime": 1791621726.410893,
|
||||||
|
"seen": 1791702801.2988422,
|
||||||
|
"ast_hash": "c2b7fabb68143e7ef782b62fbbebd7b0",
|
||||||
|
"semantic_hash": "c2b7fabb68143e7ef782b62fbbebd7b0"
|
||||||
|
},
|
||||||
|
"admin_panel/management/commands/sms_delivery.py": {
|
||||||
|
"mtime": 1791616938.869515,
|
||||||
|
"seen": 1791702801.2988422,
|
||||||
|
"ast_hash": "0eb62705447caeacfb3231489b5c66ed",
|
||||||
|
"semantic_hash": "0eb62705447caeacfb3231489b5c66ed"
|
||||||
|
},
|
||||||
|
"admin_panel/migrations/0063_alter_smsqueue_status.py": {
|
||||||
|
"mtime": 1791614588.9315987,
|
||||||
|
"seen": 1791702801.3128505,
|
||||||
|
"ast_hash": "46ba8d6f3f9ece80d9ee110402f87c04",
|
||||||
|
"semantic_hash": "46ba8d6f3f9ece80d9ee110402f87c04"
|
||||||
|
},
|
||||||
|
"admin_panel/migrations/0064_sms_webservice_delivery.py": {
|
||||||
|
"mtime": 1791616958.7307367,
|
||||||
|
"seen": 1791702801.3128505,
|
||||||
|
"ast_hash": "6bca50002a68d0afb2e347760d033b56",
|
||||||
|
"semantic_hash": "6bca50002a68d0afb2e347760d033b56"
|
||||||
|
},
|
||||||
|
"admin_panel/migrations/0065_smsproviderconfig_test_message.py": {
|
||||||
|
"mtime": 1791618499.3972054,
|
||||||
|
"seen": 1791702801.3128505,
|
||||||
|
"ast_hash": "ad74a954c47d7e0ea78adc662a0b3bed",
|
||||||
|
"semantic_hash": "ad74a954c47d7e0ea78adc662a0b3bed"
|
||||||
|
},
|
||||||
|
"admin_panel/migrations/0066_smsdedup.py": {
|
||||||
|
"mtime": 1791619553.233239,
|
||||||
|
"seen": 1791702801.3128505,
|
||||||
|
"ast_hash": "47a79ea36779cdad4f8f7a417f6e406f",
|
||||||
|
"semantic_hash": "47a79ea36779cdad4f8f7a417f6e406f"
|
||||||
|
},
|
||||||
|
"admin_panel/migrations/0067_customer_notes.py": {
|
||||||
|
"mtime": 1791621805.5383422,
|
||||||
|
"seen": 1791702801.3128505,
|
||||||
|
"ast_hash": "32682104a2a4b50ca62272af04ac1dfb",
|
||||||
|
"semantic_hash": "32682104a2a4b50ca62272af04ac1dfb"
|
||||||
|
},
|
||||||
|
"admin_panel/migrations/0068_auto_20261010_1305.py": {
|
||||||
|
"mtime": 1791624904.5148928,
|
||||||
|
"seen": 1791702801.3128505,
|
||||||
|
"ast_hash": "2a0a7a25845704a7ed78aa5d4236e4ce",
|
||||||
|
"semantic_hash": "2a0a7a25845704a7ed78aa5d4236e4ce"
|
||||||
|
},
|
||||||
|
"admin_panel/migrations/0069_smstemplate_auto_send.py": {
|
||||||
|
"mtime": 1791627084.5738328,
|
||||||
|
"seen": 1791702801.3128505,
|
||||||
|
"ast_hash": "11ab014ce619013bb94a610c920e501c",
|
||||||
|
"semantic_hash": "11ab014ce619013bb94a610c920e501c"
|
||||||
|
},
|
||||||
|
"admin_panel/sms/notices.py": {
|
||||||
|
"mtime": 1791720564.6536124,
|
||||||
|
"seen": 1791721256.78409,
|
||||||
|
"ast_hash": "5d5eb4733a3930448ee2039140936ec1",
|
||||||
|
"semantic_hash": ""
|
||||||
|
},
|
||||||
|
"admin_panel/sms/phone_cleanup.py": {
|
||||||
|
"mtime": 1791622431.750531,
|
||||||
|
"seen": 1791702801.3150773,
|
||||||
|
"ast_hash": "43ce6a9e4caa2637a25985415fb4cae0",
|
||||||
|
"semantic_hash": "43ce6a9e4caa2637a25985415fb4cae0"
|
||||||
|
},
|
||||||
|
"integrations/sms/sms0098_codes.py": {
|
||||||
|
"mtime": 1791616840.122996,
|
||||||
|
"seen": 1791702801.3178282,
|
||||||
|
"ast_hash": "b6471cf64bb71a5516e9570ed092f6a7",
|
||||||
|
"semantic_hash": "b6471cf64bb71a5516e9570ed092f6a7"
|
||||||
|
},
|
||||||
|
"integrations/sms/sms0098ws.py": {
|
||||||
|
"mtime": 1791617713.8571854,
|
||||||
|
"seen": 1791702801.3178282,
|
||||||
|
"ast_hash": "4646af2624034410d9f3a37c3177abbc",
|
||||||
|
"semantic_hash": "4646af2624034410d9f3a37c3177abbc"
|
||||||
|
},
|
||||||
|
"0098sms.md": {
|
||||||
|
"mtime": 1789361433.0,
|
||||||
|
"seen": 1791702801.3208632,
|
||||||
|
"ast_hash": "be32d185f057513a3685621c73b9352f",
|
||||||
|
"semantic_hash": "be32d185f057513a3685621c73b9352f"
|
||||||
|
},
|
||||||
|
"CLAUDE.md": {
|
||||||
|
"mtime": 1791614843.2875943,
|
||||||
|
"seen": 1791702873.4684987,
|
||||||
|
"ast_hash": "d9c1aa00474b45b2de5f587fe328cb8d",
|
||||||
|
"semantic_hash": "d9c1aa00474b45b2de5f587fe328cb8d"
|
||||||
|
},
|
||||||
|
"admin_panel/templates/admin_panel/_customer_phones.html": {
|
||||||
|
"mtime": 1791713591.3869643,
|
||||||
|
"seen": 1791714098.633808,
|
||||||
|
"ast_hash": "50587dd6e8714f0a77a67c9c6476404e",
|
||||||
|
"semantic_hash": ""
|
||||||
|
},
|
||||||
|
"fix_customer_phones.md": {
|
||||||
|
"mtime": 1791622725.5216887,
|
||||||
|
"seen": 1791702801.3208632,
|
||||||
|
"ast_hash": "da981f26faa780317b4d763f955fef93",
|
||||||
|
"semantic_hash": "da981f26faa780317b4d763f955fef93"
|
||||||
|
},
|
||||||
|
"admin_panel/migrations/0070_sms_types_seven.py": {
|
||||||
|
"mtime": 1791709618.641997,
|
||||||
|
"seen": 1791710591.5772536,
|
||||||
|
"ast_hash": "596a225d3884b10e9eb089cb7162f482",
|
||||||
|
"semantic_hash": ""
|
||||||
|
},
|
||||||
|
"admin_panel/sms/triggers.py": {
|
||||||
|
"mtime": 1791710210.1741562,
|
||||||
|
"seen": 1791710591.5772536,
|
||||||
|
"ast_hash": "3ca8ed084f8c407cc6416cb79994b497",
|
||||||
|
"semantic_hash": ""
|
||||||
|
},
|
||||||
|
"admin_panel/templates/admin_panel/_customer_info.html": {
|
||||||
|
"mtime": 1791713645.1335497,
|
||||||
|
"seen": 1791714098.633808,
|
||||||
|
"ast_hash": "7097ae80aef5846f82a20aece2084586",
|
||||||
|
"semantic_hash": ""
|
||||||
|
},
|
||||||
|
"admin_panel/templates/admin_panel/customerSmsHistory.html": {
|
||||||
|
"mtime": 1791710836.1307735,
|
||||||
|
"seen": 1791714098.633808,
|
||||||
|
"ast_hash": "52189e9b058fcf0ffd8439d643ff6f8a",
|
||||||
|
"semantic_hash": ""
|
||||||
|
},
|
||||||
|
"admin_panel/management/commands/recalc_last_bazdid.py": {
|
||||||
|
"mtime": 1791716465.206951,
|
||||||
|
"seen": 1791720362.5829337,
|
||||||
|
"ast_hash": "72df3ebb464b770d9ca91ddc2ca32404",
|
||||||
|
"semantic_hash": ""
|
||||||
|
},
|
||||||
|
"admin_panel/migrations/0071_sms_settings_permission.py": {
|
||||||
|
"mtime": 1791719590.1329694,
|
||||||
|
"seen": 1791720362.5829337,
|
||||||
|
"ast_hash": "9e87942eb05e2d30028f0dc865191412",
|
||||||
|
"semantic_hash": ""
|
||||||
|
},
|
||||||
|
"admin_panel/sms/parts.py": {
|
||||||
|
"mtime": 1791717280.6100771,
|
||||||
|
"seen": 1791720362.5829337,
|
||||||
|
"ast_hash": "6a1bbc04fda9c4cb784ae2f02e98f879",
|
||||||
|
"semantic_hash": ""
|
||||||
|
},
|
||||||
|
"admin_panel/templates/admin_panel/sms_report.html": {
|
||||||
|
"mtime": 1791718419.8521838,
|
||||||
|
"seen": 1791720362.5829337,
|
||||||
|
"ast_hash": "b5da47a8870d5bf470b933956160f9c5",
|
||||||
|
"semantic_hash": ""
|
||||||
|
},
|
||||||
|
"recalc_last_bazdid.md": {
|
||||||
|
"mtime": 1791716578.4375794,
|
||||||
|
"seen": 1791720362.5829337,
|
||||||
|
"ast_hash": "29e04949da66478a11268a7672440f2b",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
# بازمحاسبهٔ «آخرین بازدید» مشتریان
|
||||||
|
|
||||||
|
## چرا؟
|
||||||
|
|
||||||
|
فیلد `Customer.last_bazdid_dt` یعنی «آخرین بازدیدی که برای مشتری انجام شده» و صفحهٔ
|
||||||
|
`/panel/entezar/` بر پایهٔ آن مشتریانِ **معوق** (بیش از N ماه بازدید نشده) را لیست
|
||||||
|
میکند.
|
||||||
|
|
||||||
|
قاعدهٔ درست (منبع حقیقت): این مقدار باید برابر **بزرگترین `finishDateTime` میان
|
||||||
|
اقدامهای `actionType='bazdid'` و `status='finish'` همان مشتری** باشد — دقیقاً همان
|
||||||
|
چیزی که ویوهای `customerNewAction` و `customerEditAction` مینویسند:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if newAction.status == 'finish' and newAction.actionType == 'bazdid':
|
||||||
|
customer_obj.last_bazdid_dt = newAction.finishDateTime
|
||||||
|
customer_obj.save()
|
||||||
|
```
|
||||||
|
|
||||||
|
پس اگر رکوردی از راهی **غیر از این ویوها** وارد شود (اسکریپت، ایمپورت، ویرایش دستی
|
||||||
|
دیتابیس، اجرای دستی `gt_esh` و…)، این فیلد عقب میماند و مشتریِ واقعاً
|
||||||
|
بازدیدشده، اشتباهاً «معوق» شمرده میشود. این دستور آن واگرایی (drift) را از روی
|
||||||
|
جدول `Action` ترمیم میکند.
|
||||||
|
|
||||||
|
> دستور **idempotent** است؛ اجرای دوبارهاش هیچ تغییری ایجاد نمیکند.
|
||||||
|
|
||||||
|
## دستور
|
||||||
|
|
||||||
|
نام دستور منیجمنت: `recalc_last_bazdid`
|
||||||
|
مسیر فایل: `admin_panel/management/commands/recalc_last_bazdid.py`
|
||||||
|
|
||||||
|
### ۱) اول «شبیهسازی» — بدون هیچ تغییری
|
||||||
|
|
||||||
|
فقط گزارش میدهد چه چیزی تغییر میکند. **قبل از هر اجرای واقعی این را بزنید.**
|
||||||
|
|
||||||
|
از WSL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/Scripts/python.exe manage.py recalc_last_bazdid --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
از Windows:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.venv\Scripts\python.exe manage.py recalc_last_bazdid --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
### ۲) اجرای واقعی
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/Scripts/python.exe manage.py recalc_last_bazdid
|
||||||
|
```
|
||||||
|
|
||||||
|
## گزینهها
|
||||||
|
|
||||||
|
| گزینه | پیشفرض | کار |
|
||||||
|
|---|---|---|
|
||||||
|
| `--dry-run` | خاموش | فقط گزارش؛ **هیچ رکوردی ذخیره نمیشود**. اول این را بزنید. |
|
||||||
|
| `--clear-orphans` | خاموش | مشتریانی که مقدار دارند ولی **هیچ اقدام بازدیدِ تمامشدهای** پشتشان نیست را هم `NULL` میکند. بهطور پیشفرض دستنخورده میمانند. فقط اگر مطمئنید این ستون آلوده است. |
|
||||||
|
| `--batch-size` | `500` | اندازهٔ دستهٔ خواندن مشتریان در هر گام. |
|
||||||
|
|
||||||
|
## دقیقاً چه میکند؟
|
||||||
|
|
||||||
|
برای هر مشتری، `MAX(finishDateTime)` اقدامهای بازدیدِ تمامشده محاسبه میشود، سپس:
|
||||||
|
|
||||||
|
| وضعیت مشتری | اقدام دستور |
|
||||||
|
|---|---|
|
||||||
|
| بازدید دارد و مقدار فعلی **برابر** MAX است | دست نمیزند (`از قبل درست`) |
|
||||||
|
| بازدید دارد و مقدار فعلی **کوچکتر یا NULL** است | به MAX **بهروز** میکند — از لیست معوقها خارج میشود (`جدیدتر شد`) |
|
||||||
|
| بازدید دارد و مقدار فعلی **بزرگتر از MAX** است | به MAX **عقب** میبرد (`عقبتر شد`) |
|
||||||
|
| بازدید ندارد و مقدار `NULL` است | کاری ندارد (طبیعی) |
|
||||||
|
| بازدید ندارد ولی مقداری دارد | **دست نمیزند** — مگر `--clear-orphans` بدهید (`خالی شد`) |
|
||||||
|
|
||||||
|
نوشتن با `Customer.objects.filter(pk=…).update(last_bazdid_dt=…)` داخل یک
|
||||||
|
`transaction.atomic()` انجام میشود؛ یعنی **فقط همین یک ستون** لمس میشود و هیچ
|
||||||
|
`save()` کاملی که فیلدهای دیگر (`auto_now` و…) را تغییر دهد اجرا نمیشود.
|
||||||
|
|
||||||
|
## نمونهٔ خروجی
|
||||||
|
|
||||||
|
```
|
||||||
|
بازمحاسبهٔ آخرین بازدید مشتریان آغاز شد.
|
||||||
|
|
||||||
|
گزارش بازمحاسبهٔ آخرین بازدید:
|
||||||
|
بررسیشده (کل مشتریان): 10,426
|
||||||
|
دارای بازدیدِ تمامشده: 9,151
|
||||||
|
از قبل درست: 9,084
|
||||||
|
نیازمند اصلاح: 67
|
||||||
|
→ جدیدتر شد (خروج از معوق): 67
|
||||||
|
→ عقبتر شد: 0
|
||||||
|
بدون اقدام ولی دارای مقدار (دستنخورده): 0
|
||||||
|
NULL و بدون اقدام (طبیعی): 1,275
|
||||||
|
|
||||||
|
انجام شد: 67 رکورد بهروزرسانی شد.
|
||||||
|
```
|
||||||
|
|
||||||
|
## اجرا روی production
|
||||||
|
|
||||||
|
**اول از دیتابیس بکاپ بگیرید**، سپس:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python manage.py recalc_last_bazdid --dry-run
|
||||||
|
python manage.py recalc_last_bazdid
|
||||||
|
```
|
||||||
|
|
||||||
|
دستور تراکنشی است و در صورت خطا، هیچ تغییری کامیت نمیشود.
|
||||||
|
|
||||||
|
## بررسی صحت داده (SQL، پیش از اجرا)
|
||||||
|
|
||||||
|
اگر میخواهید بدون دستور، وضعیت را بسنجید:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- مشتریانی که مقدارشان با آخرین بازدید واقعی اختلاف دارد (تعداد «نیازمند اصلاح»)
|
||||||
|
SELECT COUNT(*) FROM admin_panel_customer cu
|
||||||
|
JOIN (SELECT customer_id, MAX(finishDateTime) mx FROM admin_panel_action
|
||||||
|
WHERE actionType='bazdid' AND status='finish' GROUP BY customer_id) a
|
||||||
|
ON a.customer_id = cu.id
|
||||||
|
WHERE cu.last_bazdid_dt IS NULL OR cu.last_bazdid_dt <> a.mx;
|
||||||
|
|
||||||
|
-- مشتریانی که بازدید تمامشده دارند ولی مقدارشان NULL است (بدترین حالت)
|
||||||
|
SELECT COUNT(*) FROM admin_panel_customer cu
|
||||||
|
WHERE cu.last_bazdid_dt IS NULL
|
||||||
|
AND EXISTS (SELECT 1 FROM admin_panel_action a
|
||||||
|
WHERE a.customer_id=cu.id AND a.actionType='bazdid' AND a.status='finish');
|
||||||
|
|
||||||
|
-- تعداد سالم (مقدار دارد)
|
||||||
|
SELECT COUNT(*) FROM admin_panel_customer WHERE last_bazdid_dt IS NOT NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
## نکتههای مهم
|
||||||
|
|
||||||
|
- مقادیر `NULL` مشتریانی که **هرگز** بازدید تمامشده نداشتهاند، **طبیعی** است و
|
||||||
|
دستور آنها را پر نمیکند (اگر پر کند، یعنی داده ساختن — که غلط است).
|
||||||
|
- دستور **هیچ فاکتور، سند، اقدام یا مشتری نمیسازد و حذف نمیکند**؛ فقط یک ستون را
|
||||||
|
همراستا میکند.
|
||||||
|
- صفحهٔ `/panel/entezar/` یک کش در `CacheTable(key='9mahbazdid')` نگه میدارد؛ عددِ
|
||||||
|
روی صفحه ممکن است یک اجرا عقب باشد. هر بار باز شدن صفحه آن را بهروز میکند.
|
||||||
|
- منبع حقیقت همیشه جدول `Action` است، نه این ستون. در صورت شک، همان کوئریهای بخش
|
||||||
|
«بررسی صحت داده» ملاک هستند.
|
||||||
|
```
|
||||||
+89
-1
@@ -4,7 +4,7 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from admin_panel.models import Article, Customer, PhoneNumber
|
from admin_panel.models import Article, Customer, Factor, PhoneNumber
|
||||||
|
|
||||||
from .factories import login_superuser, make_customer, make_user
|
from .factories import login_superuser, make_customer, make_user
|
||||||
|
|
||||||
@@ -55,6 +55,94 @@ class CustomerCrudTests(TestCase):
|
|||||||
self.assertEqual(phones[0].phone_number, '09120009999')
|
self.assertEqual(phones[0].phone_number, '09120009999')
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerInfoCardUITests(TestCase):
|
||||||
|
"""کارت پروفایل مشتری (ستون راست): ساختار حرفهای، ویرایش و تعیین شمارهٔ اصلی.
|
||||||
|
|
||||||
|
این تستها قرارداد ویوها/قالبها را قفل میکنند تا بازطراحی UI به ویرایش مشتری
|
||||||
|
یا تعیین «شمارهٔ اصلی پیامک» آسیب نزند.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = login_superuser(self.client, 'cardUser')
|
||||||
|
|
||||||
|
def _customer_page(self, customer):
|
||||||
|
return self.client.get(
|
||||||
|
reverse('admin_panel:customer', kwargs={'c_id': customer.id}),
|
||||||
|
).content.decode('utf-8')
|
||||||
|
|
||||||
|
def test_profile_card_structure_is_rendered(self):
|
||||||
|
customer = make_customer('مشتری کارت', phones=['09120000000'])
|
||||||
|
html = self._customer_page(customer)
|
||||||
|
for needle in ('has-background-white brad5', 'id="formcsEdit"',
|
||||||
|
'dynamic-phones-edit', 'شمارههای تماس', 'لغو بازدید'):
|
||||||
|
self.assertIn(needle, html, msg=needle)
|
||||||
|
|
||||||
|
def test_template_comments_never_leak_into_the_page(self):
|
||||||
|
"""رگرسیون: کامنت چندخطی {# #} جنگو معتبر نیست و بهصورت متن چاپ میشود.
|
||||||
|
|
||||||
|
قالب کارت پروفایل باید از ``{% comment %}`` استفاده کند؛ این تست تضمین میکند
|
||||||
|
هیچ متن کامنتی در HTML نهایی دیده نشود.
|
||||||
|
"""
|
||||||
|
customer = make_customer('مشتری کامنت', phones=['09120000000'])
|
||||||
|
html = self._customer_page(customer)
|
||||||
|
for leaked in ('تکمنبع', 'DRY', 'شناسههای موردنیاز', 'کارت پروفایل مشتری —'):
|
||||||
|
self.assertNotIn(leaked, html, msg=f'متن کامنت در صفحه لو رفته: {leaked}')
|
||||||
|
|
||||||
|
def test_edit_controls_present_and_single_form(self):
|
||||||
|
customer = make_customer('مشتری ویرایشکارت', phones=['09120000000'])
|
||||||
|
html = self._customer_page(customer)
|
||||||
|
for needle in ('id="formcsEdit"', 'id="enEdit"', 'id="editCs"',
|
||||||
|
'id="cancelEdit"', 'id="showUserDT"'):
|
||||||
|
self.assertIn(needle, html, msg=needle)
|
||||||
|
# نباید فرم تودرتو ساخته شود؛ فقط یک فرم ویرایش در ستون وجود دارد.
|
||||||
|
self.assertEqual(html.count('id="formcsEdit"'), 1)
|
||||||
|
|
||||||
|
def test_primary_toggle_present_next_to_phone(self):
|
||||||
|
customer = make_customer('مشتری اصلیکارت')
|
||||||
|
PhoneNumber.objects.create(customer=customer, phone_number='09120000000')
|
||||||
|
html = self._customer_page(customer)
|
||||||
|
self.assertIn('sms-primary-toggle', html)
|
||||||
|
self.assertIn('data-phone="09120000000"', html)
|
||||||
|
self.assertIn(
|
||||||
|
'data-url="%s"' % reverse('admin_panel:sms_primary_toggle',
|
||||||
|
kwargs={'c_id': customer.id}),
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_card_also_renders_on_legacy_base_page(self):
|
||||||
|
"""صفحهٔ جزئیات فاکتور (customerFactor) از customerBase.html استفاده میکند."""
|
||||||
|
factor = Factor.objects.create(customer=make_customer('مشتری فاکتورکارت'),
|
||||||
|
amount=1000, pay_status='unpaid')
|
||||||
|
html = self.client.get(
|
||||||
|
reverse('admin_panel:customerFactor',
|
||||||
|
kwargs={'c_id': factor.customer_id, 'f_id': factor.id}),
|
||||||
|
).content.decode('utf-8')
|
||||||
|
self.assertIn('has-background-white brad5', html)
|
||||||
|
self.assertIn('id="formcsEdit"', html)
|
||||||
|
|
||||||
|
def test_edit_persists_phones_and_primary_selection(self):
|
||||||
|
"""ویرایش مشتری با markup جدید باید شمارهها و انتخاب «اصلی» را ذخیره کند."""
|
||||||
|
customer = make_customer('مشتری ویرایشنهایی', phones=['09120000001'])
|
||||||
|
url = reverse('admin_panel:customerEdit', kwargs={'c_id': customer.id})
|
||||||
|
response = self.client.post(url, {
|
||||||
|
'an_name': 'مشتری ویرایشنهایی', 'an_address': 'قم',
|
||||||
|
'installDate': '1401/02/03', 'idnCode': '555',
|
||||||
|
'laghv_bazdid': 'true', 'notes': 'یادداشت',
|
||||||
|
'phone_titles': ['موبایل'], 'phones': ['09120009999'],
|
||||||
|
'phone_primary': ['0'],
|
||||||
|
})
|
||||||
|
self.assertIn(response.status_code, (200, 302))
|
||||||
|
customer.refresh_from_db()
|
||||||
|
self.assertEqual(customer.name_family, 'مشتری ویرایشنهایی')
|
||||||
|
self.assertTrue(customer.laghv_bazdid)
|
||||||
|
self.assertEqual(customer.notes, 'یادداشت')
|
||||||
|
phones = list(PhoneNumber.objects.filter(customer=customer))
|
||||||
|
self.assertEqual(len(phones), 1)
|
||||||
|
self.assertEqual(phones[0].phone_number, '09120009999')
|
||||||
|
# شمارهٔ تیکخورده بهعنوان «اصلی» ذخیره شده است.
|
||||||
|
self.assertEqual(customer.sms_primary_phones, '+989120009999')
|
||||||
|
|
||||||
|
|
||||||
class InactiveCustomerHiddenTests(TestCase):
|
class InactiveCustomerHiddenTests(TestCase):
|
||||||
"""مشتری غیرفعال (is_active=False) در فهرست و در جستجو دیده نمیشود."""
|
"""مشتری غیرفعال (is_active=False) در فهرست و در جستجو دیده نمیشود."""
|
||||||
|
|
||||||
|
|||||||
+760
-124
File diff suppressed because it is too large.
Load diff
@@ -7,7 +7,7 @@
|
|||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
# snapshot قطعی نامهای مسیرهای اپ admin_panel (۷۲ مسیر)
|
# snapshot قطعی نامهای مسیرهای اپ admin_panel (۷۷ مسیر)
|
||||||
EXPECTED_URL_NAMES = {
|
EXPECTED_URL_NAMES = {
|
||||||
'index',
|
'index',
|
||||||
'p_login',
|
'p_login',
|
||||||
@@ -76,10 +76,12 @@ EXPECTED_URL_NAMES = {
|
|||||||
'sms_settings',
|
'sms_settings',
|
||||||
'sms_templates',
|
'sms_templates',
|
||||||
'sms_template_edit',
|
'sms_template_edit',
|
||||||
|
'customerSmsHistory',
|
||||||
'sms_customer_toggle',
|
'sms_customer_toggle',
|
||||||
'sms_primary_toggle',
|
'sms_primary_toggle',
|
||||||
'sms_send_custom',
|
'sms_send_custom',
|
||||||
'sms_queue',
|
'sms_queue',
|
||||||
|
'sms_report',
|
||||||
'sms_queue_retry',
|
'sms_queue_retry',
|
||||||
'sms_queue_approve',
|
'sms_queue_approve',
|
||||||
'sms_queue_delete',
|
'sms_queue_delete',
|
||||||
@@ -124,6 +126,7 @@ PARAMETERIZED = {
|
|||||||
'addSarresid': {'cs_id': 1},
|
'addSarresid': {'cs_id': 1},
|
||||||
'pardakhtNesiye': {'cs_id': 1},
|
'pardakhtNesiye': {'cs_id': 1},
|
||||||
'sms_template_edit': {'t_id': 1},
|
'sms_template_edit': {'t_id': 1},
|
||||||
|
'customerSmsHistory': {'c_id': 1},
|
||||||
'sms_customer_toggle': {'c_id': 1},
|
'sms_customer_toggle': {'c_id': 1},
|
||||||
'sms_primary_toggle': {'c_id': 1},
|
'sms_primary_toggle': {'c_id': 1},
|
||||||
'sms_send_custom': {'c_id': 1},
|
'sms_send_custom': {'c_id': 1},
|
||||||
|
|||||||
@@ -224,7 +224,9 @@ class ViewSmokeTests(TestCase):
|
|||||||
'phone': '09120000000', 'is_primary': 'true',
|
'phone': '09120000000', 'is_primary': 'true',
|
||||||
}),
|
}),
|
||||||
('sms_send_custom', {'c_id': c.id}, POST, {'text': 'پیامک دودی'}),
|
('sms_send_custom', {'c_id': c.id}, POST, {'text': 'پیامک دودی'}),
|
||||||
|
('customerSmsHistory', {'c_id': c.id}, GET, None),
|
||||||
('sms_queue', {}, GET, None),
|
('sms_queue', {}, GET, None),
|
||||||
|
('sms_report', {}, GET, None),
|
||||||
('sms_queue_retry', {'q_id': self.sms_queue.id}, POST, None),
|
('sms_queue_retry', {'q_id': self.sms_queue.id}, POST, None),
|
||||||
# در انتهای لیست: خروج از حساب، چون بعد از آن تمام درخواستها بیاعتبار میشوند
|
# در انتهای لیست: خروج از حساب، چون بعد از آن تمام درخواستها بیاعتبار میشوند
|
||||||
('p_logout', {}, GET, None),
|
('p_logout', {}, GET, None),
|
||||||
|
|||||||
Reference in new issue
Block a user