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.
@@ -0,0 +1,118 @@
1
+ """EmailService for email integration."""
2
+
3
+ from amsdal_data.transactions.decorators import async_transaction
4
+ from amsdal_data.transactions.decorators import transaction
5
+
6
+ from amsdal_crm.models.activity import ActivityRelatedTo
7
+ from amsdal_crm.models.activity import ActivityType
8
+ from amsdal_crm.models.activity import EmailActivity
9
+
10
+
11
+ class EmailService:
12
+ """Service for email integration and logging."""
13
+
14
+ @classmethod
15
+ @transaction
16
+ def log_email(
17
+ cls,
18
+ subject: str,
19
+ body: str,
20
+ from_address: str,
21
+ to_addresses: list[str],
22
+ cc_addresses: list[str] | None,
23
+ related_to_type: ActivityRelatedTo,
24
+ related_to_id: str,
25
+ owner_email: str,
26
+ *,
27
+ is_outbound: bool = True,
28
+ ) -> EmailActivity:
29
+ """Log an email as an activity.
30
+
31
+ This can be called when:
32
+ - User sends email from CRM
33
+ - Incoming email is parsed and associated with CRM record
34
+
35
+ Args:
36
+ subject: Email subject
37
+ body: Email body
38
+ from_address: Sender email address
39
+ to_addresses: List of recipient email addresses
40
+ cc_addresses: List of CC email addresses
41
+ related_to_type: Type of related record (Contact, Account, Deal)
42
+ related_to_id: ID of related record
43
+ owner_email: Email of user who owns this activity
44
+ is_outbound: True if sent from CRM, False if received
45
+
46
+ Returns:
47
+ The created EmailActivity
48
+ """
49
+ email_activity = EmailActivity(
50
+ activity_type=ActivityType.EMAIL,
51
+ subject=subject,
52
+ body=body,
53
+ from_address=from_address,
54
+ to_addresses=to_addresses,
55
+ cc_addresses=cc_addresses,
56
+ related_to_type=related_to_type,
57
+ related_to_id=related_to_id,
58
+ owner_email=owner_email,
59
+ is_outbound=is_outbound,
60
+ description=f'Email: {subject}',
61
+ due_date=None,
62
+ completed_at=None,
63
+ is_completed=False,
64
+ )
65
+ email_activity.save(force_insert=True)
66
+
67
+ return email_activity
68
+
69
+ @classmethod
70
+ @async_transaction
71
+ async def alog_email(
72
+ cls,
73
+ subject: str,
74
+ body: str,
75
+ from_address: str,
76
+ to_addresses: list[str],
77
+ cc_addresses: list[str] | None,
78
+ related_to_type: ActivityRelatedTo,
79
+ related_to_id: str,
80
+ owner_email: str,
81
+ *,
82
+ is_outbound: bool = True,
83
+ ) -> EmailActivity:
84
+ """Async version of log_email.
85
+
86
+ Args:
87
+ subject: Email subject
88
+ body: Email body
89
+ from_address: Sender email address
90
+ to_addresses: List of recipient email addresses
91
+ cc_addresses: List of CC email addresses
92
+ related_to_type: Type of related record (Contact, Account, Deal)
93
+ related_to_id: ID of related record
94
+ owner_email: Email of user who owns this activity
95
+ is_outbound: True if sent from CRM, False if received
96
+
97
+ Returns:
98
+ The created EmailActivity
99
+ """
100
+ email_activity = EmailActivity(
101
+ activity_type=ActivityType.EMAIL,
102
+ subject=subject,
103
+ body=body,
104
+ from_address=from_address,
105
+ to_addresses=to_addresses,
106
+ cc_addresses=cc_addresses,
107
+ related_to_type=related_to_type,
108
+ related_to_id=related_to_id,
109
+ owner_email=owner_email,
110
+ is_outbound=is_outbound,
111
+ description=f'Email: {subject}',
112
+ due_date=None,
113
+ completed_at=None,
114
+ is_completed=False,
115
+ )
116
+ await email_activity.asave(force_insert=True)
117
+
118
+ return email_activity
@@ -0,0 +1,177 @@
1
+ """WorkflowService for executing workflow automation rules."""
2
+
3
+ from amsdal_models.classes.model import Model
4
+ from amsdal_utils.models.enums import Versions
5
+
6
+ from amsdal_crm.errors import WorkflowExecutionError
7
+ from amsdal_crm.models.activity import ActivityRelatedTo
8
+ from amsdal_crm.models.activity import ActivityType
9
+ from amsdal_crm.models.activity import Note
10
+ from amsdal_crm.models.workflow_rule import WorkflowRule
11
+
12
+
13
+ class WorkflowService:
14
+ """Execute workflow rules for automation."""
15
+
16
+ @classmethod
17
+ def execute_rules(cls, entity_type: str, trigger_event: str, entity: Model) -> None:
18
+ """Execute workflow rules for an entity event.
19
+
20
+ Called from lifecycle hooks (post_create, post_update, post_delete).
21
+
22
+ Args:
23
+ entity_type: Type of entity (Contact, Account, Deal, Activity)
24
+ trigger_event: Event that triggered the rule (create, update, delete)
25
+ entity: The entity instance
26
+ """
27
+ # Load active rules for this entity and trigger
28
+ rules = WorkflowRule.objects.filter(
29
+ entity_type=entity_type,
30
+ trigger_event=trigger_event,
31
+ is_active=True,
32
+ _address__object_version=Versions.LATEST,
33
+ ).execute()
34
+
35
+ for rule in rules:
36
+ try:
37
+ if cls._evaluate_condition(rule, entity):
38
+ cls._execute_action(rule, entity)
39
+ except Exception as exc:
40
+ # Log error but don't fail the entire operation
41
+ error_msg = f'Failed to execute workflow rule {rule.name}: {exc}'
42
+ raise WorkflowExecutionError(error_msg) from exc
43
+
44
+ @classmethod
45
+ async def aexecute_rules(cls, entity_type: str, trigger_event: str, entity: Model) -> None:
46
+ """Execute workflow rules for an entity event.
47
+
48
+ Called from lifecycle hooks (post_create, post_update, post_delete).
49
+
50
+ Args:
51
+ entity_type: Type of entity (Contact, Account, Deal, Activity)
52
+ trigger_event: Event that triggered the rule (create, update, delete)
53
+ entity: The entity instance
54
+ """
55
+ # Load active rules for this entity and trigger
56
+ rules = await WorkflowRule.objects.filter(
57
+ entity_type=entity_type,
58
+ trigger_event=trigger_event,
59
+ is_active=True,
60
+ _address__object_version=Versions.LATEST,
61
+ ).aexecute()
62
+
63
+ for rule in rules:
64
+ try:
65
+ if cls._evaluate_condition(rule, entity):
66
+ await cls._aexecute_action(rule, entity)
67
+ except Exception as exc:
68
+ # Log error but don't fail the entire operation
69
+ error_msg = f'Failed to execute workflow rule {rule.name}: {exc}'
70
+ raise WorkflowExecutionError(error_msg) from exc
71
+
72
+ @classmethod
73
+ def _evaluate_condition(cls, rule: WorkflowRule, entity: Model) -> bool:
74
+ """Evaluate if rule condition matches.
75
+
76
+ Args:
77
+ rule: The workflow rule
78
+ entity: The entity to evaluate
79
+
80
+ Returns:
81
+ True if condition matches, False otherwise
82
+ """
83
+ if not rule.condition_field:
84
+ return True # No condition = always match
85
+
86
+ entity_value = getattr(entity, rule.condition_field, None)
87
+
88
+ if rule.condition_operator == 'equals':
89
+ return entity_value == rule.condition_value
90
+ elif rule.condition_operator == 'not_equals':
91
+ return entity_value != rule.condition_value
92
+ elif rule.condition_operator == 'contains':
93
+ if rule.condition_value is None or entity_value is None:
94
+ return False
95
+ return str(rule.condition_value) in str(entity_value)
96
+ elif rule.condition_operator == 'greater_than':
97
+ if entity_value is None or rule.condition_value is None:
98
+ return False
99
+ return entity_value > rule.condition_value
100
+ elif rule.condition_operator == 'less_than':
101
+ if entity_value is None or rule.condition_value is None:
102
+ return False
103
+ return entity_value < rule.condition_value
104
+
105
+ return False
106
+
107
+ @classmethod
108
+ def _execute_action(cls, rule: WorkflowRule, entity: Model) -> None:
109
+ """Execute rule action.
110
+
111
+ Args:
112
+ rule: The workflow rule
113
+ entity: The entity to act upon
114
+ """
115
+ if rule.action_type == 'update_field':
116
+ # Update field on entity
117
+ field_name = rule.action_config.get('field_name')
118
+ new_value = rule.action_config.get('value')
119
+ if field_name:
120
+ setattr(entity, str(field_name), new_value)
121
+ entity.save()
122
+
123
+ elif rule.action_type == 'create_activity':
124
+ # Create a Note activity
125
+ note = Note(
126
+ activity_type=ActivityType.NOTE,
127
+ subject=rule.action_config.get('subject', f'Workflow: {rule.name}'),
128
+ description=rule.action_config.get('description', ''),
129
+ related_to_type=ActivityRelatedTo[rule.entity_type.upper()],
130
+ related_to_id=entity._object_id,
131
+ owner_email=entity.owner_email if hasattr(entity, 'owner_email') else '',
132
+ due_date=None,
133
+ completed_at=None,
134
+ is_completed=False,
135
+ )
136
+ note.save(force_insert=True)
137
+
138
+ elif rule.action_type == 'send_notification':
139
+ # TODO: Implement notification system
140
+ # Placeholder for future notification integration
141
+ pass
142
+
143
+ @classmethod
144
+ async def _aexecute_action(cls, rule: WorkflowRule, entity: Model) -> None:
145
+ """Execute rule action.
146
+
147
+ Args:
148
+ rule: The workflow rule
149
+ entity: The entity to act upon
150
+ """
151
+ if rule.action_type == 'update_field':
152
+ # Update field on entity
153
+ field_name = rule.action_config.get('field_name')
154
+ new_value = rule.action_config.get('value')
155
+ if field_name:
156
+ setattr(entity, str(field_name), new_value)
157
+ await entity.asave()
158
+
159
+ elif rule.action_type == 'create_activity':
160
+ # Create a Note activity
161
+ note = Note(
162
+ activity_type=ActivityType.NOTE,
163
+ subject=rule.action_config.get('subject', f'Workflow: {rule.name}'),
164
+ description=rule.action_config.get('description', ''),
165
+ related_to_type=ActivityRelatedTo[rule.entity_type.upper()],
166
+ related_to_id=entity._object_id,
167
+ owner_email=entity.owner_email if hasattr(entity, 'owner_email') else '',
168
+ due_date=None,
169
+ completed_at=None,
170
+ is_completed=False,
171
+ )
172
+ await note.asave(force_insert=True)
173
+
174
+ elif rule.action_type == 'send_notification':
175
+ # TODO: Implement notification system
176
+ # Placeholder for future notification integration
177
+ pass
amsdal_crm/settings.py ADDED
@@ -0,0 +1,26 @@
1
+ """CRM Settings."""
2
+
3
+ from pydantic import Field
4
+ from pydantic_settings import BaseSettings
5
+ from pydantic_settings import SettingsConfigDict
6
+
7
+
8
+ class CRMSettings(BaseSettings):
9
+ """Settings for the CRM module."""
10
+
11
+ model_config = SettingsConfigDict(env_prefix='AMSDAL_CRM_')
12
+
13
+ # Activity settings
14
+ DEFAULT_ACTIVITY_TIMELINE_LIMIT: int = Field(100, title='Default Activity Timeline Limit')
15
+
16
+ # Custom field settings
17
+ MAX_CUSTOM_FIELDS_PER_ENTITY: int = Field(50, title='Max Custom Fields Per Entity')
18
+
19
+ # Workflow settings
20
+ MAX_WORKFLOW_RULES_PER_ENTITY: int = Field(100, title='Max Workflow Rules Per Entity')
21
+
22
+ # Deal settings
23
+ DEFAULT_CURRENCY: str = Field('USD', title='Default Currency')
24
+
25
+
26
+ crm_settings = CRMSettings()
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: amsdal_crm
3
+ Version: 0.1.0
4
+ Summary: amsdal-crm plugin for AMSDAL Framework
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: aiohttp==3.12.15
7
+ Requires-Dist: amsdal-cli>=0.5.7
8
+ Requires-Dist: amsdal-data>=0.5.9
9
+ Requires-Dist: amsdal-models>=0.5.9
10
+ Requires-Dist: amsdal-utils>=0.5.4
11
+ Requires-Dist: amsdal>=0.5.6
12
+ Requires-Dist: mcp>=0.1
13
+ Requires-Dist: openai==1.100.2
14
+ Requires-Dist: pydantic-settings~=2.12
15
+ Requires-Dist: pydantic~=2.12
16
+ Requires-Dist: pymupdf>=1.24.10
17
+ Description-Content-Type: text/markdown
18
+
19
+ # amsdal-crm
20
+
21
+ This plugin provides custom models, properties, transactions, and hooks for the AMSDAL Framework.
22
+
23
+ ## Plugin Structure
24
+
25
+ - `src/models/` - Contains model definitions in Python format
26
+ - `src/transactions/` - Contains transaction definitions
27
+ - `pyproject.toml` - Plugin configuration file
28
+ - `config.yml` - Configuration for connections
29
+
30
+ ## Installing this Plugin
31
+
32
+ To use this plugin in an AMSDAL application:
33
+
34
+ 1. Copy the plugin directory to your AMSDAL application
35
+ 2. Import the models and transactions as needed
36
+ 3. Register the plugin in your application configuration
37
+
38
+ ## Development
39
+
40
+ This plugin uses sync mode.
41
+
42
+ ### Adding Models
43
+
44
+ ```bash
45
+ amsdal generate model ModelName --format py
46
+ ```
47
+
48
+ ### Adding Properties
49
+
50
+ ```bash
51
+ amsdal generate property --model ModelName property_name
52
+ ```
53
+
54
+ ### Adding Transactions
55
+
56
+ ```bash
57
+ amsdal generate transaction TransactionName
58
+ ```
59
+
60
+ ### Adding Hooks
61
+
62
+ ```bash
63
+ amsdal generate hook --model ModelName on_create
64
+ ```
65
+
66
+ ## Testing
67
+
68
+ Test your plugin by integrating it with an AMSDAL application and running the application's test suite.
@@ -0,0 +1,31 @@
1
+ amsdal_crm/__about__.py,sha256=IMjkMO3twhQzluVTo8Z6rE7Eg-9U79_LGKMcsWLKBkY,22
2
+ amsdal_crm/__init__.py,sha256=b4wxJYesA5Ctk1IrAvlw64i_0EU3SiK1Tw6sUYakd18,303
3
+ amsdal_crm/app.py,sha256=eQV30L4QB43jjmGQzK_2crLmKK9P886KdFz7zTyDB8Y,1029
4
+ amsdal_crm/constants.py,sha256=5Ga7q9zEKcQZnAoKv_SE_7w8WxvhPFkM9gY9NruOEaA,347
5
+ amsdal_crm/errors.py,sha256=kTcDyKb-sEWkoYQ6OtokyzLIySMdOq32O3_qoFYUXuQ,514
6
+ amsdal_crm/settings.py,sha256=YbwDeiKaahqipGoBGkMRzYKGk8flt7IrkmMTLDyC9OQ,751
7
+ amsdal_crm/fixtures/__init__.py,sha256=1tDNXZhcbZBd4tX3lTKKlom1NUg1TX2aa2IbymWO9f0,20
8
+ amsdal_crm/fixtures/permissions.py,sha256=cYA-gWkKQdoN79GymQVHtT0GyFXMzaskwp13Ietp9wE,1107
9
+ amsdal_crm/fixtures/pipelines.py,sha256=ZCLmgrA700Sl7Oy7l4IQ8FbIbC1378OkcJTrZe5701o,2064
10
+ amsdal_crm/lifecycle/__init__.py,sha256=B8nw19lEIr7U15Lnu6jh7yzZwF9LWWh4-p3X63sAicQ,31
11
+ amsdal_crm/lifecycle/consumer.py,sha256=7tjPxWYMUZY0x77IrzGYPbaq2ozqDpflkyn17G5E2Zc,1293
12
+ amsdal_crm/migrations/0000_initial.py,sha256=8XjM-sbrNKJfcyGE_K2ITW4fOz7gmUxWH_NbX48o4XI,57028
13
+ amsdal_crm/models/__init__.py,sha256=DSuGeLKPNL_EUGohWtrH6Eof6Nk--dHyZpfqbGWmYIY,1350
14
+ amsdal_crm/models/account.py,sha256=b9JguizB-eM1BkDar4nGhayZ-icdfIsQp5cdB4sVZaQ,4897
15
+ amsdal_crm/models/activity.py,sha256=UtO1--oSPfrQCfQwWfIlEkjdXuaRG9znesPrGe1JqGM,4775
16
+ amsdal_crm/models/attachment.py,sha256=CzS8sUMw0_8T_a4Ey6uzrSdEc12Fki5FbrFRXrsTExk,1525
17
+ amsdal_crm/models/contact.py,sha256=J_ULPcM947AjRI35UCXWa_CAXK7HDgijXmcIlN2UdSI,4903
18
+ amsdal_crm/models/custom_field_definition.py,sha256=Z0k_QR6rZ1hWkg4Wn-w8Rn9GSIgpOg_moUYRsmRPZQI,1666
19
+ amsdal_crm/models/deal.py,sha256=ECG5Jd1zy286YJFGfxKDlJjUPW9PhLEvvL3kIeGttws,6434
20
+ amsdal_crm/models/pipeline.py,sha256=DXJh5MbCCRctEHhDfxef5RxFWSKN0D4v6UK75q5ssL8,925
21
+ amsdal_crm/models/stage.py,sha256=Ch4O1Aj2LtBhGpDN7MqY4iNRPrEQig0q6QtEU5cp4hA,1608
22
+ amsdal_crm/models/workflow_rule.py,sha256=cnIEaX-hWcRYvN5gR4uPt7Cirr8sPPdJibZapD8VRpY,1547
23
+ amsdal_crm/services/__init__.py,sha256=ngHA-MUPrsHvga8vFP61b8v7rrAWl-h1VZTjhlJCXkI,465
24
+ amsdal_crm/services/activity_service.py,sha256=3iuEmaSicwm81rouLPum5VaWW0bAMnNWNPRCHkNhewU,1873
25
+ amsdal_crm/services/custom_field_service.py,sha256=rqTlzcmjc-tqc3nuo2m0cMFdMgo1lIIlPniDHIpJLv8,5146
26
+ amsdal_crm/services/deal_service.py,sha256=ZF7cAc6r10xgXZ8D8Oy3hnE-6GgAXq8fuG_Y4QerRGw,4839
27
+ amsdal_crm/services/email_service.py,sha256=kung83otZAzm5MjezbWFP_Bp9CJ2NXLlDdjMX9MvNIc,3788
28
+ amsdal_crm/services/workflow_service.py,sha256=oKOFJrwnMZxAiHuScs63tZxL801z-6ryrUuPMC7OXlE,7036
29
+ amsdal_crm-0.1.0.dist-info/METADATA,sha256=xZ3eDOo_eJ0pTI8uqQYCkS-F1Cgax3lx86xlZBs-cak,1596
30
+ amsdal_crm-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
31
+ amsdal_crm-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any