This commit is contained in:
javad committed 2026-10-11 19:04:20 +03:30
1 parent 1dea3509a7
commit e3a6d1f9d4
42 files changed
+33084 -15777

No files matched your search

+66
View File
@@ -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
View File
@@ -1,11 +1,11 @@
Project Root: khatibi
Generated At: 2026-10-10T06:32:47+00:00
Python Files: 153
Generated At: 2026-10-11T12:20:45+00:00
Python Files: 171
Django Files: 12
JavaScript/TypeScript Files: 14
HTML Files: 54
Total Files: 523
Total Directories: 74
HTML Files: 58
Total Files: 682
Total Directories: 76
khatibi
├── .cursor
@@ -20,6 +20,10 @@ khatibi
│ │ └── graphify.js
│ └── opencode.json
├── .run
│ ├── delivery.pid
│ ├── scanner.pid
│ ├── web.pid
│ └── worker.pid
├── admin
│ ├── migrations
│ │ └── __init__.py
@@ -34,8 +38,11 @@ khatibi
│ ├── management
│ │ ├── commands
│ │ │ ├── __init__.py
│ │ │ ├── fix_customer_phones.py
│ │ │ ├── gt_esh.py
│ │ │ ├── recalc_last_bazdid.py
│ │ │ ├── send_daily_report.py
│ │ │ ├── sms_delivery.py
│ │ │ ├── sms_scan.py
│ │ │ ├── sms_worker.py
│ │ │ └── sqlite_db_backup.py
@@ -103,6 +110,15 @@ khatibi
│ │ ├── 0060_sms_permission_label.py
│ │ ├── 0061_sms_timing_and_primary.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
│ ├── models
│ │ ├── __init__.py
@@ -118,12 +134,16 @@ khatibi
│ ├── sms
│ │ ├── __init__.py
│ │ ├── enqueue.py
│ │ ├── notices.py
│ │ ├── parts.py
│ │ ├── phone_cleanup.py
│ │ ├── phones.py
│ │ ├── recipients.py
│ │ ├── render.py
│ │ ├── rules.py
│ │ ├── scanner.py
│ │ └── timing.py
│ │ ├── timing.py
│ │ └── triggers.py
│ ├── static
│ │ └── admin_panel
│ │ ├── DataTables
@@ -208,6 +228,8 @@ khatibi
│ │ ├── customer
│ │ │ ├── customerNazarInc.html
│ │ │ └── nazar.html
│ │ ├── _customer_info.html
│ │ ├── _customer_phones.html
│ │ ├── _customer_sms.html
│ │ ├── ActionList.html
│ │ ├── base.html
@@ -234,6 +256,7 @@ khatibi
│ │ ├── customers.html
│ │ ├── customerSarresid.html
│ │ ├── customersBK.html
│ │ ├── customerSmsHistory.html
│ │ ├── entezar.html
│ │ ├── entezarBK.html
│ │ ├── financial_dashboard.html
@@ -253,6 +276,7 @@ khatibi
│ │ ├── reportCheckNesiye.html
│ │ ├── rezayat.html
│ │ ├── sms_queue.html
│ │ ├── sms_report.html
│ │ ├── sms_settings.html
│ │ ├── sms_templates.html
│ │ ├── store.html
@@ -299,19 +323,45 @@ khatibi
│ │ ├── graph.json
│ │ ├── GRAPH_REPORT.md
│ │ └── 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
│ │ ├── ast
│ │ │ └── v0.9.82-s7
│ │ │ ├── 0075bdcd95747f7c750eba8757614aab71d4d8c77bd6764e4c5a98fe18dc8997.json
│ │ │ ├── 0175e34128b5ac5119a5da4cc53d889dce334d8f20357631d54627c53cae9b8e.json
│ │ │ ├── 01850b7fad89dcb9e521fceed2c41e64e9909a0f4c4a54d818d346ccf63bbc5d.json
│ │ │ ├── 0348dd4662ebd96572421864ce77a266334a327f8bcbdc237d54c313d116bafb.json
│ │ │ ├── 03c6fb41770a9293658a5db5af36f7e4afe64ae04b3ce8cc6c5874320706fd22.json
│ │ │ ├── 0400cc0eadd706ff70ef627ffe39f401fc72a591dcbcd5463fca07fc271f080d.json
│ │ │ ├── 0442dd7e8445ba796e02aaefde16760c8ed37f63359d4319498be512f60266bd.json
│ │ │ ├── 056f0cb55bfd5a1ddf744c7c824c342048c1db47f3bb350030a1533ab56bf8d7.json
│ │ │ ├── 05b753e15288fbd02b721b00cfd03c3a523e9e234420330b980a462a1de2dad6.json
│ │ │ ├── 07eb6ef411f8e9b78d19cfd5f262bfc9fc482701f0834c8936cbb5ba2b14efab.json
│ │ │ ├── 0b48631fc75356bef276f694d7db5b457bd2315410fd4538680612fe10b95eb2.json
│ │ │ ├── 0b8f8e3b6e4d84256e784dabb96f5646fa6e7295adbd20648c235e7821b4ca1d.json
│ │ │ ├── 0e6bfa570caca1f79f549f5cdc37f8bdf9c9b43a023ca28a8fcb83351389b399.json
│ │ │ ├── 100f37182d082767395fcd09f1ec937fd74e57ed039885843bf81d349c3fc1f5.json
│ │ │ ├── 10615f5a999e04257afc03cdc52e1b9e4e11ae7818d7c64ae13b976e3974feb4.json
│ │ │ ├── 11042a6b38137d4e725c18d536cde9c98652782cb58390aa6216dacad2d18cf5.json
│ │ │ ├── 1285cdf9b89689e3ac99cea19470a3c14fdbf3a9d1bb2cd8da2cfdbd42ca940f.json
│ │ │ ├── 128b2b616c097fbfbcbc3fddafc224df0919ba261bb5ded7bcafa8b445fe118b.json
│ │ │ ├── 12d922593c75865b8068057f9244ad61a5ee573b7f3f38a16c31fa83abb9d668.json
│ │ │ ├── 13a39530fbb076496d510363b40bf16093d8d117751e7240bcc09a447bdace35.json
│ │ │ ├── 13ca1f37c3eea3c07228d2d98d47f7db6a9c1d8468b259651f3fea5419651569.json
│ │ │ ├── 14f64f9b156b6b520e7af189b303a2844728f3ab8bbe1e65fed49160b1431cb2.json
│ │ │ ├── 15be5091a5bfbf8f48c3f3ae344a980be5452eab00f054c001958aae96557514.json
│ │ │ ├── 174e406c2649f2384921581cbf912945a95d54aab73707b9aa45752aaa7c54d7.json
│ │ │ ├── 183467d5c1ee7daac78dfb428bd335c17e17272bd4510358f38fc5924140c816.json
@@ -319,47 +369,88 @@ khatibi
│ │ │ ├── 1d5a29a9dabb365c079039c788988636b66c3a4bd2dbe66b249df001c2b5396f.json
│ │ │ ├── 1d9f15f04104af07c60349f21bcd9f6f2c0e65c915e3e9c69040a10197b0fa6f.json
│ │ │ ├── 1dd0c6958a307a1e4e59c24f20225292277585dd8652b35537ede94fc04807ee.json
│ │ │ ├── 20652a8d348bf514ac266f30a6400f923674dae819d18046d008066943377804.json
│ │ │ ├── 21af53aa7322868a25b47458e59f339a8f6e58b7914fbe8e3389ab86e0b0b643.json
│ │ │ ├── 22310d4d49f2beee611c83915933ea06e80ec562fd92c170345c556cd3772561.json
│ │ │ ├── 2356cb9f067d43739312459bad2cb6d9ecbacac703e7e40a282d5b89d64332b3.json
│ │ │ ├── 23da13b308596ede7678e92e57041bcecdd8e9baff04b759ddf9a69803dd790a.json
│ │ │ ├── 23f4e3a62e03a38743659d94dd1350d17775da8be9175846e4ff3f38fa87c515.json
│ │ │ ├── 24508e7a9b6fa93cdd28b9436bdb55fa24ad8f324baa95adb3afd37e301af9ac.json
│ │ │ ├── 2501a9885d6fa5292276ee8cc7db63a9a3c08605f38d199d425d33f650dac35e.json
│ │ │ ├── 25dbf169bbd8ae50f7cc568b4dbb7d259eb993ccddc9dbb20db96018ed093f5a.json
│ │ │ ├── 28df8bbd275806fd77c086629211231bd1329fe8156f521b19b5c5979f0d3b51.json
│ │ │ ├── 294e2d390f56acc0445564d428d90c641d76496cbc457bcfb09876382e9d87ab.json
│ │ │ ├── 2976c530804df2075019fedaf2b908eeeff37b2703310c7e031b653d1fec71e5.json
│ │ │ ├── 2c587e304ae13b0717d72f5099508220c82234a8a293219c2ed2c97df9152404.json
│ │ │ ├── 2f36d1565911fdcb414da716162085b0fdc4b92d9d2d3fbb9fecc403b1916766.json
│ │ │ ├── 3089c169c5eb18e61b13a19751166813d18c7bbe35b938b2b149406ff349feda.json
│ │ │ ├── 3258762ed4d4077750967504aecb42df00c35a69571f5d4839d4cfebd9b1c838.json
│ │ │ ├── 3330ae8555dab8f7ff4e014183f014681feb3f40a0d8969f30a4b2e9658e27f8.json
│ │ │ ├── 338e7d5d2e7d3a7260ab74d9b7b19bbab6b9ce84e7036623060190d9e0cc4bcd.json
│ │ │ ├── 33b32e27be802f61375110f7f4ac9419fd4eb633523e01e31774b874448849c1.json
│ │ │ ├── 3614ae46baf3e1b6efa17011c37407a5d2931aba1886d8f8bdf8e4e154ab6c3f.json
│ │ │ ├── 370c10e9d6f01aa67799b1f2191d5749a69ce4541aa55cbf4a28d45732bd641b.json
│ │ │ ├── 38980449465279974120694e1c667f229049f75d656fe518196d9cadc2429155.json
│ │ │ ├── 3ba0d4c75ab8bfa5bc2a60dccb74197cb94bd161e526e54770a9b0a2aa757806.json
│ │ │ ├── 3c9f8987dc7426b442fe2a90c7de5d909fc85988e172e7722c11f24a2c331481.json
│ │ │ ├── 3ff644065147b53af40ea703202c0fd832114936fe67e6c9a30d15e3da915455.json
│ │ │ ├── 40e7fbe5e765171201ab8016acce76a28304e1339d9aeb5a9cc18da87c5604d2.json
│ │ │ ├── 4162977f2c2251e7a8b331d7229bd43dff56df31356191b19886b5bf5d56377d.json
│ │ │ ├── 41952e7c846da15fe658603ff001dafdb0e12d3372a7f0306adfa3a71de8bee7.json
│ │ │ ├── 4204828f3cbb3465c469e28345e0fc4d7ff9a84e30e3dd9f00ab4dbe27014e0d.json
│ │ │ ├── 458c1b9a3ce257e789f75305939ab2e79029c557657102eebf2082e50a47a0e5.json
│ │ │ ├── 45f7e59ef3f291171180b6763e8da408d8b7579bb7cb96dd4ca859e48aa5ce14.json
│ │ │ ├── 46616763804b2b938d73c75f7651158e94803e0b86b3a759ecdd59375759eea3.json
│ │ │ ├── 46bde576f6f1e69457e4ad33238fdbd602d2a7f027644a84cb81068215fbd5e1.json
│ │ │ ├── 4da7df08a0ef803551d404438fe24d6c8cc11d9a2c3cc8d85e37d5228ba3a166.json
│ │ │ ├── 4e24d52f79dfa76aa2406de05583fa49ce966a28fb653fa284fb8e8204797385.json
│ │ │ ├── 4e604dd03a70463ae25f6615ea3ce4fe8e98fab15d6b11ddafd51d601062e84d.json
│ │ │ ├── 506e815cbeaaf3aa6e448dcbf70c10b4d118dff41e05a292fb8ec6d76dd391c4.json
│ │ │ ├── 507e42a0bbe7a582ce1fcb77651e1cfde643907ca0429fedfe11afe7ae3b11c4.json
│ │ │ ├── 52608b276c6f0a2f190186e5bac578715ad888ff5ba3c10ff30879c3baf9d9ce.json
│ │ │ ├── 527de91bd8bbc5a7ab79951457a9f192909faa9bbf02aaec72bb3d26e2298b57.json
│ │ │ ├── 5316515ee7fc3e1038e676b361d9028edc735993c3d34791a77a14503deb064d.json
│ │ │ ├── 53bcc38a002a291b8a370a1c24dde42e704cbda5d409d77b8405599e45844cae.json
│ │ │ ├── 5730d72cc177474584197921cce656696e3177780028c4629950f5514b530c6d.json
│ │ │ ├── 57a18b5350b4a567305ca01b2951efee6eeb3b289b5ffbbf390c3962ff29d61c.json
│ │ │ ├── 58ad02e0930221b4f44603dbcf01d3b5d07d9da2772edafe48a94a9894ea148a.json
│ │ │ ├── 58bfae9cf8467d2bb9363884d517e98cae38a934f4c474e9e02aed520fdaab95.json
│ │ │ ├── 58f2e3419fb2de4a3bdc4a9c0bf48e50f2cc225506096109dbf2b9bfe4778bb5.json
│ │ │ ├── 5901400fc36893a9ad85acd97eb73b691cb4b8d1440047c5f85c1534a2c7e492.json
│ │ │ ├── 59dd2e5089bebfb0397e9c492c890c619fcaa4d73b9d035e533320f276e1ebf9.json
│ │ │ ├── 5a15a341e370fb512c0b3f5ae1c30ec0d8a6729a690f286369015d107210124e.json
│ │ │ ├── 5b59421d9713dc21f9f77603d9708edd8c3eb8ba198636f80f1cedcc79ffcaca.json
│ │ │ ├── 5b63b5d7fc7a09b2dbf5e2f7db6fe00ccab773762b3291bd666c8e29d32100d3.json
│ │ │ ├── 5c8593cf08ce32f0c569aacfaf83b8baeb352934f6f8749aa4f652d5ce2acff1.json
│ │ │ ├── 5c922734de6f6345b5230507bee5c43dc284c308901e84846666b939493e6258.json
│ │ │ ├── 5ce9ee869c507986bf00ae6929f3aae3ab7aa58b5c2bded4965374fcf7f5654b.json
│ │ │ ├── 5d5b6249e3cfa0aeee94ba2053fc73b4b7b5b8fa1d21df09d40bda7c90ef273d.json
│ │ │ ├── 5d994876cad71133f4bf804932fb2604ffdc8bb8cc434ca1434667ef789144e7.json
│ │ │ ├── 5e19aa3aba5509cd48a9a93b39db071cfc2c2728ccae46b34caa0e214cc6be01.json
│ │ │ ├── 5f050da3769e151e52138ee1ea5dcbcb6160cb57952c75665e607628d0e08d67.json
│ │ │ ├── 60002a01d6fc22147e2994d1a1a447d5e577c02aed2443789af20571ec2d2a19.json
│ │ │ ├── 60e00560038df059b1696e54dcf055cd658677d6e1201570a8500531addc0ac6.json
│ │ │ ├── 62d88411a61ee9d3b60dda6c4febf33539b8b9e8226b5d2b9205f8e2b0c4478a.json
│ │ │ ├── 63c38f2a8abe98c0d1e9cb5b7d9fd0451e1e46c2bcbf1d2c3b241b263ac0a9cd.json
│ │ │ ├── 6448d65fdcfe330b5d4278c8d1540e7b075238672b8101f9d2e1c4052f7c9c49.json
│ │ │ ├── 64fc04d8c553b2121a71955d48257160a40f6dd34258e3287945873f02d897dc.json
│ │ │ ├── 65d5139792e0ee840c5b1710d7659ecfb5dbdcadbd02d205f7517a6a3a374211.json
│ │ │ ├── 6603c5eb7403f7426c429341d43018fdffcb3430f22972c426ac43956d229b33.json
│ │ │ ├── 6648bc2b25d0bd25846d48f406d3e9a6f89677542600e9f3f57533747ed9b08c.json
│ │ │ ├── 6691dea01045e5561ac1c72f902f96ba84564c221498829443f793fee8784021.json
│ │ │ ├── 6805914035b171d32a1b80948179da15f78c5741e5a80b216d51a6586185c0c3.json
│ │ │ ├── 6876f75a9cb4a5f5857c47e3b0d6b38e51941aff76bc6d92cb1b4eef1c51071e.json
│ │ │ ├── 6a88f82e5b9f9da12b50f8531eac30c10e1c2a0cf39601d275fd90ee3e28da55.json
│ │ │ ├── 6aca679f90689aa4c275389c8b4e2462a9456775e8b124071070af2c988ca0fc.json
│ │ │ ├── 6afa6136d9df8317bdd8d49f4109412776deb21ba123fdd2a97331b400a0230d.json
│ │ │ ├── 6bfbb9eed6842bf331dec78724d7e740919c76345b54be844964f44640482a87.json
│ │ │ ├── 6d4d7d7f829f13e46e4898aceb760d66d2255dc838eab0933f9a92d720f420a7.json
│ │ │ ├── 6df9c1e88224e95c55dee4238fa91358360231ee47362294e884bc6e24d73612.json
│ │ │ ├── 6e262456570c174e207055c7c964fe85431addb0f59298b1e4d912500dfb23a3.json
│ │ │ ├── 6e2ed39911a83c75e432ffd0dab22fd095ce8c5bb186d6ab22d594375f3b32bc.json
│ │ │ ├── 6ed362dbf43e46eeb440246d52fdc4e0783c895114249ea33f3de7ddf6898a3a.json
│ │ │ ├── 71a07328d56403d55714d76cefc05db3e68c6498719e629e4e76889a1940efc2.json
│ │ │ ├── 72577fde1eae8cf079aff518cecce910ac310cefcc038fbbcb0a5d11f20fa20d.json
│ │ │ ├── 73a129efd08cc38274f3c56503c234eef17f58faf74dadbdbbe475efcbfc2777.json
│ │ │ ├── 7483cdf00a2d7de8809278d9dcdf2f2330354b02117952da4b9452d4bbd6b81e.json
│ │ │ ├── 74efb913c62b374d818cfc5af6d7ee9bb46966eb2bce0e26e0bba461683b98a7.json
@@ -369,107 +460,176 @@ khatibi
│ │ │ ├── 767dd58146a5d4a713611561a9e5c1b706e5f509a5621891c9b2e195f3b8ee0a.json
│ │ │ ├── 77a1520156879de714ee872c29b1387967bcc5ce470b47d37087ea2a110f3875.json
│ │ │ ├── 780977bca042ffdde1cca2811a3a46c36bd3fcff2723c94ac527428113131c64.json
│ │ │ ├── 784dd791c21e8837a3e925331cf234b2138ded3d2012968739604d5535a18e09.json
│ │ │ ├── 7be7f428e6acfe4c99370707cc7de9253f801feff1564088f2584d5dc8c42a89.json
│ │ │ ├── 7bf404104bb32fc04568c2d287891fa69955ac3f46718c99dcca64a4bfff6fe3.json
│ │ │ ├── 7c4fc90402a7836b364425c33cc2ffc98e7634391ebe78143df03c88945d8988.json
│ │ │ ├── 7d106c54b917d9e26357fa71aa871e94cb99c940b6c48808e93cd57415da91e3.json
│ │ │ ├── 7d59da18a8b644e48a164eb8a764efc9f4c3538b99d41c8e93ce38cc5556ba05.json
│ │ │ ├── 7e9bad82d0eb642b739dcf11e86c2fcf2005dfb12c94478af143deb9a8129bfa.json
│ │ │ ├── 8055bf9fe36a9fa183d0c5dc152684e65410d58c5634e544e8f38f6c9216f6ad.json
│ │ │ ├── 81d6d00c4c975b3289ac212d355856c3d6e60bccde0780fe2e3fc8cac31f3c19.json
│ │ │ ├── 82833f6235cb6559bad38e93372d42284eb4e9097e90519337266113da2495bf.json
│ │ │ ├── 828dacee98d6f71fcf53c8a051b3bf1bfc9d94c272a79ed8539801e3fb90b3df.json
│ │ │ ├── 837801d7d0e218e49de562a7e03711e75941d42ce5cc2f77552bc6293a45a4e8.json
│ │ │ ├── 84281ba94e70a71d52b5179cf2e933f8412da3a9eaf5e7177b815f8fadd0afc5.json
│ │ │ ├── 846ba1087a1ee6b5cda2ebdf22d0bc61ad9175b21aba29783429277288dbfaaf.json
│ │ │ ├── 84cef5656079ececae35930e1b26620186b50f25fd977c569537daad2d59db86.json
│ │ │ ├── 86e57806cd08cf765ce1f56f0638dd321e93c653bb6613aa35d7ad375808433c.json
│ │ │ ├── 881adf7185e68d203906537117de576a11fba814941cf9eeedd0dfac2d65e536.json
│ │ │ ├── 88e349e879d601e1f5c75f3b019ffb4958e90639317ba893c54f82b0cc9066c2.json
│ │ │ ├── 8a70e1324d7f834d2c41a7c3b4cbb3b8ccad92d413970316bc95e13f5132ff76.json
│ │ │ ├── 8d658a3a8b1ae03cbc881e3c47cf2cfe111f9be2b564fb3739cf3d9367845a8b.json
│ │ │ ├── 8ffb83ce6f13d2f97d0197c7381748e9b66956c13f0158bc519350b7dd1b9ad0.json
│ │ │ ├── 908e0129cefe129d0b562243460fe20793c84876244cef2e99ca1becf8cf6a1f.json
│ │ │ ├── 909129924b48dede801eb946cf98bb93003ab8116be2b6acbd2f7fe8dc166358.json
│ │ │ ├── 92ecdeb228d91f4adb6c1e9bfd1b3f0eea65faec8dbcb27fca3211d1d264cb46.json
│ │ │ ├── 940d0349821b0fb7f2c3effcb81fd13bb1c046daa6f6c5f2cd1b157058c55877.json
│ │ │ ├── 957f37be1d5d6a5207f90ad07704f8423c915c5bcdfcd130acc85a388fc89d19.json
│ │ │ ├── 95e88ece49092778e7709f6b87db4c05dd055b838cfdf20d7d4705b6373739ba.json
│ │ │ ├── 96c3ba545db191ae5811d5dfdde74e7c6396a932b759513c94160cb7b7c81fc8.json
│ │ │ ├── 96f5469edd566b92d9d38423a27779142fbe2b77cfc9d85005d43ffc99ce3de1.json
│ │ │ ├── 9766508e5a013c3d3cbb2602719391199e90af5720c9782a77ca71565aa346b1.json
│ │ │ ├── 9880af8b991c19abe5ed019ad7b7291b8b281ed9dcb894a00c3fd8259a59b4a4.json
│ │ │ ├── 996411678d23c3cf8a0e6c1c5076701e8bf5f3dad21ca907ec4116d15366a668.json
│ │ │ ├── 9c2c2f6f8d4d4c3a9a60da3ed38a26dc789e8cfaff5d46d140d2f208e5fef3c3.json
│ │ │ ├── 9cfb6e4f7b08a863675af032e7823c0523170f5201e686c5ef72b39c04b07a12.json
│ │ │ ├── 9eabdb0cacba38c8fb6b3e777932e06bd82172cdc0bcccf2fc0bfc23b5bd4974.json
│ │ │ ├── a186e74ebd87e23212ffe7fd72c40e9383c0624a3efdb2fe1e3271d9ea96d9b9.json
│ │ │ ├── a31ab0e36b30049a602cc060d77c0715187f7c3838db91b8c20bfaa802f49899.json
│ │ │ ├── a43d68ff03b49e9153a73777c0bde346a6b1969f640d67eae500d60d410a02c0.json
│ │ │ ├── a7c605f57dd37c8725a92f08d4fe18701628701fa986cbb98d3a4aeb3a74abbe.json
│ │ │ ├── a823c9c28027c8d525714e6571bc3f3ca38f3bd0420aaf4f4921067cf0672475.json
│ │ │ ├── a8311f6e51027ea4606e35bf14d50edc1795e71b2d63cddeae8c7bb7ece215b8.json
│ │ │ ├── aabf6dbb2202699bb1b06fd582c88465b9cf2b8cc060bda2e2bc0c67fc493bed.json
│ │ │ ├── add004e78a62164d7565c102eb825debea20811f7fe76afa0567a1b6b289f264.json
│ │ │ ├── aeaae7b60339b1283cab517b4ab42806c64933c272d007a68431988ba8e0edb5.json
│ │ │ ├── af128ca7fc8d02ae40fff36925a5b96034862ce0d581e22a8a0c88f41040e526.json
│ │ │ ├── b26c22033963df1727407c1fa6c79d8241bfc16056e7a8d3c4383fb2d050d3cd.json
│ │ │ ├── b26e251bde84a040dbb2d1a4f700ebcd761f51a124a98a104704e82537e320eb.json
│ │ │ ├── b34cb0a1a28e753a900b3d23f01d3810f9c06635b0650d34be4ec9d98daa8fbb.json
│ │ │ ├── b543952627caaf0a5c52bec09b33fae93443fca7234365680bbe75acc260dd1c.json
│ │ │ ├── b59999c2fd322c241c7bd37e6ed0a95c9124cbb79f8424f1fa7cb2e92fe63817.json
│ │ │ ├── b82d5f6840b4930eb62fdb1c7b3a58124fff805e529ede7684516a65322fcae0.json
│ │ │ ├── b9f8ea31056fe0c0fc40a84640c58f29895312c65bf694567487204d8f1170e9.json
│ │ │ ├── bb140cfe12cfb3e32dc9815393035fc33c7649c2faeeccb82cdee56a930ea635.json
│ │ │ ├── bc24ddb05a8aa685edc1376cbd110423adf05bcc488fb97a30fe286e7e74c2da.json
│ │ │ ├── bc6f93c7938b3565d50cdfc4abe4fd62f0739fd6299f35d7c50075cd860cab58.json
│ │ │ ├── bcea7c584e5031a9c5734144f8169d016ef1ed316e6f4827609509ba2a79d3eb.json
│ │ │ ├── bd00f0b0db2eb10f7e889df5a0f08994e8c1eaa0eb69be8398e45e0e634724fd.json
│ │ │ ├── bd6e156d8e427b14676b4707710f2b340026e679cb7bd29b22b247a4b637e7bd.json
│ │ │ ├── be1200d7c9b2881ba54c7fa95daf5f3e2ce993619b974c7760a9d44a416e00c0.json
│ │ │ ├── be72e639c12e860a2120c3ecf96b94c86e5c69b2f1f0715cd2804d0475409cd6.json
│ │ │ ├── c12baf851ec40542280cb62d2f878d5e7383357365281938cfb55a64c08a9a10.json
│ │ │ ├── c36b696b57c1deb51e47a057b32d1dd75e3f1560e83931aab1f8ba86abcc1543.json
│ │ │ ├── c46ad3fc8fd3db1315ab2e40add760b44e8f98c24c79e334c342c6a411fc671c.json
│ │ │ ├── c52300dce4cf464158f8db56df5030359a501a81bbe4a5f36e50c8c38a5ac48c.json
│ │ │ ├── c535c8a7523b768d28903b84f4cfbce7d62181a2a05f3db08ec3a7c8cbf86ce8.json
│ │ │ ├── c56beb43cf063fd1cc92950ca14695b543b3971bf7e7dbd4a676242057258533.json
│ │ │ ├── c5c93ccc7715c31fc7538bcd9327c8fb7c1e495bd17d6444b384ec426b7a231f.json
│ │ │ ├── c72226544540cc75217ba02f1d09d989eb810d879409ac086c76245f27220cfa.json
│ │ │ ├── c795f43bd1f10cf666ff987d18af538b2164c1683629ac689f0a54cb875118be.json
│ │ │ ├── c7eb97fa16701c6d9ee531fa032479a7c771292f3e9d8921d3ea12a4897fd43a.json
│ │ │ ├── c846b5de286163b5eed16ff6d6e00f2038f30bf7edd2ab3f8d831369e190ae18.json
│ │ │ ├── c850d65440fa6ea38db7300c89c5052b3db430ea2c7d20d35cf38c6b41807d0d.json
│ │ │ ├── c8719ca6bbbd7a4815ce5f982745c94d54d2f753312c08b96964ab7ead661291.json
│ │ │ ├── ca0c19553fc1b40bdec0973b3e0c5363d6bbbf69d9961567483ba1c1cb9023eb.json
│ │ │ ├── cb3b2a1c1afd8d0837cc29b5040ec984dc0c0070af2135d0b0e5cb5ff726f2a7.json
│ │ │ ├── cdc5059cf09416d24d03fe57a48af5219727866c84b8109d986456fbaa789dc4.json
│ │ │ ├── cf17cbc72af5f2b56eff6cc03157817d7552c6c5e4565e97278e02ae69a362f3.json
│ │ │ ├── cf6668c54cd4f65d268a17df44f100e1cb77f91eae90dd2dc052564731b2f195.json
│ │ │ ├── d1289d5406d360e3ff617e213db4e262cd43a32711a61aba97bdcd0a906cbcb5.json
│ │ │ ├── d12cd2b31145213be1b4d89e7be64f3aecc122841cdeac7660eceae3dd871f01.json
│ │ │ ├── d12dccf07d58d7c0a2b5ea5b6aa821aa419fdc8b34a48939ebf9c565bc439551.json
│ │ │ ├── d1f2090208e1aab3b0e377278b02e9fc1007e5e1724c9000fb69d050365c8660.json
│ │ │ ├── d3456cd690135f9630e6b0bb79fef5d205804c7acabffc61cbd5ac4032b17bc7.json
│ │ │ ├── d51b2eb414ee4ea432987cd45d6b623013e994a6080c89cde57b69004b576e9d.json
│ │ │ ├── d5676c133bc21ec531e7c17c5e9416781e3459852a4e6e1fac9c77ecdb5c05a2.json
│ │ │ ├── d606b856d99eb59fe3e5378e597bbeff679c812e9f1b173ecced4639ede8ad11.json
│ │ │ ├── d88e1e365cafd5d26ce47c2a0fe1f067dc3bfe5ceed1f75a21393fae950be939.json
│ │ │ ├── d899e8cc575f125b908d78f0c8d34408a4af1972ee0544d0517f8d1933239214.json
│ │ │ ├── d94bebfe941bb0acc30b633423eaee11690652709e5f8f28d157473d31a07ee3.json
│ │ │ ├── db68b8db5217e2295eb1195760d7352960627da044a4c4f8f6b89c67ad2f8f57.json
│ │ │ ├── db794bbcafe219af962673ef26efb1ee1eacf3923e13113352f3eedfd3192d23.json
│ │ │ ├── dbbdad9d992922aff62ad3262a531716cfc32e2f15f600773b9a0cf10545058d.json
│ │ │ ├── dc0344515d1b3ffe8643560516b88418be0b306fba56101b8a630ed73d48db46.json
│ │ │ ├── dcad55d51bb4494a642af555d5bfe78c5b8ee3d11de1b3d8f4286d608980854b.json
│ │ │ ├── e08c4784f0f82403ff24a1676e3d75e11a208aadf8fe94a723461c9658afd1e0.json
│ │ │ ├── e16f78807ff202fa2c81867b321acfacede2d24cc785856eef0633d8d352796b.json
│ │ │ ├── e1a7be59904a3dc503c5b12bddc7604c6783b02462c8ec76834c65c5610c3bda.json
│ │ │ ├── e2967fb2aed9420b2cdf823d95f971b7038323c97a6ed139ce63b7a1912381d1.json
│ │ │ ├── e2c62f6012dcae176d19340f6db8db3e94547ebbc4a21577b63caf35cc72ecfb.json
│ │ │ ├── e3ee8d9942c74a74be83ca00d5438e4a1e1cb5b23f55d95d308a42954e4757f0.json
│ │ │ ├── e86dc6bb7703d4329623bd33ccbc5e5e6c6daebf8d36d84fd415d6f868c779b0.json
│ │ │ ├── e8e380264b41f789dddd25b7af5d0e3c517b957a14fe8ba2ce7bb8a529e005cf.json
│ │ │ ├── e9589ded446a5befb285a0a2e93a182ca9431c76661e68c9210ed252580dd7f0.json
│ │ │ ├── e9e14d581ed59799b2d83c67402e8555b5b59e7d49425950ec490422bd0307d9.json
│ │ │ ├── eaceb29a907e3e67bed5255cc1f8b8b3eab9d9946f4d4df2cc99070151285029.json
│ │ │ ├── ec07e022005d3513838eee7e6618ff060e25d914cab7745aa6172f22d4089497.json
│ │ │ ├── eccad1f007b52643ce8d81576efb7d1969ecbff2e9db288989fd9a9c21fd7288.json
│ │ │ ├── ecd9c1dbfa0a72e253adb680ae7088eff24c26360e53a65862b274ea1281ceae.json
│ │ │ ├── ee450e6251e62d8972efb53bc35145c4a65050a7b6db2954b84bda42fca7b704.json
│ │ │ ├── ee557c6ed214858651cd3ebceb5e0fd00999354a4c54bb379f26c64b48b509ab.json
│ │ │ ├── efd113025c3fb3c0becaecd8090e8494b13d92ff3c49220d54665b3a8af586af.json
│ │ │ ├── f081ba494afddbf02c2599aedee9fc9d38ccdb159ade7f07ce3bdad3323a001e.json
│ │ │ ├── f1af209a02f2ec5c5a344e3fa7dd8ad7d443e0479e55f5465449c0a2eed314a1.json
│ │ │ ├── f3a6285dcf9eb39df500bc0b76b03b5941103e8b90c06ffaeab1b3129f18fedc.json
│ │ │ ├── f3fe81220df98d0410b1505fa995df65427035dd552c85f1d80e35bb75a9aba1.json
│ │ │ ├── f48decaf1220666fbda1a5328b963dbd999caab9ebbb1d84459d4c556a6e6ba7.json
│ │ │ ├── f537b36e2ee33d1303c10e950a6910cc7fd954c7fce3220be2c95d75d70dfd40.json
│ │ │ ├── f72f6f3fe9d226e4717e158dedd318fff0a1d52ece54f965311c2b4f862dcfee.json
│ │ │ ├── f76726d2898f2a3ba7168d3fb77c21087432eb9c200631b664ecb3b1837aaf17.json
│ │ │ ├── f92b94479afedf47039d8821a77facb55d9a862338e85876b80cac3b41c844f6.json
│ │ │ ├── fb6825c4e9ffba13d17a2da18a1e07aef792089a7cd24b31e7f4f32ee39287e8.json
│ │ │ ├── fbfc351c35c5272de5612b4740e380b06a24c37f8074b977e35f62ad85a6dcc0.json
│ │ │ ├── fc6ae63ae2639372c4ae4fbe9718feca814261dda099ba58bb780d14c69086cd.json
│ │ │ ├── fd714e03c631694d30457ac529d05d8d131c183665e086246da607f752dea5eb.json
│ │ │ ├── fe7f3f3c4b234398f7c0fb728456ac41c92c6e939634e147d2fdda12067df842.json
│ │ │ ├── fe8b7b639a9f98fd1b883b7dccf8490ee905a2a21df5671ca1838a173fa0f921.json
│ │ │ └── ff862e0ffad7bba911716072ae82f268240d82105ffefe78c7714a160c4c31ba.json
│ │ ├── semantic
│ │ │ └── p5e80268fecd6
│ │ │ ├── 0be9cb2f924f20c8cd09e0361a90c8f0b4c8859120101bbe21eee930294ebdfd.json
│ │ │ ├── 0c4f87b180cd5b0576b197ff5cb0e9cc5dfd8d82d97d618036df00c7955fdbac.json
│ │ │ ├── 14328edb08c286b631c2ef0ead8ac33b522157be84855de346e8c0688e30c965.json
│ │ │ ├── 14e03f8f9b4ebd7cf018188e2351ca859aa4e27d02d5dbfc7b1f8b932d282d16.json
│ │ │ ├── 212a2dc6dcc30c1b45f6cf5bfa9bf16386fa138bf382bbde438f8d6883769b29.json
│ │ │ ├── 2679b1fb1647af24f007334e6560733c957ca3cf3831eb02a9c8fb7a7ae44d62.json
│ │ │ ├── 296d532aced4658e74a296977efe4fd8a2ec3cf02a997ce3837f59ce36e61343.json
│ │ │ ├── 2a13d2bb6f18a81d9511481df0edcdb1a8779e1ec03e919991693cae7e84c8e0.json
│ │ │ ├── 2ac09852870126bb1792a5e9242b34d3f55365bfa6e3b09aa432461a6c72045e.json
│ │ │ ├── 2d3b8c0bb7eea6a50a24093abfc3457c0938079b0771b2568498060f6f3a4a74.json
│ │ │ ├── 40373bd2bcd76ed8ed27031299e541005d4c8c0c7df312a9312f390b173d7248.json
│ │ │ ├── 42a95a4a8f651dbebe297ce92beca376cb19eab99d6ef6b5d10a16d79d1842e4.json
│ │ │ ├── 4f53e6b5fc48381f8afe6052371989c4df69347c84c2ec65a46b73cd1c464b5f.json
│ │ │ ├── 538cdc2fb71798c581f20d37d52a1484c2ef21691d2e01ee79edcf253ba06394.json
│ │ │ ├── 569db52f8b9229dc3cd5cbde156a4ac012ab711d32bdeb4175b60ba863ac8cd0.json
│ │ │ ├── 57dba5c2a37beb12ccc107807b7544f687d6d8ba6394c8977c3c59b433d2dab4.json
│ │ │ ├── 5ad5f87fc2eb132405e3aab9ef8a84e41f76c958fbcb188873c86abd4fbc12fd.json
│ │ │ ├── 5c0f2ea11f5bdcc5272d6344c9015c20bf79f315d74a910d56bf7a2922c92315.json
│ │ │ ├── 5dbe87f1a5fa172581ed5abebee35b5d08ff488e798a334992a2756bff277cdc.json
│ │ │ ├── 6aaa93a769a41eb1d37a91608cd6f98b9252c2a5598bd859a130986429d1d53f.json
│ │ │ ├── 7b504b4173cb8d683a7021270372c5c65f078ac3e3e1351601bedfec9c585632.json
│ │ │ ├── 7edbffbd4455d071df034927a69faaf76f8e8bbf6495921ce2b70eea1abe8e4f.json
│ │ │ ├── 82db37687728a8df60ccf231c06838b4a0074d22ad93e89d3f5e93d4de48132f.json
│ │ │ ├── 8a79c80fca0ed04957e7b4df749ab80b5d0f4ce3d0ae032a47d51d4a2fbfcd82.json
│ │ │ ├── 8f60bda631cbe7457886d1d574b2e3d2850034f469592a3e8c333f0623a2d3c6.json
│ │ │ ├── 8b6c538ab1a901fb71decc7f7f511c8c67a76d72f05f02b1bddc10f5026f9e4e.json
│ │ │ ├── 96f5469edd566b92d9d38423a27779142fbe2b77cfc9d85005d43ffc99ce3de1.json
│ │ │ ├── 9897fa1997a41e155ed8de922722238e1f684f5c77a7f56405c8ac7cd1aa376f.json
│ │ │ ├── 9b674bec71dfe6e133af9fb4e35a421fa56cecd230544008ed83a44e6eaae3bf.json
│ │ │ ├── a3f3d3cf99dd02b4d8c41297d4634e120953f1650257c0c6a1888481aa4c5745.json
│ │ │ ├── aa41b77ce8563f4b4bee015be5ffa598f1c98d4f2854ec9a465f271e8c3f974b.json
│ │ │ ├── b5f6ef338a857f51d581abe7c50f57f41757aa6900bf94f6ed8fc2ad442f6019.json
│ │ │ ├── be1200d7c9b2881ba54c7fa95daf5f3e2ce993619b974c7760a9d44a416e00c0.json
│ │ │ ├── c109dbf29a8233cec6f0e4e3c1256a94ecee99887756af2060e620aaa5979e4b.json
│ │ │ ├── c1d9e0aa5d7450242681eebd003d4f502744a3624112b10ab091011a50820ce3.json
│ │ │ ├── c89f13bada10536a104ce57788562563a61c2682430e4273ca3195890d45c68c.json
│ │ │ ├── c97d49fed39b9aca8dea35a49c3e031a641420e4452e61d1ea2a912e899f6dd9.json
│ │ │ ├── cacf6773249daafca21d21004106209d86aa215140de071c64363aff4d9e5431.json
│ │ │ ├── cfb57017bc764ee7e9011b52f1e44ad2f41b2670778af73232ecfc309b406a0d.json
│ │ │ ├── dc89fcddf8c6f20bc9f52ae15e5cc8b226320d2776dc8f27be16894e53165013.json
│ │ │ ├── dd78862d0f7862653c702e86f7acfabfe33ec5d85d40ba764138c0d1784f7b06.json
│ │ │ ├── de67f63e3f49d4ee32811dd67676da33a1e584f671d396fca130f3681d54bd76.json
│ │ │ ├── e0a52ba8204140f195e8d30847dda00d3de5439cf903c576b77fac1ec852b5fb.json
│ │ │ ├── e86dc6bb7703d4329623bd33ccbc5e5e6c6daebf8d36d84fd415d6f868c779b0.json
│ │ │ ├── e9ca16b55daa5d6bdf6b0257a4e08c35caeae32eaaeb5c0be11532b77ab658ea.json
│ │ │ ├── ea11661eb53bce4c7a43acb2d636de01d960acdad246c1f1372df351e25bac25.json
│ │ │ ├── eaceb29a907e3e67bed5255cc1f8b8b3eab9d9946f4d4df2cc99070151285029.json
│ │ │ ├── f71a50c524a31da468cf7c0c08d929d4d31bf17e77473db59b5830f4c583ce25.json
│ │ │ ├── f7fdf480e35ed096b57529bfcafd8695aff9a98851050be1f00e4a736ad79719.json
│ │ │ └── fb1977d392a5009580c7a608fd403ac2ae96c72f6f2835ba6e49cf662dd0bf6d.json
@@ -484,12 +644,10 @@ khatibi
│ │ │ ├── 2a13d2bb6f18a81d9511481df0edcdb1a8779e1ec03e919991693cae7e84c8e0.json
│ │ │ ├── 2d3b8c0bb7eea6a50a24093abfc3457c0938079b0771b2568498060f6f3a4a74.json
│ │ │ ├── 32267f72ce9ef01cd2e804d43c6828549af8eb6f868c76e7d1619502a953f14d.json
│ │ │ ├── 40373bd2bcd76ed8ed27031299e541005d4c8c0c7df312a9312f390b173d7248.json
│ │ │ ├── 40a84c5465882009ab69d2874adbb9aaed373cd706d70c92e9e1f4c35640472e.json
│ │ │ ├── 4280dd1094314da773fe73ba1ffb91f58f22eea26281bd5cb51f5e9a201c95f6.json
│ │ │ ├── 42a95a4a8f651dbebe297ce92beca376cb19eab99d6ef6b5d10a16d79d1842e4.json
│ │ │ ├── 4b8f3b6c0ccb3b5494fb2af8c0f9892b6b198a8d503f53498bd1fe2548ef112a.json
│ │ │ ├── 4f53e6b5fc48381f8afe6052371989c4df69347c84c2ec65a46b73cd1c464b5f.json
│ │ │ ├── 538cdc2fb71798c581f20d37d52a1484c2ef21691d2e01ee79edcf253ba06394.json
│ │ │ ├── 569db52f8b9229dc3cd5cbde156a4ac012ab711d32bdeb4175b60ba863ac8cd0.json
│ │ │ ├── 57dba5c2a37beb12ccc107807b7544f687d6d8ba6394c8977c3c59b433d2dab4.json
@@ -497,15 +655,12 @@ khatibi
│ │ │ ├── 60002a01d6fc22147e2994d1a1a447d5e577c02aed2443789af20571ec2d2a19.json
│ │ │ ├── 6aaa93a769a41eb1d37a91608cd6f98b9252c2a5598bd859a130986429d1d53f.json
│ │ │ ├── 7101777f271bbc6bce9a2a4fd3bcf487bb62a846a2436bc8c70008bfb23ce33b.json
│ │ │ ├── 7b504b4173cb8d683a7021270372c5c65f078ac3e3e1351601bedfec9c585632.json
│ │ │ ├── 7db83b9e384d80a1584b152f9fbc781e0ae5a7524dac0a57ce98bb4d3432bfae.json
│ │ │ ├── 7edbffbd4455d071df034927a69faaf76f8e8bbf6495921ce2b70eea1abe8e4f.json
│ │ │ ├── 8a79c80fca0ed04957e7b4df749ab80b5d0f4ce3d0ae032a47d51d4a2fbfcd82.json
│ │ │ ├── 8f60bda631cbe7457886d1d574b2e3d2850034f469592a3e8c333f0623a2d3c6.json
│ │ │ ├── 908e0129cefe129d0b562243460fe20793c84876244cef2e99ca1becf8cf6a1f.json
│ │ │ ├── 92e1d82ed4571249c09163413ceae360dd4d7d03c162c554f3b5a47e94474cf1.json
│ │ │ ├── 9897fa1997a41e155ed8de922722238e1f684f5c77a7f56405c8ac7cd1aa376f.json
│ │ │ ├── 9b674bec71dfe6e133af9fb4e35a421fa56cecd230544008ed83a44e6eaae3bf.json
│ │ │ ├── a3f1dac3e797de04ab83d263a12eda947904d8e5015b77363db2d29aba8b4928.json
│ │ │ ├── aa41b77ce8563f4b4bee015be5ffa598f1c98d4f2854ec9a465f271e8c3f974b.json
│ │ │ ├── b205a6dda565ed51a38a6b75c509f41589425f0c2006aad838978f5dc6810b91.json
@@ -522,7 +677,6 @@ khatibi
│ │ │ ├── dc89fcddf8c6f20bc9f52ae15e5cc8b226320d2776dc8f27be16894e53165013.json
│ │ │ ├── dd78862d0f7862653c702e86f7acfabfe33ec5d85d40ba764138c0d1784f7b06.json
│ │ │ ├── de67f63e3f49d4ee32811dd67676da33a1e584f671d396fca130f3681d54bd76.json
│ │ │ ├── e0a52ba8204140f195e8d30847dda00d3de5439cf903c576b77fac1ec852b5fb.json
│ │ │ ├── e13320e9716b0be66b7cc677a64b44cf3127f7317f35cfa3b849dfbaa9c1f367.json
│ │ │ ├── e1c07127e4cef79fe0bfc63a0ab904e5d1b0d9c4be5278b553b6e48d39753b4d.json
│ │ │ ├── f0478454ef37ecdc5128db97c18d0ffd92005c1d1db586b6750d985e30a25d76.json
@@ -558,7 +712,9 @@ khatibi
│ │ ├── http.py
│ │ ├── normalize.py
│ │ ├── providers.py
│ │ └── sms0098.py
│ │ ├── sms0098.py
│ │ ├── sms0098_codes.py
│ │ └── sms0098ws.py
│ ├── __init__.py
│ ├── announcements.py
│ └── voip.py
@@ -581,16 +737,20 @@ khatibi
│ ├── test_sms.py
│ ├── test_urls_contract.py
│ └── test_view_smoke.py
├── .clinerules
├── .dockerignore
├── .gitignore
├── .graphifyignore
├── 0098sms.md
├── AGENTS.md
├── check_finance.py
├── CLAUDE.md
├── db.json
├── docker-compose.yml
├── Dockerfile
├── entrypoint.sh
├── financial_audit_20260816_151225.csv
├── fix_customer_phones.md
├── GEMINI.md
├── ghost_hunter.py
├── khatibi.zip
@@ -598,6 +758,7 @@ khatibi
├── manage.py
├── production.env
├── project_tree.py
├── recalc_last_bazdid.md
├── reconciliation_tool.py
├── requirements.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),
]
+14 -7
View File
@@ -11,16 +11,20 @@ from django.db import models
class SMSType(models.TextChoices):
"""انواع پیامک‌های سیستم."""
"""انواع پیامک‌های سیستم (دقیقاً ۷ نوع، به ترتیب چرخهٔ کاری).
ترتیب تعریف = ترتیب نمایش در پنل. هر نوع یا از یک رویداد (view) یا از یک
اسکنر دوره‌ای در صف قرار می‌گیرد؛ نگاشت آن در ``admin_panel/sms/triggers.py``
و ``admin_panel/sms/scanner.py`` است.
"""
WELCOME = "welcome", "خوش‌آمدگویی (ایجاد مشتری)"
CHEQUE_DUE = "cheque_due", "یادآوری سررسید چک"
NESIYE_DUE = "nesiye_due", "یادآوری سررسید نسیه"
SURVEY = "survey", "نظرسنجی پس از ثبت فاکتور"
CHEQUE_REGISTRATION = "cheque_registration", "یادآوری تأمین موجودی چک"
DEBT_DUE = "debt_due", "یادآوری بدهی (سررسید وصول مطالبات)"
VISIT = "visit", "یادآوری بازدید خدمات"
SURVEY = "survey", "نظرسنجی پس از فاکتور"
CUSTOM = "custom", "پیام سفارشی"
VISIT = "visit", "یادآوری بازدید خدمات دستگاه"
VISIT_CREATED = "visit_created", "ثبت بازدید (اقدام از نوع بازدید)"
PAYMENT_THANKS = "payment_thanks", "تشکر از پرداخت"
NESIYE_DUE = "nesiye_due", "یادآوری سررسید نسیه"
class SMSProviderConfig(models.Model):
@@ -33,6 +37,9 @@ class SMSProviderConfig(models.Model):
# نام نمایشی مطابق قرارداد سایر پرمیشن‌های پروژه (ViewStore/AddCustomer/...)
# تا در فهرست پرمیشن‌های Django Admin به‌سادگی پیدا شود.
("ManageSMS", "ManageSMS"),
# سطح دسترسی جداگانه برای صفحات «تنظیمات» و «قالب‌ها»: مدیر می‌تواند فقط
# به کاربران مشخصی اجازهٔ دیدن/ویرایش این‌ها را بدهد (نه هر کاربر پیامک).
("ManageSMSSettings", "ManageSMSSettings"),
]
ProviderChoices = [
+4
View File
@@ -30,6 +30,10 @@ NOTICES = {
"approved": ("success", "پیامک تأیید شد و برای ارسال در صف قرار گرفت."),
"not_pending": ("warning", "این پیامک در وضعیت «در انتظار تأیید» نیست."),
"deleted": ("success", "پیامک از صف حذف شد."),
"delete_done_blocked": (
"warning",
"این پیامک ارسال شده است و از صف حذف نمی‌شود؛ سابقهٔ آن در «گزارش ارسال» می‌ماند.",
),
"bulk_approved_all": ("success", "{count} پیامک تأیید شد و برای ارسال در صف قرار گرفت."),
"bulk_approved": ("success", "{count} پیامک تأیید شد."),
"bulk_deleted": ("success", "{count} پیامک از صف حذف شد."),
+90
View File
@@ -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),
}
+1 -2
View File
@@ -4,7 +4,7 @@ from ..models import CustomerSMSSetting, SMSType
def is_sms_enabled(customer, sms_type):
"""اگر رکوردی با is_enabled=False وجود داشته باشد غیرفعال است؛ در غیر این‌صورت فعال."""
if customer is None or sms_type == SMSType.CUSTOM:
if customer is None:
return True
setting = CustomerSMSSetting.objects.filter(customer=customer, sms_type=sms_type).first()
if setting is None:
@@ -21,5 +21,4 @@ def customer_toggle_map(customer):
return [
{"code": code, "label": label, "enabled": overrides.get(code, True)}
for code, label in SMSType.choices
if code != SMSType.CUSTOM
]
+27 -137
View File
@@ -9,31 +9,29 @@
در هر اجرا حداکثر ``backlog_max`` مورد قدیمی هم صف می‌شود.
* **سقف هر اجرا (``max_per_run``)**: سقف کل صف‌کردن هر اسکنر در یک اجرا.
اسکنرها فقط انواع «دوره‌ای» را پوشش می‌دهند (تأمین موجودی چک، سررسید نسیه، بازدید
دوره‌ای). انواع «رویدادی» (خوش‌آمدگویی، نظرسنجی، ثبت بازدید، تشکر از پرداخت) هنگام
وقوع رخداد توسط ویوها و از طریق ``admin_panel/sms/triggers.py`` صف می‌شوند.
هر اسکنر مستقل و مقاوم به خطاست؛ یک خطا بقیه را متوقف نمی‌کند. ``dedup_key`` از
ارسال تکراری جلوگیری می‌کند و تضمین می‌کند اجرای دوبارهٔ اسکنر رکورد جدیدی نسازد.
یادآوری‌های **دوره‌ای** (بازدید و ثبت چک) کلیدشان شامل شمارهٔ دوره است. یعنی اگر
مدیر یک مورد را از صف حذف کند، فقط همان دوره کنسل می‌شود و در دورهٔ بعد دوباره
ساخته می‌شود؛ برای همیشه خاموش نمی‌ماند.
یادآوری‌های **دوره‌ای** (بازدید) کلیدشان شامل شمارهٔ دوره است. یعنی اگر مدیر یک مورد
را از صف حذف کند، فقط همان دوره کنسل می‌شود و در دورهٔ بعد دوباره ساخته می‌شود؛ برای
همیشه خاموش نمی‌ماند.
"""
import logging
import os
from datetime import datetime, time, timedelta
from django.db.models import Q, Sum
from django.utils import timezone
from ..models import (Article, Customer, Factor, Sarresid, SMSProviderConfig,
SMSTemplate, SMSType)
from ..models import Customer, Sarresid, SMSProviderConfig, SMSTemplate, SMSType
from .enqueue import enqueue_sms
from .timing import DEFAULT_VISIT_MONTHS
logger = logging.getLogger(__name__)
# پیش‌فرض فاصلهٔ تکرار «یادآوری ثبت چک» (روز) وقتی قالبی تنظیم نشده باشد.
CHEQUE_REG_INTERVAL_DEFAULT = 3
def _template(sms_type):
return SMSTemplate.objects.filter(sms_type=sms_type, is_enabled=True).first()
@@ -82,33 +80,39 @@ class _Budget:
self.new_count += 1
def scan_cheque_due(today):
template = _template(SMSType.CHEQUE_DUE)
def scan_cheque_funding(today):
"""یادآوری «تأمین موجودی چک» چند روز پس از سررسید چکِ پرداخت‌نشده.
چند روزِ پس از سررسید از قالب همین نوع (قابل تنظیم از پنل) خوانده می‌شود و مبلغ
چک در متن قرار می‌گیرد. اگر چک پرداخت شود (``payStatus=true``) دیگر یادآوری
نمی‌گیرد؛ ``dedup_key`` نیز مانع ارسال تکراری در اجراهای بعدی می‌شود. اگر اسکنر
چند روز از دست برود، چک‌های «معوق‌تر» هم واجد شرط می‌شوند تا یادآوری از دست نرود.
"""
template = _template(SMSType.CHEQUE_REGISTRATION)
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(
type="check", payStatus="false",
sarresidDT__date__gte=today,
sarresidDT__date__lte=today + timedelta(days=offset),
type="check", payStatus="false", sarresidDT__date__lte=target,
).select_related("customer").order_by("sarresidDT")
count = 0
for s in qs:
if not s.customer or not s.sarresidDT:
continue
# پنجرهٔ یادآوری از «سررسید منهای offset» باز می‌شود.
trigger = _aware(s.sarresidDT) - timedelta(days=offset)
# لحظهٔ باز شدن پنجرهٔ یادآوری = سررسید + offset روز.
trigger = _aware(s.sarresidDT) + timedelta(days=offset)
if not budget.can_send(trigger):
continue
days = (s.sarresidDT.date() - today).days
days = (today - s.sarresidDT.date()).days
made = enqueue_sms(
customer=s.customer, sms_type=SMSType.CHEQUE_DUE,
customer=s.customer, sms_type=SMSType.CHEQUE_REGISTRATION,
context={
"name": s.customer.name_family, "amount": s.price,
"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:
budget.record(trigger)
@@ -149,119 +153,6 @@ def scan_nesiye_due(today):
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):
"""یادآوری بازدید برای مشتریان عقب‌افتاده از بازدید (منطق صفحهٔ «انتظار»).
@@ -313,12 +204,11 @@ def scan_visit(today):
return count
# فقط همان ۷ نوع مجاز؛ انواع رویدادی (خوش‌آمدگویی، نظرسنجی، ثبت بازدید، تشکر از
# پرداخت) در ``admin_panel/sms/triggers.py`` توسط ویوها صف می‌شوند، نه این‌جا.
SCANNERS = [
("cheque_due", scan_cheque_due),
("cheque_registration", scan_cheque_funding),
("nesiye_due", scan_nesiye_due),
("cheque_registration", scan_cheque_registration),
("debt_due", scan_debt_due),
("survey", scan_survey),
("visit", scan_visit),
]
+24 -34
View File
@@ -9,8 +9,7 @@ from ..models import SMSType
NONE = "none"
BEFORE_DUE = "before_due"
AFTER_CREATION = "after_creation"
RECURRING = "recurring"
AFTER_DUE = "after_due"
PERIOD_MONTHS = "period_months"
# پیش‌فرض دورهٔ بازدید (ماه) وقتی مدیر مقداری در قالب نگذاشته باشد.
@@ -22,39 +21,18 @@ TIMING = {
"field": None,
"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: {
"kind": AFTER_CREATION,
"field": "offset_days",
"label": "چند روز بعد از صدور فاکتور",
"default": 3,
"note": "این تعداد روز پس از ثبت فاکتور ارسال می‌شود.",
},
SMSType.DEBT_DUE: {
"kind": NONE,
"field": None,
"note": "روی سررسیدِ وصول مطالبات و فقط برای مشتریانی که مانده بدهکارند ارسال می‌شود.",
"note": "بی‌درنگ پس از ثبت فاکتور ارسال می‌شود؛ تنظیم زمانی ندارد.",
},
SMSType.CHEQUE_REGISTRATION: {
"kind": AFTER_DUE,
"field": "offset_days",
"label": "چند روز پس از سررسید چک",
"default": 1,
"note": "این تعداد روز پس از سررسید چکِ پرداخت‌نشده، پیامک یادآوری "
"تأمین موجودی با مبلغ چک ارسال می‌شود.",
},
SMSType.VISIT: {
"kind": PERIOD_MONTHS,
@@ -63,10 +41,22 @@ TIMING = {
"default": DEFAULT_VISIT_MONTHS,
"note": "برای مشتریانی که از این مدت بازدید نشده‌اند ارسال می‌شود.",
},
SMSType.CUSTOM: {
SMSType.VISIT_CREATED: {
"kind": 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": "پنجرهٔ یادآوری از «سررسید − این تعداد روز» باز می‌شود و تا سررسید ادامه دارد.",
},
}
+82
View File
@@ -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,
)
+1
View File
@@ -130,3 +130,4 @@ input[disabled]{
text-align: center;
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>
</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 %}
<div class="control">
<button type="button" disabled class="button is-danger remove-phone-btn">-</button>
@@ -35,3 +45,50 @@
</button>
{% endif %}
</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>
<div class="columns is-multiline">
<div class="column is-6-desktop is-12-mobile">
<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">
<div class="column is-12">
<h3 class="subtitle is-6">فعال/غیرفعال پیامک‌ها برای این مشتری</h3>
{% for item in sms_toggles %}
<form method="post" action="{% url 'admin_panel:sms_customer_toggle' customer.id %}" class="field has-addons">
@@ -58,7 +28,7 @@
{% csrf_token %}
<div class="field">
<div class="control">
<textarea class="textarea" name="text" rows="2" placeholder="متن پیامک... (خالی بگذارید تا متن قالب «پیام سفارشی» استفاده شود)"></textarea>
<textarea class="textarea" name="text" rows="2" placeholder="متن پیامک..."></textarea>
</div>
</div>
<p class="help">می‌توانید از متغیرهایی مثل {name} (نام مشتری) استفاده کنید.</p>
+8 -1
View File
@@ -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">
<a class="navbar-link">پیامک</a>
<div class="navbar-dropdown">
{% if perms.admin_panel.ManageSMSSettings %}
<a href="{% url 'admin_panel:sms_settings' %}" style="padding-right: 10px;" class="navbar-item">
تنظیمات سامانه
</a>
<a href="{% url 'admin_panel:sms_templates' %}" style="padding-right: 10px;" class="navbar-item">
قالب‌ها
</a>
{% endif %}
{% if perms.admin_panel.ManageSMS %}
<a href="{% url 'admin_panel:sms_queue' %}" style="padding-right: 10px;" class="navbar-item">
صف ارسال
</a>
<a href="{% url 'admin_panel:sms_report' %}" style="padding-right: 10px;" class="navbar-item">
گزارش ارسال
</a>
{% endif %}
</div>
</div>
{% endif %}
@@ -6,55 +6,7 @@
<div class="">
<div class="columns m-0">
<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">
{% 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>
{% include "./_customer_info.html" %}
</div>
<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(){
$("#addNewIteam").removeClass("is-active");
@@ -318,22 +265,6 @@ $(document).ready(function() {
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(){
$(".SearchBox").removeClass("is-active")
});
@@ -18,76 +18,7 @@
<div class="">
<div class="columns m-0">
<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">
{% 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>
{% include "./_customer_info.html" %}
</div>
<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="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>
{% 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 %}
@@ -473,11 +405,6 @@
});
$("#del_customer").click(function(){
if(confirm("با حذف مشتری، تمام سوابق، فاکتورها و اطلاعات از سیستم حذف میشود، مطمئن هستید؟")){
window.location.href = "/panel/customer/{{ customer.id }}/del";
}
});
$(".closeNewIteam").click(function(){
$("#addNewIteam").removeClass("is-active");
@@ -603,50 +530,6 @@ $(document).ready(function() {
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(){
$(".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> دوباره ساخته می‌شوند؛ پس با حذف یک
مورد، پیامک برای آن مشتری برای همیشه قطع نمی‌شود.
<br>
پیامک‌های <strong>ارسال‌شده</strong> از این صف بیرون می‌روند؛ فهرست و سابقهٔ
کامل‌شان در <a href="{% url 'admin_panel:sms_report' %}">گزارش ارسال</a> است.
</div>
<div class="columns is-multiline mb-2" dir="rtl">
@@ -21,6 +24,13 @@
<p class="title is-5 mb-0">{{ total_count }}</p>
</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 %}
<div class="column is-narrow">
<a href="?status={{ code }}" class="box py-3 px-4 has-text-centered"
@@ -71,11 +81,12 @@
</form>
<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' %}"
onsubmit="return confirm('کل صف پیامک خالی شود؟ این پیامک‌ها دیگر هرگز ارسال یا بازسازی نمی‌شوند.');">
onsubmit="return confirm('صف پیامک خالی شود؟ این پیامک‌ها دیگر هرگز ارسال یا بازسازی نمی‌شوند.');">
{% csrf_token %}
<button class="button is-danger" type="submit">خالی کردن کل صف</button>
<button class="button is-danger" type="submit">خالی کردن صف</button>
</form>
</div>
@@ -119,11 +130,17 @@
<button class="button is-small is-warning" type="submit">ارسال مجدد</button>
</form>
{% endif %}
{% if item.status != 'sent' %}
<form method="post" action="{% url 'admin_panel:sms_queue_delete' item.id %}"
onclick="return confirm('این پیامک از صف حذف شود؟');">
{% csrf_token %}
<button class="button is-small is-danger" type="submit">حذف</button>
</form>
{% else %}
<a class="button is-small is-light"
href="{% url 'admin_panel:sms_report' %}?q={{ item.phone_number }}"
title="این پیامک ارسال شده است؛ سابقهٔ آن در گزارش ارسال است">در گزارش</a>
{% endif %}
</div>
</td>
</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">
<form method="post" action="{% url 'admin_panel:sms_settings' %}">
{% csrf_token %}
<h2 class="title is-5">اتصال به سامانه پیامک</h2>
<p class="help mb-3">
این بخش مشخص می‌کند پیامک‌ها <strong>از چه مسیری و با چه حساب کاربری</strong> به
اپراتور سپرده می‌شوند. تا وقتی «وضعیت ارسال» روشن نباشد، هیچ‌کدام از سرویس‌های
پس‌زمینه (worker) پیامکی نمی‌فرستند.
</p>
<div class="columns is-multiline">
<div class="field column is-4-desktop is-12-mobile">
<label class="label">سرویس‌دهنده</label>
@@ -19,6 +26,19 @@
</select>
</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 class="field column is-4-desktop is-12-mobile">
@@ -29,6 +49,11 @@
فعال باشد
</label>
</div>
<p class="help">
<strong>کلید اصلی خاموش/روشن ارسال.</strong> اگر تیک نخورد، سرویس worker هیچ
پیامکی نمی‌فرستد (ردیف‌ها در صف می‌مانند). اسکنر و صف‌کردن مستقل از این است،
پس می‌توانید پیامک‌ها را از قبل در صف بچینید و بعد ارسال را روشن کنید.
</p>
</div>
<div class="field column is-4-desktop is-12-mobile">
@@ -36,6 +61,13 @@
<div class="control">
<input class="input" type="number" name="timeout_seconds" value="{{ config.timeout_seconds }}">
</div>
<p class="help">
<strong>حداکثر زمان انتظار worker برای پاسخ سرویس‌دهنده</strong> در هر تلاش.
اگر پاسخ دیرتر بیاید، آن تلاش «ناموفق» ثبت می‌شود و بر اساس سیاست تلاش مجدد
دوباره در نوبت قرار می‌گیرد. مقدار کم (مثل ۵) ممکن است در سرویس کند، ارسال‌های
سالم را هم ناموفق کند؛ مقدار زیاد (مثل ۱۲۰) در قطعی سرویس، worker را بیکار
نگه می‌دارد. پیش‌فرض ۳۰ ثانیه متعادل است.
</p>
</div>
<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">
</div>
<p class="help">
برای 0098SMS همین آدرس لینکی ارسال است
<strong>آدرس درخواست ارسال برای providerهای «لینکی HTTP» و «HTTP عمومی».</strong>
<br>• برای 0098SMS لینکی همین آدرس است
(<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>
</div>
@@ -56,8 +93,9 @@
<input class="input" type="text" name="webservice_url" value="{{ config.webservice_url }}" dir="ltr">
</div>
<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>
</p>
</div>
@@ -67,7 +105,11 @@
<div class="control">
<input class="input" type="text" name="panel_no" value="{{ config.panel_no }}" dir="ltr">
</div>
<p class="help">اگر خالی بماند از «شماره خط فرستنده» استفاده می‌شود.</p>
<p class="help">
<strong>شمارهٔ اختصاصی حساب شما در وب‌سرویس</strong> (فقط provider وب‌سرویس).
اگر خالی بماند، سیستم از «شماره خط فرستنده» استفاده می‌کند. مقدار اشتباه باعث
رد شدن ارسال با خطای احراز هویت می‌شود.
</p>
</div>
<div class="field column is-4-desktop is-12-mobile">
@@ -75,6 +117,10 @@
<div class="control">
<input class="input" type="text" name="username" value="{{ config.username }}" dir="ltr">
</div>
<p class="help">
<strong>نام کاربری حساب سامانهٔ پیامک</strong> (در همهٔ providerها به‌جز Console
لازم است). اگر اشتباه باشد، ارسال‌ها رد می‌شوند و در «خطا» صف ثبت می‌گردد.
</p>
</div>
<div class="field column is-4-desktop is-12-mobile">
@@ -82,6 +128,11 @@
<div class="control">
<input class="input" type="password" name="password" placeholder="{% if config.password %}(بدون تغییر)‌{% endif %}" dir="ltr">
</div>
<p class="help">
<strong>رمز حساب سامانهٔ پیامک.</strong> برای امنیت، مقدار ذخیره‌شده بازخوانی
نمی‌شود؛ این کادر را <strong>خالی بگذارید</strong> تا رمز قبلی بماند و فقط وقتی
رمز عوض شده، مقدار جدید را وارد کنید.
</p>
</div>
<div class="field column is-4-desktop is-12-mobile">
@@ -89,6 +140,12 @@
<div class="control">
<input class="input" type="text" name="sender" value="{{ config.sender }}" dir="ltr">
</div>
<p class="help">
<strong>شماره یا عنوان خطی که پیامک با آن به دست مشتری می‌رسد.</strong> باید
جزو خطوط مجاز حساب شما باشد؛ در غیر این صورت اپراتور ارسال را رد می‌کند.
همچنین در provider وب‌سرویس، اگر «شمارهٔ پنل» خالی باشد همین مقدار جای آن
استفاده می‌شود.
</p>
</div>
<div class="field column is-12-mobile">
@@ -96,15 +153,23 @@
<div class="control">
<input class="input" type="password" name="api_token" placeholder="{% if config.api_token %}(بدون تغییر)‌{% endif %}" dir="ltr">
</div>
<p class="help">
<strong>کلید/توکن احراز هویت providerهای «HTTP عمومی».</strong> برای 0098SMS
لازم نیست. مثل رمز، خالی گذاشتن آن یعنی «بدون تغییر».
</p>
</div>
</div>
<hr>
<h2 class="title is-5">کنترل جریان ارسال</h2>
<p class="help mb-3">
برای جلوگیری از سیل پیامک: تنها مواردی ارسال می‌شوند که پنجرهٔ یادآوری‌شان از
«خط شروع» به بعد باز شود. موارد قدیمی‌تر تنها با روشن‌کردن «اجازهٔ موارد قدیمی»
و در سقف تعیین‌شده ارسال می‌شوند. بین هر پیامک هم فاصلهٔ زمانی رعایت می‌شود.
این بخش تعیین می‌کند اسکنر دوره‌ای <strong>چه چیزی و چقدر</strong> در هر اجرا در صف
بگذارد. اسکنر هر چند دقیقه یک‌بار اجرا می‌شود و بر اساس «سررسیدها» تصمیم می‌گیرد
کدام مشتری باید یادآوری بگیرد. منطق بودجه به این شکل است:
<strong>سقف هر اجرا</strong> ← تعیین می‌کند کلاً چند مورد صف شود؛
<strong>خط شروع</strong> ← موارد قدیمی‌تر از آن را کنار می‌گذارد؛
<strong>اجازهٔ موارد قدیمی + سقف آن</strong> ← تصمیم می‌گیرد از آن موارد کنارگذاشته‌شده
چند تا (اگر لازم بود) ارسال شود.
</p>
<div class="columns is-multiline">
@@ -116,9 +181,15 @@
placeholder="مثلاً 1403/07/18 — خالی = همین حالا" dir="ltr">
</div>
<p class="help">
فقط یادآوری‌هایی صف می‌شوند که «پنجرهٔ» آن‌ها از این تاریخ به بعد باز شده باشد.
مشتریان قدیمی‌تری که پنجره‌شان پیش از این تاریخ باز شده، وارد صف نمی‌شوند.
قالب: <span dir="ltr">YYYY/MM/DD</span> (شمسی) — خالی = همین حالا.
<strong>خط جداکنندهٔ «موارد جدید» از «موارد قدیمی».</strong>
هر مورد (چک، نسیه، بازدید) یک «لحظهٔ باز شدن پنجرهٔ یادآوری» دارد. اگر این
لحظه <strong>از این تاریخ به بعد</strong> باشد، مورد «جدید» است و آزادانه صف
می‌شود؛ اگر <strong>قبل از</strong> آن باشد، «قدیمی» (backlog) حساب می‌شود.
<br>• هدف اصلی: در نخستین راه‌اندازی، هزاران مشتری قدیمیِ واجد شرط یک‌باره پیامک
نگیرند.
<br>• قالب: <span dir="ltr">YYYY/MM/DD</span> شمسی — خالی = همین حالا.
<br>• عوض‌کردن آن به گذشته، مشتریان قدیمی‌تر را «جدید» می‌کند و ممکن است سیل پیامک
بسازد؛ با احتیاط تغییر دهید.
</p>
</div>
@@ -127,6 +198,19 @@
<div class="control">
<input class="input" type="number" min="0" name="max_per_run" value="{{ config.max_per_run }}">
</div>
<p class="help">
<strong>سقف کل صف‌کردن در یک اجرا — به‌ازای هر اسکنر جداگانه.</strong>
<br>• «هر اسکنر» یعنی این عدد برای هر نوع یادآوری مستقل اعمال می‌شود
(تأمین موجودی چک، سررسید نسیه، بازدید دورهای)؛ پس اگر ۱۰۰ باشد و هر سه اسکنر
واجد شرط باشند، در یک اجرا حداکثر ۱۰۰ + ۱۰۰ + ۱۰۰ = ۳۰۰ ردیف صف می‌شود.
<br>• شمارش از مجموع «جدیدها + موارد قدیمی» است، نه فقط جدیدها.
<br>• <strong>۰ = بی‌نهایت</strong> (هیچ سقفی اعمال نمی‌شود).
<br>• چون اسکنر دوره‌ای اجرا می‌شود، سقف باعث «دسته‌دسته» شدن ارسال‌های انبوه
می‌شود (بقیه در اجراهای بعدی می‌آیند) و از ریختن یک‌بارهٔ صدها پیامک روی ارسال
جلوگیری می‌کند.
<br>• نکته: این سقف‌ها روی «صف‌کردن» اثر دارند، نه سرعت ارسال واقعی؛ سرعت ارسال
به «فاصلهٔ بین هر پیامک» بستگی دارد.
</p>
</div>
<div class="field column is-3-desktop is-6-mobile">
@@ -134,6 +218,11 @@
<div class="control">
<input class="input" type="number" min="0" name="send_interval_seconds" value="{{ config.send_interval_seconds }}">
</div>
<p class="help">
<strong>مکث سرویس worker پس از هر ارسال</strong> تا اپراتور زیر بار زیاد نرود.
مثال: با مقدار ۳، ارسال ۱۰۰ پیامک حدود ۵ دقیقه طول می‌کشد. مقدار ۰ یعنی بدون
مکث (سریع ولی پرخطر — ممکن است سرویس‌دهنده محدودتان کند).
</p>
</div>
<div class="field column is-3-desktop is-6-mobile">
@@ -144,6 +233,12 @@
فعال باشد
</label>
</div>
<p class="help">
<strong>دربِ موارد «قدیمی‌تر از تاریخ شروع دوره».</strong> به‌صورت پیش‌فرض بسته
است. تا وقتی بسته باشد، موارد قدیمی <strong>هرگز</strong> صف نمی‌شوند (حتی اگر
سقف باقی باشد). اگر لازم بود این موارد هم پردازش شوند، آن را روشن کنید و مقدار
«حداکثر موارد قدیمی در هر اجرا» را تعیین کنید.
</p>
</div>
<div class="field column is-3-desktop is-6-mobile">
@@ -151,7 +246,15 @@
<div class="control">
<input class="input" type="number" min="0" name="backlog_max" value="{{ config.backlog_max }}">
</div>
<p class="help">صفر = هیچ مورد قدیمی ارسال نمی‌شود.</p>
<p class="help">
<strong>سقف ارسال موارد قدیمی در هر اجرا، به‌ازای هر اسکنر.</strong>
<br>• فقط وقتی «اجازهٔ ارسال موارد قدیمی» روشن باشد اثر دارد.
<br>• صفر = هیچ مورد قدیمی ارسال نمی‌شود (حتی با روشن‌بودن کلید).
<br>• این سقف <strong>در کنار</strong> «حداکثر پیامک هر اسکنر» اعمال می‌شود؛ پس
ممکن است ابتدا سقف کل (max_per_run) پر شود.
<br>• برای پاک‌کردن تدریجی انبار قدیمی، عدد کوچکی مثل ۵۰ بگذارید تا در هر اجرا
دسته‌ای ارسال شود.
</p>
</div>
</div>
+2
View File
@@ -81,6 +81,7 @@ urlpatterns = [
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/templates', views.sms_templates, name='sms_templates'),
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>/send', views.sms_send_custom, name='sms_send_custom'),
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/empty', views.sms_queue_empty, name='sms_queue_empty'),
path('sms/queue/<int:q_id>/approve', views.sms_queue_approve, name='sms_queue_approve'),
+5
View File
@@ -7,6 +7,7 @@ from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
from ..models import Action, Customer
from ..sms.triggers import notify_visit_scheduled
@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.save()
# پیامک «بازدید شما برای تاریخ … ثبت شد» فقط برای اقدام از نوع بازدید.
if newAction.actionType == 'bazdid':
notify_visit_scheduled(customer_obj, newAction, created_by=request.user)
return redirect('/panel/customer/'+str(c_id)+'/action')
+10 -16
View File
@@ -11,11 +11,10 @@ from django.contrib.auth.decorators import login_required, permission_required
from persiantools.jdatetime import JalaliDate
from ..models import (Customer, PhoneNumber, ProductCategory, Product, Factor,
FactorIteam, Article, Sarresid, Action, CacheTable, SMSType)
from ..sms.enqueue import enqueue_sms
from ..sms.phones import (customer_phone_entries, normalize_primary_values,
FactorIteam, Article, Sarresid, Action, CacheTable)
from ..sms.phones import (normalize_primary_values,
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 ._helpers import logger, _parse_jalali_date, _parse_phone_rows
@@ -122,15 +121,10 @@ def customerNew2(request ,c_id):
#return JsonResponse(all_prd,safe = False)
dateNow = datetime.datetime.now().date
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 = {
'customer': customer, 'categorys': categorys, 'dateNow': dateNow,
'factorha': factorha, 'mandehesabCs': mandehesabCs,
'sms_toggles': sms_toggles, 'sms_phone_entries': sms_phone_entries,
'sms_effective': sms_effective,
'sms_recipients': sorted(sms_effective),
'sms_toggles': sms_toggles,
}
return render(request, 'admin_panel/customerNew2.html', data)
@@ -271,12 +265,7 @@ def customersAdd(request):
)
# پیامک خوش‌آمدگویی (در صورت فعال بودن قالب و تنظیم مشتری)
enqueue_sms(
customer=newCs, sms_type=SMSType.WELCOME,
context={'name': newCs.name_family},
dedup_key=f'welcome:{newCs.id}',
created_by=request.user,
)
notify_welcome(newCs, created_by=request.user)
return redirect('/panel/customers/')
@@ -494,4 +483,9 @@ def customerHesabdariAdd(request ,c_id):
newArticl.user = user_op
newArticl.nahvePardakht = nahvePardakhat
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')
+15 -2
View File
@@ -12,6 +12,7 @@ from persiantools import digits
from ..models import (Customer, StoreRoom, Factor, FactorIteam, Product,
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
@@ -196,6 +197,8 @@ def newFactor2(request, c_id):
if not str(item.get('garanty_Esh', '')).strip()
)
new_factor = None
payment_articles = []
try:
with transaction.atomic():
@@ -231,7 +234,7 @@ def newFactor2(request, c_id):
if key in payment_map:
nahve_pardakht = payment_map[key]
Article.objects.create(
payment_articles.append((Article.objects.create(
debit=0,
credit=amount,
customer=customer_obj,
@@ -240,7 +243,7 @@ def newFactor2(request, c_id):
nahvePardakht=nahve_pardakht,
payStatus="true",
user=user_op
)
), nahve_pardakht))
elif key in ['l_nesiye', 'l_checkPrice']:
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)
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/')
+11
View File
@@ -16,6 +16,7 @@ from persiantools.jdatetime import JalaliDate
from ..models import (Article, Sarresid, Customer, Factor, ProductInStore,
StoreRoom, Action)
from ..sms.triggers import notify_payment_thanks
from ._helpers import logger, _safe_int
@@ -713,6 +714,11 @@ def passCheck(request, articl_id):
sarresidINC.desc = desc
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 redirect(request.META.get('HTTP_REFERER'))
@@ -916,6 +922,11 @@ def pardakhtNesiye(request, cs_id):
articleHesabDariObj.user = user_op
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'))
except:
+217 -30
View File
@@ -1,11 +1,11 @@
"""ویوهای مدیریت پیامک در پنل (نیازمند پرمیشن ``admin_panel.ManageSMS``)."""
import re
from datetime import datetime, time
from datetime import datetime, time, timedelta
from urllib.parse import urlencode
from django.contrib.auth.decorators import login_required, permission_required
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.urls import reverse
from django.utils import timezone
@@ -13,12 +13,34 @@ from django.utils import timezone
from ..models import (Customer, SMSDedup, SMSProviderConfig, SMSQueue,
SMSTemplate, SMSType)
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,
stored_primary_normalized)
from ..sms.render import format_jalali, render_template
from ..sms.timing import apply_timing, current_timing_value, timing_for
_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):
@@ -47,14 +69,13 @@ TEMPLATE_VARIABLES = [
# فقط برای نمایش: آیکن و توضیح کوتاه «چه زمانی ارسال می‌شود» هر نوع (لایهٔ UI، نه دامنه).
SMS_TYPE_UI = {
SMSType.WELCOME: {"icon": "👋", "when": "بیدرنگ پس از ثبت مشتری"},
SMSType.CHEQUE_DUE: {"icon": "🧾", "when": "پیش از سررسید چک"},
SMSType.WELCOME: {"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.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/')
@_manage_required
@_settings_required
def sms_settings(request):
config = SMSProviderConfig.load()
if request.method == "POST":
@@ -82,7 +103,7 @@ def sms_settings(request):
config.test_message = test_text
config.save(update_fields=["test_message"])
enqueue_sms(
customer=None, sms_type=SMSType.CUSTOM,
customer=None, sms_type="",
message=test_text or config.test_message,
recipients=resolve_recipients_test(phone), created_by=request.user,
approved=True, # ارسال دستیِ مدیر؛ نیاز به بازبینی صف ندارد.
@@ -192,7 +213,7 @@ def _sample_context():
@login_required(login_url='/panel/p_login/')
@_manage_required
@_settings_required
def sms_templates(request):
raw_sample, text_sample = _sample_context()
rows = []
@@ -236,7 +257,7 @@ def _templates_redirect(anchor="", notice="", **extra):
@login_required(login_url='/panel/p_login/')
@_manage_required
@_settings_required
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):
"""ارسال پیامک با متن سفارشی برای همین مشتری.
اگر کادر متن خالی بماند، متن قالب «پیام سفارشی» استفاده می‌شود. متغیرهایی مثل
متن باید توسط مدیر نوشته شود (قالب پیش‌فرضی وجود ندارد)؛ متغیرهایی مثل
``{name}`` در متن واردشده هم رندر می‌شوند.
"""
customer = get_object_or_404(Customer, id=c_id)
if request.method == "POST":
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()
if not rendered:
return _notice(_customer_url(c_id), "custom_empty")
created = enqueue_sms(
customer=customer, sms_type=SMSType.CUSTOM, message=rendered,
customer=customer, sms_type="", message=rendered,
created_by=request.user,
approved=True, # متن را خودِ مدیر نوشته و ارسال را خواسته است.
)
@@ -348,9 +364,19 @@ def sms_send_custom(request, c_id):
@login_required(login_url='/panel/p_login/')
@_manage_required
def sms_queue(request):
"""صف ارسال: فقط مواردی که هنوز «کارِ باقی‌مانده» دارند.
پیامک‌های ارسال‌شده (``sent``) از این فهرست بیرون می‌مانند چون کارشان تمام است و
سابقه‌شان در «گزارش ارسال» دیده می‌شود؛ ماندن‌شان در صف فقط باعث می‌شد دکمهٔ
«حذف» روی تاریخچه ظاهر شود. بقیهٔ وضعیت‌ها (در انتظار تأیید، تأییدشده، در حال
پردازش، ناموفق) در صف می‌مانند چون هنوز نیاز به اقدام دارند.
"""
status = request.GET.get("status", "")
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:
qs = qs.filter(status=status)
if delivery:
@@ -364,24 +390,177 @@ def sms_queue(request):
counts = dict(
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 = {
"queue": page,
"status": status,
"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,
"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,
# (کد وضعیت، برچسب، تعداد) — به ترتیب تعریف Status.
# (کد وضعیت، برچسب، تعداد) — فقط وضعیت‌های درون صف.
"status_stats": [
(code, label, counts.get(code, 0))
for code, label in SMSQueue.Status.choices
if code not in DONE_STATUSES
],
}
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):
"""پیامک‌های «در انتظار تأیید» را تأیید و آمادهٔ ارسال می‌کند."""
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/')
@_manage_required
def sms_queue_delete(request, q_id):
"""حذف یک پیامک از صف (مثلاً پس از بازبینی و رد آن)."""
"""حذف یک پیامک از صف (مثلاً پس از بازبینی و رد آن).
پیامکِ ارسال‌شده حذف نمی‌شود؛ سابقهٔ آن در «گزارش ارسال» می‌ماند و حذفش یعنی
پاک‌کردنِ تاریخچه. برای چنین موردی فقط پیام هشدار برمی‌گردد.
"""
item = get_object_or_404(SMSQueue, id=q_id)
if request.method == "POST":
if item.status in DONE_STATUSES:
return _notice(reverse("admin_panel:sms_queue"), "delete_done_blocked")
item.delete()
return _notice(reverse("admin_panel:sms_queue"), "deleted")
return redirect("admin_panel:sms_queue")
@@ -427,6 +612,8 @@ def sms_queue_bulk(request):
count = _approve(selected)
code, extra = "bulk_approved", {"count": count}
elif action == "delete_selected":
# تاریخچهٔ ارسال‌شده‌ها از طریق صف حذف نمی‌شود.
selected = selected.exclude(status__in=DONE_STATUSES)
count = selected.count()
selected.delete()
code, extra = "bulk_deleted", {"count": count}
@@ -439,14 +626,14 @@ def sms_queue_bulk(request):
@login_required(login_url='/panel/p_login/')
@_manage_required
def sms_queue_empty(request):
"""خالی کردن کل صف — هر وضعیتی، بدون بازگشت.
"""خالی کردن صف — فقط موارد باقی‌مانده (بدون بازگشت).
پیش از حذف، کلید یکتاسازیِ هر رکورد در دفتر ``SMSDedup`` تضمین می‌شود تا اسکنر
در اجرای بعدی همان پیامک‌ها را از نو نسازد. رکوردهایی که کلید ندارند (مثلاً
پیامک دستی) کلید مشتق از خودشان را می‌گیرند.
پیامک‌های ارسال‌شده جزو صف نیستند و حذف نمی‌شوند؛ تاریخچه‌شان در «گزارش ارسال»
لازم است. پیش از حذف، کلید یکتاسازیِ هر رکورد در دفتر ``SMSDedup`` تضمین می‌شود
تا اسکنر در اجرای بعدی همان پیامک‌ها را از نو نسازد.
"""
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]
SMSDedup.objects.bulk_create(
[SMSDedup(key=k) for k in keys], ignore_conflicts=True,
File diff suppressed because it is too large. Load diff
+169 -138
View File
@@ -1,142 +1,173 @@
{
"0": "views/store.py",
"1": "project_tree.py",
"2": "sqlite_db_backup.py",
"3": "django_db",
"4": "Customer ID Backup/New Factor Template",
"5": "views/customers.py",
"6": "cheque_inquiry.py",
"7": "admin_panel/templates/admin_panel/baseHesabdari.html",
"8": "django_db_models_deletion",
"9": "0051_auto_20260705_1356.py",
"10": "admin_panel/storeBK1.html",
"11": "ViewSmokeTests",
"12": "admin_panel/templates/admin_panel/customerAction.html",
"13": "User",
"14": "run_all",
"15": "admin_panel/admin.py",
"16": "stock_in",
"17": "admin_panel/templates/admin_panel/bedehi.html",
"18": "hesabdariCheckPassFail.html",
"19": "Reference guide",
"20": "normalize_mobile",
"21": "graphify",
"22": "hesabdariResidNashode.html",
"23": "0017_factoriteam_garantyesh.py",
"24": "scanner.py",
"25": "0002_auto_20220114_1616.py",
"26": "SMSResult",
"27": "Rip'n Fast. Fewer Tokens. Better Code.",
"28": "models/sms.py",
"29": "admin_panel/templates/admin_panel/baseStore.html",
"30": "views/finance.py",
"31": "Customer Modals UI",
"32": "views/factors.py",
"33": "0034_alter_article_sarresid.py",
"34": "views/sms.py",
"35": "0035_alter_article_sarresid.py",
"36": "django_db_models",
"37": "0010_auto_20220122_1546.py",
"38": "0040_customer_laghv_bazdid.py",
"39": "test_sms.py",
"40": "0048_alter_productinstore_options.py",
"41": "0032_article_paystatus.py",
"42": "0033_article_nahvepardakht.py",
"43": "make_customer",
"44": "sms0098.py",
"45": "0044_sarresid_desc.py",
"46": "models/__init__.py",
"47": "make_user",
"48": "GEMINI.md",
"49": "admin/migrations/__init__.py",
"50": "Customer Action Include",
"51": "New Customer Base Template",
"52": "Customer Feedback Partial",
"53": "Customer Factor Detail",
"54": "Customer Accounting Template",
"55": "Customer Factor List",
"56": "Customer Feedback Page",
"57": "commands/__init__.py",
"58": "management/__init__.py",
"59": "admin_panel/migrations/__init__.py",
"60": "Action List Template",
"61": "base.html",
"62": "Customer New Factor",
"63": "Customer New Factor V2",
"64": "Customer Satisfaction List",
"65": "Customer Satisfaction Partial",
"66": "Customer Check/Debt Management",
"67": "admin_panel/templates/admin_panel/customerBaseNew.html",
"68": "Customer List",
"69": "admin_panel/templates/admin_panel/entezarBK.html",
"70": "Financial Dashboard",
"71": "Accounting Main View",
"72": "admin_panel/templates/admin_panel/index.html",
"73": "New Inventory Operation",
"74": "Inventory Operations List",
"75": "Operations Backup Template",
"76": "Financial Report Template",
"77": "Debt Collection Dashboard",
"78": "Inventory Movement Report",
"79": "Customer Satisfaction Dashboard",
"80": "Check and Credit Report Template",
"81": "Satisfaction List Template",
"82": "Store Inventory Template",
"83": "Store Transfers Template",
"84": "Stores List Template",
"85": "toCurrency",
"86": "templatetags/__init__.py",
"87": "URL: newStore",
"88": "URL: process_transfer_action",
"89": "URL: rezayatEdit",
"90": "khatibi/__init__.py",
"91": "Extended Requirements File",
"92": "Requirements File",
"93": "django_shortcuts",
"94": "resolve_recipients",
"95": "factories.py",
"96": "write_tree",
"97": "TemplatesPageUITests",
"0": "views/sms.py",
"1": "run_all",
"2": "django_db",
"3": "views/store.py",
"4": "ModelsPackageContractTests",
"5": "PhoneCleanupTests",
"6": ".load",
"7": "models/sms.py",
"8": "logging",
"9": "views/customers.py",
"10": "django_db_models_deletion",
"11": "make_customer",
"12": "customer_phone_entries",
"13": "EventTriggerTests",
"14": "views/finance.py",
"15": "SMSResult",
"16": "sms0098ws.py",
"17": "Admin Panel Customization",
"18": "User",
"19": "django_conf",
"20": "test_customers.py",
"21": "phones.py",
"22": "Login Template Forms",
"23": "cheque_inquiry.py",
"24": "factories.py",
"25": "Rip'n Fast. Fewer Tokens. Better Code.",
"26": "views/factors.py",
"27": "to_local",
"28": "make_user",
"29": "QueueReviewTests",
"30": "NoticeMechanismTests",
"31": "enqueue.py",
"32": "enqueue_sms",
"33": "PanelTestCase",
"34": "ProductInStore",
"35": "admin_panel/templates/admin_panel/baseHesabdari.html",
"36": "Reference guide",
"37": "Factor Creation UI",
"38": "admin_panel/templates/admin_panel/bedehi.html",
"39": "project_tree.py",
"40": "CustomerSmsHistoryTests",
"41": "TemplatesPageUITests",
"42": "SMS Integration Docs",
"43": "timing_for",
"44": "django_shortcuts",
"45": "stock_in",
"46": "admin_panel/templates/admin_panel/customerAction.html",
"47": "parts_count",
"48": "write_tree",
"49": "CustomerInfoCardUITests",
"50": "Customer",
"51": "Phone Cleanup Guide",
"52": "SmsReportTests",
"53": "manage.py",
"54": "Agent Operating Guidelines",
"55": "SMSDeliveryCommandTests",
"56": "BehinSMSProvider",
"57": "test_sms.py",
"58": "Financial Data Objects",
"59": "Hybrid Migration Command",
"60": "بازمحاسبهٔ «آخرین بازدید» مشتریان",
"61": "Resource Cleanup Handler",
"62": "SMS Timing Migration",
"63": "Articl Model",
"64": "customerIDFactorBackup.html",
"65": "test_factors.py",
"66": "os",
"67": "Graphify Tooling Docs",
"68": "django_urls",
"69": "0063_alter_smsqueue_status.py",
"70": "SMS Template Seeding",
"71": "admin_panel/entezar.html",
"72": "customerNewBackup.html",
"73": "hesabdariResidNashode.html",
"74": "0070_sms_types_seven.py",
"75": "Endpoint Smoke Tests",
"76": "Timezone Migration History",
"77": "models/__init__.py",
"78": "UrlContractTests",
"79": "Brand Scrubbing Migration",
"80": "SMS Dedup Migration",
"81": "Installation Guide",
"82": "Project Honesty Contract",
"83": "Flow Control Migration",
"84": "Permission Label Migration",
"85": "stop.sh",
"86": "Output Format Specs",
"87": "Performance Benchmarks",
"88": "SMS Integration Overview",
"89": "System Cache Table",
"90": "Customer Modal UI",
"91": "SentQueueProtectionTests",
"92": "Infrastructure Services",
"93": "Agent Setup Guide",
"94": "First Use Guide",
"95": "0056_auto_20260731_1824.py",
"96": "0009_remove_storeroom_user.py",
"97": "PhoneEntry",
"98": "reconciliation_tool.py",
"99": "ModelsPackageContractTests",
"100": "django_conf",
"99": "announcements.py",
"100": "pathlib",
"101": "Command",
"102": "UrlContractTests",
"103": "customerIDFactorBackup.html",
"104": "system.py",
"105": "BehinSMSProvider",
"106": "test_customers.py",
"107": "tests/__init__.py",
"108": "Command",
"109": "Command",
"110": "0061_sms_timing_and_primary.py",
"111": "bale/providers.py",
"112": "os",
"113": "Command",
"114": "0058_seed_sms_templates.py",
"115": "test_factors.py",
"116": "Customer",
"117": "customerNewBackup.html",
"118": "0059_sms_flow_control.py",
"119": "0060_sms_permission_label.py",
"120": "datetime",
"121": "0008_storeroom_user.py",
"122": "0013_alter_factor_options.py",
"123": "0018_alter_factoriteam_product.py",
"124": "0020_auto_20220126_1532.py",
"125": "0022_article_typepy.py",
"126": "0025_action_actiontype.py",
"127": "0028_action_grade.py",
"128": "0030_auto_20220414_1047.py",
"129": "0036_sarresid.py",
"130": "0037_sarresid_sarresiddt.py",
"131": "0039_customer_last_bazdid_dt.py",
"132": "0041_auto_20240216_1607.py",
"133": "0047_alter_productinstore_movement_type.py",
"134": "0049_alter_productinstore_options.py",
"135": "0057_sms.py",
"136": "admin_panel/sms/__init__.py",
"137": "bale/__init__.py",
"138": "integrations/__init__.py",
"139": "integrations/sms/__init__.py"
"102": "0064_sms_webservice_delivery.py",
"103": "0040_customer_laghv_bazdid.py",
"104": "0068_auto_20261010_1305.py",
"105": "0069_smstemplate_auto_send.py",
"106": "0047_alter_productinstore_movement_type.py",
"107": "Command",
"108": "SMS Domain Logic",
"109": "Customer Communication Partials",
"110": "SMS Template Management",
"111": "Documentation and Graphing",
"112": "Admin Migration Package",
"113": "bale/providers.py",
"114": "Customer Action Templates",
"115": "Customer Feedback Components",
"116": "Customer Invoice Details",
"117": "Accounting Document Templates",
"118": "Customer Invoice List",
"119": "Customer Feedback Interface",
"120": "Custom Management Commands",
"121": "Django Management Package",
"122": "Admin Panel Migrations",
"123": "Action List Interface",
"124": "Base HTML Layout",
"125": "New Invoice Creation",
"126": "Customer Satisfaction Records",
"127": "Satisfaction UI Components",
"128": "Debt and Credit Management",
"129": "Customer Base Layout",
"130": "Waiting List Template",
"131": "Financial Dashboard",
"132": "Accounting Main View",
"133": "Admin Dashboard Index",
"134": "Inventory Entry Form",
"135": "Inventory Operations History",
"136": "Operations Backup View",
"137": "Financial Reporting",
"138": "Debt Collection Interface",
"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
View File
@@ -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
View File
@@ -1 +1 @@
{"output_tokens": 126792}
{"output_tokens": 201}
+449 -311
View File
@@ -1,183 +1,213 @@
# Graph Report - khatibi (2026-10-10)
# Graph Report - khatibi (2026-10-11)
## Corpus Check
- 160 files · ~127,728 words
- 182 files · ~145,325 words
- 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
- 1280 nodes · 2237 edges · 140 communities (47 shown, 93 thin omitted)
- Extraction: 94% EXTRACTED · 6% INFERRED · 0% AMBIGUOUS · INFERRED: 131 edges (avg confidence: 0.87)
- 1684 nodes · 3008 edges · 171 communities (74 shown, 97 thin omitted)
- Extraction: 95% EXTRACTED · 5% INFERRED · 0% AMBIGUOUS · INFERRED: 151 edges (avg confidence: 0.89)
- Token cost: 0 input · 0 output
## 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 `graphify update .` after code changes (no API cost).
## 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
- 0035_alter_article_sarresid.py
- django_db_models
- 0010_auto_20220122_1546.py
- 0040_customer_laghv_bazdid.py
- test_sms.py
- 0048_alter_productinstore_options.py
- 0032_article_paystatus.py
- 0033_article_nahvepardakht.py
- run_all
- django_db
- views/store.py
- ModelsPackageContractTests
- PhoneCleanupTests
- .load
- models/sms.py
- logging
- views/customers.py
- django_db_models_deletion
- make_customer
- sms0098.py
- 0044_sarresid_desc.py
- models/__init__.py
- customer_phone_entries
- EventTriggerTests
- 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
- GEMINI.md
- Customer Action Include
- New Customer Base Template
- Customer Feedback Partial
- Customer Factor Detail
- Customer Accounting Template
- Customer Factor List
- Customer Feedback Page
- Action List Template
- base.html
- Customer New Factor
- Customer New Factor V2
- Customer Satisfaction List
- Customer Satisfaction Partial
- Customer Check/Debt Management
- admin_panel/templates/admin_panel/customerBaseNew.html
- Customer List
- QueueReviewTests
- NoticeMechanismTests
- enqueue.py
- enqueue_sms
- PanelTestCase
- ProductInStore
- admin_panel/templates/admin_panel/baseHesabdari.html
- Reference guide
- Factor Creation UI
- admin_panel/templates/admin_panel/bedehi.html
- project_tree.py
- CustomerSmsHistoryTests
- TemplatesPageUITests
- SMS Integration Docs
- timing_for
- django_shortcuts
- 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
- Accounting Main View
- New Inventory Operation
- Inventory Operations List
- Operations Backup Template
- Financial Report Template
- Debt Collection Dashboard
- Inventory Movement Report
- Customer Satisfaction Dashboard
- Check and Credit Report Template
- Satisfaction List Template
- Store Inventory Template
- Store Transfers Template
- Stores List Template
- toCurrency
- Extended Requirements File
- Requirements File
- django_shortcuts
- resolve_recipients
- factories.py
- write_tree
- TemplatesPageUITests
- reconciliation_tool.py
- ModelsPackageContractTests
- django_conf
- Inventory Entry Form
- Inventory Operations History
- Operations Backup View
- Financial Reporting
- Debt Collection Interface
- Inventory Movement Analytics
- Satisfaction Analytics Dashboard
- Credit Status Reports
- Satisfaction List View
- SMS Queue Management
- Store Inventory View
- Store Transfer Interface
- Store Directory Template
- Currency Formatting Utility
- Data Cleanup Documentation
- Project Dependencies
- QueueStatsTests
- sms_settings
- Command
- UrlContractTests
- customerIDFactorBackup.html
- system.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
- 0071_sms_settings_permission.py
- index
- 0017_factoriteam_garantyesh.py
- 0028_action_grade.py
- 0030_auto_20220414_1047.py
- 0036_sarresid.py
- 0037_sarresid_sarresiddt.py
- 0034_alter_article_sarresid.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)
1. `make_customer()` - 37 edges
2. `make_user()` - 24 edges
3. `Rip'n Fast. Fewer Tokens. Better Code.` - 23 edges
4. `User` - 22 edges
5. `create_transfer_request()` - 22 edges
6. `enqueue_sms()` - 21 edges
7. `SMSResult` - 20 edges
8. `normalize_mobile()` - 20 edges
9. `SMSProvider` - 19 edges
10. `Reference guide` - 18 edges
1. `make_customer()` - 70 edges
2. `make_user()` - 28 edges
3. `run_all()` - 24 edges
4. `enqueue_sms()` - 23 edges
5. `SMSResult` - 23 edges
6. `Rip'n Fast. Fewer Tokens. Better Code.` - 23 edges
7. `User` - 22 edges
8. `create_transfer_request()` - 22 edges
9. `normalize_mobile()` - 22 edges
10. `SMSProvider` - 21 edges
## 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_finance.py → admin/models.py
- `run_360_audit()` --uses--> `Customer` [INFERRED]
ghost_hunter.py → admin/models.py
- ``nest=` is a max, so it cannot tell a long function from a tangled one` --references--> `index()` [INFERRED]
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
- None detected.
## 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 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]
- **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]
- **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]
- **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]
- **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 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]
- **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]
@@ -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]
- **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"
Cohesion: 0.07
Nodes (26): StoreTransferRequest, Meta, Status, approve_transfer_request(), cancel_transfer_request(), _check_source_store_permission(), _check_target_store_permission(), create_transfer_request() (+18 more)
### Community 0 - "views/sms.py"
Cohesion: 0.14
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"
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"
### Community 1 - "run_all"
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
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"
Cohesion: 0.13
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)
### Community 23 - "cheque_inquiry.py"
Cohesion: 0.19
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"
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"
### Community 24 - "factories.py"
Cohesion: 0.27
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
Nodes (6): Counts, format_header(), generate_snapshot(), normalized_path(), write_tree(), visit()
### Community 98 - "reconciliation_tool.py"
Cohesion: 0.19
Nodes (4): remove_query_param(), check_financial_discrepancy(), run_360_audit(), run_audit()
### Community 50 - "Customer"
Cohesion: 0.16
Nodes (6): Customer, Factor, FactorIteam, VisitQueue, Command, NewFactor2ViewTests
### Community 99 - "ModelsPackageContractTests"
Cohesion: 0.05
Nodes (15): AdminConfig, AdminPanelConfig, make_std_streams_unicode_safe(), configure_sqlite_connection(), entrypoint.sh script, main(), run.sh script, start_service() (+7 more)
### Community 51 - "Phone Cleanup Guide"
Cohesion: 0.17
Nodes (11): اجرا روی سرور اصلی (production), اجرای دستور, اصلاح دستی شماره‌های قدیمی مشتریان, راه جایگزین: مهاجرت (migration), قاعدهٔ پاک‌سازی, مشاهدهٔ نتیجه, نکته‌ها, چرا؟ (+3 more)
### Community 100 - "django_conf"
### Community 53 - "manage.py"
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
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
Nodes (4): backfill_primary_phones(), Migration, _normalize(), normalize_template_timing()
### Community 111 - "bale/providers.py"
Cohesion: 0.31
Nodes (4): BaleBotProvider, ConsoleBaleProvider, get_bale_provider(), NotificationResult
### Community 63 - "Articl Model"
Cohesion: 0.67
Nodes (3): Articl Model, Customer Model, articls
### Community 116 - "Customer"
Cohesion: 0.29
Nodes (3): Customer, Meta, PhoneNumber
### Community 64 - "customerIDFactorBackup.html"
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/
### 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
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
- `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
## Knowledge Gaps
- **110 isolated node(s):** `DataTables Library`, `Customer Data Object`, `Factor Data Object`, `admin_panel:pardakhtNesiye URL`, `/panel/passCheck/ URL` (+105 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.)
- **93 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
- **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; 738 node(s) total have ≤1 connection when file, concept and rationale nodes are included.)
- **97 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `SMSProviderConfig` connect `models/sms.py` to `scanner.py`?**
_High betweenness centrality (0.031) - this node is a cross-community bridge._
- **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.046) - this node is a cross-community bridge._
- **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._
- **What connects `DataTables Library`, `Customer Data Object`, `Factor Data Object` to the rest of the system?**
_110 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `views/store.py` be split into smaller, more focused modules?**
_Cohesion score 0.06540825285338016 - nodes in this community are weakly interconnected._
- **Why does `Languages` connect `Rip'n Fast. Fewer Tokens. Better Code.` to `scanner.py`?**
_High betweenness centrality (0.026) - this node is a cross-community bridge._
- **Should `django_db` be split into smaller, more focused modules?**
_Cohesion score 0.06896551724137931 - nodes in this community are weakly interconnected._
- **What connects `base.html`, `toCurrency`, `DataTables Library` to the rest of the system?**
_118 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `views/sms.py` be split into smaller, more focused modules?**
_Cohesion score 0.1425287356321839 - nodes in this community are weakly interconnected._
- **Why does `Languages` connect `.load` to `Rip'n Fast. Fewer Tokens. Better Code.`?**
_High betweenness centrality (0.044) - this node is a cross-community bridge._
- **Should `run_all` be split into smaller, more focused modules?**
_Cohesion score 0.05552617662612375 - nodes in this community are weakly interconnected._
### Low-confidence Hints
_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._
+5 -5
View File
@@ -63,12 +63,12 @@
</div>
<div id="legend"></div>
</div>
<div id="stats">1280 nodes &middot; 2237 edges &middot; 140 communities</div>
<div id="stats">1684 nodes &middot; 3008 edges &middot; 171 communities</div>
</div>
<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_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 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&#x27;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 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_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/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&#x27;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
function esc(s) {
@@ -293,7 +293,7 @@ LEGEND.forEach(c => {
</script>
<script>
// 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.
// Draw node positions raw — no manual pan/zoom/DPR math needed.
+27452 -13833
View File
File diff suppressed because it is too large. Load diff
+342 -186
View File
@@ -66,22 +66,22 @@
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
},
"admin_panel/admin.py": {
"mtime": 1791567672.622296,
"seen": 1791614104.8488438,
"ast_hash": "79e7d7e10f67c3ba818373d384f4ad17",
"semantic_hash": ""
"mtime": 1791627073.834386,
"seen": 1791702801.2978444,
"ast_hash": "bf3715ab483bd7eda55aa56913ae347e",
"semantic_hash": "bf3715ab483bd7eda55aa56913ae347e"
},
"admin_panel/apps.py": {
"mtime": 1791570183.2597606,
"seen": 1791614104.8488438,
"seen": 1791702801.2978444,
"ast_hash": "d0b73aeac890b520b7d1f8796b59ae21",
"semantic_hash": ""
"semantic_hash": "d0b73aeac890b520b7d1f8796b59ae21"
},
"admin_panel/context_processor.py": {
"mtime": 1785506207.060478,
"seen": 1791552045.91651,
"ast_hash": "a9bc61278ecd5950347b24fd8438583b",
"semantic_hash": "a9bc61278ecd5950347b24fd8438583b"
"mtime": 1791618283.1369228,
"seen": 1791702801.2978444,
"ast_hash": "8d915c82c3a2690e90d3367fd34cf383",
"semantic_hash": "8d915c82c3a2690e90d3367fd34cf383"
},
"admin_panel/management/__init__.py": {
"mtime": 1782242229.7079034,
@@ -468,10 +468,10 @@
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
},
"admin_panel/templatetags/custom_tags.py": {
"mtime": 1783251836.4148617,
"seen": 1791552045.91651,
"ast_hash": "8d8c826e168cec5bb68a21c8457380ba",
"semantic_hash": "8d8c826e168cec5bb68a21c8457380ba"
"mtime": 1791641617.9080276,
"seen": 1791702801.3156137,
"ast_hash": "849cd0b0dc695c9b16de2f5af51d1166",
"semantic_hash": "849cd0b0dc695c9b16de2f5af51d1166"
},
"admin_panel/templatetags/remove_q_p.py": {
"mtime": 1683307703.0,
@@ -486,9 +486,9 @@
"semantic_hash": "e08f0582500f6562bf0a931ef9503b39"
},
"admin_panel/urls.py": {
"mtime": 1791564580.742213,
"seen": 1791614104.8488438,
"ast_hash": "c17170a3be54ce7a173bf4d215864538",
"mtime": 1791717588.9114156,
"seen": 1791720362.5829337,
"ast_hash": "5b14236a71f471fee84008cb051566b9",
"semantic_hash": ""
},
"check_finance.py": {
@@ -499,9 +499,9 @@
},
"entrypoint.sh": {
"mtime": 1791564734.2804396,
"seen": 1791614104.8488438,
"seen": 1791702801.3178282,
"ast_hash": "5efb2100271785fa69a864bc6a89edb8",
"semantic_hash": ""
"semantic_hash": "5efb2100271785fa69a864bc6a89edb8"
},
"ghost_hunter.py": {
"mtime": 1786881510.522436,
@@ -529,9 +529,9 @@
},
"khatibi/settings.py": {
"mtime": 1791564345.9580202,
"seen": 1791614104.8488438,
"seen": 1791702801.3183815,
"ast_hash": "b3f5a8c5895df16b918fc132147d8859",
"semantic_hash": ""
"semantic_hash": "b3f5a8c5895df16b918fc132147d8859"
},
"khatibi/urls.py": {
"mtime": 1650778147.0,
@@ -576,9 +576,9 @@
"semantic_hash": "4ba55e04b37e585d96f0d628c695d228"
},
"admin_panel/templates/admin_panel/base.html": {
"mtime": 1791564697.854277,
"seen": 1791614104.8488438,
"ast_hash": "d6fd88c25741fd4b8173ade946f7c26c",
"mtime": 1791719559.991003,
"seen": 1791720362.5829337,
"ast_hash": "2a59c1f5b13d05f75c5dd56b20c228ce",
"semantic_hash": ""
},
"admin_panel/templates/admin_panel/baseHesabdari.html": {
@@ -618,15 +618,15 @@
"semantic_hash": "3232cbf82efe94504f69735791ca5cf4"
},
"admin_panel/templates/admin_panel/customerBase.html": {
"mtime": 1650778147.0,
"seen": 1791553187.9024515,
"ast_hash": "2724fa49985d8c8fa76ddf5c31b64054",
"semantic_hash": "2724fa49985d8c8fa76ddf5c31b64054"
"mtime": 1791712775.7233438,
"seen": 1791714098.633808,
"ast_hash": "2fe76f54223135513e31b95e40fe5891",
"semantic_hash": ""
},
"admin_panel/templates/admin_panel/customerBaseNew.html": {
"mtime": 1791567422.453766,
"seen": 1791614104.8488438,
"ast_hash": "2059fbc03889c7641f43a77f2ca66974",
"mtime": 1791712756.7746606,
"seen": 1791714098.633808,
"ast_hash": "488e036cf11d96e273c6dcd04544598e",
"semantic_hash": ""
},
"admin_panel/templates/admin_panel/customerFactor.html": {
@@ -679,9 +679,9 @@
},
"admin_panel/templates/admin_panel/customerNew2.html": {
"mtime": 1791564687.384954,
"seen": 1791614104.8488438,
"seen": 1791702836.3239095,
"ast_hash": "5ebe9f703b4f229f55a6658064b609ef",
"semantic_hash": ""
"semantic_hash": "5ebe9f703b4f229f55a6658064b609ef"
},
"admin_panel/templates/admin_panel/customerNewBackup.html": {
"mtime": 1650778147.0,
@@ -720,10 +720,10 @@
"semantic_hash": "1cb1e5086a0f291b86437f03af835407"
},
"admin_panel/templates/admin_panel/customers.html": {
"mtime": 1791564720.7973502,
"seen": 1791614104.8488438,
"ast_hash": "fb4b36a0b24edde3fae32542d675449e",
"semantic_hash": ""
"mtime": 1791626647.6043096,
"seen": 1791702836.3239095,
"ast_hash": "4364f20bb55d32a42d903202afdf736d",
"semantic_hash": "4364f20bb55d32a42d903202afdf736d"
},
"admin_panel/templates/admin_panel/customersBK.html": {
"mtime": 1650778147.0,
@@ -865,9 +865,9 @@
},
"requirements.txt": {
"mtime": 1791564743.1777852,
"seen": 1791614104.8488438,
"seen": 1791702801.3208632,
"ast_hash": "41f5e0e5a503cf2eddb6bafb3aff3214",
"semantic_hash": ""
"semantic_hash": "41f5e0e5a503cf2eddb6bafb3aff3214"
},
"requirements2.txt": {
"mtime": 1654964543.0,
@@ -877,9 +877,9 @@
},
"admin_panel/models/__init__.py": {
"mtime": 1791564332.991965,
"seen": 1791614104.8488438,
"seen": 1791702801.313386,
"ast_hash": "7cd6b8258d07f9d1a4f668a95b1c9c58",
"semantic_hash": ""
"semantic_hash": "7cd6b8258d07f9d1a4f668a95b1c9c58"
},
"admin_panel/models/actions.py": {
"mtime": 1791557666.0745652,
@@ -888,10 +888,10 @@
"semantic_hash": "8213389e796cad243cdb758fc78076a5"
},
"admin_panel/models/customers.py": {
"mtime": 1791567220.2646446,
"seen": 1791614104.8488438,
"ast_hash": "c9623b3b8f8b1e4b03f2113919ef3608",
"semantic_hash": ""
"mtime": 1791621708.4566169,
"seen": 1791702801.313386,
"ast_hash": "b70fa9395bb5cacb452b84cf0a794efe",
"semantic_hash": "b70fa9395bb5cacb452b84cf0a794efe"
},
"admin_panel/models/factors.py": {
"mtime": 1791557664.8430376,
@@ -931,21 +931,21 @@
},
"admin_panel/views/__init__.py": {
"mtime": 1791564503.1344602,
"seen": 1791614104.8488438,
"seen": 1791702801.3156137,
"ast_hash": "88b45aececacd04d8f8faadaa295524b",
"semantic_hash": ""
"semantic_hash": "88b45aececacd04d8f8faadaa295524b"
},
"admin_panel/views/_helpers.py": {
"mtime": 1791568674.700356,
"seen": 1791614104.8488438,
"seen": 1791702801.3156137,
"ast_hash": "4412599acacb9e21ffa7a2d2748d9484",
"semantic_hash": ""
"semantic_hash": "4412599acacb9e21ffa7a2d2748d9484"
},
"admin_panel/views/actions.py": {
"mtime": 1791557931.7421222,
"seen": 1791560671.8564537,
"ast_hash": "4a96fd10db79b691fa606e5d521d776b",
"semantic_hash": "4a96fd10db79b691fa606e5d521d776b"
"mtime": 1791708825.7957535,
"seen": 1791710591.5772536,
"ast_hash": "e0f571ab730d0c1de7a17c8e908f161d",
"semantic_hash": ""
},
"admin_panel/views/auth.py": {
"mtime": 1791557771.327603,
@@ -960,22 +960,22 @@
"semantic_hash": "364f4190f934261a082c189d8f44d29c"
},
"admin_panel/views/customers.py": {
"mtime": 1791568689.7329805,
"seen": 1791614104.8488438,
"ast_hash": "10f28fbc7076400ec0074172cc0471ea",
"mtime": 1791711853.5219991,
"seen": 1791714098.633808,
"ast_hash": "43945c8ffdbde356447635b1203b9566",
"semantic_hash": ""
},
"admin_panel/views/factors.py": {
"mtime": 1791557824.3959832,
"seen": 1791560671.8564537,
"ast_hash": "4edb8f009937a4d52ae4355c15950797",
"semantic_hash": "4edb8f009937a4d52ae4355c15950797"
"mtime": 1791710085.8869143,
"seen": 1791710591.5772536,
"ast_hash": "d47aa24b93c787acf8f385e0765bdbeb",
"semantic_hash": ""
},
"admin_panel/views/finance.py": {
"mtime": 1791557914.4702559,
"seen": 1791560671.8564537,
"ast_hash": "6d192f725bec56cac50f8ab1a3f7529a",
"semantic_hash": "6d192f725bec56cac50f8ab1a3f7529a"
"mtime": 1791713964.8948607,
"seen": 1791714098.633808,
"ast_hash": "d199e23a302ef9eee93c9abf459bc511",
"semantic_hash": ""
},
"admin_panel/views/store.py": {
"mtime": 1791557842.9065113,
@@ -996,16 +996,16 @@
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
},
"tests/factories.py": {
"mtime": 1791558395.0672476,
"seen": 1791560671.8564537,
"ast_hash": "50fd151eba01ea60af0eca0412c6769c",
"semantic_hash": "50fd151eba01ea60af0eca0412c6769c"
"mtime": 1791615046.5393274,
"seen": 1791702801.3202996,
"ast_hash": "44b675e6955ee776b91f3f3bbdbb1fc0",
"semantic_hash": "44b675e6955ee776b91f3f3bbdbb1fc0"
},
"tests/test_customers.py": {
"mtime": 1791558470.3088942,
"seen": 1791560671.8564537,
"ast_hash": "59649733efa1edadc23835a27f68bbba",
"semantic_hash": "59649733efa1edadc23835a27f68bbba"
"mtime": 1791713632.3402467,
"seen": 1791714098.633808,
"ast_hash": "cf9df1225c3c9d7b101ab3b2e2db8f76",
"semantic_hash": ""
},
"tests/test_factors.py": {
"mtime": 1791558462.1194556,
@@ -1026,15 +1026,15 @@
"semantic_hash": "55662ec48c0ef4b683a87b94808b91b0"
},
"tests/test_urls_contract.py": {
"mtime": 1791564827.3614242,
"seen": 1791614104.8488438,
"ast_hash": "c500c49133546f55ce37a11fe374a5dc",
"mtime": 1791718031.9609113,
"seen": 1791720362.5829337,
"ast_hash": "e02361bf950d2549018c1b19fad366bb",
"semantic_hash": ""
},
"tests/test_view_smoke.py": {
"mtime": 1791567549.952807,
"seen": 1791614104.8488438,
"ast_hash": "1f375dcb28dd39eb1d7ca203cac2b8d9",
"mtime": 1791718041.2553785,
"seen": 1791720362.5829337,
"ast_hash": "527f0f3fcffdddd54cf8db80ed21ce2b",
"semantic_hash": ""
},
"AGENTS.md": {
@@ -1051,254 +1051,410 @@
},
"admin_panel/management/commands/sms_scan.py": {
"mtime": 1791564935.0555663,
"seen": 1791614104.8488438,
"seen": 1791702801.2988422,
"ast_hash": "ed3caa947399bcd9d8d7785c0400c81e",
"semantic_hash": ""
"semantic_hash": "ed3caa947399bcd9d8d7785c0400c81e"
},
"admin_panel/management/commands/sms_worker.py": {
"mtime": 1791570227.772429,
"seen": 1791614104.8488438,
"ast_hash": "b632f000bc231f91c876a57819e94d4c",
"semantic_hash": ""
"mtime": 1791617008.8882565,
"seen": 1791702801.2988422,
"ast_hash": "fe7d671672a25ef84c428955640bb136",
"semantic_hash": "fe7d671672a25ef84c428955640bb136"
},
"admin_panel/migrations/0057_sms.py": {
"mtime": 1791564769.8150086,
"seen": 1791614104.8488438,
"seen": 1791702801.3128505,
"ast_hash": "48769309cf799b598f1511a37865c665",
"semantic_hash": ""
"semantic_hash": "48769309cf799b598f1511a37865c665"
},
"admin_panel/migrations/0058_seed_sms_templates.py": {
"mtime": 1791564787.7929118,
"seen": 1791614104.8488438,
"ast_hash": "8e42b8ea8b81ab4b3662cb70075e055b",
"semantic_hash": ""
"mtime": 1791618477.6292322,
"seen": 1791702801.3128505,
"ast_hash": "644789822fa1fab80f29f0ef995b95aa",
"semantic_hash": "644789822fa1fab80f29f0ef995b95aa"
},
"admin_panel/migrations/0059_sms_flow_control.py": {
"mtime": 1791565703.6785793,
"seen": 1791614104.8488438,
"seen": 1791702801.3128505,
"ast_hash": "d44f0ff09baf2be5dfee52a09abb2326",
"semantic_hash": ""
"semantic_hash": "d44f0ff09baf2be5dfee52a09abb2326"
},
"admin_panel/migrations/0060_sms_permission_label.py": {
"mtime": 1791566229.439728,
"seen": 1791614104.8488438,
"seen": 1791702801.3128505,
"ast_hash": "6c11511c7b690a0fcb7ca5fae64c512c",
"semantic_hash": ""
"semantic_hash": "6c11511c7b690a0fcb7ca5fae64c512c"
},
"admin_panel/migrations/0061_sms_timing_and_primary.py": {
"mtime": 1791567453.636376,
"seen": 1791614104.8488438,
"seen": 1791702801.3128505,
"ast_hash": "f389c45783e25be977e01db9175b96cd",
"semantic_hash": ""
"semantic_hash": "f389c45783e25be977e01db9175b96cd"
},
"admin_panel/migrations/0062_sms_link_provider.py": {
"mtime": 1791571832.394663,
"seen": 1791614104.8488438,
"seen": 1791702801.3128505,
"ast_hash": "be04ce55506d075f0afe266cadeda08e",
"semantic_hash": ""
"semantic_hash": "be04ce55506d075f0afe266cadeda08e"
},
"admin_panel/models/sms.py": {
"mtime": 1791571800.3552737,
"seen": 1791614104.8488438,
"ast_hash": "e8e1edef3b27bd220a755b3519f168aa",
"mtime": 1791719506.0225618,
"seen": 1791720362.5829337,
"ast_hash": "8e4bb3e917888a16e675f3f139207b1b",
"semantic_hash": ""
},
"admin_panel/signals.py": {
"mtime": 1791564587.418286,
"seen": 1791614104.8488438,
"seen": 1791702801.3150773,
"ast_hash": "2eace8c40bf29514679f1113c3d28fdb",
"semantic_hash": ""
"semantic_hash": "2eace8c40bf29514679f1113c3d28fdb"
},
"admin_panel/sms/__init__.py": {
"mtime": 1791564397.2313416,
"seen": 1791614104.8488438,
"seen": 1791702801.3150773,
"ast_hash": "22f7602ea6f852c9a1cf8a71a40c7f5c",
"semantic_hash": ""
"semantic_hash": "22f7602ea6f852c9a1cf8a71a40c7f5c"
},
"admin_panel/sms/enqueue.py": {
"mtime": 1791564422.1932628,
"seen": 1791614104.8488438,
"ast_hash": "f3b793473bbf7f4d0f699966fc778610",
"semantic_hash": ""
"mtime": 1791626998.7211082,
"seen": 1791702801.3150773,
"ast_hash": "7d00e585643244a5e694427047972c74",
"semantic_hash": "7d00e585643244a5e694427047972c74"
},
"admin_panel/sms/phones.py": {
"mtime": 1791567752.8740702,
"seen": 1791614104.8488438,
"seen": 1791702801.3150773,
"ast_hash": "e7a2f0d3b2ccfbc3e37351f134c0a3c8",
"semantic_hash": ""
"semantic_hash": "e7a2f0d3b2ccfbc3e37351f134c0a3c8"
},
"admin_panel/sms/recipients.py": {
"mtime": 1791567203.9483874,
"seen": 1791614104.8488438,
"seen": 1791702801.3150773,
"ast_hash": "19bd0d12c9932809424ddcc58d9d5ccd",
"semantic_hash": ""
"semantic_hash": "19bd0d12c9932809424ddcc58d9d5ccd"
},
"admin_panel/sms/render.py": {
"mtime": 1791564403.1354272,
"seen": 1791614104.8488438,
"seen": 1791702801.3150773,
"ast_hash": "9b82ac87329178259ffa6929b1f0bd94",
"semantic_hash": ""
"semantic_hash": "9b82ac87329178259ffa6929b1f0bd94"
},
"admin_panel/sms/rules.py": {
"mtime": 1791564642.983444,
"seen": 1791614104.8488438,
"ast_hash": "fc8fcea92189cc20de4dc722f309150f",
"mtime": 1791708897.995327,
"seen": 1791710591.5772536,
"ast_hash": "ec604f627bcc4534ae16da8503f64085",
"semantic_hash": ""
},
"admin_panel/sms/scanner.py": {
"mtime": 1791567251.4876964,
"seen": 1791614104.8488438,
"ast_hash": "24a6409edcdd755c353092a56d12af15",
"mtime": 1791708785.8918655,
"seen": 1791710591.5772536,
"ast_hash": "e9d675f9673e33de929cd9c06fe7de41",
"semantic_hash": ""
},
"admin_panel/sms/timing.py": {
"mtime": 1791567400.39615,
"seen": 1791614104.8488438,
"ast_hash": "675024399edacf343357c3481471fa19",
"mtime": 1791708745.8135014,
"seen": 1791710591.5772536,
"ast_hash": "4819c52781b07b14c5c19f66a89bea9c",
"semantic_hash": ""
},
"admin_panel/views/sms.py": {
"mtime": 1791568027.236827,
"seen": 1791614104.8488438,
"ast_hash": "dc0f5c6b03f46965c52d60a70661a882",
"mtime": 1791720558.729439,
"seen": 1791721256.78409,
"ast_hash": "c0041541c95e34dd74b4d5439a701f73",
"semantic_hash": ""
},
"integrations/__init__.py": {
"mtime": 1788591142.756229,
"seen": 1791614104.8488438,
"seen": 1791702801.3178282,
"ast_hash": "d41d8cd98f00b204e9800998ecf8427e",
"semantic_hash": ""
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
},
"integrations/announcements.py": {
"mtime": 1790610911.1591492,
"seen": 1791614104.8488438,
"seen": 1791702801.3178282,
"ast_hash": "4addf10259bf16babf4b6ad98fe822bb",
"semantic_hash": ""
"semantic_hash": "4addf10259bf16babf4b6ad98fe822bb"
},
"integrations/bale/__init__.py": {
"mtime": 1788597944.5804,
"seen": 1791614104.8488438,
"seen": 1791702801.3178282,
"ast_hash": "d41d8cd98f00b204e9800998ecf8427e",
"semantic_hash": ""
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
},
"integrations/bale/providers.py": {
"mtime": 1791366465.5345488,
"seen": 1791614104.8488438,
"seen": 1791702801.3178282,
"ast_hash": "f2ccd57a80e8e97b1ab8be7325b1f27a",
"semantic_hash": ""
"semantic_hash": "f2ccd57a80e8e97b1ab8be7325b1f27a"
},
"integrations/sms/__init__.py": {
"mtime": 1788591146.1142309,
"seen": 1791614104.8488438,
"seen": 1791702801.3178282,
"ast_hash": "d41d8cd98f00b204e9800998ecf8427e",
"semantic_hash": ""
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
},
"integrations/sms/base.py": {
"mtime": 1789362040.0196111,
"seen": 1791614104.8488438,
"seen": 1791702801.3178282,
"ast_hash": "03c0d180df50cb521d7120ee6f76108c",
"semantic_hash": ""
"semantic_hash": "03c0d180df50cb521d7120ee6f76108c"
},
"integrations/sms/behin.py": {
"mtime": 1788695832.9056084,
"seen": 1791614104.8488438,
"seen": 1791702801.3178282,
"ast_hash": "e2028ce13ec6cdc7f603aa44bac7e7fd",
"semantic_hash": ""
"semantic_hash": "e2028ce13ec6cdc7f603aa44bac7e7fd"
},
"integrations/sms/console.py": {
"mtime": 1791570192.9150548,
"seen": 1791614104.8488438,
"seen": 1791702801.3178282,
"ast_hash": "4120946c1d52e58f0ec2fa110c2442a6",
"semantic_hash": ""
"semantic_hash": "4120946c1d52e58f0ec2fa110c2442a6"
},
"integrations/sms/factory.py": {
"mtime": 1791564358.8557389,
"seen": 1791614104.8488438,
"ast_hash": "92d89fa13a0e75ecdc5de5181c5cb13b",
"semantic_hash": ""
"mtime": 1791616908.4313548,
"seen": 1791702801.3178282,
"ast_hash": "d02f691930a144885ef755f5229e8299",
"semantic_hash": "d02f691930a144885ef755f5229e8299"
},
"integrations/sms/http.py": {
"mtime": 1791570455.5151258,
"seen": 1791614104.8488438,
"seen": 1791702801.3178282,
"ast_hash": "5dbf2f727c47d6bcc8ae067134205317",
"semantic_hash": ""
"semantic_hash": "5dbf2f727c47d6bcc8ae067134205317"
},
"integrations/sms/normalize.py": {
"mtime": 1791564352.0481617,
"seen": 1791614104.8488438,
"seen": 1791702801.3178282,
"ast_hash": "e022b15a9a5e16f7cb9b3f7c8aba9eb0",
"semantic_hash": ""
"semantic_hash": "e022b15a9a5e16f7cb9b3f7c8aba9eb0"
},
"integrations/sms/providers.py": {
"mtime": 1791571810.6925046,
"seen": 1791614104.8488438,
"ast_hash": "0ad971198d05e572b2feab738cf3d315",
"semantic_hash": ""
"mtime": 1791616909.5377069,
"seen": 1791702801.3178282,
"ast_hash": "7f0d619716ffdb880cff970e58b81fa6",
"semantic_hash": "7f0d619716ffdb880cff970e58b81fa6"
},
"integrations/sms/sms0098.py": {
"mtime": 1791573482.2129214,
"seen": 1791614104.8488438,
"seen": 1791702801.3178282,
"ast_hash": "66945d0a7a930c90522b6ab18a83c950",
"semantic_hash": ""
"semantic_hash": "66945d0a7a930c90522b6ab18a83c950"
},
"integrations/voip.py": {
"mtime": 1790611362.4278078,
"seen": 1791614104.8488438,
"seen": 1791702801.3178282,
"ast_hash": "3d171dd1f3023ad1372d8fc1a1be0c7c",
"semantic_hash": ""
"semantic_hash": "3d171dd1f3023ad1372d8fc1a1be0c7c"
},
"run.sh": {
"mtime": 1791570234.3635445,
"seen": 1791614104.8488438,
"ast_hash": "a2b5cef99cc254f1f7c811bdf27d01fe",
"semantic_hash": ""
"mtime": 1791617114.3584476,
"seen": 1791702801.3199909,
"ast_hash": "776fa3896434b36ecc788f36f0b9a598",
"semantic_hash": "776fa3896434b36ecc788f36f0b9a598"
},
"stop.sh": {
"mtime": 1791569764.8847363,
"seen": 1791614104.8488438,
"seen": 1791702801.3199909,
"ast_hash": "d181c7e91c9fc824e45ee36afe852884",
"semantic_hash": ""
"semantic_hash": "d181c7e91c9fc824e45ee36afe852884"
},
"tests/test_sms.py": {
"mtime": 1791571899.7188718,
"seen": 1791614104.8488438,
"ast_hash": "e09952b55a7aa687607ee5de505e0992",
"mtime": 1791720635.36276,
"seen": 1791721256.78409,
"ast_hash": "797136590c528a8a52e6884b355122d2",
"semantic_hash": ""
},
"admin_panel/templates/admin_panel/_customer_sms.html": {
"mtime": 1791567700.7075107,
"seen": 1791614104.8488438,
"ast_hash": "7a044490275116dc8f47bdfc29410938",
"mtime": 1791711840.1595013,
"seen": 1791714098.633808,
"ast_hash": "a310d9aa207143cbe4f705da3f9d5253",
"semantic_hash": ""
},
"admin_panel/templates/admin_panel/sms_queue.html": {
"mtime": 1791564635.2349718,
"seen": 1791614104.8488438,
"ast_hash": "e89844164967411d20f2e74f4aaaa501",
"mtime": 1791720613.7829041,
"seen": 1791721256.7850897,
"ast_hash": "e6c1ae1d848bc0c7e5ed4984850c7876",
"semantic_hash": ""
},
"admin_panel/templates/admin_panel/sms_settings.html": {
"mtime": 1791571805.9115343,
"seen": 1791614104.8488438,
"ast_hash": "89e5910fc3a560dca5b8253047b4aa6c",
"mtime": 1791719660.6053514,
"seen": 1791720362.5829337,
"ast_hash": "4f8933e5687929857d93448098e6806e",
"semantic_hash": ""
},
"admin_panel/templates/admin_panel/sms_templates.html": {
"mtime": 1791568503.831997,
"seen": 1791614104.8488438,
"ast_hash": "43820b9951c1a89e17388293f3383302",
"semantic_hash": ""
"mtime": 1791627052.1153367,
"seen": 1791702801.3208632,
"ast_hash": "46a8ec7859956045631cfa8433461236",
"semantic_hash": "46a8ec7859956045631cfa8433461236"
},
"docker-compose.yml": {
"mtime": 1791564755.02377,
"seen": 1791614104.8488438,
"ast_hash": "45bf00ab0fc7ce59c6fb247c42358e0d",
"semantic_hash": ""
"mtime": 1791636506.4167986,
"seen": 1791702801.3208632,
"ast_hash": "0b75a943cb3429658925f450e26d3e7d",
"semantic_hash": "0b75a943cb3429658925f450e26d3e7d"
},
"screencapture-localhost-8000-panel-sms-templates-2026-10-09-23_01_18.png": {
"mtime": 1791574286.9688969,
"seen": 1791614104.8488438,
"seen": 1791702801.3208632,
"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": ""
}
}
+138
View File
@@ -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
View File
@@ -4,7 +4,7 @@
from django.test import TestCase
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
@@ -55,6 +55,94 @@ class CustomerCrudTests(TestCase):
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):
"""مشتری غیرفعال (is_active=False) در فهرست و در جستجو دیده نمی‌شود."""
+760 -124
View File
File diff suppressed because it is too large. Load diff
+4 -1
View File
@@ -7,7 +7,7 @@
from django.test import SimpleTestCase
from django.urls import reverse
# snapshot قطعی نام‌های مسیرهای اپ admin_panel (۷۲ مسیر)
# snapshot قطعی نام‌های مسیرهای اپ admin_panel (۷۷ مسیر)
EXPECTED_URL_NAMES = {
'index',
'p_login',
@@ -76,10 +76,12 @@ EXPECTED_URL_NAMES = {
'sms_settings',
'sms_templates',
'sms_template_edit',
'customerSmsHistory',
'sms_customer_toggle',
'sms_primary_toggle',
'sms_send_custom',
'sms_queue',
'sms_report',
'sms_queue_retry',
'sms_queue_approve',
'sms_queue_delete',
@@ -124,6 +126,7 @@ PARAMETERIZED = {
'addSarresid': {'cs_id': 1},
'pardakhtNesiye': {'cs_id': 1},
'sms_template_edit': {'t_id': 1},
'customerSmsHistory': {'c_id': 1},
'sms_customer_toggle': {'c_id': 1},
'sms_primary_toggle': {'c_id': 1},
'sms_send_custom': {'c_id': 1},
+2
View File
@@ -224,7 +224,9 @@ class ViewSmokeTests(TestCase):
'phone': '09120000000', 'is_primary': 'true',
}),
('sms_send_custom', {'c_id': c.id}, POST, {'text': 'پیامک دودی'}),
('customerSmsHistory', {'c_id': c.id}, GET, None),
('sms_queue', {}, GET, None),
('sms_report', {}, GET, None),
('sms_queue_retry', {'q_id': self.sms_queue.id}, POST, None),
# در انتهای لیست: خروج از حساب، چون بعد از آن تمام درخواست‌ها بی‌اعتبار می‌شوند
('p_logout', {}, GET, None),