amsdal_crm 0.1.9__py3-none-any.whl → 0.2.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 -1
- amsdal_crm/fixtures/permissions.py +5 -10
- amsdal_crm/lifecycle/consumer.py +1 -1
- amsdal_crm/migrations/0000_initial.py +146 -186
- amsdal_crm/models/__init__.py +0 -45
- amsdal_crm/models/activity.py +5 -9
- amsdal_crm/models/attachment.py +1 -1
- amsdal_crm/models/custom_field_definition.py +3 -1
- amsdal_crm/models/deal.py +25 -17
- amsdal_crm/models/entity.py +318 -0
- amsdal_crm/models/stage.py +4 -2
- amsdal_crm/models/workflow_rule.py +1 -1
- amsdal_crm/services/deal_service.py +4 -6
- amsdal_crm/services/email_service.py +2 -8
- amsdal_crm/services/workflow_service.py +4 -4
- {amsdal_crm-0.1.9.dist-info → amsdal_crm-0.2.0.dist-info}/METADATA +1 -1
- amsdal_crm-0.2.0.dist-info/RECORD +31 -0
- amsdal_crm/models/account.py +0 -127
- amsdal_crm/models/contact.py +0 -132
- amsdal_crm-0.1.9.dist-info/RECORD +0 -32
- {amsdal_crm-0.1.9.dist-info → amsdal_crm-0.2.0.dist-info}/WHEEL +0 -0
amsdal_crm/models/__init__.py
CHANGED
|
@@ -1,46 +1 @@
|
|
|
1
1
|
"""CRM Models."""
|
|
2
|
-
|
|
3
|
-
# Import File first for Attachment's forward reference
|
|
4
|
-
from amsdal.models.core.file import File # noqa: F401
|
|
5
|
-
|
|
6
|
-
from amsdal_crm.models.account import Account
|
|
7
|
-
from amsdal_crm.models.activity import Activity
|
|
8
|
-
from amsdal_crm.models.activity import ActivityRelatedTo
|
|
9
|
-
from amsdal_crm.models.activity import ActivityType
|
|
10
|
-
from amsdal_crm.models.activity import Call
|
|
11
|
-
from amsdal_crm.models.activity import EmailActivity
|
|
12
|
-
from amsdal_crm.models.activity import Event
|
|
13
|
-
from amsdal_crm.models.activity import Note
|
|
14
|
-
from amsdal_crm.models.activity import Task
|
|
15
|
-
from amsdal_crm.models.attachment import Attachment
|
|
16
|
-
from amsdal_crm.models.contact import Contact
|
|
17
|
-
from amsdal_crm.models.custom_field_definition import CustomFieldDefinition
|
|
18
|
-
from amsdal_crm.models.deal import Deal
|
|
19
|
-
from amsdal_crm.models.pipeline import Pipeline
|
|
20
|
-
from amsdal_crm.models.stage import Stage
|
|
21
|
-
from amsdal_crm.models.workflow_rule import WorkflowRule
|
|
22
|
-
|
|
23
|
-
__all__ = [
|
|
24
|
-
'Account',
|
|
25
|
-
'Activity',
|
|
26
|
-
'ActivityRelatedTo',
|
|
27
|
-
'ActivityType',
|
|
28
|
-
'Attachment',
|
|
29
|
-
'Call',
|
|
30
|
-
'Contact',
|
|
31
|
-
'CustomFieldDefinition',
|
|
32
|
-
'Deal',
|
|
33
|
-
'EmailActivity',
|
|
34
|
-
'Event',
|
|
35
|
-
'Note',
|
|
36
|
-
'Pipeline',
|
|
37
|
-
'Stage',
|
|
38
|
-
'Task',
|
|
39
|
-
'WorkflowRule',
|
|
40
|
-
]
|
|
41
|
-
|
|
42
|
-
# Rebuild models to resolve forward references
|
|
43
|
-
Contact.model_rebuild()
|
|
44
|
-
Deal.model_rebuild()
|
|
45
|
-
Stage.model_rebuild()
|
|
46
|
-
Attachment.model_rebuild()
|
amsdal_crm/models/activity.py
CHANGED
|
@@ -26,8 +26,7 @@ class ActivityType(str, Enum):
|
|
|
26
26
|
class ActivityRelatedTo(str, Enum):
|
|
27
27
|
"""What type of record this activity is related to."""
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
ACCOUNT = 'Account'
|
|
29
|
+
ENTITY = 'Entity'
|
|
31
30
|
DEAL = 'Deal'
|
|
32
31
|
|
|
33
32
|
|
|
@@ -41,7 +40,6 @@ class Activity(TimestampMixin, Model):
|
|
|
41
40
|
__module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
|
|
42
41
|
__indexes__: ClassVar[list[IndexInfo]] = [
|
|
43
42
|
IndexInfo(name='idx_activity_related_to', field='related_to_id'),
|
|
44
|
-
IndexInfo(name='idx_activity_owner', field='owner_email'),
|
|
45
43
|
IndexInfo(name='idx_activity_created_at', field='created_at'),
|
|
46
44
|
IndexInfo(name='idx_activity_due_date', field='due_date'),
|
|
47
45
|
]
|
|
@@ -54,12 +52,10 @@ class Activity(TimestampMixin, Model):
|
|
|
54
52
|
description: str | None = Field(default=None, title='Description')
|
|
55
53
|
|
|
56
54
|
# Polymorphic relationship (generic FK pattern)
|
|
57
|
-
related_to_type: ActivityRelatedTo = Field(title='Related To Type')
|
|
58
|
-
related_to_id: str = Field(title='Related To ID')
|
|
59
|
-
|
|
60
|
-
# Owner
|
|
61
|
-
owner_email: str = Field(title='Owner Email')
|
|
55
|
+
related_to_type: ActivityRelatedTo | None = Field(title='Related To Type')
|
|
56
|
+
related_to_id: str | None = Field(title='Related To ID')
|
|
62
57
|
|
|
58
|
+
assigned_to: User | None = Field(default=None, title='Assigned To')
|
|
63
59
|
# Timing
|
|
64
60
|
due_date: datetime | None = Field(default=None, title='Due Date')
|
|
65
61
|
completed_at: datetime | None = Field(default=None, title='Completed At')
|
|
@@ -83,7 +79,7 @@ class Activity(TimestampMixin, Model):
|
|
|
83
79
|
True if user has permission, False otherwise
|
|
84
80
|
"""
|
|
85
81
|
# Owner has all permissions
|
|
86
|
-
if self.
|
|
82
|
+
if self.assigned_to and self.assigned_to.email == user.email:
|
|
87
83
|
return True
|
|
88
84
|
|
|
89
85
|
# Check admin permissions
|
amsdal_crm/models/attachment.py
CHANGED
|
@@ -27,7 +27,7 @@ class Attachment(Model):
|
|
|
27
27
|
file: File = Field(title='File')
|
|
28
28
|
|
|
29
29
|
# Polymorphic relationship
|
|
30
|
-
related_to_type: Literal['
|
|
30
|
+
related_to_type: Literal['Entity', 'Deal', 'Activity'] = Field(title='Related To Type')
|
|
31
31
|
related_to_id: str = Field(title='Related To ID')
|
|
32
32
|
|
|
33
33
|
# Metadata
|
|
@@ -22,7 +22,9 @@ class CustomFieldDefinition(Model):
|
|
|
22
22
|
UniqueConstraint(name='unq_custom_field_entity_name', fields=['entity_type', 'field_name'])
|
|
23
23
|
]
|
|
24
24
|
|
|
25
|
-
entity_type: Literal[
|
|
25
|
+
entity_type: Literal[
|
|
26
|
+
'Entity', 'EntityRelationship', 'Deal', 'EntityIdentifier', 'EntityContactPoint', 'EntityAddress'
|
|
27
|
+
] = Field(title='Entity Type')
|
|
26
28
|
field_name: str = Field(title='Field Name')
|
|
27
29
|
field_label: str = Field(title='Field Label')
|
|
28
30
|
field_type: Literal['text', 'number', 'date', 'choice'] = Field(title='Field Type')
|
amsdal_crm/models/deal.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import datetime as _dt
|
|
4
4
|
from typing import Any
|
|
5
5
|
from typing import ClassVar
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import Literal
|
|
7
7
|
|
|
8
8
|
from amsdal.contrib.auth.models.user import User
|
|
9
9
|
from amsdal.models.mixins import TimestampMixin
|
|
@@ -29,7 +29,6 @@ class Deal(TimestampMixin, Model):
|
|
|
29
29
|
|
|
30
30
|
__module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
|
|
31
31
|
__indexes__: ClassVar[list[IndexInfo]] = [
|
|
32
|
-
IndexInfo(name='idx_deal_owner_email', field='owner_email'),
|
|
33
32
|
IndexInfo(name='idx_deal_close_date', field='expected_close_date'),
|
|
34
33
|
IndexInfo(name='idx_deal_created_at', field='created_at'),
|
|
35
34
|
]
|
|
@@ -40,18 +39,16 @@ class Deal(TimestampMixin, Model):
|
|
|
40
39
|
currency: str = Field(default='USD', title='Currency')
|
|
41
40
|
|
|
42
41
|
# Relationships
|
|
43
|
-
|
|
44
|
-
contact: Optional['Contact'] = Field(default=None, title='Primary Contact')
|
|
42
|
+
entity: 'Entity' = Field(title='Entity')
|
|
45
43
|
stage: 'Stage' = Field(title='Stage')
|
|
46
|
-
|
|
44
|
+
assigned_to: User | None = Field(default=None, title='Assigned To')
|
|
47
45
|
|
|
48
46
|
# Dates
|
|
49
47
|
expected_close_date: _dt.datetime | None = Field(default=None, title='Expected Close Date')
|
|
50
48
|
closed_date: _dt.datetime | None = Field(default=None, title='Closed Date')
|
|
51
49
|
|
|
52
50
|
# Status tracking
|
|
53
|
-
|
|
54
|
-
is_won: bool = Field(default=False, title='Is Won')
|
|
51
|
+
status: Literal['open', 'closed_won', 'closed_lost'] = Field(default='open', title='Status')
|
|
55
52
|
|
|
56
53
|
# Custom fields (JSON)
|
|
57
54
|
custom_fields: dict[str, Any] | None = Field(default=None, title='Custom Fields')
|
|
@@ -78,8 +75,9 @@ class Deal(TimestampMixin, Model):
|
|
|
78
75
|
Returns:
|
|
79
76
|
True if user has permission, False otherwise
|
|
80
77
|
"""
|
|
78
|
+
|
|
81
79
|
# Owner has all permissions
|
|
82
|
-
if self.
|
|
80
|
+
if self.assigned_to and self.assigned_to.email == user.email:
|
|
83
81
|
return True
|
|
84
82
|
|
|
85
83
|
# Check admin permissions
|
|
@@ -124,10 +122,16 @@ class Deal(TimestampMixin, Model):
|
|
|
124
122
|
from amsdal_models.classes.helpers.reference_loader import ReferenceLoader
|
|
125
123
|
|
|
126
124
|
stage = ReferenceLoader(self.stage).load_reference() if isinstance(self.stage, Reference) else self.stage
|
|
127
|
-
self.is_closed = stage.is_closed_won or stage.is_closed_lost
|
|
128
|
-
self.is_won = stage.is_closed_won
|
|
129
125
|
|
|
130
|
-
if
|
|
126
|
+
if stage.status == 'open':
|
|
127
|
+
self.status = 'open'
|
|
128
|
+
if stage.status == 'closed_won':
|
|
129
|
+
self.status = 'closed_won'
|
|
130
|
+
|
|
131
|
+
if stage.status == 'closed_lost':
|
|
132
|
+
self.status = 'closed_lost'
|
|
133
|
+
|
|
134
|
+
if self.status in ('closed_won', 'closed_lost') and not self.closed_date:
|
|
131
135
|
self.closed_date = _dt.datetime.now(_dt.UTC)
|
|
132
136
|
|
|
133
137
|
# Call parent to handle timestamps
|
|
@@ -148,10 +152,15 @@ class Deal(TimestampMixin, Model):
|
|
|
148
152
|
# Load stage if it's a reference and sync closed status
|
|
149
153
|
|
|
150
154
|
stage = await self.stage
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
if stage.status == 'open':
|
|
156
|
+
self.status = 'open'
|
|
157
|
+
if stage.status == 'closed_won':
|
|
158
|
+
self.status = 'closed_won'
|
|
153
159
|
|
|
154
|
-
if
|
|
160
|
+
if stage.status == 'closed_lost':
|
|
161
|
+
self.status = 'closed_lost'
|
|
162
|
+
|
|
163
|
+
if self.status in ('closed_won', 'closed_lost') and not self.closed_date:
|
|
155
164
|
self.closed_date = _dt.datetime.now(_dt.UTC)
|
|
156
165
|
|
|
157
166
|
# Call parent to handle timestamps
|
|
@@ -167,11 +176,10 @@ class Deal(TimestampMixin, Model):
|
|
|
167
176
|
"""Async hook called after updating deal."""
|
|
168
177
|
from amsdal_crm.services.workflow_service import WorkflowService
|
|
169
178
|
|
|
170
|
-
WorkflowService.
|
|
179
|
+
await WorkflowService.aexecute_rules('Deal', 'update', self)
|
|
171
180
|
|
|
172
181
|
|
|
173
|
-
from amsdal_crm.models.
|
|
174
|
-
from amsdal_crm.models.contact import Contact
|
|
182
|
+
from amsdal_crm.models.entity import Entity
|
|
175
183
|
from amsdal_crm.models.stage import Stage
|
|
176
184
|
|
|
177
185
|
Deal.model_rebuild()
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Account Model."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from amsdal.contrib.auth.models.user import User
|
|
8
|
+
from amsdal.models.mixins import TimestampMixin
|
|
9
|
+
from amsdal_models.classes.data_models.constraints import UniqueConstraint
|
|
10
|
+
from amsdal_models.classes.data_models.indexes import IndexInfo
|
|
11
|
+
from amsdal_models.classes.model import Model
|
|
12
|
+
from amsdal_utils.models.enums import ModuleType
|
|
13
|
+
from pydantic.fields import Field
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Entity(TimestampMixin, Model):
|
|
17
|
+
"""Entity (Person/Organization/Trust) model.
|
|
18
|
+
|
|
19
|
+
Represents a company or organization in the CRM system.
|
|
20
|
+
Owned by individual users with permission controls.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
__module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
|
|
24
|
+
__constraints__: ClassVar[list[UniqueConstraint]] = [UniqueConstraint(name='unq_entity_name', fields=['name'])]
|
|
25
|
+
__indexes__: ClassVar[list[IndexInfo]] = [
|
|
26
|
+
IndexInfo(name='idx_entity_created_at', field='created_at'),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# Core fields
|
|
30
|
+
name: str = Field(title='Entity Name')
|
|
31
|
+
legal_name: str | None = Field(default=None, title='Legal Name')
|
|
32
|
+
status: Literal['Active', 'Inactive'] = Field(default='Active', title='Status')
|
|
33
|
+
note: str | None = Field(default=None, title='Note')
|
|
34
|
+
|
|
35
|
+
assigned_to: User | None = Field(default=None, title='Assigned To')
|
|
36
|
+
|
|
37
|
+
# Custom fields (JSON)
|
|
38
|
+
custom_fields: dict[str, Any] | None = Field(default=None, title='Custom Fields')
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def display_name(self) -> str:
|
|
42
|
+
"""Return display name for the account."""
|
|
43
|
+
return self.name
|
|
44
|
+
|
|
45
|
+
def has_object_permission(self, user: 'User', action: str) -> bool:
|
|
46
|
+
"""Check if user has permission to perform action on this account.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
user: The user attempting the action
|
|
50
|
+
action: The action being attempted (read, create, update, delete)
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
True if user has permission, False otherwise
|
|
54
|
+
"""
|
|
55
|
+
if self.assigned_to and self.assigned_to.email == user.email:
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
# Check admin permissions
|
|
59
|
+
if user.permissions:
|
|
60
|
+
for permission in user.permissions:
|
|
61
|
+
if permission.model == '*' and permission.action in ('*', action):
|
|
62
|
+
return True
|
|
63
|
+
if permission.model == 'Entity' and permission.action in ('*', action):
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
def pre_create(self) -> None:
|
|
69
|
+
"""Hook called before creating account."""
|
|
70
|
+
if self.custom_fields:
|
|
71
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
72
|
+
|
|
73
|
+
self.custom_fields = CustomFieldService.validate_custom_fields('Entity', self.custom_fields)
|
|
74
|
+
super().pre_create()
|
|
75
|
+
|
|
76
|
+
async def apre_create(self) -> None:
|
|
77
|
+
"""Async hook called before creating account."""
|
|
78
|
+
if self.custom_fields:
|
|
79
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
80
|
+
|
|
81
|
+
self.custom_fields = await CustomFieldService.avalidate_custom_fields('Entity', self.custom_fields)
|
|
82
|
+
await super().apre_create()
|
|
83
|
+
|
|
84
|
+
def pre_update(self) -> None:
|
|
85
|
+
"""Hook called before updating account."""
|
|
86
|
+
# Validate custom fields first
|
|
87
|
+
if self.custom_fields:
|
|
88
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
89
|
+
|
|
90
|
+
self.custom_fields = CustomFieldService.validate_custom_fields('Entity', self.custom_fields)
|
|
91
|
+
|
|
92
|
+
# Call parent to handle timestamps
|
|
93
|
+
super().pre_update()
|
|
94
|
+
|
|
95
|
+
async def apre_update(self) -> None:
|
|
96
|
+
"""Async hook called before updating account."""
|
|
97
|
+
# Validate custom fields first
|
|
98
|
+
if self.custom_fields:
|
|
99
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
100
|
+
|
|
101
|
+
self.custom_fields = await CustomFieldService.avalidate_custom_fields('Entity', self.custom_fields)
|
|
102
|
+
|
|
103
|
+
# Call parent to handle timestamps
|
|
104
|
+
await super().apre_update()
|
|
105
|
+
|
|
106
|
+
def post_update(self) -> None:
|
|
107
|
+
"""Hook called after updating account."""
|
|
108
|
+
from amsdal_crm.services.workflow_service import WorkflowService
|
|
109
|
+
|
|
110
|
+
WorkflowService.execute_rules('Entity', 'update', self)
|
|
111
|
+
|
|
112
|
+
async def apost_update(self) -> None:
|
|
113
|
+
"""Async hook called after updating account."""
|
|
114
|
+
from amsdal_crm.services.workflow_service import WorkflowService
|
|
115
|
+
|
|
116
|
+
await WorkflowService.aexecute_rules('Entity', 'update', self)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class EntityRelationship(TimestampMixin, Model):
|
|
120
|
+
from_entity: Entity = Field(title='From Entity')
|
|
121
|
+
to_entity: Entity = Field(title='To Entity')
|
|
122
|
+
start_date: str | None = Field(default=None, title='Start Date')
|
|
123
|
+
end_date: str | None = Field(default=None, title='End Date')
|
|
124
|
+
relationship_group_name: str | None = Field(default=None, title='Relationship Group Name')
|
|
125
|
+
|
|
126
|
+
def pre_create(self) -> None:
|
|
127
|
+
"""Hook called before creating account."""
|
|
128
|
+
if self.custom_fields:
|
|
129
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
130
|
+
|
|
131
|
+
self.custom_fields = CustomFieldService.validate_custom_fields('EntityRelationship', self.custom_fields)
|
|
132
|
+
super().pre_create()
|
|
133
|
+
|
|
134
|
+
async def apre_create(self) -> None:
|
|
135
|
+
"""Async hook called before creating account."""
|
|
136
|
+
if self.custom_fields:
|
|
137
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
138
|
+
|
|
139
|
+
self.custom_fields = await CustomFieldService.avalidate_custom_fields(
|
|
140
|
+
'EntityRelationship', self.custom_fields
|
|
141
|
+
)
|
|
142
|
+
await super().apre_create()
|
|
143
|
+
|
|
144
|
+
def pre_update(self) -> None:
|
|
145
|
+
"""Hook called before updating account."""
|
|
146
|
+
# Validate custom fields first
|
|
147
|
+
if self.custom_fields:
|
|
148
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
149
|
+
|
|
150
|
+
self.custom_fields = CustomFieldService.validate_custom_fields('EntityRelationship', self.custom_fields)
|
|
151
|
+
|
|
152
|
+
# Call parent to handle timestamps
|
|
153
|
+
super().pre_update()
|
|
154
|
+
|
|
155
|
+
async def apre_update(self) -> None:
|
|
156
|
+
"""Async hook called before updating account."""
|
|
157
|
+
# Validate custom fields first
|
|
158
|
+
if self.custom_fields:
|
|
159
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
160
|
+
|
|
161
|
+
self.custom_fields = await CustomFieldService.avalidate_custom_fields(
|
|
162
|
+
'EntityRelationship', self.custom_fields
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Call parent to handle timestamps
|
|
166
|
+
await super().apre_update()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class EntityIdentifier(TimestampMixin, Model):
|
|
170
|
+
entity: Entity = Field(title='Entity')
|
|
171
|
+
value: str = Field(title='Identifier Value')
|
|
172
|
+
country: str | None = Field(default=None, title='Country')
|
|
173
|
+
|
|
174
|
+
# TODO: validate one per entity
|
|
175
|
+
is_primary: bool = Field(default=False, title='Is Primary')
|
|
176
|
+
|
|
177
|
+
def pre_create(self) -> None:
|
|
178
|
+
"""Hook called before creating account."""
|
|
179
|
+
if self.custom_fields:
|
|
180
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
181
|
+
|
|
182
|
+
self.custom_fields = CustomFieldService.validate_custom_fields('EntityIdentifier', self.custom_fields)
|
|
183
|
+
super().pre_create()
|
|
184
|
+
|
|
185
|
+
async def apre_create(self) -> None:
|
|
186
|
+
"""Async hook called before creating account."""
|
|
187
|
+
if self.custom_fields:
|
|
188
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
189
|
+
|
|
190
|
+
self.custom_fields = await CustomFieldService.avalidate_custom_fields(
|
|
191
|
+
'EntityIdentifier', self.custom_fields
|
|
192
|
+
)
|
|
193
|
+
await super().apre_create()
|
|
194
|
+
|
|
195
|
+
def pre_update(self) -> None:
|
|
196
|
+
"""Hook called before updating account."""
|
|
197
|
+
# Validate custom fields first
|
|
198
|
+
if self.custom_fields:
|
|
199
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
200
|
+
|
|
201
|
+
self.custom_fields = CustomFieldService.validate_custom_fields('EntityIdentifier', self.custom_fields)
|
|
202
|
+
|
|
203
|
+
# Call parent to handle timestamps
|
|
204
|
+
super().pre_update()
|
|
205
|
+
|
|
206
|
+
async def apre_update(self) -> None:
|
|
207
|
+
"""Async hook called before updating account."""
|
|
208
|
+
# Validate custom fields first
|
|
209
|
+
if self.custom_fields:
|
|
210
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
211
|
+
|
|
212
|
+
self.custom_fields = await CustomFieldService.avalidate_custom_fields(
|
|
213
|
+
'EntityIdentifier', self.custom_fields
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Call parent to handle timestamps
|
|
217
|
+
await super().apre_update()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class EntityContactPoint(TimestampMixin, Model):
|
|
221
|
+
entity: Entity = Field(title='Entity')
|
|
222
|
+
value: str = Field(title='Contact Point Value')
|
|
223
|
+
|
|
224
|
+
# TODO: validate one per entity
|
|
225
|
+
is_primary: bool = Field(default=False, title='Is Primary')
|
|
226
|
+
can_contact: bool = Field(default=True, title='Can Contact')
|
|
227
|
+
|
|
228
|
+
def pre_create(self) -> None:
|
|
229
|
+
"""Hook called before creating account."""
|
|
230
|
+
if self.custom_fields:
|
|
231
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
232
|
+
|
|
233
|
+
self.custom_fields = CustomFieldService.validate_custom_fields('EntityContactPoint', self.custom_fields)
|
|
234
|
+
super().pre_create()
|
|
235
|
+
|
|
236
|
+
async def apre_create(self) -> None:
|
|
237
|
+
"""Async hook called before creating account."""
|
|
238
|
+
if self.custom_fields:
|
|
239
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
240
|
+
|
|
241
|
+
self.custom_fields = await CustomFieldService.avalidate_custom_fields(
|
|
242
|
+
'EntityContactPoint', self.custom_fields
|
|
243
|
+
)
|
|
244
|
+
await super().apre_create()
|
|
245
|
+
|
|
246
|
+
def pre_update(self) -> None:
|
|
247
|
+
"""Hook called before updating account."""
|
|
248
|
+
# Validate custom fields first
|
|
249
|
+
if self.custom_fields:
|
|
250
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
251
|
+
|
|
252
|
+
self.custom_fields = CustomFieldService.validate_custom_fields('EntityContactPoint', self.custom_fields)
|
|
253
|
+
|
|
254
|
+
# Call parent to handle timestamps
|
|
255
|
+
super().pre_update()
|
|
256
|
+
|
|
257
|
+
async def apre_update(self) -> None:
|
|
258
|
+
"""Async hook called before updating account."""
|
|
259
|
+
# Validate custom fields first
|
|
260
|
+
if self.custom_fields:
|
|
261
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
262
|
+
|
|
263
|
+
self.custom_fields = await CustomFieldService.avalidate_custom_fields(
|
|
264
|
+
'EntityContactPoint', self.custom_fields
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Call parent to handle timestamps
|
|
268
|
+
await super().apre_update()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class EntityAddress(TimestampMixin, Model):
|
|
272
|
+
line1: str | None = Field(title='Address Line 1')
|
|
273
|
+
line2: str | None = Field(default=None, title='Address Line 2')
|
|
274
|
+
city: str | None = Field(title='City')
|
|
275
|
+
region: str | None = Field(default=None, title='Region/State')
|
|
276
|
+
postal_code: str | None = Field(default=None, title='Postal Code')
|
|
277
|
+
country: str | None = Field(title='Country')
|
|
278
|
+
|
|
279
|
+
# TODO: validate one per entity
|
|
280
|
+
is_primary: bool = Field(default=False, title='Is Primary')
|
|
281
|
+
|
|
282
|
+
def pre_create(self) -> None:
|
|
283
|
+
"""Hook called before creating account."""
|
|
284
|
+
if self.custom_fields:
|
|
285
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
286
|
+
|
|
287
|
+
self.custom_fields = CustomFieldService.validate_custom_fields('EntityAddress', self.custom_fields)
|
|
288
|
+
super().pre_create()
|
|
289
|
+
|
|
290
|
+
async def apre_create(self) -> None:
|
|
291
|
+
"""Async hook called before creating account."""
|
|
292
|
+
if self.custom_fields:
|
|
293
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
294
|
+
|
|
295
|
+
self.custom_fields = await CustomFieldService.avalidate_custom_fields('EntityAddress', self.custom_fields)
|
|
296
|
+
await super().apre_create()
|
|
297
|
+
|
|
298
|
+
def pre_update(self) -> None:
|
|
299
|
+
"""Hook called before updating account."""
|
|
300
|
+
# Validate custom fields first
|
|
301
|
+
if self.custom_fields:
|
|
302
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
303
|
+
|
|
304
|
+
self.custom_fields = CustomFieldService.validate_custom_fields('EntityAddress', self.custom_fields)
|
|
305
|
+
|
|
306
|
+
# Call parent to handle timestamps
|
|
307
|
+
super().pre_update()
|
|
308
|
+
|
|
309
|
+
async def apre_update(self) -> None:
|
|
310
|
+
"""Async hook called before updating account."""
|
|
311
|
+
# Validate custom fields first
|
|
312
|
+
if self.custom_fields:
|
|
313
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
314
|
+
|
|
315
|
+
self.custom_fields = await CustomFieldService.avalidate_custom_fields('EntityAddress', self.custom_fields)
|
|
316
|
+
|
|
317
|
+
# Call parent to handle timestamps
|
|
318
|
+
await super().apre_update()
|
amsdal_crm/models/stage.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Stage Model."""
|
|
2
2
|
|
|
3
3
|
from typing import ClassVar
|
|
4
|
+
from typing import Literal
|
|
4
5
|
|
|
5
6
|
from amsdal_models.classes.data_models.indexes import IndexInfo
|
|
6
7
|
from amsdal_models.classes.model import Model
|
|
@@ -33,10 +34,11 @@ class Stage(Model):
|
|
|
33
34
|
|
|
34
35
|
pipeline: 'Pipeline' = Field(title='Pipeline')
|
|
35
36
|
name: str = Field(title='Stage Name')
|
|
37
|
+
description: str | None = Field(default=None, title='Description')
|
|
36
38
|
order: int = Field(title='Order')
|
|
37
39
|
probability: float = Field(default=0.0, title='Win Probability (%)', ge=0, le=100)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
|
|
41
|
+
status: Literal['open', 'closed_won', 'closed_lost'] = Field(default='open', title='Status')
|
|
40
42
|
|
|
41
43
|
@property
|
|
42
44
|
def display_name(self) -> str:
|
|
@@ -19,7 +19,7 @@ class WorkflowRule(Model):
|
|
|
19
19
|
__module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
|
|
20
20
|
|
|
21
21
|
name: str = Field(title='Rule Name')
|
|
22
|
-
entity_type: Literal['
|
|
22
|
+
entity_type: Literal['Entity', 'Deal', 'Activity'] = Field(title='Entity Type')
|
|
23
23
|
|
|
24
24
|
# Trigger
|
|
25
25
|
trigger_event: Literal['create', 'update', 'delete'] = Field(title='Trigger Event')
|
|
@@ -50,7 +50,6 @@ class DealService:
|
|
|
50
50
|
description=note or f'Deal stage changed from {old_stage_name} to {new_stage.name}',
|
|
51
51
|
related_to_type=ActivityRelatedTo.DEAL,
|
|
52
52
|
related_to_id=deal._object_id,
|
|
53
|
-
owner_email=user_email,
|
|
54
53
|
due_date=None,
|
|
55
54
|
completed_at=None,
|
|
56
55
|
is_completed=False,
|
|
@@ -66,9 +65,9 @@ class DealService:
|
|
|
66
65
|
user_email=user_email,
|
|
67
66
|
)
|
|
68
67
|
|
|
69
|
-
if new_stage.
|
|
68
|
+
if new_stage.status == 'closed_won':
|
|
70
69
|
LifecycleProducer.publish(CRMLifecycleEvent.ON_DEAL_WON, deal=deal, user_email=user_email) # type: ignore[arg-type]
|
|
71
|
-
elif new_stage.
|
|
70
|
+
elif new_stage.status == 'closed_lost':
|
|
72
71
|
LifecycleProducer.publish(CRMLifecycleEvent.ON_DEAL_LOST, deal=deal, user_email=user_email) # type: ignore[arg-type]
|
|
73
72
|
|
|
74
73
|
return deal
|
|
@@ -107,7 +106,6 @@ class DealService:
|
|
|
107
106
|
description=note or f'Deal stage changed from {old_stage_name} to {new_stage.name}',
|
|
108
107
|
related_to_type=ActivityRelatedTo.DEAL,
|
|
109
108
|
related_to_id=deal._object_id,
|
|
110
|
-
owner_email=user_email,
|
|
111
109
|
due_date=None,
|
|
112
110
|
completed_at=None,
|
|
113
111
|
is_completed=False,
|
|
@@ -123,9 +121,9 @@ class DealService:
|
|
|
123
121
|
user_email=user_email,
|
|
124
122
|
)
|
|
125
123
|
|
|
126
|
-
if new_stage.
|
|
124
|
+
if new_stage.status == 'closed_won':
|
|
127
125
|
await LifecycleProducer.publish_async(CRMLifecycleEvent.ON_DEAL_WON, deal=deal, user_email=user_email) # type: ignore[arg-type]
|
|
128
|
-
elif new_stage.
|
|
126
|
+
elif new_stage.status == 'closed_lost':
|
|
129
127
|
await LifecycleProducer.publish_async(CRMLifecycleEvent.ON_DEAL_LOST, deal=deal, user_email=user_email) # type: ignore[arg-type]
|
|
130
128
|
|
|
131
129
|
return deal
|
|
@@ -22,7 +22,6 @@ class EmailService:
|
|
|
22
22
|
cc_addresses: list[str] | None,
|
|
23
23
|
related_to_type: ActivityRelatedTo,
|
|
24
24
|
related_to_id: str,
|
|
25
|
-
owner_email: str,
|
|
26
25
|
*,
|
|
27
26
|
is_outbound: bool = True,
|
|
28
27
|
) -> EmailActivity:
|
|
@@ -38,9 +37,8 @@ class EmailService:
|
|
|
38
37
|
from_address: Sender email address
|
|
39
38
|
to_addresses: List of recipient email addresses
|
|
40
39
|
cc_addresses: List of CC email addresses
|
|
41
|
-
related_to_type: Type of related record (
|
|
40
|
+
related_to_type: Type of related record (Entity, Deal)
|
|
42
41
|
related_to_id: ID of related record
|
|
43
|
-
owner_email: Email of user who owns this activity
|
|
44
42
|
is_outbound: True if sent from CRM, False if received
|
|
45
43
|
|
|
46
44
|
Returns:
|
|
@@ -55,7 +53,6 @@ class EmailService:
|
|
|
55
53
|
cc_addresses=cc_addresses,
|
|
56
54
|
related_to_type=related_to_type,
|
|
57
55
|
related_to_id=related_to_id,
|
|
58
|
-
owner_email=owner_email,
|
|
59
56
|
is_outbound=is_outbound,
|
|
60
57
|
description=f'Email: {subject}',
|
|
61
58
|
due_date=None,
|
|
@@ -77,7 +74,6 @@ class EmailService:
|
|
|
77
74
|
cc_addresses: list[str] | None,
|
|
78
75
|
related_to_type: ActivityRelatedTo,
|
|
79
76
|
related_to_id: str,
|
|
80
|
-
owner_email: str,
|
|
81
77
|
*,
|
|
82
78
|
is_outbound: bool = True,
|
|
83
79
|
) -> EmailActivity:
|
|
@@ -89,9 +85,8 @@ class EmailService:
|
|
|
89
85
|
from_address: Sender email address
|
|
90
86
|
to_addresses: List of recipient email addresses
|
|
91
87
|
cc_addresses: List of CC email addresses
|
|
92
|
-
related_to_type: Type of related record (
|
|
88
|
+
related_to_type: Type of related record (Entity, Deal)
|
|
93
89
|
related_to_id: ID of related record
|
|
94
|
-
owner_email: Email of user who owns this activity
|
|
95
90
|
is_outbound: True if sent from CRM, False if received
|
|
96
91
|
|
|
97
92
|
Returns:
|
|
@@ -106,7 +101,6 @@ class EmailService:
|
|
|
106
101
|
cc_addresses=cc_addresses,
|
|
107
102
|
related_to_type=related_to_type,
|
|
108
103
|
related_to_id=related_to_id,
|
|
109
|
-
owner_email=owner_email,
|
|
110
104
|
is_outbound=is_outbound,
|
|
111
105
|
description=f'Email: {subject}',
|
|
112
106
|
due_date=None,
|