From 7be9310a965a60b9de93bb089e75aae6c229deca Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 23 Nov 2025 01:09:28 +0000 Subject: [PATCH] v1.1 --- config/__pycache__/settings.cpython-311.pyc | Bin 5396 -> 5662 bytes config/settings.py | 9 + core/__pycache__/forms.cpython-311.pyc | Bin 0 -> 2520 bytes core/__pycache__/middleware.cpython-311.pyc | Bin 0 -> 1818 bytes core/__pycache__/models.cpython-311.pyc | Bin 209 -> 13468 bytes .../opportunity_urls.cpython-311.pyc | Bin 0 -> 1134 bytes core/__pycache__/urls.cpython-311.pyc | Bin 347 -> 1882 bytes core/__pycache__/views.cpython-311.pyc | Bin 1364 -> 32949 bytes core/forms.py | 44 + .../seed_customers.cpython-311.pyc | Bin 0 -> 6329 bytes core/management/commands/seed_customers.py | 101 + core/middleware.py | 25 + core/migrations/0001_initial.py | 157 + ..._managers_remove_user_username_and_more.py | 29 + core/migrations/0003_customer_owner.py | 20 + ...d_at_contacthistory_deleted_by_and_more.py | 115 + core/migrations/0005_opportunity_name.py | 18 + ...006_contacthistory_restored_at_and_more.py | 65 + core/migrations/0007_activitylog.py | 30 + core/migrations/0008_activitylog_details.py | 18 + .../0009_alter_activitylog_timestamp.py | 18 + .../__pycache__/0001_initial.cpython-311.pyc | Bin 0 -> 11404 bytes ...ove_user_username_and_more.cpython-311.pyc | Bin 0 -> 1181 bytes .../0003_customer_owner.cpython-311.pyc | Bin 0 -> 1141 bytes ...istory_deleted_by_and_more.cpython-311.pyc | Bin 0 -> 5005 bytes .../0005_opportunity_name.cpython-311.pyc | Bin 0 -> 870 bytes ...story_restored_at_and_more.cpython-311.pyc | Bin 0 -> 2704 bytes .../0007_activitylog.cpython-311.pyc | Bin 0 -> 1806 bytes .../0008_activitylog_details.cpython-311.pyc | Bin 0 -> 806 bytes ...lter_activitylog_timestamp.cpython-311.pyc | Bin 0 -> 841 bytes core/models.py | 204 +- core/opportunity_urls.py | 18 + core/templates/base.html | 36 +- core/templates/core/_activity_feed.html | 20 + .../core/customer_confirm_delete.html | 13 + core/templates/core/customer_detail.html | 206 + core/templates/core/customer_form.html | 50 + core/templates/core/customer_list.html | 465 + core/templates/core/index.html | 188 +- .../core/opportunity_confirm_delete.html | 13 + core/templates/core/opportunity_detail.html | 49 + core/templates/core/opportunity_form.html | 35 + core/templates/core/opportunity_list.html | 39 + core/templatetags/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 170 bytes .../__pycache__/query_string.cpython-311.pyc | Bin 0 -> 756 bytes core/templatetags/query_string.py | 10 + core/urls.py | 15 +- core/views.py | 509 + requirements.txt | 1 + static/admin/css/autocomplete.css | 279 + static/admin/css/base.css | 1180 ++ static/admin/css/changelists.css | 343 + static/admin/css/dark_mode.css | 130 + static/admin/css/dashboard.css | 29 + static/admin/css/forms.css | 498 + static/admin/css/login.css | 61 + static/admin/css/nav_sidebar.css | 150 + static/admin/css/responsive.css | 904 ++ static/admin/css/responsive_rtl.css | 89 + static/admin/css/rtl.css | 293 + static/admin/css/unusable_password_field.css | 19 + .../css/vendor/select2/LICENSE-SELECT2.md | 21 + static/admin/css/vendor/select2/select2.css | 481 + .../admin/css/vendor/select2/select2.min.css | 1 + static/admin/css/widgets.css | 613 + static/admin/img/LICENSE | 20 + static/admin/img/README.txt | 7 + static/admin/img/calendar-icons.svg | 63 + static/admin/img/gis/move_vertex_off.svg | 1 + static/admin/img/gis/move_vertex_on.svg | 1 + static/admin/img/icon-addlink.svg | 3 + static/admin/img/icon-alert.svg | 3 + static/admin/img/icon-calendar.svg | 9 + static/admin/img/icon-changelink.svg | 3 + static/admin/img/icon-clock.svg | 9 + static/admin/img/icon-deletelink.svg | 3 + static/admin/img/icon-hidelink.svg | 3 + static/admin/img/icon-no.svg | 3 + static/admin/img/icon-unknown-alt.svg | 3 + static/admin/img/icon-unknown.svg | 3 + static/admin/img/icon-viewlink.svg | 3 + static/admin/img/icon-yes.svg | 3 + static/admin/img/inline-delete.svg | 3 + static/admin/img/search.svg | 3 + static/admin/img/selector-icons.svg | 34 + static/admin/img/sorting-icons.svg | 19 + static/admin/img/tooltag-add.svg | 3 + static/admin/img/tooltag-arrowright.svg | 3 + static/admin/js/SelectBox.js | 116 + static/admin/js/SelectFilter2.js | 311 + static/admin/js/actions.js | 204 + static/admin/js/admin/DateTimeShortcuts.js | 408 + static/admin/js/admin/RelatedObjectLookups.js | 252 + static/admin/js/autocomplete.js | 33 + static/admin/js/calendar.js | 239 + static/admin/js/cancel.js | 29 + static/admin/js/change_form.js | 16 + static/admin/js/core.js | 184 + static/admin/js/filters.js | 30 + static/admin/js/inlines.js | 359 + static/admin/js/jquery.init.js | 8 + static/admin/js/nav_sidebar.js | 79 + static/admin/js/popup_response.js | 15 + static/admin/js/prepopulate.js | 43 + static/admin/js/prepopulate_init.js | 15 + static/admin/js/theme.js | 51 + static/admin/js/unusable_password_field.js | 29 + static/admin/js/urlify.js | 169 + static/admin/js/vendor/jquery/LICENSE.txt | 20 + static/admin/js/vendor/jquery/jquery.js | 10716 ++++++++++++++++ static/admin/js/vendor/jquery/jquery.min.js | 2 + static/admin/js/vendor/select2/LICENSE.md | 21 + static/admin/js/vendor/select2/i18n/af.js | 3 + static/admin/js/vendor/select2/i18n/ar.js | 3 + static/admin/js/vendor/select2/i18n/az.js | 3 + static/admin/js/vendor/select2/i18n/bg.js | 3 + static/admin/js/vendor/select2/i18n/bn.js | 3 + static/admin/js/vendor/select2/i18n/bs.js | 3 + static/admin/js/vendor/select2/i18n/ca.js | 3 + static/admin/js/vendor/select2/i18n/cs.js | 3 + static/admin/js/vendor/select2/i18n/da.js | 3 + static/admin/js/vendor/select2/i18n/de.js | 3 + static/admin/js/vendor/select2/i18n/dsb.js | 3 + static/admin/js/vendor/select2/i18n/el.js | 3 + static/admin/js/vendor/select2/i18n/en.js | 3 + static/admin/js/vendor/select2/i18n/es.js | 3 + static/admin/js/vendor/select2/i18n/et.js | 3 + static/admin/js/vendor/select2/i18n/eu.js | 3 + static/admin/js/vendor/select2/i18n/fa.js | 3 + static/admin/js/vendor/select2/i18n/fi.js | 3 + static/admin/js/vendor/select2/i18n/fr.js | 3 + static/admin/js/vendor/select2/i18n/gl.js | 3 + static/admin/js/vendor/select2/i18n/he.js | 3 + static/admin/js/vendor/select2/i18n/hi.js | 3 + static/admin/js/vendor/select2/i18n/hr.js | 3 + static/admin/js/vendor/select2/i18n/hsb.js | 3 + static/admin/js/vendor/select2/i18n/hu.js | 3 + static/admin/js/vendor/select2/i18n/hy.js | 3 + static/admin/js/vendor/select2/i18n/id.js | 3 + static/admin/js/vendor/select2/i18n/is.js | 3 + static/admin/js/vendor/select2/i18n/it.js | 3 + static/admin/js/vendor/select2/i18n/ja.js | 3 + static/admin/js/vendor/select2/i18n/ka.js | 3 + static/admin/js/vendor/select2/i18n/km.js | 3 + static/admin/js/vendor/select2/i18n/ko.js | 3 + static/admin/js/vendor/select2/i18n/lt.js | 3 + static/admin/js/vendor/select2/i18n/lv.js | 3 + static/admin/js/vendor/select2/i18n/mk.js | 3 + static/admin/js/vendor/select2/i18n/ms.js | 3 + static/admin/js/vendor/select2/i18n/nb.js | 3 + static/admin/js/vendor/select2/i18n/ne.js | 3 + static/admin/js/vendor/select2/i18n/nl.js | 3 + static/admin/js/vendor/select2/i18n/pl.js | 3 + static/admin/js/vendor/select2/i18n/ps.js | 3 + static/admin/js/vendor/select2/i18n/pt-BR.js | 3 + static/admin/js/vendor/select2/i18n/pt.js | 3 + static/admin/js/vendor/select2/i18n/ro.js | 3 + static/admin/js/vendor/select2/i18n/ru.js | 3 + static/admin/js/vendor/select2/i18n/sk.js | 3 + static/admin/js/vendor/select2/i18n/sl.js | 3 + static/admin/js/vendor/select2/i18n/sq.js | 3 + .../admin/js/vendor/select2/i18n/sr-Cyrl.js | 3 + static/admin/js/vendor/select2/i18n/sr.js | 3 + static/admin/js/vendor/select2/i18n/sv.js | 3 + static/admin/js/vendor/select2/i18n/th.js | 3 + static/admin/js/vendor/select2/i18n/tk.js | 3 + static/admin/js/vendor/select2/i18n/tr.js | 3 + static/admin/js/vendor/select2/i18n/uk.js | 3 + static/admin/js/vendor/select2/i18n/vi.js | 3 + static/admin/js/vendor/select2/i18n/zh-CN.js | 3 + static/admin/js/vendor/select2/i18n/zh-TW.js | 3 + .../admin/js/vendor/select2/select2.full.js | 6820 ++++++++++ .../js/vendor/select2/select2.full.min.js | 2 + static/admin/js/vendor/xregexp/LICENSE.txt | 21 + static/admin/js/vendor/xregexp/xregexp.js | 6126 +++++++++ static/admin/js/vendor/xregexp/xregexp.min.js | 17 + static/css/custom.css | 88 + 178 files changed, 34264 insertions(+), 152 deletions(-) create mode 100644 core/__pycache__/forms.cpython-311.pyc create mode 100644 core/__pycache__/middleware.cpython-311.pyc create mode 100644 core/__pycache__/opportunity_urls.cpython-311.pyc create mode 100644 core/forms.py create mode 100644 core/management/commands/__pycache__/seed_customers.cpython-311.pyc create mode 100644 core/management/commands/seed_customers.py create mode 100644 core/middleware.py create mode 100644 core/migrations/0001_initial.py create mode 100644 core/migrations/0002_alter_user_managers_remove_user_username_and_more.py create mode 100644 core/migrations/0003_customer_owner.py create mode 100644 core/migrations/0004_contacthistory_deleted_at_contacthistory_deleted_by_and_more.py create mode 100644 core/migrations/0005_opportunity_name.py create mode 100644 core/migrations/0006_contacthistory_restored_at_and_more.py create mode 100644 core/migrations/0007_activitylog.py create mode 100644 core/migrations/0008_activitylog_details.py create mode 100644 core/migrations/0009_alter_activitylog_timestamp.py create mode 100644 core/migrations/__pycache__/0001_initial.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0002_alter_user_managers_remove_user_username_and_more.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0003_customer_owner.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0004_contacthistory_deleted_at_contacthistory_deleted_by_and_more.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0005_opportunity_name.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0006_contacthistory_restored_at_and_more.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0007_activitylog.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0008_activitylog_details.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0009_alter_activitylog_timestamp.cpython-311.pyc create mode 100644 core/opportunity_urls.py create mode 100644 core/templates/core/_activity_feed.html create mode 100644 core/templates/core/customer_confirm_delete.html create mode 100644 core/templates/core/customer_detail.html create mode 100644 core/templates/core/customer_form.html create mode 100644 core/templates/core/customer_list.html create mode 100644 core/templates/core/opportunity_confirm_delete.html create mode 100644 core/templates/core/opportunity_detail.html create mode 100644 core/templates/core/opportunity_form.html create mode 100644 core/templates/core/opportunity_list.html create mode 100644 core/templatetags/__init__.py create mode 100644 core/templatetags/__pycache__/__init__.cpython-311.pyc create mode 100644 core/templatetags/__pycache__/query_string.cpython-311.pyc create mode 100644 core/templatetags/query_string.py create mode 100644 static/admin/css/autocomplete.css create mode 100644 static/admin/css/base.css create mode 100644 static/admin/css/changelists.css create mode 100644 static/admin/css/dark_mode.css create mode 100644 static/admin/css/dashboard.css create mode 100644 static/admin/css/forms.css create mode 100644 static/admin/css/login.css create mode 100644 static/admin/css/nav_sidebar.css create mode 100644 static/admin/css/responsive.css create mode 100644 static/admin/css/responsive_rtl.css create mode 100644 static/admin/css/rtl.css create mode 100644 static/admin/css/unusable_password_field.css create mode 100644 static/admin/css/vendor/select2/LICENSE-SELECT2.md create mode 100644 static/admin/css/vendor/select2/select2.css create mode 100644 static/admin/css/vendor/select2/select2.min.css create mode 100644 static/admin/css/widgets.css create mode 100644 static/admin/img/LICENSE create mode 100644 static/admin/img/README.txt create mode 100644 static/admin/img/calendar-icons.svg create mode 100644 static/admin/img/gis/move_vertex_off.svg create mode 100644 static/admin/img/gis/move_vertex_on.svg create mode 100644 static/admin/img/icon-addlink.svg create mode 100644 static/admin/img/icon-alert.svg create mode 100644 static/admin/img/icon-calendar.svg create mode 100644 static/admin/img/icon-changelink.svg create mode 100644 static/admin/img/icon-clock.svg create mode 100644 static/admin/img/icon-deletelink.svg create mode 100644 static/admin/img/icon-hidelink.svg create mode 100644 static/admin/img/icon-no.svg create mode 100644 static/admin/img/icon-unknown-alt.svg create mode 100644 static/admin/img/icon-unknown.svg create mode 100644 static/admin/img/icon-viewlink.svg create mode 100644 static/admin/img/icon-yes.svg create mode 100644 static/admin/img/inline-delete.svg create mode 100644 static/admin/img/search.svg create mode 100644 static/admin/img/selector-icons.svg create mode 100644 static/admin/img/sorting-icons.svg create mode 100644 static/admin/img/tooltag-add.svg create mode 100644 static/admin/img/tooltag-arrowright.svg create mode 100644 static/admin/js/SelectBox.js create mode 100644 static/admin/js/SelectFilter2.js create mode 100644 static/admin/js/actions.js create mode 100644 static/admin/js/admin/DateTimeShortcuts.js create mode 100644 static/admin/js/admin/RelatedObjectLookups.js create mode 100644 static/admin/js/autocomplete.js create mode 100644 static/admin/js/calendar.js create mode 100644 static/admin/js/cancel.js create mode 100644 static/admin/js/change_form.js create mode 100644 static/admin/js/core.js create mode 100644 static/admin/js/filters.js create mode 100644 static/admin/js/inlines.js create mode 100644 static/admin/js/jquery.init.js create mode 100644 static/admin/js/nav_sidebar.js create mode 100644 static/admin/js/popup_response.js create mode 100644 static/admin/js/prepopulate.js create mode 100644 static/admin/js/prepopulate_init.js create mode 100644 static/admin/js/theme.js create mode 100644 static/admin/js/unusable_password_field.js create mode 100644 static/admin/js/urlify.js create mode 100644 static/admin/js/vendor/jquery/LICENSE.txt create mode 100644 static/admin/js/vendor/jquery/jquery.js create mode 100644 static/admin/js/vendor/jquery/jquery.min.js create mode 100644 static/admin/js/vendor/select2/LICENSE.md create mode 100644 static/admin/js/vendor/select2/i18n/af.js create mode 100644 static/admin/js/vendor/select2/i18n/ar.js create mode 100644 static/admin/js/vendor/select2/i18n/az.js create mode 100644 static/admin/js/vendor/select2/i18n/bg.js create mode 100644 static/admin/js/vendor/select2/i18n/bn.js create mode 100644 static/admin/js/vendor/select2/i18n/bs.js create mode 100644 static/admin/js/vendor/select2/i18n/ca.js create mode 100644 static/admin/js/vendor/select2/i18n/cs.js create mode 100644 static/admin/js/vendor/select2/i18n/da.js create mode 100644 static/admin/js/vendor/select2/i18n/de.js create mode 100644 static/admin/js/vendor/select2/i18n/dsb.js create mode 100644 static/admin/js/vendor/select2/i18n/el.js create mode 100644 static/admin/js/vendor/select2/i18n/en.js create mode 100644 static/admin/js/vendor/select2/i18n/es.js create mode 100644 static/admin/js/vendor/select2/i18n/et.js create mode 100644 static/admin/js/vendor/select2/i18n/eu.js create mode 100644 static/admin/js/vendor/select2/i18n/fa.js create mode 100644 static/admin/js/vendor/select2/i18n/fi.js create mode 100644 static/admin/js/vendor/select2/i18n/fr.js create mode 100644 static/admin/js/vendor/select2/i18n/gl.js create mode 100644 static/admin/js/vendor/select2/i18n/he.js create mode 100644 static/admin/js/vendor/select2/i18n/hi.js create mode 100644 static/admin/js/vendor/select2/i18n/hr.js create mode 100644 static/admin/js/vendor/select2/i18n/hsb.js create mode 100644 static/admin/js/vendor/select2/i18n/hu.js create mode 100644 static/admin/js/vendor/select2/i18n/hy.js create mode 100644 static/admin/js/vendor/select2/i18n/id.js create mode 100644 static/admin/js/vendor/select2/i18n/is.js create mode 100644 static/admin/js/vendor/select2/i18n/it.js create mode 100644 static/admin/js/vendor/select2/i18n/ja.js create mode 100644 static/admin/js/vendor/select2/i18n/ka.js create mode 100644 static/admin/js/vendor/select2/i18n/km.js create mode 100644 static/admin/js/vendor/select2/i18n/ko.js create mode 100644 static/admin/js/vendor/select2/i18n/lt.js create mode 100644 static/admin/js/vendor/select2/i18n/lv.js create mode 100644 static/admin/js/vendor/select2/i18n/mk.js create mode 100644 static/admin/js/vendor/select2/i18n/ms.js create mode 100644 static/admin/js/vendor/select2/i18n/nb.js create mode 100644 static/admin/js/vendor/select2/i18n/ne.js create mode 100644 static/admin/js/vendor/select2/i18n/nl.js create mode 100644 static/admin/js/vendor/select2/i18n/pl.js create mode 100644 static/admin/js/vendor/select2/i18n/ps.js create mode 100644 static/admin/js/vendor/select2/i18n/pt-BR.js create mode 100644 static/admin/js/vendor/select2/i18n/pt.js create mode 100644 static/admin/js/vendor/select2/i18n/ro.js create mode 100644 static/admin/js/vendor/select2/i18n/ru.js create mode 100644 static/admin/js/vendor/select2/i18n/sk.js create mode 100644 static/admin/js/vendor/select2/i18n/sl.js create mode 100644 static/admin/js/vendor/select2/i18n/sq.js create mode 100644 static/admin/js/vendor/select2/i18n/sr-Cyrl.js create mode 100644 static/admin/js/vendor/select2/i18n/sr.js create mode 100644 static/admin/js/vendor/select2/i18n/sv.js create mode 100644 static/admin/js/vendor/select2/i18n/th.js create mode 100644 static/admin/js/vendor/select2/i18n/tk.js create mode 100644 static/admin/js/vendor/select2/i18n/tr.js create mode 100644 static/admin/js/vendor/select2/i18n/uk.js create mode 100644 static/admin/js/vendor/select2/i18n/vi.js create mode 100644 static/admin/js/vendor/select2/i18n/zh-CN.js create mode 100644 static/admin/js/vendor/select2/i18n/zh-TW.js create mode 100644 static/admin/js/vendor/select2/select2.full.js create mode 100644 static/admin/js/vendor/select2/select2.full.min.js create mode 100644 static/admin/js/vendor/xregexp/LICENSE.txt create mode 100644 static/admin/js/vendor/xregexp/xregexp.js create mode 100644 static/admin/js/vendor/xregexp/xregexp.min.js create mode 100644 static/css/custom.css diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index a3bee3fac6a23a87c05eefdff79b683c0ea35916..ee7f696c70c72e572c0892630872a699679e3731 100644 GIT binary patch delta 891 zcmZ8dO>YuG7@k?E1X`dAZK2R2LaTg#R7`{Y5(`EWTWVPj&Ot(a7u=>Um8~)91r8oO zctE!Y4~;R^&?b!s!_|XVJ=ug)|A9A7UOebPC3SZu^US=T&-2cH;9bacWHcHOT}PZp zz+e=9h8D$)1;VSKS?La4B;shV~spW$}AA@}i3Amyq zQxGQ85FsvL$z^)n5G7Z5J;e5g5h9**p2z=K5yW9mNkqIVDc99f$qdbyRb?gMhO$o! z)A6wUO0zU1 z>Gv4YTP3klbxZv2t{_$HFbkPaVOk6IN=ZYJ-e3r}R z*7tO~)txXef@ zV2jv}tgeQ+t2LOIE*e0?(6zU}TC&V?_YxYlzLc8D=G=br?Ze~EQ){eajh&j~9do=f dLcr%UK6jVUBADz)bw6IDznH^vv$$4P0S{U_a?fCuw5Hnv}_X-w2>$m zwP+PWwD2z!{)m2nLEErNDe%6`IWRBJoWtB^u2t)!#Udd-mj|BuzVqJJ2-1YcIO;bJ!t4}BSd#2ui8V;1 z6?-5EJ#kG2;*j7O3msz}Q$Hjj#cxSa#;%S%UB@+%1|UO&kfkAzX_zYlIU3bWuy}4p zh|cL+mtc9iht5xk{C{yCBpnjtNL&}R0tH5ekT0RzuYzUrnwb8 zNX}kJ@*40xTeG!^l4BpX5&N1Qv2WQY9q~_NuCsnGYCpuX$x4Rk&)U+^-Mew@67vAr J`Dx0W`~hhpYFPjP diff --git a/config/settings.py b/config/settings.py index 7253f29..cb514ee 100644 --- a/config/settings.py +++ b/config/settings.py @@ -58,9 +58,12 @@ INSTALLED_APPS = [ 'core', ] +AUTH_USER_MODEL = 'core.User' + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'core.middleware.UndoSessionCleanup', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -171,3 +174,9 @@ if EMAIL_USE_SSL: # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Undo timeout duration in seconds (default: 30s). Affects both delete and update operations. +UNDO_TIMEOUT = int(os.getenv("UNDO_TIMEOUT", 30)) + +# Toast Notification Position +TOAST_POSITION = os.getenv("TOAST_POSITION", "top-end") diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3b8eacce7dac8f51ca322f520b2b6c906be203bf GIT binary patch literal 2520 zcmbVN&2Jk;6rcU{CUzPpwbKwckOKX1X>1UxDx^Y_HZ(#su0*L+RtU54OjC!o*UawH z4;56Q9&*efQhUgu;!si*i5$z7zhI3lVU5IzQ*W-4Q%=0M8z-?%OT>8hx1M=#-pu=$ z-~2o<&`+TKcIM6UWsQ)(vD0qQ58K9%@OeNep^{B1k|{}q%2cuCief4i)l@5*sa14S zhjBtxJ617FLn1PnB~-glsLrItzHM9Nu|mjGc%x?ahgvbvGG3t-r+rsQw*NU!%1T1J z?(mBHT*96zj0?W&ITgl*desGtU#`_0?)lZScU!2Jm_=dAf$uE4Of8QUS;Z>brD*vG z)$Ptk4*onKjF=K7rX21}f}P2aW!N*kQ8N{)+$W|=RoIgz67A(L!lpXx?!@m#hh3(a z>H@^_C&|=g!t}*h+cHMAEd8QB2G5?NcVXL)4%Jp=)1W%+D7H1UcSpH7=2@^!wQ5zE z&_eHpR=eR;nb2L&@_bil&dn+WPAl1#>u$v{x|1%mS;>0>!L~e)yIGkJ!EX34Kq1>F zjA9WD7K@_4SgbhIxAA|XSp3$v>`21Vf1!bp*+RcwW;S&>W?tx5LgONKvrHG5=RP4D zFif^4^EV)md4JxodVZeWVkO^mc>botzj13;iRDWUXZbMsxf2VQ-|068sOf} zubCWO(ps`U+}|Q?SkhZba_FsPwqmaxU;i-3P6s3B>m%nIBj=mxBgt z3ewZ{^mHRV-O`kUU;tn#hCluWEW(cfly>q19rOy{jcWi8$X&7s->5y>tJfZc?04q) zd&z583+UYKl1eZFnUxEC5Tsc}BuX~3s*Dz)qAj673^^XKtSnT>+x1Y*#e$uf;1lo> z@o+*^`*%h=>SK>21*+~l0QX2UnQA1D{FyxZNAl>Sg?cjANamKbzl@P(zix~-jPbx2 zZw{uG5@AFNS&bipd3+S%H3W9ZHg^Sw z6)g>E>49h+T%zM7sxW|U;{?F|PB_F{6@z=Iq~~~t#EC|?I*k6W<86S6u)Tr;_M@{y zvtUPi36<C%JxC~2~@q;kqP9h|rl2i4#WAii&ajZOvyJp@Gjrg7+^4nYnsqu92C031>A^6JY() zT5NqhIDRfjP1RFVjnq_gEc@{2YW`U+`0QHn?icusne{QVF=n;~V#sm;-7Htgs@(w$ z9qtMO9xIN3fpyPz1feAl;hFA*7dW>0FM{Y0qdCZ*(CMO8U2wKffO>+>+5 zcr?UFq?p4?BDa0rJKPPKQS{B>HH2q4BA{@`onCAiXQPYkBmM!{L*90O1!yUfBsIxs z(D^jU>EOSfCV4ODd|K+T1kBY7%cob)y}6%ga6> literal 0 HcmV?d00001 diff --git a/core/__pycache__/middleware.cpython-311.pyc b/core/__pycache__/middleware.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3b2817090112f74fc57442b9b74118453b0470f3 GIT binary patch literal 1818 zcmZ`(O-$TI6nTOX zdw%ogd;TVq=>bY#ExxLC3jn{fMMQKR(8(N~c0dIc>0m$+5R*9_8C;PIWM1bDp(r5W z;5MkjV^9-&9KZp6Ln6a$vk~eq##la z)j+0FNGi}6s6f-`UBQk_uwAuG8|OG*u(4hyDQaf32&1FVujN-Q1LxgOUDI*%cmtPQ z$0GT4i>%pIrG)b(i{QMWsj80G6@sU#5B#JoYo_MN^5*dQG^e7?F7}lH${s=U;%i%i zmmI1G$M`l95^5 zr^atmXz!lMF0uoNTRIF3ZK|XrX)S<1NuI5$irxitlAUjeUbgyx%T4S2sMG?kuUfumx5ZR9i#Xu)*rT`7}Z?A-))CL_jw9e z!mi8Dl!;|%wpF%>p*VzHho97J*;KUq*zYlvH7qL(kG{kpuHv%d>W&)mn>7Y39EzOyHuE4kQe7aytS~j==Xf2=q|oRh(-L=;)7}^1`U!+5cWs$y9vJM z(o{)5TlED;=p^qaqvo<%ildAOt32zGlA`PX+auv-(20Lid)gz?@bO<3Xw78zk^zm5E8+-d{>W4SH8->Q$f;YDCr0B1x==yK6O-P=ji;A>$$InU#{8-`zuK74yb0||;hA)C`-9I4+Xd<&y>%qc z9*VOKan2Lxe%|nIFCU7_b#b|w9r>bvKV8qxoSq&_m%dKb#n)TFWv2gSzp=b`->*w= zhNG3B+oz|ett3dWC@Z1`y?i zT!K~2R17T3evd30mg?$E?v>?xuA+xKq?dY=5z4*=l3_BdH(?DIFm@02HOd}!o)(7^ zIa-j2bqyr4EjZ7@a4b?G*A?P0tyUCs#hRiBDz^dD*0_$Q+W|1cZ(n8!bn^KF|JQy1)NpBS_~TudJhA literal 0 HcmV?d00001 diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 9aa598b9d5171b2bb0d207f045e07f9fbbb95a64..7c8e2738fc009787a3cfe535a2429ba53b6ae863 100644 GIT binary patch literal 13468 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 IC?+!h4Itz2kpKVy literal 209 zcmZ3^%ge<81S>LrWmp2~#~=<2FhLogg@BCd3@HpLj5!Rsj8Tk?3@J>(44TX@K?*b( zZ?Wa(r=;c-`)M-W;!Md(%uCPLOGzqX21>4E_zY6>OHV%|KQ~psG^sSNq*On(A~m_R zB)>?%JijQrxF9h(RX;huC{-U~j9x+IFAf_ZyEG@&u80Guoe_wOWr4&8W=2NF8w@fR Ku%RM0pb7xTsWln^ diff --git a/core/__pycache__/opportunity_urls.cpython-311.pyc b/core/__pycache__/opportunity_urls.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e783a079cae87c044509cfe91dbaa8a7a18c70f GIT binary patch literal 1134 zcmbu7J#W)M7{|}{`6WqBLkSTOEFq?*m1>2k0!2}mcti~m0wOP!iBGLtzsWC@WXgmX z*!c!U2tLeN@=$qY>K3V6C+?i4gfx+mINOiT|NH+QKlk{)$#M~p@#Xnb>k|vWFZwbY zh3m;>ivi#(FoDU~z+rTT0VZp5HtTRY=Rh4gyv{p4TQ%RkvuvyFL^nU^#Y2YC$(i&Jq_ zzG36Mqy^VhoO79Zg$u5!xL+@2CG*&D551i@u)~0!m5l#DIu!;ynFj(5A`KD^^sHr) z;rj=48<}W?2h^OE2u*q)p}-BlgG=H9S1;5f&p~SZK6ay6MIX^1j=VrU^@5YoHwH)@ zcmY!9cTY>$@rPGZ+wsiUMti{{l8FA!P$6-`F${AK9^=372`ez{1pIF(gSFaNnh1a| zjJ3}jSl-0)lT_YJUfAN4P2?Gl}1u&rj;gc?PJ(YU^j)`+<^-G(oU5&?!3dWpTK?!`~P5> Pl!SG5T%uR-fVAm1xDz(@ literal 0 HcmV?d00001 diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 1f807fae1897c89361c99c5e868a26d00799b73f..837330f36bfdd540cb8df40764dd86c7b3e6b0e1 100644 GIT binary patch literal 1882 zcmc(fO=}xR7{_PjNAHs5I<}QW0kIXTWSV#h#+bx$j4G#?B3fu;#jH)Y8|{*8td+#R zG>vk}G2b9Q=HyVE50GPzJWlSd{!l_|tBMPxElnb}yTgiLlSGZ)Kzg-mWLGat)bK_)+yS%_s8kts}NmSUMQ zG70UfcJ2N3W4sF|_)Xg7(UY7!t5tp>%Qrsp>E(nEBh`88r;S0c?`rxdfvZ^`o(@gj zpKH6egB`15*iP5bpZoKXvaLI+(Z8(ijx^QLFKgR+Uk|k_5&N@_S*?k2DH6YP`-iQb zV+;pDf`yCv?m!y`>RCZ&3b9hWV!?-jYU<-$FRa*>#>J?AtB>W67owpp-E&<-yE{6( zFZrcNRkUD5mOHQ%+_QZB!4cNR6=RI6e+g! z829Ct8f=W@pRGaRvNDk`t}Jl7xLuMzALW>Mxj3GUG-={FU(#fHI2sKt#~m1sq2mrN z_VAci&czpsYxQlvc&?xg_dd(_-X5tv{m12!pVywLgD1lk?zQ+UV&Stz{N(R=f$ZS8 ztTb0MJY;#`51f_fp}+%wWvp2Yep;~e%}%i=GWZ*7vt%SAP65xoyPr7fe0@X zr)d8lV~QlRueM*$zkYa>qEPjq%AooYGN|34Wr>y>tSouu+pK*1Ly5wU2RjUQ0t3xf zsklMKRVHqD;wBR}>Gm##t_NKP-Oy5_^(|UoXZ0?xW&XR`n^nH--CSy`=R9;t*p|@x2&@2Rn}N#?ZZzL qIv#WwbRx&m`q4VA-TiAwD_yVBWfWp8tO>ehr=g delta 263 zcmcb`cblnxIWI340}vcY|CNyiq#uJgFu)3Be6|5HrZc24q%h_%q6ZA(7f{h8HZg$@%uM`D4cs7D!~@g}0ETus4gdfE diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 6867ddf286a519cf1754ac97038f9f1e4605fbf2..7b21ecd4ee1c5119b8d54881fa6102b25db1699f 100644 GIT binary patch literal 32949 zcmeHwX>c1^dRXHkn*>OJ1W1AeK!W!vUXnje$FhfOYs;`pyVhj52`jZul$3u=s%Gu6oh^4G&j!*7W4avKwXXc2qS;uP zik*DlYXE%!oRMa|NmVMX*2nI5zt`XU-uJ%a;kSy43K_T_o~xVs`xhDJ&+#RFSyB(r zzhz{Y4;YT&3?XJ*{%aUFz`HSIoH31?4H#w$S!VLa^XRiVlt06cv-H^#Dwru8FNEhj zE(erT5~R6A2QUPsF-3)Rmwj5pBd@=)VU(|8kou7LhEk2lk2 zcc^8ib-Z<^ZM-S$IQ0zZ8M$Yoit5VsB5NsygSt{57g}qw(08bq3!ZP zTy?Nr7uQSUY9P*^)#mLqt`_3zvf_5oxO#|d$co!Z;~F8ZDJ!mz#x+A+OV;@H)3{cM zYYR5(+PRCywL@G-uvHhgo5pQ}xX!G;@1b#B5Z9g6_r09qEYsTa6a0K*tp>?@Js1hR z9puAPbF=W8#|LM*AP-M$WNIe3FgF{-=Mz)m$m>(V+dnZtHVhcN6^_i!1o=q}NMBZX zK+JeP451$|Q6`p-(fs{BynU1jiK$D-glf|)8lncya1-Vb!|+DVIBVcci^fQLc0uTL z1|xMjO`JJuj1*)FX0-7UY;)?G&PzXO(?m^CL&TnrgSy!CGiTxQz7MVXh_(zvw@8<- zPsgzj*+p~2nJ$Te(c%iCX6;voFIBiOaE5*of8<8cADWoure?4E!;^e4IP1SX6}jOL z%}u};^Fxe3IQ#Y#KQ}uQoQ?RoU}Rz{6z=@oHZebcdun0fZs+9OjKm(ApTB@_Qo(R& zA`+UrJ~jDy!C3IN|7_%LC@48kpE-4GXz*O%$k5rrGoz=^jh-5l7^x&3JbLob$WY+? znG>z7WSi&brh}7_!0g0KP_j;3hk657d1#4L%uP?sUY|>2mQ*}{H*#ZcHuG+qyu|~m zMFNOrQo)V6aHA!J&Kr@L(1(Q* zi>QnxNtV#u?I6!1en_U-x!XLBv&7Crhp)}?GrS95jdNis53<0Z@n#74&?4pY!MAP& z!x6q5-uY4lUQC)lIX4>#-id^aE$xt*+pm;N-2y9N%?&j=ve6 zpO_4G(e}L!b0*w5e^)YNYQ6!oBf^D$1He6I!%>lN_~Q=0=%^!(x`d-8?r0GmZN$-b zZ#3y|eS9VE-z)g{F1IbWeeAh+e7&&re#d*A_dD+mZsswS4athS^{N)Js*O~&tyk5N zsvfawJE_{9bbFFz)$2925AO=C10Ng3nn6-CxbCZYcu8p9^W?DTJ3xE~HcJZZd7BJ? zdm}$}F}Bj>X2H|(__}EABi25_-1i(B$^8Eb{&Sz;wWX0MN?vCHPE{7SWv|IbViP6q_sHNh;M&Bs!Da6jM_b2V}BArxag_ znf(A1yyxq&m||^ez6AoSG2Q>RwcDcg0I?nr%m?^J zXeusZ;b0^J)HDpw1*a#jPt8ui6mB(3hSwy^;2cbBi9LJkDo_OeuEd_Y6$$cZg4ZD4 zd_ElHrQ$KNr&qp&v4K?>`s`T03Ma%*-f0xyRqBU5j~(}}t3P<#oJrn#$>SkCc* z9o;)1(2OhUB(x=SrI(TCyCCSBu!IH~WD>`XL0}O%!?=Yrj^}Zv@%$y`nt?Na0~kd5 zWPzuGCFYJ{ybzX*Jjsz>p4030LIwTElc`lZ5P~+-Z!8t0!@@to&#js6`4B+Y^M46+ z@0Sf9FcH;Stz9EuW)+X6yO-%NK5&NbL#ZESN`|#wwVF>OWu{jQX>ZoW8MV3j(-E8; zsY_E5_J&@u%=(=U!N1!6XiGi9yxFj5j2dGqK0?XMnY5|-qTKaHjr*DVjheVG>z!F! z@+;-F{0w>Xeulid@5$vqG%uQ>rWoBNDWfJoX-6H#`M_nsl11a9IcolU&Zt?_h0p2oF~Ub(+Uq$V8?U&gL2Y1N^9kKt;Wb#W!SRJfRCU0kU)6$iWC ze_&L4yJ&$?!xcE|iZm-hQQgdpv?_6&iz{0#*RmX|725YLdf?W!4fjm27*vi)mbu%r zuwE4bUopW4u1$qtAqz`oX~ItzgH&)pvW0KV-3Bfo1dK5!8Qzle!jXx{t#GTE-w(~? z2M`=Uum`~KhX$!|Dx6Lai<%wx0)fC3a?BG`v$(c8f-|ra(S{XGahd>HB&20&TLV*^ zWSqZ=@99=9ShvBRX95}(n21P)$}^8tK`NTdEWT60@Hx!M!5TL;I~i#;O197h>@R4O zCL>ablJ+hH;yQ1}R=1W(mhi247%Ces*L2+n7C|Zw!)U-RhY!O3A;|HySfOcT=$vGN zbdnhtQz<_j4C5YJGT#i|4NHYEYJ4P&8#Ku>6$#G3UPB&vz8wdz8_UR-If5|XiEr#2 z5Bs>l)w@#O+Y=$!hDhwh?CczjevsdWc{%{J=1XR1&NYe6G|vPTN=0(vf%yr30_w8R z5~MP9zXRbkFH;Z*OhQaJ5D4So(G89N9%b$LQxLG=8%!|@Y5t*8Js^k&;Xej4{o)?8 z?sR{<|3QDk*&KH^i_TW!Y)v?K#htsJREW+)#Cd3mg{_Lsaep7NdJ@)}xV0u`7p>jI z+P%q`3VPQoJS#^Yo*>@tgm-7$yHoV`6K}s*v5Qpf+GLDHy%a2)lFsrE2HqbK>UN9H zJ;b?ZlQER^Cf(kLHsbDhd{%JxynuRqwOCX~!rLA9cCS^7-h;$@Fj-Rez)MPM6D5uD zlE&DiSkgvH+LGl}4??88DN)`QFK>IiODx|(%6BBoDj!@XWsQll)_7U#<7Tm}mz4Ex z7Fa3^HyHp+MVrNprzKIfEnc-vtm-0FU7L)#;7x-J-#fN6xLlUBy9B#GIedKC{J=&W z4MOALCuae`Ejk7T$KXa|>!Zu0ad)C|f4p(OFz|-(`Xz(|Z-|W$etEfQWfHc8f};-l z6afB)-ijmuw`hiK_2j&9|OlURMwz=GmbigiaxY}=ZN)a?)) zJ4MG%;@F8n``4VLey8B*6CHiT(f7<`DCvI+;YE)piN9BHY!@BdiDUaSi?L+KGn3g} z{22oPDk;WF+{9Xk%@{GHYI!{Ky91+ms*s-x*tYei44lkQ!cHkLftS$Tu!Cw}Zeye49zgSpH z3TuC>={K7md&RmwQrEY}KQXNFpIMBxdj;0F$ucf)!r2gaHmq0H3U&QrPgQR+J)55qKpD`vE6b|68^BAiWitiH(t4LwhWqu*8{jGtl~FZ80yJ7d3JwWUMrTE*-lJX$+VK zm^~wBmY=3G%p!zYXbHH9O^VwtymBcez}^5}D8fwWkrPXa;HE!yX>jLD$4EEM zuQI+m#`2Zw)vy; zoSS2`Sg}|TE#S+dYz(DLSyxoIEi!;$TSaXYjLBVFWmav4(ZVmNO}Als#oDT}YO_YI zUr?KF)AfqAd9rG=MQwZyj3mmVvo6m24JK+k@`Zv4XaNgf)<@H5jY;Mkw;3*A|q|uf;A9x%8xS=t@$)wz5Z3l73;&sK})#S`<@Jb|ouUt7`*_ zT$D8}Q*X2=C2~>XzN~k3T)ht8i#26`S^fsDQJ3HTO8J|#J$t3R%|Aol7G2)rsGVzl zXjybb9WjqG^2(Ez%gBgsiWW4CMY!5%kw$c*e3?v|E$}XtQ`b&MvaLRrLmlRh&J77 zhqf+SPV-Baqe{2BEk`*ms&YmO7^qnl67ws4QJ%O?Z8=md=UjAc*&Aonwc4e_{L)!R zJrg;sl-j`XCIfScnKj>L?wBqyw++yiI#uh=Yu^{kqGeoLO7U9`b=N6nDNkItwyle$ zvptZn=S%bTX5}l}s*b2`eTMH-8dLE_s@81RmKtkN;*_VzapjrQfJ}Fw_}SLlYl7$`iL2`2T&{UdL3zIVGiTURP})kjy=|t5)}G z#|HX1@XBr7{?*zFbseDNnR89t0#{r)_7DeOxs1 z6mEzcMl+*DFnNlcSHAc(istC0{q{yC>fuIKk7~c@0_;&2y<4n0xv|2#I;t(>Wt1Y& z>SMI;MiA$o7?TR-(FP(swGfod_y~3l7yzUG$r9=Wif6_HVPtJc)xZ{ z%A@kuuvDH&j?&DuH25v5UzRMg3e=M`ja;r|mxZJto=pW*WaWd z=GHvMbAe2=L0;`l$;>r@E*`8&z)(e-v@BO>SR->n`48@|o}{93Y@4QLQS5E(?3PTEQ(PE%EWe)$ct0=PhhG>>f#wGmb`Iu}^QMel z0?@~a&V8Y|$%#;S03OoWegjGiBg1r$5jrl&_mlb`oLzH%cyaY&CWs2sB=?P}>o-F1 zUpHQ1$#!d&o1^wWbGJaq9+^WK{rp^b3cnA(7s{8)a(*PKC=(n8h5JI`HZ&pvw40fi zN;6TJS@IJcOI03YpkK8P#R&;X7u=t-_ef;mD#LnGPbaspI&vXHlpzAhBW ze*+RbF}?_aKom(42<+$>h227{I_2$`C<3(_X{r1#VOJNid?WvL?6H}u zF|??}O-+6kRLW$Npx`j_bbv}tSLORtS1j`Q=%aVnB2PwsxR?o+(aC`$igp#C&6t^) z0J94P87MJSUct02ti^{EX;g3olE~zd|3&O5m}Fc76%T$ms-Bt!Lzi@Qh&5qJsU9Qd zW`S^K=$FEmk?Y;a1PU$CT8zSrun&fm_qaO zVE-uPf%zT$Z>j*9& z=tXcEfaI9xgV%zQ$s1{X9#j?zf#z>MGzE&q*HQI0#Pf4JKaP3cKyVoWQew#}tLCWA zls|?cEPgy3R{ndK3W*Ih8f>lR-^BR5o3|(U>tPsE2{;y*xIV{ELYZ`dkTr{2z?8!A zpF-k=+Am#*cr5cMAkd3@OtQ4?mm9y`{GgeXwh5&>;C}qN{C6oo>2g0fL0k<9S4-U0 z^4PP+k-lNkH9}k?O9f9|pleE0cg3r_#OfYW-Ltk^bnPdu{YwQpePEwh-b>1Rm#j%A zXqOt}&c@hH(b-F!y-Vyyb<OBn*p)$YozgOSX-^g)+HNS9-SZ!yAloi;tl(R{g;F{-V__I zkcKPi_9W|?9(8~wEm5~8UbjcsdqwzKP^`N~>aIa0#Ufkt)Ki@Vo$U!?^CxT#aT};G z_lvdxVjIv09~Nzc#5Sl8J|x->6Wd{Z@KMn=N^GP0;8D?bjM$FpgO7_gXu%16@F~%D zn%GWn+F5_$GlunAKSLm}?#)uBxgYyjC$jY$^^F4SPgd7G43X;IM0H=hx=-jo@hK-( zpC#321=f?aR->NskRfSnNZ8urwsxW8fM`2NYzGC~!Hut;v!?!Gl+a3LeI`lg5Tw=!s0?>Am*e(jTiyNK8I1+6l+y1n!jnu)o&@a2*&_NpZ zrV#;^$Nwi(^Bc0Fz9r%Ah=aO5@FXO@O4FwuG-I?&}eHhd(ulzGK99 zOkk^^b?_0C*tIUvcbNDNC*=X|O4MwR*T57R5^IJ@&2X~5`O!X7zdcdkAFqdTKPlFa zk@~TuuPs@(W7C>f2P%930$aW5U_3pEs=jztpIEhvRDq>|x!@Yr;a@YntPa1kcQr&h z4<|Z@56;0)>zTAn|O97 zJcr|+!#^(j^v>_s{JR>#b6E6TBA!b?CXzCv`Pu}qmSWRY(sVUVGZ5i+Bz*mGAE3{P zPbWm*Y2rH#n7Hn1iY*F#M@8Q#@r^=&76sQ%i8V(^%@Lg6-p+)#Kkmf>MDI!BJt?r2 z8}&P2`q@RGM=g7>vIdcDgh$eHF#WdP0@P^K5$Cxjb{#2xqM<+D(EsF)(9kb793>4$u^77gH1&|C-HE25 zc+=3wH-z(-gr*^}X`D2TCz>YXO_O3%kTeA`HKm&!-K!@^$H7F$k$A@uVQ5OYIU{z= zl8#wS(IT>~Ngd|T2y<_VojmE}F$GebWN+UOJ4o+vqW4(5_n2_}mT>#7*tOYq>3U+#wxz(zR@`O@j7b_qRN<{YLSl;vq2GX8y!o z_TSGtDf^TS6Mhe*~3LzL-aTM=DBLU`+-?U>NtIL1vlIbmX9HhSi7Z5%k0V@H_zE zd(!`sHrKtOq-)!~lgX+^!Ca9nb=^C@Y2I#eZ!$T+v|-R_sn}#*4buGpBWZ?G(a0L#pF3bf zqcu8@;1(x!Df+&cI(Ir649med9MUWH3^8*iQuV-r$uD)}g4mBTDWb;Jd~I$tVvd>= z%bZxAs*ud1R&ogGGZs28nI18VUN4y*N9$G6;|4`eUzmPU|NVmYzg&8yJui_y=MsPl|m{wnJRO7_~f=@?bkSF&p7WAul+51t&Pz?c>ygUCRv+Re?)J0IfGIU_u!T z!xS5agy1DJ0)J>GUcj8-M@F**QhE3|#-G5bLM${TJ}tyjfg1=8&=A{YND01zzU-ca62W4q*5)xlF3zYpCp$%Nnxye^byPbRd; z{2bH&1pu_eY*@Fsh|Mc=vX4jBF3Jr1pBDMzMZT4}$G34wyDk>JPKsVn+DkU`%`RY{ z0f3F4s}RgIZA@it($`3Q+vO!t^zA0T-N`cFgDZ)$=6D$xL$;E#)?`gHso9gL8Hm>m zh&2aE&B5#>wXLMKKT*3sUb|ncJwR#?B&+Hkb|$Ji<5itvRX3^X&I$mp9Vako0G9H> z8_iL^e0?QCYI@fCMf+Z22UEYjlp>A6*nI+B=6{6XcM;%-Qc(nzTgY^)9>ac|27e6? z;T8aC3JQBjSJ%Chn`Vb)-zJj{U2hl+`7zj-)+yxu4l0ewVI^DL29~;L{KU`Pld0EHIO^(GLR3s3EiP}5Ty4U3R=Kx$S|Hy3AF4nWRg9GQeH%b)OW}V zgxjJJZl#@i?;-S|q~1dsi6atWO1Ay*pe5USA0Md5I0gpkYIh_)0oh=h;=~B!DkICQ zB|FNW!nY{WZ z{?k^2vo#oZev|p8;hXs%Fx0Ke@D27o!!)L%mtOt)t^5e)OkLlHzqrcFS8iXY_jDrr zMdOlTDgPRHG=FhTizanD_+NN0r-#&g`7h(m{4yq^P(D&OdQE2tx}u!LB@;|Tz(*%{ z%9P2bimZeGGkg=3mzRahk6L~W{Aj*wTFg^>u1shAx71}vb$-^Mxbvo42p-uPbz5%V z2z5!*8P5)`0Kzk0a2okRdQCgiPiUy3P7^hOhmKjzCV&AJs^zR>nyG+8hJ`#w0VG-s zQ}KHvx@4#|IBN8HS-pxFUrRQML{(1Dn~>1;|gZ<0*ltt{ok9GQr~LtaV^VWJ{? zS!hTn6n4{}w()a=hBktZEa>GXxjMaKsvzQ1ftjZYJf&wJo(uc#!{qXi?B zDMOt`J31Wm&c3?Ib+G}Sa0;=lzQ=_~(NP?=;yFPK?S$br~)0-RbQd`g-5!62w)*9YIDd+&Ub__v-R-RF@Ym6Tzylvse{QT>TqsN@ zo$$kW-pBDowG{u4u~GRllOJw%!N}i0eX2b2vTmVJK{)=uVxD0H$N|ZI_XShoIZQGU zky$0w4e+>_^GO5}0C2pK*9KX{k9q_tPxcOy%qXp)R54Tl{aTNr@!_!%9$NZVZ+}*km4(&T8@-%!9gla7)~u=KqLYmecIrOkjv6v}fDcnMWNHB(#EN%F#XC#3 z4Xd44y_<}+V8;f?#@>1OZnCa>-Pic=@^`L0ypr(kjQe(qzJB8CPsuH|lNw~nKqyvO z48oww;-yh=@#xvJbcD)$V;<4AZM_0KD8z~#q+-X?(Pa5m;ryEdKzc8Y$js)-xrdh@ z_lvge#I{|qZGXx-3F}F)b#ahFmzF>1Af@$5=fRlyk(Ky&690hUJP3EvQ}b|d(&gVQ zU<&G=f!7MKpa9T+r4YDK#R)^bPUAeJ>+}LV=zWn@gh6HU0$hiNjKZ3c*^KG^ePdXD z)}>dmQ@&-ve;P*a8ibQ4I)O!{sA-|0GU`%YnvEjH)GQ^j zADR?&lu=AWr1Jr37HYO4(y21`AbNE}y8*Y>S;zh2<;a7m&^RKpM+tjWV2`fbUGLq# ze>-8XkK5~GM?`xov9}6rDBhj)4eV*tj`(fX1c&$)bxA-@Rj0ez9pbShj)aG7a*)D8tAB_8Bd0AZdIl zK>J2pFbS1sE*xFFp7#P!Bo}GddLt9e(|eNx%WMLf2Fsb@%d1#pg>7G`)ygzrH+gLe z7EzE3fdi#{jKWi>FE`Km0!HX%&&i200Z1)haI%hiZlr^C2GwvvjtbvlTb;>&9fTvl zX;{^XM?k=#N~3t*M@@fMd?l3sl6+Ncu1CcI3Xt}74&FtWUc-^M8wB*hi|fm)GSIYzu8rW`*Ye7bf`+09x}EiYHlB`!#u)#WfhsgBc!< zLt|8~sq#*c(j8^FRpwx)vfLiQjL1pA2EV-OgSq$T#PViR-n?YKZ-oHa*;6dV$Q#xE^|Aw?S}D*e-$XdJ44s zxM=St_HKdgru1$MxSLOK02R52Ld-wK2>L;gCd&kT5yKi0)FZHB1cC!Ng!>>UO|oGq zWqETy$eT;6mpfLzE|zwY(k_%aS6F&BnQR~zjKyh4Fuc?2xeNX%B$zV*kp=NFObU~7 zcFB3*PEj(Fn{>`X?|^iww&$^Q>8VQOKYGyv9^=8+!%-f&Q|qRwx_o7Sc9Sk8e2<#E zs>mfAPL{=ZSd~VpEwDM=0Uz4E%vlhHM~ao43h~T+5D8d{st%ygmIa=pnN2cmm@~3j zt=J-JLaWx?L{*zI;#RdQvt_?&#!&vmmLrq3jaQFwOx0JVbdclgJkWq}f5q{fodkab zm9Iy{uhetR*Z?XTEt9DEsyclDZ2i^P_j7Lhr127SmdSWcY=yr%&sGr8Y?V^yrw4w$ zTcs@2VNxD=keE4Z%DduYwRdQwK^f1eW!0vg2jGX87t07&l$3gA9DIp(P=6B^d<|Ky zC{>di{0_@CwY4Z^L<_QND~PnK;`6~%u0VUdo`Jc)GnyYOS5hfYT5Jcb=v3mSGq@j9 zL={Rj;zd>Zl`D?suiEwXYUW3V2?29VhjzpO$MUDW>9W-L#+4wAUN+30gl|zQkI*tG zIn-Z3)lL=rk}eH=^(>ro)upX5w4w5Vv51|s zU0L2=b&FOF?$GrDvjOf@MXe}s)t7t;j>cWIzH6Oj<-fNL87io%af|Qpcer-6ddDco zO&Q&VvQc>#??;)DKXq7cxN7TMO^1Xg(h>x zCbv@-oc$EbK#?B*9KpXwum`|`1B#pCQldFOaJ{W2oxE*fzebuTi==2PW`o(HJUpZa z#ds-BylzppH$@(P804kHSn(mOxGF7%@K1z5lf~VI4$FQGoje>fEbk1`&69T<*u2jN zHLdCKr)8Fz>@U%#g_=XNekw@u&rQ-(LpeWpi^o$?adh|iWozX+c@uI5+e{^dD1qVg zuyMHckhxzAKK*L|a9$2SH={@wk;H->2OPE&0;Mqw3s|_eV&Z>+;GZM-SD2*W+}-(< zb?|l!q%8IYeConzI0Q(R!<)kspfW^3gRCKDupM+;L^nfpWG`Y4Flz=~by&&=BjH)- z6COcIM^-8rx-%K1$De|j=1nZA7QqY5Rg68St`k(=t4Es=Oc+2gfnW%;7=mHk2>Vr+ zO2O<(f8g^erkiTipwZ!an4zdG^*lJ0a5cwW&5y03YX@=dSSr}CgRI5~$HA=-oxUyPk~v&MC6*Lfm^n@Lm8p;r+3dexYHnXxm3@`vlv*b!YPrY-<6rWr(y4 ziOyl-92RWDo8|&2351r!*O~C`i~IIHx%BZnWdB*wcaHeZEsd=^+z)KuE`CrPs}>zy z3Hw0YJ|Nl;68pg)?IMF0g3Q^(uD{?uGoaNLlzm#w(t_FBPSyYA`u!Nn)- zf@fIt3=_}rG7CcBL|tFJu1~DnMe4xn&s+i~g#f;NYYF2DP@arxbK zf6s3>JlQGu4~hOm#D8eTvhMP%9F0vr-YL3zh^t4Schcehpy>UgZ`&W(m+k8g4{_8d z9Bpw&+gimBy~6NCVd^b30dw%g!3z!^J_m95V--T%l<1fyj%mR$o!oQyciNZpAJ_#y z97EI(_eM#@%C2}xy;xHJ)afD4`h>GB?reK}^W&i4Y!jU)i1S3kc{c7mD>}~;=Xp@r zmmD*|xj}GHQ|!9XbzCevLCQ`D^oC8n+fOQ*6BV8Diq5rmp`uf)7$6k`iHf0k#gKdk z(I&%|y=hplZ_k*EKG`eO_lxx-q<&?N>%VtmtwwA(L>dl>_QS+}SYQu_w*hha zx4nm(j#M$9R2}jS?J@mmw*lbq?my~0Rl@vHN#Uvera$tyPwg`O(Jl)-k9}y?3oIz{ z*2yKxk#Zuqg475Zenl>^Y20ragx=F%x%tEsjCK$FK87(|)-m!Li0n?NvYKR)4q`Y(r z`9LN0)<2Tbe}f~e?j%BOtssuc8 zQ@x4=5t=ilcIXS7dC?3f41p{G_V6dcutT*^j3Bd=x}tfj7A>Do_h1>sPN&1aYIM#P z;lu7Xe*xrAAnCB=Hf;gHJdC({GTk48T;`OlszyB_6k`{dTCBE(uouTYPDXMjpBXm$ z5c*%3>i@4m|5@mN!B_0RN~)!dz?bx&P4yq78?VrRP-&(5zb{&#>6I0CZfb)ZPPZdp zEaZx!EST;TM+*VkziWX#orz6xk&RG4vUUHQ+-VmBWe*K zxz=nq+M#oFWTj&5kfBv%%C#Nc0qLFOjhH)|!6BRElyZ=UxQPGk{-jSn#A5 z04=TS#K_0cp71{d0DBT&!^6uNzah~#LVP3Wno;6@;7yd&#!G5rEn-PKDQREI+pv}_ zUy56+1#5NESG_cr^!S!eBx^u-VJ?!vvNZ<-?QqM#&k(!*D1^=w^mI%(CNt@%j@62e z`sL!J!<*h)h>mXJ=oaXmwEJR)6knd=fj1eZeaHy5Cndv1(B1)r5P-?OBQiG(tnjWE z8%56LJMp3#p{OS5^(}#czjx_)s-bXVs=26pi-vw+f8QRv@aQtQnK*ih0}k`-O*(wB z5;#W`TxuMigrhF*sEh4|y%TYC3H08udsp6y+x-HZR(jx=+zLprZ{5=@wCoc-`-x{i zY$K{}Ki)4?-Nt*GPd!xKUd~T(_pV3Y$G3rvCysu>(GRMr9PFXIoB|K<31Zog5d1EJ zorqpKj;|HN5d0SW&stw===z!)%Ihm;edYBvvjIk&OK*ZTny1X_dJW2dxz+W$PH#bV zPj@-vq6yXrkj&FH(xUzun@EOnL+R|Qj=94VHDhKNx2y{&cxLTf)#;RA-GYP3=^AN* zvpZEaD8=Z{?gYvB800c$)=0JPD6@{k8kw!nF+*E21#|gyHF0JFm`IuO3@a>*rLr<> z-+ zkfCnbN@?7)EV$JaD~&zKv;HCE4*v-x0diGQ{ow0~ine$~o6tV|sZp#rPAZNs*^*Xp z&9UNfxhlfc{_`SC8()zyebKbe$tCyWxFPrh90EO;Y^Sq0HrfM;6JcvZg-)t0Xed3suy1M|~PNE}*i}&M4Hv?*cFh4areStf* zEzSoQc8`9#Pjp@+&Wj0WAnptZH|GUsKy5OQK_HsZ*Su7vN zEOWs-y`ERWAI01uZE+_5mWeBpqVq&$`1Dg4$mgg(rH(4#*Zrj7WAi80 zPl|=zrv&?H(SDlP!2$U+9d->nVunmOH=iR#K}rOjgeUn@Ed5|HPJbIFfa&SAg)~W3 zLJr&6jFE?4UBAV)Q|Idfcy1h9s8tP%UpCX$*`-W+(ClOij4rRk;`Vr9DJ z;pm=7k1l@_pY*HrX$(a0xA-uI06zr{(qL$skbQMiZta4Lw%U{4)qE_ zkHyQ-SZFR%X5214|1SWLF2~?+_NC*V&9Lc!VcFN5tfe#d(fLuW9ztk=`sWR8ie`KS! zy4wi5O<=bv+JqsI9VYCszz%}~;U$c5b=(+bA*eAf9RM9)b{@wFfeqWepSD}O1i1F3 zjB$mU9Uw?+_{~Uo8zdF@-HsKS>vFHw(Zd@pvPWb{# z4k>o%Bxj$q`(Ui#w2}e-2IPgC2^Yis2c@pTkYoz((SJ$Cevkg!V7dkEon-0-?VV(5 z1?`<=S_SQ$WW0j*-pq3t`aw&d22BpbLEutUuz@i^!G86fWZDJoon&?kUv?*%!@`%{ zNv2cK-buzMXzxw)yG8>DVsgXfJI`p$XX^Nac}@cy9*_-IT+cA{GbO5xF*FKHgZfS~ z9fJ1GHTu~@a#Ucpyp!6`nfba&W}BeBH_a^u@TbZKD^<@h^fM*ub%WOcR3#f=QV9J_ zi5eI%pgKM`T#h`WF`ud94;%AnmfUcq;TeN5pHT?j6`0oHr#W)Nm2=M+jQNbhO&Jaw z95hRAxU%CJU?yf!#ka8r*!*XMl@>U0&fta<=hE@zCIj-@SzzTD{MwYE1%7QR9c8oP iQ#Qbq5c-)C<;_k+Gv-KHh~Fj~U`h!6EJqYw4E{G{#&-t* delta 257 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 Gwf_QOIzWs7 literal 0 HcmV?d00001 diff --git a/core/management/commands/seed_customers.py b/core/management/commands/seed_customers.py new file mode 100644 index 0000000..c191551 --- /dev/null +++ b/core/management/commands/seed_customers.py @@ -0,0 +1,101 @@ +import random +from faker import Faker +from django.core.management.base import BaseCommand +from django.contrib.auth import get_user_model +from core.models import Customer, Lead, Opportunity, ContactHistory, Tenant + +User = get_user_model() + +class Command(BaseCommand): + help = 'Seeds the database with sample customers, leads, opportunities, and contact histories.' + + def handle(self, *args, **options): + self.stdout.write("Starting database seeding...") + + faker = Faker() + + # Clean up existing data + Tenant.objects.all().delete() + User.objects.filter(is_superuser=False).delete() + Customer.objects.all().delete() + Lead.objects.all().delete() + Opportunity.objects.all().delete() + ContactHistory.objects.all().delete() + + self.stdout.write("Cleared existing data.") + + # Create sample tenants + tenants = [] + for _ in range(3): + tenant = Tenant.objects.create(name=faker.company()) + tenants.append(tenant) + self.stdout.write(f"Created {len(tenants)} sample tenants.") + + # Create sample users + users = [] + for _ in range(5): + email = faker.email() + # Avoid creating a user that might already exist + if not User.objects.filter(email=email).exists(): + user = User.objects.create_user(email=email, password='password123') + users.append(user) + + if not users: + # If all generated users already existed, create one with a predictable email + user = User.objects.create_user(email='testuser@example.com', password='password123') + users.append(user) + + + self.stdout.write(f"Created {len(users)} sample users.") + + # Create sample customers + customers = [] + for _ in range(150): + customer = Customer.objects.create( + name=faker.company(), + email=faker.unique.email(), + phone=faker.phone_number(), + status=random.choice(['Active', 'Inactive', 'Prospective']), + tenant=random.choice(tenants), + created_by=random.choice(users), + updated_by=random.choice(users), + ) + customers.append(customer) + + self.stdout.write(f"Created {len(customers)} sample customers.") + + # Create related objects + for customer in customers: + # Create contact history + for _ in range(random.randint(0, 5)): + ContactHistory.objects.create( + customer=customer, + user=random.choice(users), + note=faker.sentence(), + interaction_type=random.choice(['Call', 'Email', 'Meeting']), + ) + + # Create leads + leads = [] + for _ in range(random.randint(0, 3)): + lead = Lead.objects.create( + customer=customer, + source=random.choice(['Web', 'Referral', 'Partner']), + status=random.choice(['New', 'Contacted', 'Qualified']), + assigned_to=random.choice(users), + ) + leads.append(lead) + + # Create opportunities + if leads: + for _ in range(random.randint(0, 2)): + Opportunity.objects.create( + lead=random.choice(leads), + value=faker.pydecimal(left_digits=5, right_digits=2, positive=True), + stage=random.choice(['Prospecting', 'Proposal', 'Negotiation', 'Closed Won', 'Closed Lost']), + probability=random.uniform(0.1, 0.9), + close_date=faker.future_date(), + ) + + + self.stdout.write("Seeding complete.") diff --git a/core/middleware.py b/core/middleware.py new file mode 100644 index 0000000..8d39eff --- /dev/null +++ b/core/middleware.py @@ -0,0 +1,25 @@ +from datetime import datetime +from django.conf import settings +from django.utils import timezone + +class UndoSessionCleanup: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + undo_data = request.session.get('undo_data') + if undo_data and 'timestamp' in undo_data: + try: + undo_timestamp = datetime.fromisoformat(undo_data['timestamp']) + # Ensure timestamp is timezone-aware for comparison + if timezone.is_naive(undo_timestamp): + undo_timestamp = timezone.make_aware(undo_timestamp, timezone.get_default_timezone()) + + if (timezone.now() - undo_timestamp).total_seconds() > settings.UNDO_TIMEOUT: + del request.session['undo_data'] + except (ValueError, TypeError): + # If timestamp is invalid, remove it + del request.session['undo_data'] + + response = self.get_response(request) + return response diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..a28497e --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,157 @@ +# Generated by Django 5.2.7 on 2025-11-22 14:20 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='Tenant', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('role', models.CharField(blank=True, max_length=100)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.tenant')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name='Customer', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('is_deleted', models.BooleanField(default=False)), + ('name', models.CharField(max_length=255)), + ('email', models.EmailField(max_length=254)), + ('phone', models.CharField(blank=True, max_length=50)), + ('status', models.CharField(choices=[('active', 'Active'), ('inactive', 'Inactive'), ('lead', 'Lead')], db_index=True, default='active', max_length=20)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL)), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL)), + ('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.tenant')), + ], + ), + migrations.CreateModel( + name='ContactHistory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('is_deleted', models.BooleanField(default=False)), + ('note', models.TextField()), + ('interaction_type', models.CharField(choices=[('email', 'Email'), ('phone', 'Phone Call'), ('meeting', 'Meeting'), ('note', 'Note')], default='note', max_length=20)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL)), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contact_history', to='core.customer')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Lead', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('is_deleted', models.BooleanField(default=False)), + ('source', models.CharField(blank=True, max_length=255)), + ('status', models.CharField(choices=[('new', 'New'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('unqualified', 'Unqualified')], db_index=True, default='new', max_length=20)), + ('assigned_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='leads', to=settings.AUTH_USER_MODEL)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL)), + ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leads', to='core.customer')), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Note', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('is_deleted', models.BooleanField(default=False)), + ('object_id', models.PositiveIntegerField()), + ('content', models.TextField()), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL)), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Opportunity', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('is_deleted', models.BooleanField(default=False)), + ('value', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('stage', models.CharField(choices=[('prospecting', 'Prospecting'), ('qualification', 'Qualification'), ('proposal', 'Proposal'), ('negotiation', 'Negotiation'), ('closed_won', 'Closed Won'), ('closed_lost', 'Closed Lost')], db_index=True, default='prospecting', max_length=20)), + ('probability', models.FloatField(blank=True, null=True)), + ('close_date', models.DateField(blank=True, null=True)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL)), + ('lead', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='core.lead')), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddIndex( + model_name='customer', + index=models.Index(fields=['tenant', 'status'], name='core_custom_tenant__21c6bd_idx'), + ), + migrations.AddIndex( + model_name='customer', + index=models.Index(fields=['created_at'], name='core_custom_created_629a7b_idx'), + ), + migrations.AlterUniqueTogether( + name='customer', + unique_together={('tenant', 'email')}, + ), + ] diff --git a/core/migrations/0002_alter_user_managers_remove_user_username_and_more.py b/core/migrations/0002_alter_user_managers_remove_user_username_and_more.py new file mode 100644 index 0000000..4c9e730 --- /dev/null +++ b/core/migrations/0002_alter_user_managers_remove_user_username_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 5.2.7 on 2025-11-22 14:32 + +import core.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.AlterModelManagers( + name='user', + managers=[ + ('objects', core.models.UserManager()), + ], + ), + migrations.RemoveField( + model_name='user', + name='username', + ), + migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(max_length=254, unique=True), + ), + ] diff --git a/core/migrations/0003_customer_owner.py b/core/migrations/0003_customer_owner.py new file mode 100644 index 0000000..a51f6a0 --- /dev/null +++ b/core/migrations/0003_customer_owner.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.7 on 2025-11-22 15:26 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_alter_user_managers_remove_user_username_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='customer', + name='owner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customers', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/core/migrations/0004_contacthistory_deleted_at_contacthistory_deleted_by_and_more.py b/core/migrations/0004_contacthistory_deleted_at_contacthistory_deleted_by_and_more.py new file mode 100644 index 0000000..ffc7977 --- /dev/null +++ b/core/migrations/0004_contacthistory_deleted_at_contacthistory_deleted_by_and_more.py @@ -0,0 +1,115 @@ +# Generated by Django 5.2.7 on 2025-11-22 22:19 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0003_customer_owner'), + ] + + operations = [ + migrations.AddField( + model_name='contacthistory', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='contacthistory', + name='deleted_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_deleted_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='customer', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='customer', + name='deleted_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_deleted_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='lead', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='lead', + name='deleted_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_deleted_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='note', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='note', + name='deleted_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_deleted_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='opportunity', + name='deleted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='opportunity', + name='deleted_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_deleted_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='contacthistory', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='contacthistory', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='customer', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='customer', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='lead', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='lead', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='note', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='note', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='opportunity', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='opportunity', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/core/migrations/0005_opportunity_name.py b/core/migrations/0005_opportunity_name.py new file mode 100644 index 0000000..d71d18e --- /dev/null +++ b/core/migrations/0005_opportunity_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-11-22 22:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_contacthistory_deleted_at_contacthistory_deleted_by_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='opportunity', + name='name', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/core/migrations/0006_contacthistory_restored_at_and_more.py b/core/migrations/0006_contacthistory_restored_at_and_more.py new file mode 100644 index 0000000..7b99ff7 --- /dev/null +++ b/core/migrations/0006_contacthistory_restored_at_and_more.py @@ -0,0 +1,65 @@ +# Generated by Django 5.2.7 on 2025-11-22 22:27 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_opportunity_name'), + ] + + operations = [ + migrations.AddField( + model_name='contacthistory', + name='restored_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='contacthistory', + name='restored_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_restored_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='customer', + name='restored_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='customer', + name='restored_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_restored_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='lead', + name='restored_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='lead', + name='restored_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_restored_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='note', + name='restored_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='note', + name='restored_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_restored_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='opportunity', + name='restored_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='opportunity', + name='restored_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_restored_by', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/core/migrations/0007_activitylog.py b/core/migrations/0007_activitylog.py new file mode 100644 index 0000000..1fbe5a7 --- /dev/null +++ b/core/migrations/0007_activitylog.py @@ -0,0 +1,30 @@ +# Generated by Django 5.2.7 on 2025-11-22 22:39 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('core', '0006_contacthistory_restored_at_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ActivityLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('action', models.CharField(max_length=255)), + ('object_id', models.PositiveIntegerField()), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('actor', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ], + options={ + 'ordering': ['-timestamp'], + }, + ), + ] diff --git a/core/migrations/0008_activitylog_details.py b/core/migrations/0008_activitylog_details.py new file mode 100644 index 0000000..1bff45f --- /dev/null +++ b/core/migrations/0008_activitylog_details.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-11-22 23:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0007_activitylog'), + ] + + operations = [ + migrations.AddField( + model_name='activitylog', + name='details', + field=models.JSONField(blank=True, null=True), + ), + ] diff --git a/core/migrations/0009_alter_activitylog_timestamp.py b/core/migrations/0009_alter_activitylog_timestamp.py new file mode 100644 index 0000000..f640bf3 --- /dev/null +++ b/core/migrations/0009_alter_activitylog_timestamp.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2025-11-23 00:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0008_activitylog_details'), + ] + + operations = [ + migrations.AlterField( + model_name='activitylog', + name='timestamp', + field=models.DateTimeField(auto_now_add=True, db_index=True), + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..001784e7d8bad46e71002510539636509fe3cc62 GIT binary patch literal 11404 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 TnSK{AgGYW~oiAR(!q)KL;Vp25 literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/0002_alter_user_managers_remove_user_username_and_more.cpython-311.pyc b/core/migrations/__pycache__/0002_alter_user_managers_remove_user_username_and_more.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e69ffdb471b6372ce41977b66dbd67122f31777f GIT binary patch literal 1181 zcmZuw%}*0S6rcULJm$t8D5&7gKEMkeM zja-)gQOmS{=N)eB9xtqlb)hZq@Eh;)F=)xIdS>xmm9~0DM23vBfO9-2*T^5|XjpF> zr*qo*(@fN*N&y!*7eh>6&N$$N$hCkHR+yWcyJ0!L({gN=$x%os%LUa2skK4~@@tw**C`-6o^llYkUHHU;0>def0Zi~=mK(U+xq)-$p zgE#+zLrvFN{weW-SL6Yl$c!W})$ar5R1caYeIj4cA>RWXIpwv$Bju~{JpNgn|52RZD=u`43!RbMJaTL} zRutVhKwR`e#08{JLE2<*e4;mcDOMzKbI}J8Ye=62_eA2p&@-pEX1eC|m+;%Ij=8vJ zE_ThuI44667kv=%_=u@)K7vlCGV~>t<$;vh!9SK1gz30p`}JTJSCh&m1sYs<67rJd z%UnCs?hML*mriq2;3KSZ5ley~a1w-b9b^u^9=hH+>xV$0}`Ts;6s{|s+} F;V+FmEJFYQ literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/0003_customer_owner.cpython-311.pyc b/core/migrations/__pycache__/0003_customer_owner.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9ccf645b441b4921ea2c37065da25ace19d9448e GIT binary patch literal 1141 zcmZuvzi-n(6h8lq?WPn85}+~=S^-&-3M3|kR76@t(-zSLi3}W0?i!N%M>yM*Zbf2X z?10q&pn%$eKY@WiphJYBTbbBgwNoeF*>MV@XZzjv-ud2r?|b)oeB44XrhXPWOEN;g zgp&K5KZ_r zWFobth9V=Xb~Dz%l8A_Y%7dsxrAF7x5_a_wN>mVDH#(65+gqfYTqj-IYx`U#O>QKtprzbct<>;xy;^1BHt>nLdhIc;uU4MnrIkmO zD!2R|9d`dKIug>K%$Hw7{pF|`d0|wh?`SIueOBJ^*?Q2kTeK|nEuY?+GAMl)XCmQ5 zwNh`BiOZN613>T|%+~0Mb!09|u_9m6x2my)%xfR5zA-hpTo_!qGMJp+zS*Cg{xb8e z_Wk*f7yU}TUwPTDv}1%eq=zz03-XG5EG8_xi?8Iv#6k#goKsEf5P>-sCRX%Ged~#M z*o}-+(co88!AhiAO0Rt>Q;ki*Z+vXH-txV*R6TRFBk4xcgS5Pto*VA{9fxjZR56~HJtmh>?M*o{_tABpxP%Ipuv6`^@1I_v{ AeEo{?Lq?HhYuGrmDYF270?TQmcsl%Xc+k4lXU*{0|op;hA_ZuglLF0Rb5f)rj6W}enVk=k630#(QrIuWiTS`q4 zkcd7+So#)WnaItF)X;top~vtfr8a?uEo3Nv@b0D|Ff|}y=y;955Si9;8q^9M-wT-1 z@-cA(yRR3&uJk)6PgIVH#CU2V78*h=Cn6#>Q`CSGbBQvk%2{+T z*qDfIfVZS@8zkS1v&JP&r9Xh0g+Z&Bwy;X{=49GC$nOKX#{-0a<69d6ehp`bzc2#) zg$(f*M}WVWA%1-X`1Mzge=6NAuzx2nwfCm}@!osh_w=QF&s@T{mSNl)&MxjkhH)1% zjJud&+{Fyz)-#M-f0c1J=4#Wp1e|A<p0c+w+$@Hui0U595dk%5&P_5qi zdhYEd+qHtg2+Y)2y<^CflNG8%#qz$_A7VvHT#NgQt%;$})D$6;Tw-DHfV3%zmCkw_N|_e5@i{Yw(oJKAsb=ieG?`|a5Lx6BXctZML1eic5AR{pCLSi9 z?T~;<9KE`R@f{vDX^~jQv^$V%r{R4d#C4{|xd6V&)U8U@ z+}vJYr<|&?skQCuN9Oic<*vE$>Fvro)7IRO(7rXlP5LzTM|1+y(X+68AJ(rN)gv#A z%H%6zN3e3`U7zj*ZObNQzS!jX}!^LiC@lo}ubXZ#brL@{D zt@b9QympMBI_1@iI@h_*^+yX&sy}`H^B3L9cDM3nx6Umg z4UnS$-~2hhp=ol)ysgGzel16YlCt+Pzv#Q^QD&&@_2h(J#`WdoPoU-Wn5NfY8Y}e2 z;rP6l9M}1QM$86c4v?IV2DcjiY?$_<)w7E5H+c164042m+j#!qr3cR&pPu ijprmwhv=NUr|bxV(nIIh54G;}!f`x!`jROzzCQs_oEugE literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/0005_opportunity_name.cpython-311.pyc b/core/migrations/__pycache__/0005_opportunity_name.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c333479ebef0b7fb1fd231bc58f55488aa7ce03e GIT binary patch literal 870 zcmZ`%J8#rL5T3PtKHDS`SdqdbRfrJR#8-f5BBTfe913Yb5pu!G=Uwi6@e4mhuDgPQ zGF|=w50M{0iTDLUy5i)PDxITCl?G;=b07-rTI2b4Uf;~_$N6~!aBbXM_P!Hq%*jBmgFc}4?vOemqk%|q zDT?DL$@0+4hJ_M30e^A$3puvrqS5u8a2u!K>`9|hdXyLp)97T@7+j;&j7qhvdc9hX~TI@X`tvv5v#>0g~z6j$S6 zqGGyA2qk@qVsy4k74$7q%F=>5+9(asm);v=?Z#wrWpZV4VlMCA8k@_X7QWv6_VB>$ z{4hIXvr{$|-8cdi$3m2{YO15BVkg(?QkyNM(AmHVd(kTER_#|!pOU#M+$!JesGVqQ vr!%~tEFr{459t<)QYM5TLDD*a+ZVr5og-=q7sakIzPxfI49EYlR&{>^Wm@J8 literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/0006_contacthistory_restored_at_and_more.cpython-311.pyc b/core/migrations/__pycache__/0006_contacthistory_restored_at_and_more.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..84de67049a42b03779664ed04f79ca5e7ad70539 GIT binary patch literal 2704 zcmd^B&u`R56rSa_F%~D)kRYr9G_X+EZ_vCMQn3vDcfRAdsS^Idp8ld2eRkH=oCP z-|n+w(L&Jv`1XwZuZhs#GRT@XbhdAT^Au4;v4`pydc{-Pxq1%Ev7D#2wYt{U>pDgX zx`wFw3!)m+S`!)2dJdtN@T9FiLGdyw>;K4bsf>jYvM6%>W>{8))pncQj@-Zxh29P* z^Fn8smvl#m!FC_;o+6g;sVfvWaXqIXrnWNHz>>-leU}GSsk))9E5HVxv~B2bv)hL; zkzg3vdWpfzR&r``tfgl&t^CLFZnmE`V^f6ZI}|~^-Kpq6jUeROdOk^pT9tYPAA||?7LBy_;#C# zNhk0l+ld~yVHEJL$a5wgMu{EOAX#g8w!bP=KlZ$G4g^MuN}8%B0NQ<*d2~-+qx%sOv1CM9hxwHGj>|%>%AJkNlwOsbxl|HSyaIdI z+%`+bgmo2O-fj9nvaZl+%l4arP-sIK30V+UVJuf`WNBq_kxNY$QMe5p z#+B-#u!0U7YVZrR4NK7LyOjsv zNZbT`7QENSrBrsl<6l9D;Rz!!gB->fN)MmjM5ofbpE-L*W;fA0?ty+0>jSjkJh6I5 Ornlt8&O2rkR&M|Zmu1cX literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/0007_activitylog.cpython-311.pyc b/core/migrations/__pycache__/0007_activitylog.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..127350de20d0ae3570129255d8ffaf6ccc722b01 GIT binary patch literal 1806 zcmah}O-vg{6rNq%>wmC`!4M}-T}4$KT8wC$iUO4!W7@Pp(*ludRb4HfVOht!>+Y^e zKq}?XLyx(YV^2gC9CFMdha7t7agp|5&8bpPxf#JJm-fxB*MJhKGdr{Iy?OKAoA14u zr^#dtL3=v$u3`oe`iC)o3-yT8`#}7LFv46D)j0U`OzNk!XXlBgndH?0fY`=c&!%3+zKiRuNb>resmZfO2S>$OS{)O3634b55UJEjJ{67)rtC&10W8427!&FyY3>i;`@P>^+S10ao1EdlS|5T_U z`0~a;pBnTjl4_)V`Y3f7Rq&p^Z25of4*9n5(9x9x9?k;8Ya6^Q;0zv?IfXkKx$H^i zYFBU;GB`@3OMbQ>b&0yJSwyp}?G`cV^~uS}DbXORzD2~|ipZ8kO;QP{2kW{)=q*sY z`$;LS)!z2QDvI(a@|t46l;4?mnpB{)dkAmn=b}wLTX#OST~4d z5lrJPLs2C#@{nxPs7VY(YAQQ~Cbo#NuA82FDu6eru~+}XaiIcZs`OZF5>2)?X;fct zkh&!*m_{u{g-|551v;J^O@Aax7}F3K(2Zh%2DNsx37a$#YL!@o@|NC*!aeZL=tW>A zE-*#j$Uy+cHJE5bH!wu2$vd$R&iyh6fEN`u6w<^d4eKrUax51!G$OiKiXx4PfDvvt z3D5~q{IM-H*)GOm(jporT3sQg!RnpHD=t(^>~b0GuF%9?MJ@w~3(N2h91D%2{dR-}P>MAL<*V~%aE|JHi z4uKk_N4oLQY)N%eVr5i1e>F-_P@jk_y{uN6x;)X^HrPw#PV9P{*WpU<^?O%9(N8;( z*={J15%w0l2^78YYs`t{JL&x2>0vv2`^lD*z5P7+*G;=7+L~c!&Eu@;WKBC|b<$&Y zZhZfalN*0#+PRtI+>Dc(u~W01^ffy_y}#k)r;qb9PQEXjVzQ~1K|5bP&R3m$)lMyR z(xdj(@%{IltK-iyFGv0ybIRX1<(gBj+ncR3gcPpKcM;0nRbjGG`U~ov-`;v9zBOk9~gpPSBoxtl8-g`XZ=hb*0)M&i74t2fZbj^0*c36M$cW)T1#E zrW+U>0tHG!h=@ZX$&m<1@D!-D4^(DkAlF4l7=6EBj!czpuo}O`wk2^R3i_#+2T7c9 zHA*N8Grz7BUu{mNZ!tIo1`eU%NR;%6BTK-PK`jcereZ}MH0L!y6{;LJjwBR-?`#~a z{|dr$G}=T-8rNUcT2PqcCn@7g?RNXV>-l-`A;|Z_q|dKiCQZt6F9@?&SkUN&UcAGV zxClc_Mq|Q^8+#GMHGz$H0Sjr@(z)hV>#oZU*TuXdWEd~F?r!0QB3CfVhAgHm_5+rs zqJLa_Na+)CMyd#^Z*9NqR-7Bjkj=aJ4W`2A`7`@{5;42z6>(nJ>=W~gJW1`3NxG8_ zJ)haapZ&M{HadN9*{M7Cwm#fTRXni*5oE6sMi;YGLBCVV(y}^wUK*g^{%nl3)yc}* z^%dJ3r0N*zA-|MK?|X;Y>g&tEM{I5;MI~-_x}euG4`R_mg$n ztL#^%XG&H?T9tbfqjPOz*2B$o6)FC-ki9`DWkLw5Bi0GryZV&s0#Qr23J1n`Y3)>O Koc(9Fy7v#DK+NO- literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/0009_alter_activitylog_timestamp.cpython-311.pyc b/core/migrations/__pycache__/0009_alter_activitylog_timestamp.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..96536862e265651ca42de034ec2c10c9113d0cd5 GIT binary patch literal 841 zcmZuvy>HV%6u+|_$2N%s7E+5S45*68K%IdBqzVbCf(Su{)WTpPoi4s3b?`^<2W__u z42<16vK1jZ@F(ym6e&Y=D-)Zmbn3)ACk|BQ&VGLH-TU~x-}y_kX(3oEFYoxDF+#tj zQLWlsnXZF!h!|oxKp}QPwotqFTOPL+VTFD_ zp_v~?snEli@gVi8O!+nD#`Fy+hlnE=Gvq1^_pz%gh--ri6trg2qYvuy9KA&F?ng;vViTb~DPvuz54lzu{lMF_;@ z0SCQ7$orfIGFDoQ4>{C|JfEkDBvn|CgN!FnWR*mdj^+l<_%`4veZq=|d`|FJG?g5j zZ#g@0$ep~GM_KOhJ?;VC#MzCLcj=IN+>xU@e=o)X5*`vN`&^)6G4$GSKhdFbDWsCV z0x??bLPzGkLRGHoqvwT%%-f%=v2km%v^u%AGO=3+t+CzuzVYM!iQWBacgJ?OsB5Nm zia?x6QK-7DkDke$ELVLt+nO-hfJXgzjrB@0N&;p`mSk8Ndjqs{V|~WxqvR%-@L!|! lHHbpR7y~-oK0$XcKZV}FdVwyB18aPB^;905{pYmo`v>-k+#dh{ literal 0 HcmV?d00001 diff --git a/core/models.py b/core/models.py index 71a8362..57651b2 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,205 @@ +from django.contrib.auth.models import AbstractUser, BaseUserManager +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType from django.db import models +from django.utils import timezone -# Create your models here. +class UserManager(BaseUserManager): + """Define a model manager for User model with no username field.""" + + use_in_migrations = True + + def _create_user(self, email, password, **extra_fields): + """Create and save a User with the given email and password.""" + if not email: + raise ValueError('The given email must be set') + email = self.normalize_email(email) + user = self.model(email=email, **extra_fields) + user.set_password(password) + user.save(using=self._db) + return user + + def create_user(self, email, password=None, **extra_fields): + """Create and save a regular User with the given email and password.""" + extra_fields.setdefault('is_staff', False) + extra_fields.setdefault('is_superuser', False) + return self._create_user(email, password, **extra_fields) + + def create_superuser(self, email, password, **extra_fields): + """Create and save a SuperUser with the given email and password.""" + extra_fields.setdefault('is_staff', True) + extra_fields.setdefault('is_superuser', True) + + if extra_fields.get('is_staff') is not True: + raise ValueError('Superuser must have is_staff=True.') + if extra_fields.get('is_superuser') is not True: + raise ValueError('Superuser must have is_superuser=True.') + + return self._create_user(email, password, **extra_fields) + +class Tenant(models.Model): + name = models.CharField(max_length=255) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.name + +class User(AbstractUser): + username = None + email = models.EmailField(unique=True) + tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, null=True, blank=True) + role = models.CharField(max_length=100, blank=True) + + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = [] + + objects = UserManager() + +class BaseModel(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + is_deleted = models.BooleanField(default=False) + deleted_at = models.DateTimeField(null=True, blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='%(class)s_created_by') + updated_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='%(class)s_updated_by') + deleted_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='%(class)s_deleted_by') + restored_at = models.DateTimeField(null=True, blank=True) + restored_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='%(class)s_restored_by') + + + class Meta: + abstract = True + + def delete(self, user=None, *args, **kwargs): + self.is_deleted = True + self.deleted_at = timezone.now() + if user: + self.deleted_by = user + self.save() + ActivityLog.objects.create( + actor=user, + action=f'deleted a {self.__class__.__name__}', + content_object=self, + details={'id': self.id, 'name': str(self)} + ) + + def restore(self, user=None, *args, **kwargs): + self.is_deleted = False + self.deleted_at = None + self.deleted_by = None + self.restored_at = timezone.now() + if user: + self.restored_by = user + self.save() + ActivityLog.objects.create( + actor=user, + action=f'restored a {self.__class__.__name__}', + content_object=self, + details={'id': self.id, 'name': str(self)} + ) + +class ActivityLog(models.Model): + actor = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) + action = models.CharField(max_length=255) + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + content_object = GenericForeignKey('content_type', 'object_id') + timestamp = models.DateTimeField(auto_now_add=True, db_index=True) + details = models.JSONField(null=True, blank=True) + + class Meta: + ordering = ['-timestamp'] + + def get_action_badge(self): + if 'deleted' in self.action: + return ('bg-danger', 'deleted') + elif 'restored' in self.action: + return ('bg-success', 'restored') + elif 'created' in self.action: + return ('bg-primary', 'created') + elif 'updated' in self.action: + return ('bg-info', 'updated') + else: + action_verb = self.action.split(' ')[0] + return ('bg-secondary', action_verb) + +class Customer(BaseModel): + STATUS_CHOICES = ( + ('active', 'Active'), + ('inactive', 'Inactive'), + ('lead', 'Lead'), + ) + tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) + name = models.CharField(max_length=255) + email = models.EmailField() + phone = models.CharField(max_length=50, blank=True) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active', db_index=True) + owner = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='customers') + + class Meta: + unique_together = ('tenant', 'email') + indexes = [ + models.Index(fields=['tenant', 'status']), + models.Index(fields=['created_at']), + ] + + def __str__(self): + return self.name + +class Lead(BaseModel): + STATUS_CHOICES = ( + ('new', 'New'), + ('contacted', 'Contacted'), + ('qualified', 'Qualified'), + ('unqualified', 'Unqualified'), + ) + customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='leads') + source = models.CharField(max_length=255, blank=True) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='new', db_index=True) + assigned_to = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='leads') + + def __str__(self): + return f"Lead for {self.customer.name}" + +class Opportunity(BaseModel): + STAGE_CHOICES = ( + ('prospecting', 'Prospecting'), + ('qualification', 'Qualification'), + ('proposal', 'Proposal'), + ('negotiation', 'Negotiation'), + ('closed_won', 'Closed Won'), + ('closed_lost', 'Closed Lost'), + ) + name = models.CharField(max_length=255, blank=True, null=True) + lead = models.ForeignKey(Lead, on_delete=models.CASCADE, related_name='opportunities') + value = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + stage = models.CharField(max_length=20, choices=STAGE_CHOICES, default='prospecting', db_index=True) + probability = models.FloatField(null=True, blank=True) + close_date = models.DateField(null=True, blank=True) + + def __str__(self): + return self.name + +class ContactHistory(BaseModel): + INTERACTION_CHOICES = ( + ('email', 'Email'), + ('phone', 'Phone Call'), + ('meeting', 'Meeting'), + ('note', 'Note'), + ) + customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='contact_history') + user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) + note = models.TextField() + interaction_type = models.CharField(max_length=20, choices=INTERACTION_CHOICES, default='note') + + def __str__(self): + return f"Contact with {self.customer.name} on {self.created_at.date()}" + +class Note(BaseModel): + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + content_object = GenericForeignKey('content_type', 'object_id') + content = models.TextField() + + def __str__(self): + return f"Note for {self.content_object}" \ No newline at end of file diff --git a/core/opportunity_urls.py b/core/opportunity_urls.py new file mode 100644 index 0000000..93328a9 --- /dev/null +++ b/core/opportunity_urls.py @@ -0,0 +1,18 @@ +from django.urls import path +from .views import ( + OpportunityListView, + OpportunityDetailView, + OpportunityCreateView, + OpportunityUpdateView, + OpportunityDeleteView, +) + +app_name = 'opportunities' + +urlpatterns = [ + path('', OpportunityListView.as_view(), name='opportunity_list'), + path('/', OpportunityDetailView.as_view(), name='opportunity_detail'), + path('create/', OpportunityCreateView.as_view(), name='opportunity_create'), + path('/update/', OpportunityUpdateView.as_view(), name='opportunity_update'), + path('/delete/', OpportunityDeleteView.as_view(), name='opportunity_delete'), +] diff --git a/core/templates/base.html b/core/templates/base.html index 788576e..aefa2fb 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,11 +1,37 @@ - + - {% block title %}Knowledge Base{% endblock %} + + {% block title %}CRM Platform{% endblock %} + + + + + {% load static %} + + + {% block head %}{% endblock %} - - + + + {% block content %}{% endblock %} - + + + diff --git a/core/templates/core/_activity_feed.html b/core/templates/core/_activity_feed.html new file mode 100644 index 0000000..74b34e9 --- /dev/null +++ b/core/templates/core/_activity_feed.html @@ -0,0 +1,20 @@ +
+
+ Activity Feed +
+
    + {% for log in activity_logs %} + {% with log.get_action_badge as badge %} +
  • + {{ log.actor.email }} + {{ badge.1 }} + a {{ log.content_object.__class__.__name__ }} + - + {{ log.timestamp|timesince }} ago +
  • + {% endwith %} + {% empty %} +
  • No recent activity.
  • + {% endfor %} +
+
diff --git a/core/templates/core/customer_confirm_delete.html b/core/templates/core/customer_confirm_delete.html new file mode 100644 index 0000000..a5ca1d2 --- /dev/null +++ b/core/templates/core/customer_confirm_delete.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} +
+

