amsdal_crm 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- amsdal_crm/__about__.py +1 -0
- amsdal_crm/__init__.py +13 -0
- amsdal_crm/app.py +25 -0
- amsdal_crm/constants.py +13 -0
- amsdal_crm/errors.py +21 -0
- amsdal_crm/fixtures/__init__.py +1 -0
- amsdal_crm/fixtures/permissions.py +29 -0
- amsdal_crm/fixtures/pipelines.py +74 -0
- amsdal_crm/lifecycle/__init__.py +1 -0
- amsdal_crm/lifecycle/consumer.py +44 -0
- amsdal_crm/migrations/0000_initial.py +633 -0
- amsdal_crm/models/__init__.py +46 -0
- amsdal_crm/models/account.py +127 -0
- amsdal_crm/models/activity.py +140 -0
- amsdal_crm/models/attachment.py +43 -0
- amsdal_crm/models/contact.py +132 -0
- amsdal_crm/models/custom_field_definition.py +44 -0
- amsdal_crm/models/deal.py +172 -0
- amsdal_crm/models/pipeline.py +28 -0
- amsdal_crm/models/stage.py +47 -0
- amsdal_crm/models/workflow_rule.py +44 -0
- amsdal_crm/services/__init__.py +15 -0
- amsdal_crm/services/activity_service.py +56 -0
- amsdal_crm/services/custom_field_service.py +143 -0
- amsdal_crm/services/deal_service.py +131 -0
- amsdal_crm/services/email_service.py +118 -0
- amsdal_crm/services/workflow_service.py +177 -0
- amsdal_crm/settings.py +26 -0
- amsdal_crm-0.1.0.dist-info/METADATA +68 -0
- amsdal_crm-0.1.0.dist-info/RECORD +31 -0
- amsdal_crm-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
from amsdal_models.migration import migrations
|
|
2
|
+
from amsdal_utils.models.enums import ModuleType
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Migration(migrations.Migration):
|
|
6
|
+
operations: list[migrations.Operation] = [
|
|
7
|
+
migrations.CreateClass(
|
|
8
|
+
module_type=ModuleType.CONTRIB,
|
|
9
|
+
class_name="Account",
|
|
10
|
+
new_schema={
|
|
11
|
+
"title": "Account",
|
|
12
|
+
"required": ["name", "owner_email"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"created_at": {"type": "datetime", "title": "Created At", "format": "date-time"},
|
|
15
|
+
"updated_at": {"type": "datetime", "title": "Updated At", "format": "date-time"},
|
|
16
|
+
"name": {"type": "string", "title": "Account Name"},
|
|
17
|
+
"website": {"type": "string", "title": "Website"},
|
|
18
|
+
"phone": {"type": "string", "title": "Phone"},
|
|
19
|
+
"industry": {"type": "string", "title": "Industry"},
|
|
20
|
+
"billing_street": {"type": "string", "title": "Billing Street"},
|
|
21
|
+
"billing_city": {"type": "string", "title": "Billing City"},
|
|
22
|
+
"billing_state": {"type": "string", "title": "Billing State"},
|
|
23
|
+
"billing_postal_code": {"type": "string", "title": "Billing Postal Code"},
|
|
24
|
+
"billing_country": {"type": "string", "title": "Billing Country"},
|
|
25
|
+
"owner_email": {"type": "string", "title": "Owner Email"},
|
|
26
|
+
"custom_fields": {"type": "anything", "title": "Custom Fields"},
|
|
27
|
+
},
|
|
28
|
+
"custom_code": 'from amsdal.contrib.auth.models.user import User\n\n\n@property\ndef display_name(self) -> str:\n """Return display name for the account."""\n return self.name\n\nasync def apost_update(self) -> None:\n """Async hook called after updating account."""\n from amsdal_crm.services.workflow_service import WorkflowService\n WorkflowService.execute_rules(\'Account\', \'update\', self)\n\nasync def apre_create(self) -> None:\n """Async hook called before creating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'Account\', self.custom_fields)\n await super().apre_create()\n\nasync def apre_update(self) -> None:\n """Async hook called before updating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'Account\', self.custom_fields)\n await super().apre_update()\n\ndef has_object_permission(self, user: \'User\', action: str) -> bool:\n """Check if user has permission to perform action on this account.\n\n Args:\n user: The user attempting the action\n action: The action being attempted (read, create, update, delete)\n\n Returns:\n True if user has permission, False otherwise\n """\n if self.owner_email == user.email:\n return True\n if user.permissions:\n for permission in user.permissions:\n if permission.model == \'*\' and permission.action in (\'*\', action):\n return True\n if permission.model == \'Account\' and permission.action in (\'*\', action):\n return True\n return False\n\ndef post_update(self) -> None:\n """Hook called after updating account."""\n from amsdal_crm.services.workflow_service import WorkflowService\n WorkflowService.execute_rules(\'Account\', \'update\', self)\n\ndef pre_create(self) -> None:\n """Hook called before creating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'Account\', self.custom_fields)\n super().pre_create()\n\ndef pre_update(self) -> None:\n """Hook called before updating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'Account\', self.custom_fields)\n super().pre_update()',
|
|
29
|
+
"storage_metadata": {
|
|
30
|
+
"table_name": "Account",
|
|
31
|
+
"db_fields": {},
|
|
32
|
+
"primary_key": ["partition_key"],
|
|
33
|
+
"indexed": [["owner_email"], ["created_at"]],
|
|
34
|
+
"unique": [["name", "owner_email"]],
|
|
35
|
+
"foreign_keys": {},
|
|
36
|
+
},
|
|
37
|
+
"description": "Account (Company/Organization) model.\n\nRepresents a company or organization in the CRM system.\nOwned by individual users with permission controls.",
|
|
38
|
+
},
|
|
39
|
+
),
|
|
40
|
+
migrations.CreateClass(
|
|
41
|
+
module_type=ModuleType.CONTRIB,
|
|
42
|
+
class_name="Activity",
|
|
43
|
+
new_schema={
|
|
44
|
+
"title": "Activity",
|
|
45
|
+
"required": ["activity_type", "subject", "related_to_type", "related_to_id", "owner_email"],
|
|
46
|
+
"properties": {
|
|
47
|
+
"created_at": {"type": "datetime", "title": "Created At", "format": "date-time"},
|
|
48
|
+
"updated_at": {"type": "datetime", "title": "Updated At", "format": "date-time"},
|
|
49
|
+
"activity_type": {
|
|
50
|
+
"type": "ActivityType",
|
|
51
|
+
"options": [
|
|
52
|
+
{"key": "TASK", "value": "task"},
|
|
53
|
+
{"key": "EVENT", "value": "event"},
|
|
54
|
+
{"key": "EMAIL", "value": "email"},
|
|
55
|
+
{"key": "NOTE", "value": "note"},
|
|
56
|
+
{"key": "CALL", "value": "call"},
|
|
57
|
+
],
|
|
58
|
+
"title": "ActivityType",
|
|
59
|
+
"description": "Activity type enumeration.",
|
|
60
|
+
"enum": ["task", "event", "email", "note", "call"],
|
|
61
|
+
"x_enum_names": ["TASK", "EVENT", "EMAIL", "NOTE", "CALL"],
|
|
62
|
+
},
|
|
63
|
+
"subject": {"type": "string", "title": "Subject"},
|
|
64
|
+
"description": {"type": "string", "title": "Description"},
|
|
65
|
+
"related_to_type": {
|
|
66
|
+
"type": "ActivityRelatedTo",
|
|
67
|
+
"options": [
|
|
68
|
+
{"key": "CONTACT", "value": "Contact"},
|
|
69
|
+
{"key": "ACCOUNT", "value": "Account"},
|
|
70
|
+
{"key": "DEAL", "value": "Deal"},
|
|
71
|
+
],
|
|
72
|
+
"title": "ActivityRelatedTo",
|
|
73
|
+
"description": "What type of record this activity is related to.",
|
|
74
|
+
"enum": ["Contact", "Account", "Deal"],
|
|
75
|
+
"x_enum_names": ["CONTACT", "ACCOUNT", "DEAL"],
|
|
76
|
+
},
|
|
77
|
+
"related_to_id": {"type": "string", "title": "Related To ID"},
|
|
78
|
+
"owner_email": {"type": "string", "title": "Owner Email"},
|
|
79
|
+
"due_date": {"type": "datetime", "title": "Due Date", "format": "date-time"},
|
|
80
|
+
"completed_at": {"type": "datetime", "title": "Completed At", "format": "date-time"},
|
|
81
|
+
"is_completed": {"type": "boolean", "default": False, "title": "Is Completed"},
|
|
82
|
+
},
|
|
83
|
+
"custom_code": "import datetime as _dt\n\nfrom amsdal.contrib.auth.models.user import User\n\n\n@property\ndef display_name(self) -> str:\n \"\"\"Return display name for the activity.\"\"\"\n return f'{self.activity_type.value}: {self.subject}'\n\nasync def apre_create(self) -> None:\n self.created_at = _dt.datetime.now(tz=_dt.UTC)\n await super().apre_create()\n\nasync def apre_update(self) -> None:\n self.updated_at = _dt.datetime.now(tz=_dt.UTC)\n if not self.created_at:\n _metadata = await self.aget_metadata()\n self.created_at = _dt.datetime.fromtimestamp(_metadata.created_at / 1000, tz=_dt.UTC)\n await super().apre_update()\n\ndef has_object_permission(self, user: 'User', action: str) -> bool:\n \"\"\"Check if user has permission to perform action on this activity.\n\n Args:\n user: The user attempting the action\n action: The action being attempted (read, create, update, delete)\n\n Returns:\n True if user has permission, False otherwise\n \"\"\"\n if self.owner_email == user.email:\n return True\n if user.permissions:\n for permission in user.permissions:\n if permission.model == '*' and permission.action in ('*', action):\n return True\n if permission.model == 'Activity' and permission.action in ('*', action):\n return True\n return False\n\ndef pre_create(self) -> None:\n self.created_at = _dt.datetime.now(tz=_dt.UTC)\n super().pre_create()\n\ndef pre_update(self) -> None:\n self.updated_at = _dt.datetime.now(tz=_dt.UTC)\n if not self.created_at:\n _metadata = self.get_metadata()\n self.created_at = _dt.datetime.fromtimestamp(_metadata.created_at / 1000, tz=_dt.UTC)\n super().pre_update()",
|
|
84
|
+
"storage_metadata": {
|
|
85
|
+
"table_name": "Activity",
|
|
86
|
+
"db_fields": {},
|
|
87
|
+
"primary_key": ["partition_key"],
|
|
88
|
+
"indexed": [["related_to_id"], ["owner_email"], ["created_at"], ["due_date"]],
|
|
89
|
+
"foreign_keys": {},
|
|
90
|
+
},
|
|
91
|
+
"description": "Base activity model with polymorphic related_to field.\n\nActivities can be linked to Contacts, Accounts, or Deals using\na generic foreign key pattern (related_to_type + related_to_id).",
|
|
92
|
+
},
|
|
93
|
+
),
|
|
94
|
+
migrations.CreateClass(
|
|
95
|
+
module_type=ModuleType.CONTRIB,
|
|
96
|
+
class_name="Pipeline",
|
|
97
|
+
new_schema={
|
|
98
|
+
"title": "Pipeline",
|
|
99
|
+
"required": ["name"],
|
|
100
|
+
"properties": {
|
|
101
|
+
"name": {"type": "string", "title": "Pipeline Name"},
|
|
102
|
+
"description": {"type": "string", "title": "Description"},
|
|
103
|
+
"is_active": {"type": "boolean", "default": True, "title": "Is Active"},
|
|
104
|
+
},
|
|
105
|
+
"custom_code": '@property\ndef display_name(self) -> str:\n """Return display name for the pipeline."""\n return self.name',
|
|
106
|
+
"storage_metadata": {
|
|
107
|
+
"table_name": "Pipeline",
|
|
108
|
+
"db_fields": {},
|
|
109
|
+
"primary_key": ["partition_key"],
|
|
110
|
+
"unique": [["name"]],
|
|
111
|
+
"foreign_keys": {},
|
|
112
|
+
},
|
|
113
|
+
"description": "Sales pipeline model.\n\nRepresents a sales pipeline with multiple stages.\nPipelines are system-wide and not owned by individual users.",
|
|
114
|
+
},
|
|
115
|
+
),
|
|
116
|
+
migrations.CreateClass(
|
|
117
|
+
module_type=ModuleType.CONTRIB,
|
|
118
|
+
class_name="Contact",
|
|
119
|
+
new_schema={
|
|
120
|
+
"title": "Contact",
|
|
121
|
+
"required": ["first_name", "last_name", "email", "owner_email"],
|
|
122
|
+
"properties": {
|
|
123
|
+
"created_at": {"type": "datetime", "title": "Created At", "format": "date-time"},
|
|
124
|
+
"updated_at": {"type": "datetime", "title": "Updated At", "format": "date-time"},
|
|
125
|
+
"first_name": {"type": "string", "title": "First Name"},
|
|
126
|
+
"last_name": {"type": "string", "title": "Last Name"},
|
|
127
|
+
"email": {"type": "string", "title": "Email"},
|
|
128
|
+
"phone": {"type": "string", "title": "Phone Number"},
|
|
129
|
+
"mobile": {"type": "string", "title": "Mobile Number"},
|
|
130
|
+
"title": {"type": "string", "title": "Job Title"},
|
|
131
|
+
"account": {
|
|
132
|
+
"type": "Account",
|
|
133
|
+
"title": "Account",
|
|
134
|
+
"description": "Account (Company/Organization) model.\n\nRepresents a company or organization in the CRM system.\nOwned by individual users with permission controls.",
|
|
135
|
+
},
|
|
136
|
+
"owner_email": {"type": "string", "title": "Owner Email"},
|
|
137
|
+
"custom_fields": {"type": "anything", "title": "Custom Fields"},
|
|
138
|
+
},
|
|
139
|
+
"custom_code": 'from amsdal.contrib.auth.models.user import User\n\n\n@property\ndef display_name(self) -> str:\n """Return display name for the contact."""\n return f\'{self.first_name} {self.last_name}\'\n\n@property\ndef full_name(self) -> str:\n """Return full name of the contact."""\n return f\'{self.first_name} {self.last_name}\'\n\nasync def apost_update(self) -> None:\n """Async hook called after updating contact."""\n from amsdal_crm.services.workflow_service import WorkflowService\n WorkflowService.execute_rules(\'Contact\', \'update\', self)\n\nasync def apre_create(self) -> None:\n """Async hook called before creating contact."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'Contact\', self.custom_fields)\n await super().apre_create()\n\nasync def apre_update(self) -> None:\n """Async hook called before updating contact."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'Contact\', self.custom_fields)\n await super().apre_update()\n\ndef has_object_permission(self, user: \'User\', action: str) -> bool:\n """Check if user has permission to perform action on this contact.\n\n Args:\n user: The user attempting the action\n action: The action being attempted (read, create, update, delete)\n\n Returns:\n True if user has permission, False otherwise\n """\n if self.owner_email == user.email:\n return True\n if user.permissions:\n for permission in user.permissions:\n if permission.model == \'*\' and permission.action in (\'*\', action):\n return True\n if permission.model == \'Contact\' and permission.action in (\'*\', action):\n return True\n return False\n\ndef post_update(self) -> None:\n """Hook called after updating contact."""\n from amsdal_crm.services.workflow_service import WorkflowService\n WorkflowService.execute_rules(\'Contact\', \'update\', self)\n\ndef pre_create(self) -> None:\n """Hook called before creating contact."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'Contact\', self.custom_fields)\n super().pre_create()\n\ndef pre_update(self) -> None:\n """Hook called before updating contact."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'Contact\', self.custom_fields)\n super().pre_update()',
|
|
140
|
+
"storage_metadata": {
|
|
141
|
+
"table_name": "Contact",
|
|
142
|
+
"db_fields": {"account": ["account_partition_key"]},
|
|
143
|
+
"primary_key": ["partition_key"],
|
|
144
|
+
"indexed": [["owner_email"], ["created_at"]],
|
|
145
|
+
"unique": [["email"]],
|
|
146
|
+
"foreign_keys": {"account": [{"account_partition_key": "string"}, "Account", ["partition_key"]]},
|
|
147
|
+
},
|
|
148
|
+
"description": "Contact (Person) model.\n\nRepresents a person in the CRM system, optionally linked to an Account.\nOwned by individual users with permission controls.",
|
|
149
|
+
},
|
|
150
|
+
),
|
|
151
|
+
migrations.CreateClass(
|
|
152
|
+
module_type=ModuleType.CONTRIB,
|
|
153
|
+
class_name="Call",
|
|
154
|
+
new_schema={
|
|
155
|
+
"title": "Call",
|
|
156
|
+
"type": "Activity",
|
|
157
|
+
"required": ["subject", "related_to_type", "related_to_id", "owner_email", "phone_number"],
|
|
158
|
+
"properties": {
|
|
159
|
+
"created_at": {"type": "datetime", "title": "Created At", "format": "date-time"},
|
|
160
|
+
"updated_at": {"type": "datetime", "title": "Updated At", "format": "date-time"},
|
|
161
|
+
"activity_type": {"type": "string", "default": "call", "title": "Activity Type", "const": "call"},
|
|
162
|
+
"subject": {"type": "string", "title": "Subject"},
|
|
163
|
+
"description": {"type": "string", "title": "Description"},
|
|
164
|
+
"related_to_type": {
|
|
165
|
+
"type": "ActivityRelatedTo",
|
|
166
|
+
"options": [
|
|
167
|
+
{"key": "CONTACT", "value": "Contact"},
|
|
168
|
+
{"key": "ACCOUNT", "value": "Account"},
|
|
169
|
+
{"key": "DEAL", "value": "Deal"},
|
|
170
|
+
],
|
|
171
|
+
"title": "ActivityRelatedTo",
|
|
172
|
+
"description": "What type of record this activity is related to.",
|
|
173
|
+
"enum": ["Contact", "Account", "Deal"],
|
|
174
|
+
"x_enum_names": ["CONTACT", "ACCOUNT", "DEAL"],
|
|
175
|
+
},
|
|
176
|
+
"related_to_id": {"type": "string", "title": "Related To ID"},
|
|
177
|
+
"owner_email": {"type": "string", "title": "Owner Email"},
|
|
178
|
+
"due_date": {"type": "datetime", "title": "Due Date", "format": "date-time"},
|
|
179
|
+
"completed_at": {"type": "datetime", "title": "Completed At", "format": "date-time"},
|
|
180
|
+
"is_completed": {"type": "boolean", "default": False, "title": "Is Completed"},
|
|
181
|
+
"phone_number": {"type": "string", "title": "Phone Number"},
|
|
182
|
+
"duration_seconds": {"type": "integer", "title": "Duration (seconds)"},
|
|
183
|
+
"call_outcome": {"type": "string", "title": "Call Outcome"},
|
|
184
|
+
},
|
|
185
|
+
"custom_code": "import datetime as _dt\n\nfrom amsdal.contrib.auth.models.user import User\n\n\n@property\ndef display_name(self) -> str:\n \"\"\"Return display name for the activity.\"\"\"\n return f'{self.activity_type.value}: {self.subject}'\n\nasync def apre_create(self) -> None:\n self.created_at = _dt.datetime.now(tz=_dt.UTC)\n await super().apre_create()\n\nasync def apre_update(self) -> None:\n self.updated_at = _dt.datetime.now(tz=_dt.UTC)\n if not self.created_at:\n _metadata = await self.aget_metadata()\n self.created_at = _dt.datetime.fromtimestamp(_metadata.created_at / 1000, tz=_dt.UTC)\n await super().apre_update()\n\ndef has_object_permission(self, user: 'User', action: str) -> bool:\n \"\"\"Check if user has permission to perform action on this activity.\n\n Args:\n user: The user attempting the action\n action: The action being attempted (read, create, update, delete)\n\n Returns:\n True if user has permission, False otherwise\n \"\"\"\n if self.owner_email == user.email:\n return True\n if user.permissions:\n for permission in user.permissions:\n if permission.model == '*' and permission.action in ('*', action):\n return True\n if permission.model == 'Activity' and permission.action in ('*', action):\n return True\n return False\n\ndef pre_create(self) -> None:\n self.created_at = _dt.datetime.now(tz=_dt.UTC)\n super().pre_create()\n\ndef pre_update(self) -> None:\n self.updated_at = _dt.datetime.now(tz=_dt.UTC)\n if not self.created_at:\n _metadata = self.get_metadata()\n self.created_at = _dt.datetime.fromtimestamp(_metadata.created_at / 1000, tz=_dt.UTC)\n super().pre_update()",
|
|
186
|
+
"storage_metadata": {
|
|
187
|
+
"table_name": "Call",
|
|
188
|
+
"db_fields": {},
|
|
189
|
+
"primary_key": ["partition_key"],
|
|
190
|
+
"indexed": [["related_to_id"], ["owner_email"], ["created_at"], ["due_date"]],
|
|
191
|
+
"foreign_keys": {},
|
|
192
|
+
},
|
|
193
|
+
"description": "Phone call activity.",
|
|
194
|
+
},
|
|
195
|
+
),
|
|
196
|
+
migrations.CreateClass(
|
|
197
|
+
module_type=ModuleType.CONTRIB,
|
|
198
|
+
class_name="EmailActivity",
|
|
199
|
+
new_schema={
|
|
200
|
+
"title": "EmailActivity",
|
|
201
|
+
"type": "Activity",
|
|
202
|
+
"required": [
|
|
203
|
+
"subject",
|
|
204
|
+
"related_to_type",
|
|
205
|
+
"related_to_id",
|
|
206
|
+
"owner_email",
|
|
207
|
+
"from_address",
|
|
208
|
+
"to_addresses",
|
|
209
|
+
"body",
|
|
210
|
+
],
|
|
211
|
+
"properties": {
|
|
212
|
+
"created_at": {"type": "datetime", "title": "Created At", "format": "date-time"},
|
|
213
|
+
"updated_at": {"type": "datetime", "title": "Updated At", "format": "date-time"},
|
|
214
|
+
"activity_type": {"type": "string", "default": "email", "title": "Activity Type", "const": "email"},
|
|
215
|
+
"subject": {"type": "string", "title": "Subject"},
|
|
216
|
+
"description": {"type": "string", "title": "Description"},
|
|
217
|
+
"related_to_type": {
|
|
218
|
+
"type": "ActivityRelatedTo",
|
|
219
|
+
"options": [
|
|
220
|
+
{"key": "CONTACT", "value": "Contact"},
|
|
221
|
+
{"key": "ACCOUNT", "value": "Account"},
|
|
222
|
+
{"key": "DEAL", "value": "Deal"},
|
|
223
|
+
],
|
|
224
|
+
"title": "ActivityRelatedTo",
|
|
225
|
+
"description": "What type of record this activity is related to.",
|
|
226
|
+
"enum": ["Contact", "Account", "Deal"],
|
|
227
|
+
"x_enum_names": ["CONTACT", "ACCOUNT", "DEAL"],
|
|
228
|
+
},
|
|
229
|
+
"related_to_id": {"type": "string", "title": "Related To ID"},
|
|
230
|
+
"owner_email": {"type": "string", "title": "Owner Email"},
|
|
231
|
+
"due_date": {"type": "datetime", "title": "Due Date", "format": "date-time"},
|
|
232
|
+
"completed_at": {"type": "datetime", "title": "Completed At", "format": "date-time"},
|
|
233
|
+
"is_completed": {"type": "boolean", "default": False, "title": "Is Completed"},
|
|
234
|
+
"from_address": {"type": "string", "title": "From Address"},
|
|
235
|
+
"to_addresses": {"type": "array", "items": {"type": "string"}, "title": "To Addresses"},
|
|
236
|
+
"cc_addresses": {"type": "array", "items": {"type": "string"}, "title": "CC Addresses"},
|
|
237
|
+
"body": {"type": "string", "title": "Email Body"},
|
|
238
|
+
"is_outbound": {"type": "boolean", "default": True, "title": "Is Outbound"},
|
|
239
|
+
},
|
|
240
|
+
"custom_code": "import datetime as _dt\n\nfrom amsdal.contrib.auth.models.user import User\n\n\n@property\ndef display_name(self) -> str:\n \"\"\"Return display name for the activity.\"\"\"\n return f'{self.activity_type.value}: {self.subject}'\n\nasync def apre_create(self) -> None:\n self.created_at = _dt.datetime.now(tz=_dt.UTC)\n await super().apre_create()\n\nasync def apre_update(self) -> None:\n self.updated_at = _dt.datetime.now(tz=_dt.UTC)\n if not self.created_at:\n _metadata = await self.aget_metadata()\n self.created_at = _dt.datetime.fromtimestamp(_metadata.created_at / 1000, tz=_dt.UTC)\n await super().apre_update()\n\ndef has_object_permission(self, user: 'User', action: str) -> bool:\n \"\"\"Check if user has permission to perform action on this activity.\n\n Args:\n user: The user attempting the action\n action: The action being attempted (read, create, update, delete)\n\n Returns:\n True if user has permission, False otherwise\n \"\"\"\n if self.owner_email == user.email:\n return True\n if user.permissions:\n for permission in user.permissions:\n if permission.model == '*' and permission.action in ('*', action):\n return True\n if permission.model == 'Activity' and permission.action in ('*', action):\n return True\n return False\n\ndef pre_create(self) -> None:\n self.created_at = _dt.datetime.now(tz=_dt.UTC)\n super().pre_create()\n\ndef pre_update(self) -> None:\n self.updated_at = _dt.datetime.now(tz=_dt.UTC)\n if not self.created_at:\n _metadata = self.get_metadata()\n self.created_at = _dt.datetime.fromtimestamp(_metadata.created_at / 1000, tz=_dt.UTC)\n super().pre_update()",
|
|
241
|
+
"storage_metadata": {
|
|
242
|
+
"table_name": "EmailActivity",
|
|
243
|
+
"db_fields": {},
|
|
244
|
+
"primary_key": ["partition_key"],
|
|
245
|
+
"indexed": [["related_to_id"], ["owner_email"], ["created_at"], ["due_date"]],
|
|
246
|
+
"foreign_keys": {},
|
|
247
|
+
},
|
|
248
|
+
"description": "Email activity with sender/recipients.",
|
|
249
|
+
},
|
|
250
|
+
),
|
|
251
|
+
migrations.CreateClass(
|
|
252
|
+
module_type=ModuleType.CONTRIB,
|
|
253
|
+
class_name="Event",
|
|
254
|
+
new_schema={
|
|
255
|
+
"title": "Event",
|
|
256
|
+
"type": "Activity",
|
|
257
|
+
"required": ["subject", "related_to_type", "related_to_id", "owner_email", "start_time", "end_time"],
|
|
258
|
+
"properties": {
|
|
259
|
+
"created_at": {"type": "datetime", "title": "Created At", "format": "date-time"},
|
|
260
|
+
"updated_at": {"type": "datetime", "title": "Updated At", "format": "date-time"},
|
|
261
|
+
"activity_type": {"type": "string", "default": "event", "title": "Activity Type", "const": "event"},
|
|
262
|
+
"subject": {"type": "string", "title": "Subject"},
|
|
263
|
+
"description": {"type": "string", "title": "Description"},
|
|
264
|
+
"related_to_type": {
|
|
265
|
+
"type": "ActivityRelatedTo",
|
|
266
|
+
"options": [
|
|
267
|
+
{"key": "CONTACT", "value": "Contact"},
|
|
268
|
+
{"key": "ACCOUNT", "value": "Account"},
|
|
269
|
+
{"key": "DEAL", "value": "Deal"},
|
|
270
|
+
],
|
|
271
|
+
"title": "ActivityRelatedTo",
|
|
272
|
+
"description": "What type of record this activity is related to.",
|
|
273
|
+
"enum": ["Contact", "Account", "Deal"],
|
|
274
|
+
"x_enum_names": ["CONTACT", "ACCOUNT", "DEAL"],
|
|
275
|
+
},
|
|
276
|
+
"related_to_id": {"type": "string", "title": "Related To ID"},
|
|
277
|
+
"owner_email": {"type": "string", "title": "Owner Email"},
|
|
278
|
+
"due_date": {"type": "datetime", "title": "Due Date", "format": "date-time"},
|
|
279
|
+
"completed_at": {"type": "datetime", "title": "Completed At", "format": "date-time"},
|
|
280
|
+
"is_completed": {"type": "boolean", "default": False, "title": "Is Completed"},
|
|
281
|
+
"start_time": {"type": "datetime", "title": "Start Time", "format": "date-time"},
|
|
282
|
+
"end_time": {"type": "datetime", "title": "End Time", "format": "date-time"},
|
|
283
|
+
"location": {"type": "string", "title": "Location"},
|
|
284
|
+
},
|
|
285
|
+
"custom_code": "import datetime as _dt\n\nfrom amsdal.contrib.auth.models.user import User\n\n\n@property\ndef display_name(self) -> str:\n \"\"\"Return display name for the activity.\"\"\"\n return f'{self.activity_type.value}: {self.subject}'\n\nasync def apre_create(self) -> None:\n self.created_at = _dt.datetime.now(tz=_dt.UTC)\n await super().apre_create()\n\nasync def apre_update(self) -> None:\n self.updated_at = _dt.datetime.now(tz=_dt.UTC)\n if not self.created_at:\n _metadata = await self.aget_metadata()\n self.created_at = _dt.datetime.fromtimestamp(_metadata.created_at / 1000, tz=_dt.UTC)\n await super().apre_update()\n\ndef has_object_permission(self, user: 'User', action: str) -> bool:\n \"\"\"Check if user has permission to perform action on this activity.\n\n Args:\n user: The user attempting the action\n action: The action being attempted (read, create, update, delete)\n\n Returns:\n True if user has permission, False otherwise\n \"\"\"\n if self.owner_email == user.email:\n return True\n if user.permissions:\n for permission in user.permissions:\n if permission.model == '*' and permission.action in ('*', action):\n return True\n if permission.model == 'Activity' and permission.action in ('*', action):\n return True\n return False\n\ndef pre_create(self) -> None:\n self.created_at = _dt.datetime.now(tz=_dt.UTC)\n super().pre_create()\n\ndef pre_update(self) -> None:\n self.updated_at = _dt.datetime.now(tz=_dt.UTC)\n if not self.created_at:\n _metadata = self.get_metadata()\n self.created_at = _dt.datetime.fromtimestamp(_metadata.created_at / 1000, tz=_dt.UTC)\n super().pre_update()",
|
|
286
|
+
"storage_metadata": {
|
|
287
|
+
"table_name": "Event",
|
|
288
|
+
"db_fields": {},
|
|
289
|
+
"primary_key": ["partition_key"],
|
|
290
|
+
"indexed": [["related_to_id"], ["owner_email"], ["created_at"], ["due_date"]],
|
|
291
|
+
"foreign_keys": {},
|
|
292
|
+
},
|
|
293
|
+
"description": "Event/meeting activity with start/end times.",
|
|
294
|
+
},
|
|
295
|
+
),
|
|
296
|
+
migrations.CreateClass(
|
|
297
|
+
module_type=ModuleType.CONTRIB,
|
|
298
|
+
class_name="Note",
|
|
299
|
+
new_schema={
|
|
300
|
+
"title": "Note",
|
|
301
|
+
"type": "Activity",
|
|
302
|
+
"required": ["subject", "related_to_type", "related_to_id", "owner_email"],
|
|
303
|
+
"properties": {
|
|
304
|
+
"created_at": {"type": "datetime", "title": "Created At", "format": "date-time"},
|
|
305
|
+
"updated_at": {"type": "datetime", "title": "Updated At", "format": "date-time"},
|
|
306
|
+
"activity_type": {"type": "string", "default": "note", "title": "Activity Type", "const": "note"},
|
|
307
|
+
"subject": {"type": "string", "title": "Subject"},
|
|
308
|
+
"description": {"type": "string", "title": "Description"},
|
|
309
|
+
"related_to_type": {
|
|
310
|
+
"type": "ActivityRelatedTo",
|
|
311
|
+
"options": [
|
|
312
|
+
{"key": "CONTACT", "value": "Contact"},
|
|
313
|
+
{"key": "ACCOUNT", "value": "Account"},
|
|
314
|
+
{"key": "DEAL", "value": "Deal"},
|
|
315
|
+
],
|
|
316
|
+
"title": "ActivityRelatedTo",
|
|
317
|
+
"description": "What type of record this activity is related to.",
|
|
318
|
+
"enum": ["Contact", "Account", "Deal"],
|
|
319
|
+
"x_enum_names": ["CONTACT", "ACCOUNT", "DEAL"],
|
|
320
|
+
},
|
|
321
|
+
"related_to_id": {"type": "string", "title": "Related To ID"},
|
|
322
|
+
"owner_email": {"type": "string", "title": "Owner Email"},
|
|
323
|
+
"due_date": {"type": "datetime", "title": "Due Date", "format": "date-time"},
|
|
324
|
+
"completed_at": {"type": "datetime", "title": "Completed At", "format": "date-time"},
|
|
325
|
+
"is_completed": {"type": "boolean", "default": False, "title": "Is Completed"},
|
|
326
|
+
},
|
|
327
|
+
"custom_code": "import datetime as _dt\n\nfrom amsdal.contrib.auth.models.user import User\n\n\n@property\ndef display_name(self) -> str:\n \"\"\"Return display name for the activity.\"\"\"\n return f'{self.activity_type.value}: {self.subject}'\n\nasync def apre_create(self) -> None:\n self.created_at = _dt.datetime.now(tz=_dt.UTC)\n await super().apre_create()\n\nasync def apre_update(self) -> None:\n self.updated_at = _dt.datetime.now(tz=_dt.UTC)\n if not self.created_at:\n _metadata = await self.aget_metadata()\n self.created_at = _dt.datetime.fromtimestamp(_metadata.created_at / 1000, tz=_dt.UTC)\n await super().apre_update()\n\ndef has_object_permission(self, user: 'User', action: str) -> bool:\n \"\"\"Check if user has permission to perform action on this activity.\n\n Args:\n user: The user attempting the action\n action: The action being attempted (read, create, update, delete)\n\n Returns:\n True if user has permission, False otherwise\n \"\"\"\n if self.owner_email == user.email:\n return True\n if user.permissions:\n for permission in user.permissions:\n if permission.model == '*' and permission.action in ('*', action):\n return True\n if permission.model == 'Activity' and permission.action in ('*', action):\n return True\n return False\n\ndef pre_create(self) -> None:\n self.created_at = _dt.datetime.now(tz=_dt.UTC)\n super().pre_create()\n\ndef pre_update(self) -> None:\n self.updated_at = _dt.datetime.now(tz=_dt.UTC)\n if not self.created_at:\n _metadata = self.get_metadata()\n self.created_at = _dt.datetime.fromtimestamp(_metadata.created_at / 1000, tz=_dt.UTC)\n super().pre_update()",
|
|
328
|
+
"storage_metadata": {
|
|
329
|
+
"table_name": "Note",
|
|
330
|
+
"db_fields": {},
|
|
331
|
+
"primary_key": ["partition_key"],
|
|
332
|
+
"indexed": [["related_to_id"], ["owner_email"], ["created_at"], ["due_date"]],
|
|
333
|
+
"foreign_keys": {},
|
|
334
|
+
},
|
|
335
|
+
"description": "Simple note activity.",
|
|
336
|
+
},
|
|
337
|
+
),
|
|
338
|
+
migrations.CreateClass(
|
|
339
|
+
module_type=ModuleType.CONTRIB,
|
|
340
|
+
class_name="Task",
|
|
341
|
+
new_schema={
|
|
342
|
+
"title": "Task",
|
|
343
|
+
"type": "Activity",
|
|
344
|
+
"required": ["subject", "related_to_type", "related_to_id", "owner_email"],
|
|
345
|
+
"properties": {
|
|
346
|
+
"created_at": {"type": "datetime", "title": "Created At", "format": "date-time"},
|
|
347
|
+
"updated_at": {"type": "datetime", "title": "Updated At", "format": "date-time"},
|
|
348
|
+
"activity_type": {"type": "string", "default": "task", "title": "Activity Type", "const": "task"},
|
|
349
|
+
"subject": {"type": "string", "title": "Subject"},
|
|
350
|
+
"description": {"type": "string", "title": "Description"},
|
|
351
|
+
"related_to_type": {
|
|
352
|
+
"type": "ActivityRelatedTo",
|
|
353
|
+
"options": [
|
|
354
|
+
{"key": "CONTACT", "value": "Contact"},
|
|
355
|
+
{"key": "ACCOUNT", "value": "Account"},
|
|
356
|
+
{"key": "DEAL", "value": "Deal"},
|
|
357
|
+
],
|
|
358
|
+
"title": "ActivityRelatedTo",
|
|
359
|
+
"description": "What type of record this activity is related to.",
|
|
360
|
+
"enum": ["Contact", "Account", "Deal"],
|
|
361
|
+
"x_enum_names": ["CONTACT", "ACCOUNT", "DEAL"],
|
|
362
|
+
},
|
|
363
|
+
"related_to_id": {"type": "string", "title": "Related To ID"},
|
|
364
|
+
"owner_email": {"type": "string", "title": "Owner Email"},
|
|
365
|
+
"due_date": {"type": "datetime", "title": "Due Date", "format": "date-time"},
|
|
366
|
+
"completed_at": {"type": "datetime", "title": "Completed At", "format": "date-time"},
|
|
367
|
+
"is_completed": {"type": "boolean", "default": False, "title": "Is Completed"},
|
|
368
|
+
"priority": {
|
|
369
|
+
"type": "string",
|
|
370
|
+
"default": "medium",
|
|
371
|
+
"options": [
|
|
372
|
+
{"key": "low", "value": "low"},
|
|
373
|
+
{"key": "medium", "value": "medium"},
|
|
374
|
+
{"key": "high", "value": "high"},
|
|
375
|
+
],
|
|
376
|
+
"title": "Priority",
|
|
377
|
+
"enum": ["low", "medium", "high"],
|
|
378
|
+
},
|
|
379
|
+
"status": {
|
|
380
|
+
"type": "string",
|
|
381
|
+
"default": "not_started",
|
|
382
|
+
"options": [
|
|
383
|
+
{"key": "not_started", "value": "not_started"},
|
|
384
|
+
{"key": "in_progress", "value": "in_progress"},
|
|
385
|
+
{"key": "waiting", "value": "waiting"},
|
|
386
|
+
{"key": "completed", "value": "completed"},
|
|
387
|
+
],
|
|
388
|
+
"title": "Status",
|
|
389
|
+
"enum": ["not_started", "in_progress", "waiting", "completed"],
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
"custom_code": "import datetime as _dt\n\nfrom amsdal.contrib.auth.models.user import User\n\n\n@property\ndef display_name(self) -> str:\n \"\"\"Return display name for the activity.\"\"\"\n return f'{self.activity_type.value}: {self.subject}'\n\nasync def apre_create(self) -> None:\n self.created_at = _dt.datetime.now(tz=_dt.UTC)\n await super().apre_create()\n\nasync def apre_update(self) -> None:\n self.updated_at = _dt.datetime.now(tz=_dt.UTC)\n if not self.created_at:\n _metadata = await self.aget_metadata()\n self.created_at = _dt.datetime.fromtimestamp(_metadata.created_at / 1000, tz=_dt.UTC)\n await super().apre_update()\n\ndef has_object_permission(self, user: 'User', action: str) -> bool:\n \"\"\"Check if user has permission to perform action on this activity.\n\n Args:\n user: The user attempting the action\n action: The action being attempted (read, create, update, delete)\n\n Returns:\n True if user has permission, False otherwise\n \"\"\"\n if self.owner_email == user.email:\n return True\n if user.permissions:\n for permission in user.permissions:\n if permission.model == '*' and permission.action in ('*', action):\n return True\n if permission.model == 'Activity' and permission.action in ('*', action):\n return True\n return False\n\ndef pre_create(self) -> None:\n self.created_at = _dt.datetime.now(tz=_dt.UTC)\n super().pre_create()\n\ndef pre_update(self) -> None:\n self.updated_at = _dt.datetime.now(tz=_dt.UTC)\n if not self.created_at:\n _metadata = self.get_metadata()\n self.created_at = _dt.datetime.fromtimestamp(_metadata.created_at / 1000, tz=_dt.UTC)\n super().pre_update()",
|
|
393
|
+
"storage_metadata": {
|
|
394
|
+
"table_name": "Task",
|
|
395
|
+
"db_fields": {},
|
|
396
|
+
"primary_key": ["partition_key"],
|
|
397
|
+
"indexed": [["related_to_id"], ["owner_email"], ["created_at"], ["due_date"]],
|
|
398
|
+
"foreign_keys": {},
|
|
399
|
+
},
|
|
400
|
+
"description": "Task activity with priority and status.",
|
|
401
|
+
},
|
|
402
|
+
),
|
|
403
|
+
migrations.CreateClass(
|
|
404
|
+
module_type=ModuleType.CONTRIB,
|
|
405
|
+
class_name="Stage",
|
|
406
|
+
new_schema={
|
|
407
|
+
"title": "Stage",
|
|
408
|
+
"required": ["pipeline", "name", "order"],
|
|
409
|
+
"properties": {
|
|
410
|
+
"pipeline": {
|
|
411
|
+
"type": "Pipeline",
|
|
412
|
+
"title": "Pipeline",
|
|
413
|
+
"description": "Sales pipeline model.\n\nRepresents a sales pipeline with multiple stages.\nPipelines are system-wide and not owned by individual users.",
|
|
414
|
+
},
|
|
415
|
+
"name": {"type": "string", "title": "Stage Name"},
|
|
416
|
+
"order": {"type": "integer", "title": "Order"},
|
|
417
|
+
"probability": {
|
|
418
|
+
"type": "number",
|
|
419
|
+
"default": 0.0,
|
|
420
|
+
"title": "Win Probability (%)",
|
|
421
|
+
"maximum": 100,
|
|
422
|
+
"minimum": 0,
|
|
423
|
+
},
|
|
424
|
+
"is_closed_won": {"type": "boolean", "default": False, "title": "Is Closed Won"},
|
|
425
|
+
"is_closed_lost": {"type": "boolean", "default": False, "title": "Is Closed Lost"},
|
|
426
|
+
},
|
|
427
|
+
"custom_code": '@property\ndef display_name(self) -> str:\n """Return display name for the stage."""\n if isinstance(self.pipeline, str):\n return f\'{self.pipeline} - {self.name}\'\n return f\'{self.pipeline.display_name} - {self.name}\'',
|
|
428
|
+
"storage_metadata": {
|
|
429
|
+
"table_name": "Stage",
|
|
430
|
+
"db_fields": {"pipeline": ["pipeline_partition_key"]},
|
|
431
|
+
"primary_key": ["partition_key"],
|
|
432
|
+
"indexed": [["order"]],
|
|
433
|
+
"unique": [["pipeline", "name"]],
|
|
434
|
+
"foreign_keys": {"pipeline": [{"pipeline_partition_key": "string"}, "Pipeline", ["partition_key"]]},
|
|
435
|
+
},
|
|
436
|
+
"description": "Pipeline stage model.\n\nRepresents a stage within a sales pipeline with win probability\nand closed status indicators.",
|
|
437
|
+
},
|
|
438
|
+
),
|
|
439
|
+
migrations.CreateClass(
|
|
440
|
+
module_type=ModuleType.CONTRIB,
|
|
441
|
+
class_name="Deal",
|
|
442
|
+
new_schema={
|
|
443
|
+
"title": "Deal",
|
|
444
|
+
"required": ["name", "stage", "owner_email"],
|
|
445
|
+
"properties": {
|
|
446
|
+
"created_at": {"type": "datetime", "title": "Created At", "format": "date-time"},
|
|
447
|
+
"updated_at": {"type": "datetime", "title": "Updated At", "format": "date-time"},
|
|
448
|
+
"name": {"type": "string", "title": "Deal Name"},
|
|
449
|
+
"amount": {"type": "number", "title": "Amount"},
|
|
450
|
+
"currency": {"type": "string", "default": "USD", "title": "Currency"},
|
|
451
|
+
"account": {
|
|
452
|
+
"type": "Account",
|
|
453
|
+
"title": "Account",
|
|
454
|
+
"description": "Account (Company/Organization) model.\n\nRepresents a company or organization in the CRM system.\nOwned by individual users with permission controls.",
|
|
455
|
+
},
|
|
456
|
+
"contact": {
|
|
457
|
+
"type": "Contact",
|
|
458
|
+
"title": "Primary Contact",
|
|
459
|
+
"description": "Contact (Person) model.\n\nRepresents a person in the CRM system, optionally linked to an Account.\nOwned by individual users with permission controls.",
|
|
460
|
+
},
|
|
461
|
+
"stage": {
|
|
462
|
+
"type": "Stage",
|
|
463
|
+
"title": "Stage",
|
|
464
|
+
"description": "Pipeline stage model.\n\nRepresents a stage within a sales pipeline with win probability\nand closed status indicators.",
|
|
465
|
+
},
|
|
466
|
+
"owner_email": {"type": "string", "title": "Owner Email"},
|
|
467
|
+
"expected_close_date": {"type": "datetime", "title": "Expected Close Date", "format": "date-time"},
|
|
468
|
+
"closed_date": {"type": "datetime", "title": "Closed Date", "format": "date-time"},
|
|
469
|
+
"is_closed": {"type": "boolean", "default": False, "title": "Is Closed"},
|
|
470
|
+
"is_won": {"type": "boolean", "default": False, "title": "Is Won"},
|
|
471
|
+
"custom_fields": {"type": "anything", "title": "Custom Fields"},
|
|
472
|
+
},
|
|
473
|
+
"custom_code": 'import datetime as _dt\n\nfrom amsdal.contrib.auth.models.user import User\nfrom amsdal_utils.models.data_models.reference import Reference\n\n\n@property\ndef display_name(self) -> str:\n """Return display name for the deal."""\n return self.name\n\n@property\ndef stage_name(self) -> str:\n """Returns stage name for display."""\n if hasattr(self.stage, \'name\'):\n return self.stage.name\n return str(self.stage)\n\nasync def apost_update(self) -> None:\n """Async hook called after updating deal."""\n from amsdal_crm.services.workflow_service import WorkflowService\n WorkflowService.execute_rules(\'Deal\', \'update\', self)\n\nasync def apre_create(self) -> None:\n """Async hook called before creating deal."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'Deal\', self.custom_fields)\n await super().apre_create()\n\nasync def apre_update(self) -> None:\n """Async hook called before updating deal.\n\n Automatically syncs is_closed and is_won status with stage,\n and sets closed_date when deal is closed.\n """\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'Deal\', self.custom_fields)\n from amsdal_models.classes.helpers.reference_loader import ReferenceLoader\n stage = await ReferenceLoader(self.stage).aload_reference() if isinstance(self.stage, Reference) else self.stage\n self.is_closed = stage.is_closed_won or stage.is_closed_lost\n self.is_won = stage.is_closed_won\n if self.is_closed and (not self.closed_date):\n self.closed_date = _dt.datetime.now(_dt.UTC)\n await super().apre_update()\n\ndef has_object_permission(self, user: \'User\', action: str) -> bool:\n """Check if user has permission to perform action on this deal.\n\n Args:\n user: The user attempting the action\n action: The action being attempted (read, create, update, delete)\n\n Returns:\n True if user has permission, False otherwise\n """\n if self.owner_email == user.email:\n return True\n if user.permissions:\n for permission in user.permissions:\n if permission.model == \'*\' and permission.action in (\'*\', action):\n return True\n if permission.model == \'Deal\' and permission.action in (\'*\', action):\n return True\n return False\n\ndef post_update(self) -> None:\n """Hook called after updating deal."""\n from amsdal_crm.services.workflow_service import WorkflowService\n WorkflowService.execute_rules(\'Deal\', \'update\', self)\n\ndef pre_create(self) -> None:\n """Hook called before creating deal."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'Deal\', self.custom_fields)\n super().pre_create()\n\ndef pre_update(self) -> None:\n """Hook called before updating deal.\n\n Automatically syncs is_closed and is_won status with stage,\n and sets closed_date when deal is closed.\n """\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'Deal\', self.custom_fields)\n from amsdal_models.classes.helpers.reference_loader import ReferenceLoader\n stage = ReferenceLoader(self.stage).load_reference() if isinstance(self.stage, Reference) else self.stage\n self.is_closed = stage.is_closed_won or stage.is_closed_lost\n self.is_won = stage.is_closed_won\n if self.is_closed and (not self.closed_date):\n self.closed_date = _dt.datetime.now(_dt.UTC)\n super().pre_update()',
|
|
474
|
+
"storage_metadata": {
|
|
475
|
+
"table_name": "Deal",
|
|
476
|
+
"db_fields": {
|
|
477
|
+
"account": ["account_partition_key"],
|
|
478
|
+
"contact": ["contact_partition_key"],
|
|
479
|
+
"stage": ["stage_partition_key"],
|
|
480
|
+
},
|
|
481
|
+
"primary_key": ["partition_key"],
|
|
482
|
+
"indexed": [["owner_email"], ["expected_close_date"], ["created_at"]],
|
|
483
|
+
"foreign_keys": {
|
|
484
|
+
"account": [{"account_partition_key": "string"}, "Account", ["partition_key"]],
|
|
485
|
+
"contact": [{"contact_partition_key": "string"}, "Contact", ["partition_key"]],
|
|
486
|
+
"stage": [{"stage_partition_key": "string"}, "Stage", ["partition_key"]],
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
"description": "Deal (Sales Opportunity) model.\n\nRepresents a sales opportunity linked to an account and contact,\nprogressing through pipeline stages.",
|
|
490
|
+
},
|
|
491
|
+
),
|
|
492
|
+
migrations.CreateClass(
|
|
493
|
+
module_type=ModuleType.CONTRIB,
|
|
494
|
+
class_name="Attachment",
|
|
495
|
+
new_schema={
|
|
496
|
+
"title": "Attachment",
|
|
497
|
+
"required": ["related_to_type", "related_to_id", "uploaded_by", "file"],
|
|
498
|
+
"properties": {
|
|
499
|
+
"related_to_type": {
|
|
500
|
+
"type": "string",
|
|
501
|
+
"options": [
|
|
502
|
+
{"key": "Contact", "value": "Contact"},
|
|
503
|
+
{"key": "Account", "value": "Account"},
|
|
504
|
+
{"key": "Deal", "value": "Deal"},
|
|
505
|
+
{"key": "Activity", "value": "Activity"},
|
|
506
|
+
],
|
|
507
|
+
"title": "Related To Type",
|
|
508
|
+
"enum": ["Contact", "Account", "Deal", "Activity"],
|
|
509
|
+
},
|
|
510
|
+
"related_to_id": {"type": "string", "title": "Related To ID"},
|
|
511
|
+
"uploaded_by": {"type": "string", "title": "Uploaded By (User Email)"},
|
|
512
|
+
"uploaded_at": {"type": "datetime", "title": "Uploaded At", "format": "date-time"},
|
|
513
|
+
"description": {"type": "string", "title": "Description"},
|
|
514
|
+
"file": {"type": "File", "title": "File"},
|
|
515
|
+
},
|
|
516
|
+
"custom_code": '@property\ndef display_name(self) -> str:\n """Return display name for the attachment."""\n if hasattr(self.file, \'filename\'):\n return self.file.filename\n return f\'Attachment {self._object_id}\'',
|
|
517
|
+
"storage_metadata": {
|
|
518
|
+
"table_name": "Attachment",
|
|
519
|
+
"db_fields": {"file": ["file_partition_key"]},
|
|
520
|
+
"primary_key": ["partition_key"],
|
|
521
|
+
"indexed": [["related_to_id"], ["uploaded_at"]],
|
|
522
|
+
"foreign_keys": {"file": [{"file_partition_key": "string"}, "File", ["partition_key"]]},
|
|
523
|
+
},
|
|
524
|
+
"description": "Explicit attachment model for tracking file relationships.\n\nUses polymorphic relationship to link files to Contacts, Accounts,\nDeals, or Activities.",
|
|
525
|
+
},
|
|
526
|
+
),
|
|
527
|
+
migrations.CreateClass(
|
|
528
|
+
module_type=ModuleType.CONTRIB,
|
|
529
|
+
class_name="CustomFieldDefinition",
|
|
530
|
+
new_schema={
|
|
531
|
+
"title": "CustomFieldDefinition",
|
|
532
|
+
"required": ["entity_type", "field_name", "field_label", "field_type"],
|
|
533
|
+
"properties": {
|
|
534
|
+
"entity_type": {
|
|
535
|
+
"type": "string",
|
|
536
|
+
"options": [
|
|
537
|
+
{"key": "Contact", "value": "Contact"},
|
|
538
|
+
{"key": "Account", "value": "Account"},
|
|
539
|
+
{"key": "Deal", "value": "Deal"},
|
|
540
|
+
],
|
|
541
|
+
"title": "Entity Type",
|
|
542
|
+
"enum": ["Contact", "Account", "Deal"],
|
|
543
|
+
},
|
|
544
|
+
"field_name": {"type": "string", "title": "Field Name"},
|
|
545
|
+
"field_label": {"type": "string", "title": "Field Label"},
|
|
546
|
+
"field_type": {
|
|
547
|
+
"type": "string",
|
|
548
|
+
"options": [
|
|
549
|
+
{"key": "text", "value": "text"},
|
|
550
|
+
{"key": "number", "value": "number"},
|
|
551
|
+
{"key": "date", "value": "date"},
|
|
552
|
+
{"key": "choice", "value": "choice"},
|
|
553
|
+
],
|
|
554
|
+
"title": "Field Type",
|
|
555
|
+
"enum": ["text", "number", "date", "choice"],
|
|
556
|
+
},
|
|
557
|
+
"choices": {"type": "array", "items": {"type": "string"}, "title": "Choices (for choice type)"},
|
|
558
|
+
"is_required": {"type": "boolean", "default": False, "title": "Is Required"},
|
|
559
|
+
"default_value": {"type": "anything", "title": "Default Value"},
|
|
560
|
+
"help_text": {"type": "string", "title": "Help Text"},
|
|
561
|
+
"display_order": {"type": "integer", "default": 0, "title": "Display Order"},
|
|
562
|
+
},
|
|
563
|
+
"custom_code": '@property\ndef display_name(self) -> str:\n """Return display name for the custom field definition."""\n return f\'{self.entity_type}.{self.field_name}\'',
|
|
564
|
+
"storage_metadata": {
|
|
565
|
+
"table_name": "CustomFieldDefinition",
|
|
566
|
+
"db_fields": {},
|
|
567
|
+
"primary_key": ["partition_key"],
|
|
568
|
+
"unique": [["entity_type", "field_name"]],
|
|
569
|
+
"foreign_keys": {},
|
|
570
|
+
},
|
|
571
|
+
"description": "Metadata about custom fields available for CRM entities.\n\nDefines custom fields that users can add to Contacts, Accounts, or Deals.\nField values are stored in the entity's custom_fields JSON dict.",
|
|
572
|
+
},
|
|
573
|
+
),
|
|
574
|
+
migrations.CreateClass(
|
|
575
|
+
module_type=ModuleType.CONTRIB,
|
|
576
|
+
class_name="WorkflowRule",
|
|
577
|
+
new_schema={
|
|
578
|
+
"title": "WorkflowRule",
|
|
579
|
+
"required": ["name", "entity_type", "trigger_event", "action_type", "action_config"],
|
|
580
|
+
"properties": {
|
|
581
|
+
"name": {"type": "string", "title": "Rule Name"},
|
|
582
|
+
"entity_type": {
|
|
583
|
+
"type": "string",
|
|
584
|
+
"options": [
|
|
585
|
+
{"key": "Contact", "value": "Contact"},
|
|
586
|
+
{"key": "Account", "value": "Account"},
|
|
587
|
+
{"key": "Deal", "value": "Deal"},
|
|
588
|
+
{"key": "Activity", "value": "Activity"},
|
|
589
|
+
],
|
|
590
|
+
"title": "Entity Type",
|
|
591
|
+
"enum": ["Contact", "Account", "Deal", "Activity"],
|
|
592
|
+
},
|
|
593
|
+
"trigger_event": {
|
|
594
|
+
"type": "string",
|
|
595
|
+
"options": [
|
|
596
|
+
{"key": "create", "value": "create"},
|
|
597
|
+
{"key": "update", "value": "update"},
|
|
598
|
+
{"key": "delete", "value": "delete"},
|
|
599
|
+
],
|
|
600
|
+
"title": "Trigger Event",
|
|
601
|
+
"enum": ["create", "update", "delete"],
|
|
602
|
+
},
|
|
603
|
+
"condition_field": {"type": "string", "title": "Condition Field"},
|
|
604
|
+
"condition_operator": {"type": "string", "title": "Condition Operator"},
|
|
605
|
+
"condition_value": {"type": "anything", "title": "Condition Value"},
|
|
606
|
+
"action_type": {
|
|
607
|
+
"type": "string",
|
|
608
|
+
"options": [
|
|
609
|
+
{"key": "update_field", "value": "update_field"},
|
|
610
|
+
{"key": "create_activity", "value": "create_activity"},
|
|
611
|
+
{"key": "send_notification", "value": "send_notification"},
|
|
612
|
+
],
|
|
613
|
+
"title": "Action Type",
|
|
614
|
+
"enum": ["update_field", "create_activity", "send_notification"],
|
|
615
|
+
},
|
|
616
|
+
"action_config": {
|
|
617
|
+
"type": "anything",
|
|
618
|
+
"title": "Action Configuration",
|
|
619
|
+
"additionalProperties": True,
|
|
620
|
+
},
|
|
621
|
+
"is_active": {"type": "boolean", "default": True, "title": "Is Active"},
|
|
622
|
+
},
|
|
623
|
+
"custom_code": '@property\ndef display_name(self) -> str:\n """Return display name for the workflow rule."""\n return f\'{self.entity_type}: {self.name}\'',
|
|
624
|
+
"storage_metadata": {
|
|
625
|
+
"table_name": "WorkflowRule",
|
|
626
|
+
"db_fields": {},
|
|
627
|
+
"primary_key": ["partition_key"],
|
|
628
|
+
"foreign_keys": {},
|
|
629
|
+
},
|
|
630
|
+
"description": "Configuration for workflow automation rules.\n\nDefines rules that trigger actions when certain conditions are met\non CRM entities (create, update, delete events).",
|
|
631
|
+
},
|
|
632
|
+
),
|
|
633
|
+
]
|