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,46 @@
|
|
|
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()
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Account Model."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
|
|
6
|
+
from amsdal.contrib.auth.models.user import User
|
|
7
|
+
from amsdal.models.mixins import TimestampMixin
|
|
8
|
+
from amsdal_models.classes.data_models.constraints import UniqueConstraint
|
|
9
|
+
from amsdal_models.classes.data_models.indexes import IndexInfo
|
|
10
|
+
from amsdal_models.classes.model import Model
|
|
11
|
+
from amsdal_utils.models.enums import ModuleType
|
|
12
|
+
from pydantic.fields import Field
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Account(TimestampMixin, Model):
|
|
16
|
+
"""Account (Company/Organization) model.
|
|
17
|
+
|
|
18
|
+
Represents a company or organization in the CRM system.
|
|
19
|
+
Owned by individual users with permission controls.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
__module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
|
|
23
|
+
__constraints__: ClassVar[list[UniqueConstraint]] = [
|
|
24
|
+
UniqueConstraint(name='unq_account_name_owner', fields=['name', 'owner_email'])
|
|
25
|
+
]
|
|
26
|
+
__indexes__: ClassVar[list[IndexInfo]] = [
|
|
27
|
+
IndexInfo(name='idx_account_owner_email', field='owner_email'),
|
|
28
|
+
IndexInfo(name='idx_account_created_at', field='created_at'),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
# Core fields
|
|
32
|
+
name: str = Field(title='Account Name')
|
|
33
|
+
website: str | None = Field(default=None, title='Website')
|
|
34
|
+
phone: str | None = Field(default=None, title='Phone')
|
|
35
|
+
industry: str | None = Field(default=None, title='Industry')
|
|
36
|
+
|
|
37
|
+
# Address fields
|
|
38
|
+
billing_street: str | None = Field(default=None, title='Billing Street')
|
|
39
|
+
billing_city: str | None = Field(default=None, title='Billing City')
|
|
40
|
+
billing_state: str | None = Field(default=None, title='Billing State')
|
|
41
|
+
billing_postal_code: str | None = Field(default=None, title='Billing Postal Code')
|
|
42
|
+
billing_country: str | None = Field(default=None, title='Billing Country')
|
|
43
|
+
|
|
44
|
+
# Ownership
|
|
45
|
+
owner_email: str = Field(title='Owner Email')
|
|
46
|
+
|
|
47
|
+
# Custom fields (JSON)
|
|
48
|
+
custom_fields: dict[str, Any] | None = Field(default=None, title='Custom Fields')
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def display_name(self) -> str:
|
|
52
|
+
"""Return display name for the account."""
|
|
53
|
+
return self.name
|
|
54
|
+
|
|
55
|
+
def has_object_permission(self, user: 'User', action: str) -> bool:
|
|
56
|
+
"""Check if user has permission to perform action on this account.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
user: The user attempting the action
|
|
60
|
+
action: The action being attempted (read, create, update, delete)
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
True if user has permission, False otherwise
|
|
64
|
+
"""
|
|
65
|
+
# Owner has all permissions
|
|
66
|
+
if self.owner_email == user.email:
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
# Check admin permissions
|
|
70
|
+
if user.permissions:
|
|
71
|
+
for permission in user.permissions:
|
|
72
|
+
if permission.model == '*' and permission.action in ('*', action):
|
|
73
|
+
return True
|
|
74
|
+
if permission.model == 'Account' and permission.action in ('*', action):
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
def pre_create(self) -> None:
|
|
80
|
+
"""Hook called before creating account."""
|
|
81
|
+
if self.custom_fields:
|
|
82
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
83
|
+
|
|
84
|
+
self.custom_fields = CustomFieldService.validate_custom_fields('Account', self.custom_fields)
|
|
85
|
+
super().pre_create()
|
|
86
|
+
|
|
87
|
+
async def apre_create(self) -> None:
|
|
88
|
+
"""Async hook called before creating account."""
|
|
89
|
+
if self.custom_fields:
|
|
90
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
91
|
+
|
|
92
|
+
self.custom_fields = CustomFieldService.validate_custom_fields('Account', self.custom_fields)
|
|
93
|
+
await super().apre_create()
|
|
94
|
+
|
|
95
|
+
def pre_update(self) -> None:
|
|
96
|
+
"""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 = CustomFieldService.validate_custom_fields('Account', self.custom_fields)
|
|
102
|
+
|
|
103
|
+
# Call parent to handle timestamps
|
|
104
|
+
super().pre_update()
|
|
105
|
+
|
|
106
|
+
async def apre_update(self) -> None:
|
|
107
|
+
"""Async hook called before updating account."""
|
|
108
|
+
# Validate custom fields first
|
|
109
|
+
if self.custom_fields:
|
|
110
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
111
|
+
|
|
112
|
+
self.custom_fields = CustomFieldService.validate_custom_fields('Account', self.custom_fields)
|
|
113
|
+
|
|
114
|
+
# Call parent to handle timestamps
|
|
115
|
+
await super().apre_update()
|
|
116
|
+
|
|
117
|
+
def post_update(self) -> None:
|
|
118
|
+
"""Hook called after updating account."""
|
|
119
|
+
from amsdal_crm.services.workflow_service import WorkflowService
|
|
120
|
+
|
|
121
|
+
WorkflowService.execute_rules('Account', 'update', self)
|
|
122
|
+
|
|
123
|
+
async def apost_update(self) -> None:
|
|
124
|
+
"""Async hook called after updating account."""
|
|
125
|
+
from amsdal_crm.services.workflow_service import WorkflowService
|
|
126
|
+
|
|
127
|
+
WorkflowService.execute_rules('Account', 'update', self)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Activity Models."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from amsdal.contrib.auth.models.user import User
|
|
9
|
+
from amsdal.models.mixins import TimestampMixin
|
|
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 ActivityType(str, Enum):
|
|
17
|
+
"""Activity type enumeration."""
|
|
18
|
+
|
|
19
|
+
TASK = 'task'
|
|
20
|
+
EVENT = 'event'
|
|
21
|
+
EMAIL = 'email'
|
|
22
|
+
NOTE = 'note'
|
|
23
|
+
CALL = 'call'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ActivityRelatedTo(str, Enum):
|
|
27
|
+
"""What type of record this activity is related to."""
|
|
28
|
+
|
|
29
|
+
CONTACT = 'Contact'
|
|
30
|
+
ACCOUNT = 'Account'
|
|
31
|
+
DEAL = 'Deal'
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Activity(TimestampMixin, Model):
|
|
35
|
+
"""Base activity model with polymorphic related_to field.
|
|
36
|
+
|
|
37
|
+
Activities can be linked to Contacts, Accounts, or Deals using
|
|
38
|
+
a generic foreign key pattern (related_to_type + related_to_id).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
__module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
|
|
42
|
+
__indexes__: ClassVar[list[IndexInfo]] = [
|
|
43
|
+
IndexInfo(name='idx_activity_related_to', field='related_to_id'),
|
|
44
|
+
IndexInfo(name='idx_activity_owner', field='owner_email'),
|
|
45
|
+
IndexInfo(name='idx_activity_created_at', field='created_at'),
|
|
46
|
+
IndexInfo(name='idx_activity_due_date', field='due_date'),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
# Discriminator
|
|
50
|
+
activity_type: ActivityType = Field(title='Activity Type')
|
|
51
|
+
|
|
52
|
+
# Core fields
|
|
53
|
+
subject: str = Field(title='Subject')
|
|
54
|
+
description: str | None = Field(default=None, title='Description')
|
|
55
|
+
|
|
56
|
+
# 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')
|
|
62
|
+
|
|
63
|
+
# Timing
|
|
64
|
+
due_date: datetime | None = Field(default=None, title='Due Date')
|
|
65
|
+
completed_at: datetime | None = Field(default=None, title='Completed At')
|
|
66
|
+
|
|
67
|
+
# Status
|
|
68
|
+
is_completed: bool = Field(default=False, title='Is Completed')
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def display_name(self) -> str:
|
|
72
|
+
"""Return display name for the activity."""
|
|
73
|
+
return f'{self.activity_type.value}: {self.subject}'
|
|
74
|
+
|
|
75
|
+
def has_object_permission(self, user: 'User', action: str) -> bool:
|
|
76
|
+
"""Check if user has permission to perform action on this activity.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
user: The user attempting the action
|
|
80
|
+
action: The action being attempted (read, create, update, delete)
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True if user has permission, False otherwise
|
|
84
|
+
"""
|
|
85
|
+
# Owner has all permissions
|
|
86
|
+
if self.owner_email == user.email:
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
# Check admin permissions
|
|
90
|
+
if user.permissions:
|
|
91
|
+
for permission in user.permissions:
|
|
92
|
+
if permission.model == '*' and permission.action in ('*', action):
|
|
93
|
+
return True
|
|
94
|
+
if permission.model == 'Activity' and permission.action in ('*', action):
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class Task(Activity):
|
|
101
|
+
"""Task activity with priority and status."""
|
|
102
|
+
|
|
103
|
+
activity_type: Literal[ActivityType.TASK] = Field(ActivityType.TASK, title='Activity Type')
|
|
104
|
+
priority: Literal['low', 'medium', 'high'] = Field('medium', title='Priority')
|
|
105
|
+
status: Literal['not_started', 'in_progress', 'waiting', 'completed'] = Field('not_started', title='Status')
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class Event(Activity):
|
|
109
|
+
"""Event/meeting activity with start/end times."""
|
|
110
|
+
|
|
111
|
+
activity_type: Literal[ActivityType.EVENT] = Field(ActivityType.EVENT, title='Activity Type')
|
|
112
|
+
start_time: datetime = Field(title='Start Time')
|
|
113
|
+
end_time: datetime = Field(title='End Time')
|
|
114
|
+
location: str | None = Field(None, title='Location')
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class EmailActivity(Activity):
|
|
118
|
+
"""Email activity with sender/recipients."""
|
|
119
|
+
|
|
120
|
+
activity_type: Literal[ActivityType.EMAIL] = Field(ActivityType.EMAIL, title='Activity Type')
|
|
121
|
+
from_address: str = Field(title='From Address')
|
|
122
|
+
to_addresses: list[str] = Field(title='To Addresses')
|
|
123
|
+
cc_addresses: list[str] | None = Field(None, title='CC Addresses')
|
|
124
|
+
body: str = Field(title='Email Body')
|
|
125
|
+
is_outbound: bool = Field(True, title='Is Outbound')
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class Note(Activity):
|
|
129
|
+
"""Simple note activity."""
|
|
130
|
+
|
|
131
|
+
activity_type: Literal[ActivityType.NOTE] = Field(ActivityType.NOTE, title='Activity Type')
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class Call(Activity):
|
|
135
|
+
"""Phone call activity."""
|
|
136
|
+
|
|
137
|
+
activity_type: Literal[ActivityType.CALL] = Field(ActivityType.CALL, title='Activity Type')
|
|
138
|
+
phone_number: str = Field(title='Phone Number')
|
|
139
|
+
duration_seconds: int | None = Field(None, title='Duration (seconds)')
|
|
140
|
+
call_outcome: str | None = Field(None, title='Call Outcome')
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Attachment Model."""
|
|
2
|
+
|
|
3
|
+
import datetime as _dt
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from amsdal.models.core.file import File
|
|
8
|
+
from amsdal_models.classes.data_models.indexes import IndexInfo
|
|
9
|
+
from amsdal_models.classes.model import Model
|
|
10
|
+
from amsdal_utils.models.enums import ModuleType
|
|
11
|
+
from pydantic.fields import Field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Attachment(Model):
|
|
15
|
+
"""Explicit attachment model for tracking file relationships.
|
|
16
|
+
|
|
17
|
+
Uses polymorphic relationship to link files to Contacts, Accounts,
|
|
18
|
+
Deals, or Activities.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
__module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
|
|
22
|
+
__indexes__: ClassVar[list[IndexInfo]] = [
|
|
23
|
+
IndexInfo(name='idx_attachment_related_to', field='related_to_id'),
|
|
24
|
+
IndexInfo(name='idx_attachment_uploaded_at', field='uploaded_at'),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
file: File = Field(title='File')
|
|
28
|
+
|
|
29
|
+
# Polymorphic relationship
|
|
30
|
+
related_to_type: Literal['Contact', 'Account', 'Deal', 'Activity'] = Field(title='Related To Type')
|
|
31
|
+
related_to_id: str = Field(title='Related To ID')
|
|
32
|
+
|
|
33
|
+
# Metadata
|
|
34
|
+
uploaded_by: str = Field(title='Uploaded By (User Email)')
|
|
35
|
+
uploaded_at: _dt.datetime = Field(default_factory=lambda: _dt.datetime.now(_dt.UTC), title='Uploaded At')
|
|
36
|
+
description: str | None = Field(default=None, title='Description')
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def display_name(self) -> str:
|
|
40
|
+
"""Return display name for the attachment."""
|
|
41
|
+
if hasattr(self.file, 'filename'):
|
|
42
|
+
return self.file.filename
|
|
43
|
+
return f'Attachment {self._object_id}'
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Contact Model."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
from typing import Optional
|
|
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 Contact(TimestampMixin, Model):
|
|
17
|
+
"""Contact (Person) model.
|
|
18
|
+
|
|
19
|
+
Represents a person in the CRM system, optionally linked to an Account.
|
|
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_contact_email', fields=['email'])]
|
|
25
|
+
__indexes__: ClassVar[list[IndexInfo]] = [
|
|
26
|
+
IndexInfo(name='idx_contact_owner_email', field='owner_email'),
|
|
27
|
+
IndexInfo(name='idx_contact_created_at', field='created_at'),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
# Core fields
|
|
31
|
+
first_name: str = Field(title='First Name')
|
|
32
|
+
last_name: str = Field(title='Last Name')
|
|
33
|
+
email: str = Field(title='Email')
|
|
34
|
+
phone: str | None = Field(default=None, title='Phone Number')
|
|
35
|
+
mobile: str | None = Field(default=None, title='Mobile Number')
|
|
36
|
+
title: str | None = Field(default=None, title='Job Title')
|
|
37
|
+
|
|
38
|
+
# Relationships
|
|
39
|
+
account: Optional['Account'] = Field(default=None, title='Account')
|
|
40
|
+
owner_email: str = Field(title='Owner Email')
|
|
41
|
+
|
|
42
|
+
# Custom fields (JSON)
|
|
43
|
+
custom_fields: dict[str, Any] | None = Field(default=None, title='Custom Fields')
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def display_name(self) -> str:
|
|
47
|
+
"""Return display name for the contact."""
|
|
48
|
+
return f'{self.first_name} {self.last_name}'
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def full_name(self) -> str:
|
|
52
|
+
"""Return full name of the contact."""
|
|
53
|
+
return f'{self.first_name} {self.last_name}'
|
|
54
|
+
|
|
55
|
+
def has_object_permission(self, user: 'User', action: str) -> bool:
|
|
56
|
+
"""Check if user has permission to perform action on this contact.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
user: The user attempting the action
|
|
60
|
+
action: The action being attempted (read, create, update, delete)
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
True if user has permission, False otherwise
|
|
64
|
+
"""
|
|
65
|
+
# Owner has all permissions
|
|
66
|
+
if self.owner_email == user.email:
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
# Check admin permissions
|
|
70
|
+
if user.permissions:
|
|
71
|
+
for permission in user.permissions:
|
|
72
|
+
if permission.model == '*' and permission.action in ('*', action):
|
|
73
|
+
return True
|
|
74
|
+
if permission.model == 'Contact' and permission.action in ('*', action):
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
def pre_create(self) -> None:
|
|
80
|
+
"""Hook called before creating contact."""
|
|
81
|
+
if self.custom_fields:
|
|
82
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
83
|
+
|
|
84
|
+
self.custom_fields = CustomFieldService.validate_custom_fields('Contact', self.custom_fields)
|
|
85
|
+
super().pre_create()
|
|
86
|
+
|
|
87
|
+
async def apre_create(self) -> None:
|
|
88
|
+
"""Async hook called before creating contact."""
|
|
89
|
+
if self.custom_fields:
|
|
90
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
91
|
+
|
|
92
|
+
self.custom_fields = CustomFieldService.validate_custom_fields('Contact', self.custom_fields)
|
|
93
|
+
await super().apre_create()
|
|
94
|
+
|
|
95
|
+
def pre_update(self) -> None:
|
|
96
|
+
"""Hook called before updating contact."""
|
|
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 = CustomFieldService.validate_custom_fields('Contact', self.custom_fields)
|
|
102
|
+
|
|
103
|
+
# Call parent to handle timestamps
|
|
104
|
+
super().pre_update()
|
|
105
|
+
|
|
106
|
+
async def apre_update(self) -> None:
|
|
107
|
+
"""Async hook called before updating contact."""
|
|
108
|
+
# Validate custom fields first
|
|
109
|
+
if self.custom_fields:
|
|
110
|
+
from amsdal_crm.services.custom_field_service import CustomFieldService
|
|
111
|
+
|
|
112
|
+
self.custom_fields = CustomFieldService.validate_custom_fields('Contact', self.custom_fields)
|
|
113
|
+
|
|
114
|
+
# Call parent to handle timestamps
|
|
115
|
+
await super().apre_update()
|
|
116
|
+
|
|
117
|
+
def post_update(self) -> None:
|
|
118
|
+
"""Hook called after updating contact."""
|
|
119
|
+
from amsdal_crm.services.workflow_service import WorkflowService
|
|
120
|
+
|
|
121
|
+
WorkflowService.execute_rules('Contact', 'update', self)
|
|
122
|
+
|
|
123
|
+
async def apost_update(self) -> None:
|
|
124
|
+
"""Async hook called after updating contact."""
|
|
125
|
+
from amsdal_crm.services.workflow_service import WorkflowService
|
|
126
|
+
|
|
127
|
+
WorkflowService.execute_rules('Contact', 'update', self)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
from amsdal_crm.models.account import Account
|
|
131
|
+
|
|
132
|
+
Contact.model_rebuild()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""CustomFieldDefinition Model."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from amsdal_models.classes.data_models.constraints import UniqueConstraint
|
|
8
|
+
from amsdal_models.classes.model import Model
|
|
9
|
+
from amsdal_utils.models.enums import ModuleType
|
|
10
|
+
from pydantic.fields import Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CustomFieldDefinition(Model):
|
|
14
|
+
"""Metadata about custom fields available for CRM entities.
|
|
15
|
+
|
|
16
|
+
Defines custom fields that users can add to Contacts, Accounts, or Deals.
|
|
17
|
+
Field values are stored in the entity's custom_fields JSON dict.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
|
|
21
|
+
__constraints__: ClassVar[list[UniqueConstraint]] = [
|
|
22
|
+
UniqueConstraint(name='unq_custom_field_entity_name', fields=['entity_type', 'field_name'])
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
entity_type: Literal['Contact', 'Account', 'Deal'] = Field(title='Entity Type')
|
|
26
|
+
field_name: str = Field(title='Field Name')
|
|
27
|
+
field_label: str = Field(title='Field Label')
|
|
28
|
+
field_type: Literal['text', 'number', 'date', 'choice'] = Field(title='Field Type')
|
|
29
|
+
|
|
30
|
+
# For choice fields
|
|
31
|
+
choices: list[str] | None = Field(default=None, title='Choices (for choice type)')
|
|
32
|
+
|
|
33
|
+
# Validation
|
|
34
|
+
is_required: bool = Field(default=False, title='Is Required')
|
|
35
|
+
default_value: Any | None = Field(default=None, title='Default Value')
|
|
36
|
+
|
|
37
|
+
# Display
|
|
38
|
+
help_text: str | None = Field(default=None, title='Help Text')
|
|
39
|
+
display_order: int = Field(default=0, title='Display Order')
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def display_name(self) -> str:
|
|
43
|
+
"""Return display name for the custom field definition."""
|
|
44
|
+
return f'{self.entity_type}.{self.field_name}'
|