Delete Customer

+

Are you sure you want to delete "{{ object.name }}"?

+
+ {% csrf_token %} + + Cancel +
+
+{% endblock %} diff --git a/core/templates/core/customer_detail.html b/core/templates/core/customer_detail.html new file mode 100644 index 0000000..32733ef --- /dev/null +++ b/core/templates/core/customer_detail.html @@ -0,0 +1,206 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+ + + {% if customer.is_deleted %} + + {% endif %} + +
+
+

{{ customer.name }}

+ +
+
+
+
+

Details

+ + + + + + + + + + + + + + + + + + + + + + {% if customer.restored_at %} + + + + + + + + + {% endif %} +
Email:{{ customer.email }}
Status:{{ customer.get_status_display }}
Owner:{{ customer.owner.get_full_name|default:"N/A" }}
Created At:{{ customer.created_at|date:"Y-m-d H:i" }}
Tenant:{{ customer.tenant.name }}
Restored At:{{ customer.restored_at|date:"Y-m-d H:i" }}
Restored By:{{ customer.restored_by.get_full_name|default:"Unknown" }}
+
+
+

Summary

+
    +
  • + Opportunities + {{ opportunity_count }} +
  • +
  • + Leads + {{ lead_count }} +
  • +
  • + Notes + {{ note_count }} +
  • +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+

Opportunities

+ {% if opportunities %} + + + + + + + + + + + {% for op in opportunities %} + + + + + + + {% endfor %} + +
NameStageValueClose Date
{{ op.name }}{{ op.get_stage_display }}${{ op.value }}{{ op.close_date|date:"Y-m-d" }}
+ {% else %} +

No opportunities found for this customer.

+ {% endif %} +
+
+

Leads

+ {% if leads %} + + + + + + + + + + {% for lead in leads %} + + + + + + {% endfor %} + +
NameStatusSource
{{ lead.name }}{{ lead.get_status_display }}{{ lead.source }}
+ {% else %} +

No leads found for this customer.

+ {% endif %} +
+
+

Contact History

+ {% if contact_history %} +
    + {% for entry in contact_history %} +
  • + {{ entry.created_at|date:"Y-m-d H:i" }}: {{ entry.notes }} ({{ entry.get_interaction_type_display }}) +
  • + {% endfor %} +
+ {% else %} +

No contact history found.

+ {% endif %} +
+
+

Notes

+ {% if notes %} +
    + {% for note in notes %} +
  • +

    {{ note.note }}

    + By {{ note.user.get_full_name }} on {{ note.created_at|date:"Y-m-d H:i" }} +
  • + {% endfor %} +
+ {% else %} +

No notes found.

+ {% endif %} +
+
+
+
+
+
+
+ + +{% endblock %} diff --git a/core/templates/core/customer_form.html b/core/templates/core/customer_form.html new file mode 100644 index 0000000..a7a517c --- /dev/null +++ b/core/templates/core/customer_form.html @@ -0,0 +1,50 @@ +{% extends "base.html" %} + +{% block content %} +
+

Edit Customer: {{ customer.name }}

+ + {% if messages %} + {% for message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + +
+
+
+ {% csrf_token %} + +
+ + {{ form.first_name }} +
+
+ + {{ form.last_name }} +
+
+ + {{ form.email }} +
+
+ + {{ form.phone }} +
+
+ + {{ form.status }} +
+
+ + {{ form.owner }} +
+ + + Cancel +
+
+
+ +
+{% endblock %} diff --git a/core/templates/core/customer_list.html b/core/templates/core/customer_list.html new file mode 100644 index 0000000..631a701 --- /dev/null +++ b/core/templates/core/customer_list.html @@ -0,0 +1,465 @@ +{% extends 'base.html' %} +{% load query_string %} + +{% block content %} +
+

Customers

+ + +
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+ +
+
+
+ +
+
+
+
+
+ +
+
+ +
+
+ +
+
+ + + +
+
+ +
+
+ +
+ +
+
+
+ Showing {{ activity_logs.start_index }} - {{ activity_logs.end_index }} of {{ activity_log_count }} activities — Page {{ activity_logs.number }} of {{ activity_paginator.num_pages }} +
+
+
+ + + + + + + + + + +
+
+ +
+ Recent Activity: + {{ recent_activity_count }} actions in the last 24 hours. +
+ +
+ Activity Summary: + {% for action, count in activity_summary.items %} + {{ action }}: {{ count }} + {% empty %} + No activity in current view. + {% endfor %} +
+ + {% include 'core/_activity_feed.html' %} + + + +
+ Page {{ activity_logs.number }} of {{ activity_paginator.num_pages }} +
+
+ + + + + + + + + +
+
+
+
+
+ + +
+
+ Showing {{ page_obj.start_index }} - {{ page_obj.end_index }} of {{ customer_count }} customers — Page {{ page_obj.number }} of {{ paginator.num_pages }} +
+
+
+ + + + + + +
+
+
+ +
+ {% csrf_token %} +
+ + + + + + + + + + + + + + + + {% for customer in page_obj %} + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
NameEmailPhoneStatusOwnerLast ContactOpportunitiesActions
{{ customer.name }}{{ customer.email }}{{ customer.phone }}{{ customer.get_status_display }}{{ customer.owner.email|default:"-" }}{{ customer.last_contact|date:"Y-m-d"|default:"-" }}{{ customer.opportunity_count }} + View + Edit + Delete +
No customers found.
+
+ + +
+ + + +
+ Page {{ page_obj.number }} of {{ paginator.num_pages }} +
+
+ + + + + + +
+
+
+
+ + + + + + +{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index 0a3f404..fe83eba 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,154 +1,56 @@ {% extends "base.html" %} +{% load static %} -{% block title %}{{ project_name }}{% endblock %} +{% block title %}Autonomous CRM & AI Agents{% endblock %} {% block head %} -{% if project_description %} - - - -{% endif %} -{% if project_image_url %} - - -{% endif %} - - - - + {% endblock %} {% block content %}
-
-

Analyzing your requirements and generating your app…

-
- Loading… -
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
+
+
+

The Autonomous CRM Platform

+

Supercharge your business with AI agents for sales, support, and scheduling that work for you 24/7.

+ Get Started Now +
+
+ +
+
+

Core Features

+
+
+
+
+
AI Sales Agent
+

Automatically nurtures leads, scores prospects, and closes deals.

+
+
+
+
+
+
+
AI Support Agent
+

Resolves customer tickets instantly with a fully contextual knowledge base.

+
+
+
+
+
+
+
AI Appointment Setter
+

Schedules and confirms meetings seamlessly across all your calendars.

+
+
+
+
+
+
-