constec 0.5.2__tar.gz → 0.6.0__tar.gz
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.
- {constec-0.5.2 → constec-0.6.0}/PKG-INFO +1 -1
- constec-0.6.0/constec/db/migrations/0007_add_organization_to_automations.py +91 -0
- constec-0.6.0/constec/db/migrations/0008_refactor_creator_fields.py +173 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/models/__init__.py +1 -2
- {constec-0.5.2 → constec-0.6.0}/constec/db/models/automation.py +122 -100
- {constec-0.5.2 → constec-0.6.0}/constec.egg-info/PKG-INFO +1 -1
- {constec-0.5.2 → constec-0.6.0}/constec.egg-info/SOURCES.txt +2 -0
- {constec-0.5.2 → constec-0.6.0}/pyproject.toml +1 -1
- {constec-0.5.2 → constec-0.6.0}/LICENSE +0 -0
- {constec-0.5.2 → constec-0.6.0}/README.md +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/__init__.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/apps.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/migrations/0001_initial.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/migrations/0002_module_level.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/migrations/0003_remove_module_level.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/migrations/0004_rename_entities_company_cuit_idx_entities_company_e2c50f_idx_and_more.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/migrations/0005_event.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/migrations/0006_automation_trigger_action_executionlog_notificationtemplate.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/migrations/__init__.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/models/base.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/models/company.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/models/contact.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/models/erp.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/models/erp_entity.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/models/flow.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/models/group.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/models/module.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/models/organization.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/models/person.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/models/session.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/models/tag.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/db/models/user.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/py.typed +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/services/__init__.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/services/encryption.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/shared/__init__.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/shared/exceptions.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/utils/__init__.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/utils/cuit.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec/utils/password.py +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec.egg-info/dependency_links.txt +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec.egg-info/requires.txt +0 -0
- {constec-0.5.2 → constec-0.6.0}/constec.egg-info/top_level.txt +0 -0
- {constec-0.5.2 → constec-0.6.0}/setup.cfg +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Generated manually for constec automation organization support
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
import django.db.models.deletion
|
|
5
|
+
from django.db.models import Q
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
dependencies = [
|
|
11
|
+
('constec_db', '0006_automation_trigger_action_executionlog_notificationtemplate'),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
# Add organization field to Automation
|
|
16
|
+
migrations.AddField(
|
|
17
|
+
model_name='automation',
|
|
18
|
+
name='organization',
|
|
19
|
+
field=models.ForeignKey(
|
|
20
|
+
blank=True,
|
|
21
|
+
db_index=True,
|
|
22
|
+
help_text='Organization para automations compartidas (opcional si se define company)',
|
|
23
|
+
null=True,
|
|
24
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
25
|
+
to='constec_db.organization'
|
|
26
|
+
),
|
|
27
|
+
),
|
|
28
|
+
# Make company field nullable in Automation
|
|
29
|
+
migrations.AlterField(
|
|
30
|
+
model_name='automation',
|
|
31
|
+
name='company',
|
|
32
|
+
field=models.ForeignKey(
|
|
33
|
+
blank=True,
|
|
34
|
+
db_index=True,
|
|
35
|
+
help_text='Company específica (opcional si se define organization)',
|
|
36
|
+
null=True,
|
|
37
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
38
|
+
to='constec_db.company'
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
# Add organization field to NotificationTemplate
|
|
42
|
+
migrations.AddField(
|
|
43
|
+
model_name='notificationtemplate',
|
|
44
|
+
name='organization',
|
|
45
|
+
field=models.ForeignKey(
|
|
46
|
+
blank=True,
|
|
47
|
+
db_index=True,
|
|
48
|
+
help_text='Organization para templates compartidos (opcional si se define company)',
|
|
49
|
+
null=True,
|
|
50
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
51
|
+
to='constec_db.organization'
|
|
52
|
+
),
|
|
53
|
+
),
|
|
54
|
+
# Make company field nullable in NotificationTemplate
|
|
55
|
+
migrations.AlterField(
|
|
56
|
+
model_name='notificationtemplate',
|
|
57
|
+
name='company',
|
|
58
|
+
field=models.ForeignKey(
|
|
59
|
+
blank=True,
|
|
60
|
+
db_index=True,
|
|
61
|
+
help_text='Company específica (opcional si se define organization)',
|
|
62
|
+
null=True,
|
|
63
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
64
|
+
to='constec_db.company'
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
# Add indexes for organization
|
|
68
|
+
migrations.AddIndex(
|
|
69
|
+
model_name='automation',
|
|
70
|
+
index=models.Index(fields=['organization', 'status'], name='automations_org_status_idx'),
|
|
71
|
+
),
|
|
72
|
+
migrations.AddIndex(
|
|
73
|
+
model_name='notificationtemplate',
|
|
74
|
+
index=models.Index(fields=['organization', 'channel', 'is_active'], name='templates_org_channel_active_idx'),
|
|
75
|
+
),
|
|
76
|
+
# Add constraint: at least one of company or organization must be set
|
|
77
|
+
migrations.AddConstraint(
|
|
78
|
+
model_name='automation',
|
|
79
|
+
constraint=models.CheckConstraint(
|
|
80
|
+
check=Q(company__isnull=False) | Q(organization__isnull=False),
|
|
81
|
+
name='automation_requires_company_or_organization'
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
migrations.AddConstraint(
|
|
85
|
+
model_name='notificationtemplate',
|
|
86
|
+
constraint=models.CheckConstraint(
|
|
87
|
+
check=Q(company__isnull=False) | Q(organization__isnull=False),
|
|
88
|
+
name='template_requires_company_or_organization'
|
|
89
|
+
),
|
|
90
|
+
),
|
|
91
|
+
]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Generated manually - Refactor creator fields to support User and OrganizationUser
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
import django.db.models.deletion
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def migrate_creator_data(apps, schema_editor):
|
|
8
|
+
"""Migrate existing created_by data to created_by_user."""
|
|
9
|
+
Automation = apps.get_model('constec_db', 'Automation')
|
|
10
|
+
NotificationTemplate = apps.get_model('constec_db', 'NotificationTemplate')
|
|
11
|
+
|
|
12
|
+
# Migrate Automation creators
|
|
13
|
+
for automation in Automation.objects.all():
|
|
14
|
+
if automation.created_by:
|
|
15
|
+
automation.created_by_user = automation.created_by
|
|
16
|
+
automation.save(update_fields=['created_by_user'])
|
|
17
|
+
|
|
18
|
+
# Migrate NotificationTemplate creators
|
|
19
|
+
for template in NotificationTemplate.objects.all():
|
|
20
|
+
if template.created_by:
|
|
21
|
+
template.created_by_user = template.created_by
|
|
22
|
+
template.save(update_fields=['created_by_user'])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Migration(migrations.Migration):
|
|
26
|
+
|
|
27
|
+
dependencies = [
|
|
28
|
+
('constec_db', '0007_add_organization_to_automations'),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
operations = [
|
|
32
|
+
# 1. Add new creator fields to Automation (nullable temporarily for data migration)
|
|
33
|
+
migrations.AddField(
|
|
34
|
+
model_name='automation',
|
|
35
|
+
name='created_by_user',
|
|
36
|
+
field=models.ForeignKey(
|
|
37
|
+
blank=True,
|
|
38
|
+
help_text='User creator (company-level)',
|
|
39
|
+
null=True,
|
|
40
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
41
|
+
related_name='created_automations',
|
|
42
|
+
to='constec_db.user'
|
|
43
|
+
),
|
|
44
|
+
),
|
|
45
|
+
migrations.AddField(
|
|
46
|
+
model_name='automation',
|
|
47
|
+
name='created_by_org_user',
|
|
48
|
+
field=models.ForeignKey(
|
|
49
|
+
blank=True,
|
|
50
|
+
help_text='OrganizationUser creator (org-level)',
|
|
51
|
+
null=True,
|
|
52
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
53
|
+
related_name='created_automations',
|
|
54
|
+
to='constec_db.organizationuser'
|
|
55
|
+
),
|
|
56
|
+
),
|
|
57
|
+
|
|
58
|
+
# 2. Add new creator fields to NotificationTemplate (nullable temporarily for data migration)
|
|
59
|
+
migrations.AddField(
|
|
60
|
+
model_name='notificationtemplate',
|
|
61
|
+
name='created_by_user',
|
|
62
|
+
field=models.ForeignKey(
|
|
63
|
+
blank=True,
|
|
64
|
+
help_text='User creator (company-level)',
|
|
65
|
+
null=True,
|
|
66
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
67
|
+
related_name='created_templates',
|
|
68
|
+
to='constec_db.user'
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
migrations.AddField(
|
|
72
|
+
model_name='notificationtemplate',
|
|
73
|
+
name='created_by_org_user',
|
|
74
|
+
field=models.ForeignKey(
|
|
75
|
+
blank=True,
|
|
76
|
+
help_text='OrganizationUser creator (org-level)',
|
|
77
|
+
null=True,
|
|
78
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
79
|
+
related_name='created_templates',
|
|
80
|
+
to='constec_db.organizationuser'
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
|
|
84
|
+
# 3. Add triggered_by_org_user to ExecutionLog
|
|
85
|
+
migrations.AddField(
|
|
86
|
+
model_name='executionlog',
|
|
87
|
+
name='triggered_by_org_user',
|
|
88
|
+
field=models.ForeignKey(
|
|
89
|
+
blank=True,
|
|
90
|
+
help_text='OrganizationUser who triggered execution (org-level)',
|
|
91
|
+
null=True,
|
|
92
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
93
|
+
related_name='triggered_executions',
|
|
94
|
+
to='constec_db.organizationuser'
|
|
95
|
+
),
|
|
96
|
+
),
|
|
97
|
+
|
|
98
|
+
# 4. Update existing triggered_by_user field help text
|
|
99
|
+
migrations.AlterField(
|
|
100
|
+
model_name='executionlog',
|
|
101
|
+
name='triggered_by_user',
|
|
102
|
+
field=models.ForeignKey(
|
|
103
|
+
blank=True,
|
|
104
|
+
help_text='User who triggered execution (company-level)',
|
|
105
|
+
null=True,
|
|
106
|
+
on_delete=django.db.models.deletion.SET_NULL,
|
|
107
|
+
related_name='triggered_executions',
|
|
108
|
+
to='constec_db.user'
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
|
|
112
|
+
# 5. Migrate data from old created_by to new created_by_user
|
|
113
|
+
migrations.RunPython(migrate_creator_data, reverse_code=migrations.RunPython.noop),
|
|
114
|
+
|
|
115
|
+
# 6. Remove old created_by fields
|
|
116
|
+
migrations.RemoveField(
|
|
117
|
+
model_name='automation',
|
|
118
|
+
name='created_by',
|
|
119
|
+
),
|
|
120
|
+
migrations.RemoveField(
|
|
121
|
+
model_name='notificationtemplate',
|
|
122
|
+
name='created_by',
|
|
123
|
+
),
|
|
124
|
+
|
|
125
|
+
# 7. Add constraints for Automation
|
|
126
|
+
migrations.AddConstraint(
|
|
127
|
+
model_name='automation',
|
|
128
|
+
constraint=models.CheckConstraint(
|
|
129
|
+
check=models.Q(
|
|
130
|
+
('created_by_user__isnull', False),
|
|
131
|
+
('created_by_org_user__isnull', True)
|
|
132
|
+
) | models.Q(
|
|
133
|
+
('created_by_user__isnull', True),
|
|
134
|
+
('created_by_org_user__isnull', False)
|
|
135
|
+
),
|
|
136
|
+
name='automation_single_creator'
|
|
137
|
+
),
|
|
138
|
+
),
|
|
139
|
+
migrations.AddConstraint(
|
|
140
|
+
model_name='automation',
|
|
141
|
+
constraint=models.CheckConstraint(
|
|
142
|
+
check=models.Q(('created_by_user__isnull', False)) | models.Q(('created_by_org_user__isnull', False)),
|
|
143
|
+
name='automation_requires_creator'
|
|
144
|
+
),
|
|
145
|
+
),
|
|
146
|
+
|
|
147
|
+
# 8. Add constraints for NotificationTemplate
|
|
148
|
+
migrations.AddConstraint(
|
|
149
|
+
model_name='notificationtemplate',
|
|
150
|
+
constraint=models.CheckConstraint(
|
|
151
|
+
check=models.Q(
|
|
152
|
+
('created_by_user__isnull', False),
|
|
153
|
+
('created_by_org_user__isnull', True)
|
|
154
|
+
) | models.Q(
|
|
155
|
+
('created_by_user__isnull', True),
|
|
156
|
+
('created_by_org_user__isnull', False)
|
|
157
|
+
),
|
|
158
|
+
name='template_single_creator'
|
|
159
|
+
),
|
|
160
|
+
),
|
|
161
|
+
migrations.AddConstraint(
|
|
162
|
+
model_name='notificationtemplate',
|
|
163
|
+
constraint=models.CheckConstraint(
|
|
164
|
+
check=models.Q(('created_by_user__isnull', False)) | models.Q(('created_by_org_user__isnull', False)),
|
|
165
|
+
name='template_requires_creator'
|
|
166
|
+
),
|
|
167
|
+
),
|
|
168
|
+
|
|
169
|
+
# 9. Delete deprecated Event model
|
|
170
|
+
migrations.DeleteModel(
|
|
171
|
+
name='Event',
|
|
172
|
+
),
|
|
173
|
+
]
|
|
@@ -42,7 +42,7 @@ from .flow import FlowTemplate, Flow
|
|
|
42
42
|
from .session import Session, Message
|
|
43
43
|
|
|
44
44
|
# Automations models (automations schema)
|
|
45
|
-
from .automation import
|
|
45
|
+
from .automation import Automation, Trigger, Action, ExecutionLog, NotificationTemplate
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
__all__ = [
|
|
@@ -93,7 +93,6 @@ __all__ = [
|
|
|
93
93
|
'Session',
|
|
94
94
|
'Message',
|
|
95
95
|
# Automations (automations schema)
|
|
96
|
-
'Event',
|
|
97
96
|
'Automation',
|
|
98
97
|
'Trigger',
|
|
99
98
|
'Action',
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import uuid
|
|
4
4
|
from django.db import models
|
|
5
|
+
from django.db.models import Q
|
|
5
6
|
from django.core.exceptions import ValidationError
|
|
6
7
|
from .base import UUIDModel
|
|
7
8
|
from .company import Company
|
|
9
|
+
from .organization import Organization, OrganizationUser
|
|
8
10
|
from .user import User
|
|
9
11
|
|
|
10
12
|
|
|
@@ -34,12 +36,40 @@ class Automation(UUIDModel):
|
|
|
34
36
|
('retry', 'Retry'),
|
|
35
37
|
]
|
|
36
38
|
|
|
37
|
-
# FKs
|
|
38
|
-
company = models.ForeignKey(
|
|
39
|
-
|
|
39
|
+
# FKs - Al menos uno debe estar presente
|
|
40
|
+
company = models.ForeignKey(
|
|
41
|
+
Company,
|
|
42
|
+
on_delete=models.CASCADE,
|
|
43
|
+
null=True,
|
|
44
|
+
blank=True,
|
|
45
|
+
db_index=True,
|
|
46
|
+
help_text="Company específica (opcional si se define organization)"
|
|
47
|
+
)
|
|
48
|
+
organization = models.ForeignKey(
|
|
49
|
+
Organization,
|
|
50
|
+
on_delete=models.CASCADE,
|
|
51
|
+
null=True,
|
|
52
|
+
blank=True,
|
|
53
|
+
db_index=True,
|
|
54
|
+
help_text="Organization para automations compartidas (opcional si se define company)"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Creator - only ONE of these should be filled (XOR)
|
|
58
|
+
created_by_user = models.ForeignKey(
|
|
40
59
|
User,
|
|
41
60
|
on_delete=models.CASCADE,
|
|
42
|
-
|
|
61
|
+
null=True,
|
|
62
|
+
blank=True,
|
|
63
|
+
related_name='created_automations',
|
|
64
|
+
help_text="User creator (company-level)"
|
|
65
|
+
)
|
|
66
|
+
created_by_org_user = models.ForeignKey(
|
|
67
|
+
OrganizationUser,
|
|
68
|
+
on_delete=models.CASCADE,
|
|
69
|
+
null=True,
|
|
70
|
+
blank=True,
|
|
71
|
+
related_name='created_automations',
|
|
72
|
+
help_text="OrganizationUser creator (org-level)"
|
|
43
73
|
)
|
|
44
74
|
|
|
45
75
|
# Info básica
|
|
@@ -82,13 +112,34 @@ class Automation(UUIDModel):
|
|
|
82
112
|
app_label = 'constec_db'
|
|
83
113
|
indexes = [
|
|
84
114
|
models.Index(fields=['company', 'status']),
|
|
115
|
+
models.Index(fields=['organization', 'status']),
|
|
85
116
|
models.Index(fields=['status', 'next_execution_at']),
|
|
86
117
|
]
|
|
87
118
|
ordering = ['-created_at']
|
|
119
|
+
constraints = [
|
|
120
|
+
models.CheckConstraint(
|
|
121
|
+
check=Q(company__isnull=False) | Q(organization__isnull=False),
|
|
122
|
+
name='automation_requires_company_or_organization'
|
|
123
|
+
),
|
|
124
|
+
models.CheckConstraint(
|
|
125
|
+
check=Q(created_by_user__isnull=False) | Q(created_by_org_user__isnull=False),
|
|
126
|
+
name='automation_requires_creator'
|
|
127
|
+
),
|
|
128
|
+
models.CheckConstraint(
|
|
129
|
+
check=Q(created_by_user__isnull=False, created_by_org_user__isnull=True) |
|
|
130
|
+
Q(created_by_user__isnull=True, created_by_org_user__isnull=False),
|
|
131
|
+
name='automation_single_creator'
|
|
132
|
+
),
|
|
133
|
+
]
|
|
88
134
|
|
|
89
135
|
def __str__(self):
|
|
90
136
|
return f"{self.name} ({self.status})"
|
|
91
137
|
|
|
138
|
+
@property
|
|
139
|
+
def creator(self):
|
|
140
|
+
"""Helper to access creator regardless of type."""
|
|
141
|
+
return self.created_by_user or self.created_by_org_user
|
|
142
|
+
|
|
92
143
|
|
|
93
144
|
class Trigger(UUIDModel):
|
|
94
145
|
"""
|
|
@@ -270,7 +321,16 @@ class ExecutionLog(UUIDModel):
|
|
|
270
321
|
on_delete=models.SET_NULL,
|
|
271
322
|
null=True,
|
|
272
323
|
blank=True,
|
|
273
|
-
related_name='triggered_executions'
|
|
324
|
+
related_name='triggered_executions',
|
|
325
|
+
help_text="User who triggered execution (company-level)"
|
|
326
|
+
)
|
|
327
|
+
triggered_by_org_user = models.ForeignKey(
|
|
328
|
+
OrganizationUser,
|
|
329
|
+
on_delete=models.SET_NULL,
|
|
330
|
+
null=True,
|
|
331
|
+
blank=True,
|
|
332
|
+
related_name='triggered_executions',
|
|
333
|
+
help_text="OrganizationUser who triggered execution (org-level)"
|
|
274
334
|
)
|
|
275
335
|
|
|
276
336
|
# Estado
|
|
@@ -317,6 +377,11 @@ class ExecutionLog(UUIDModel):
|
|
|
317
377
|
def __str__(self):
|
|
318
378
|
return f"{self.automation.name} - {self.status} ({self.started_at})"
|
|
319
379
|
|
|
380
|
+
@property
|
|
381
|
+
def trigger_user(self):
|
|
382
|
+
"""Helper to access trigger user regardless of type."""
|
|
383
|
+
return self.triggered_by_user or self.triggered_by_org_user
|
|
384
|
+
|
|
320
385
|
|
|
321
386
|
class NotificationTemplate(UUIDModel):
|
|
322
387
|
"""
|
|
@@ -333,12 +398,40 @@ class NotificationTemplate(UUIDModel):
|
|
|
333
398
|
('push', 'Push Notification'),
|
|
334
399
|
]
|
|
335
400
|
|
|
336
|
-
# FKs
|
|
337
|
-
company = models.ForeignKey(
|
|
338
|
-
|
|
401
|
+
# FKs - Al menos uno debe estar presente
|
|
402
|
+
company = models.ForeignKey(
|
|
403
|
+
Company,
|
|
404
|
+
on_delete=models.CASCADE,
|
|
405
|
+
null=True,
|
|
406
|
+
blank=True,
|
|
407
|
+
db_index=True,
|
|
408
|
+
help_text="Company específica (opcional si se define organization)"
|
|
409
|
+
)
|
|
410
|
+
organization = models.ForeignKey(
|
|
411
|
+
Organization,
|
|
412
|
+
on_delete=models.CASCADE,
|
|
413
|
+
null=True,
|
|
414
|
+
blank=True,
|
|
415
|
+
db_index=True,
|
|
416
|
+
help_text="Organization para templates compartidos (opcional si se define company)"
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
# Creator - only ONE of these should be filled (XOR)
|
|
420
|
+
created_by_user = models.ForeignKey(
|
|
339
421
|
User,
|
|
340
422
|
on_delete=models.CASCADE,
|
|
341
|
-
|
|
423
|
+
null=True,
|
|
424
|
+
blank=True,
|
|
425
|
+
related_name='created_templates',
|
|
426
|
+
help_text="User creator (company-level)"
|
|
427
|
+
)
|
|
428
|
+
created_by_org_user = models.ForeignKey(
|
|
429
|
+
OrganizationUser,
|
|
430
|
+
on_delete=models.CASCADE,
|
|
431
|
+
null=True,
|
|
432
|
+
blank=True,
|
|
433
|
+
related_name='created_templates',
|
|
434
|
+
help_text="OrganizationUser creator (org-level)"
|
|
342
435
|
)
|
|
343
436
|
|
|
344
437
|
# Info básica
|
|
@@ -367,100 +460,29 @@ class NotificationTemplate(UUIDModel):
|
|
|
367
460
|
app_label = 'constec_db'
|
|
368
461
|
indexes = [
|
|
369
462
|
models.Index(fields=['company', 'channel', 'is_active']),
|
|
463
|
+
models.Index(fields=['organization', 'channel', 'is_active']),
|
|
370
464
|
]
|
|
371
465
|
ordering = ['name']
|
|
466
|
+
constraints = [
|
|
467
|
+
models.CheckConstraint(
|
|
468
|
+
check=Q(company__isnull=False) | Q(organization__isnull=False),
|
|
469
|
+
name='template_requires_company_or_organization'
|
|
470
|
+
),
|
|
471
|
+
models.CheckConstraint(
|
|
472
|
+
check=Q(created_by_user__isnull=False) | Q(created_by_org_user__isnull=False),
|
|
473
|
+
name='template_requires_creator'
|
|
474
|
+
),
|
|
475
|
+
models.CheckConstraint(
|
|
476
|
+
check=Q(created_by_user__isnull=False, created_by_org_user__isnull=True) |
|
|
477
|
+
Q(created_by_user__isnull=True, created_by_org_user__isnull=False),
|
|
478
|
+
name='template_single_creator'
|
|
479
|
+
),
|
|
480
|
+
]
|
|
372
481
|
|
|
373
482
|
def __str__(self):
|
|
374
483
|
return f"{self.channel}: {self.name}"
|
|
375
484
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
Tipos soportados:
|
|
382
|
-
- notification: Notificaciones programadas
|
|
383
|
-
- automation: Automatizaciones programadas (acciones HTTP, workflows, etc.)
|
|
384
|
-
"""
|
|
385
|
-
|
|
386
|
-
EVENT_TYPE_CHOICES = [
|
|
387
|
-
('notification', 'Notification'),
|
|
388
|
-
('automation', 'Automation'),
|
|
389
|
-
]
|
|
390
|
-
|
|
391
|
-
STATUS_CHOICES = [
|
|
392
|
-
('active', 'Active'), # Activo y programado
|
|
393
|
-
('paused', 'Paused'), # Pausado temporalmente
|
|
394
|
-
('completed', 'Completed'), # Completado (para puntuales)
|
|
395
|
-
('failed', 'Failed'), # Falló la última ejecución
|
|
396
|
-
('cancelled', 'Cancelled'), # Cancelado permanentemente
|
|
397
|
-
]
|
|
398
|
-
|
|
399
|
-
RECURRENCE_TYPE_CHOICES = [
|
|
400
|
-
('punctual', 'Punctual'), # Una o más veces en fechas específicas
|
|
401
|
-
('periodic', 'Periodic'), # Recurrente
|
|
402
|
-
]
|
|
403
|
-
|
|
404
|
-
# FKs
|
|
405
|
-
company = models.ForeignKey(Company, on_delete=models.CASCADE, db_index=True)
|
|
406
|
-
|
|
407
|
-
# Tipo de evento
|
|
408
|
-
event_type = models.CharField(max_length=50, choices=EVENT_TYPE_CHOICES)
|
|
409
|
-
|
|
410
|
-
# Info básica
|
|
411
|
-
title = models.CharField(max_length=255)
|
|
412
|
-
description = models.TextField(blank=True)
|
|
413
|
-
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active')
|
|
414
|
-
|
|
415
|
-
# Recurrencia
|
|
416
|
-
recurrence_type = models.CharField(
|
|
417
|
-
max_length=20,
|
|
418
|
-
choices=RECURRENCE_TYPE_CHOICES,
|
|
419
|
-
help_text="Tipo de recurrencia: puntual (fechas específicas) o periódico"
|
|
420
|
-
)
|
|
421
|
-
recurrence_config = models.JSONField(
|
|
422
|
-
help_text="Configuración de recurrencia en formato JSON"
|
|
423
|
-
)
|
|
424
|
-
|
|
425
|
-
# Configuración específica del tipo de evento
|
|
426
|
-
config = models.JSONField(
|
|
427
|
-
default=dict,
|
|
428
|
-
help_text="Configuración específica según event_type (mensaje, URL, etc.)"
|
|
429
|
-
)
|
|
430
|
-
|
|
431
|
-
# Control de ejecución
|
|
432
|
-
next_execution_at = models.DateTimeField(
|
|
433
|
-
null=True,
|
|
434
|
-
blank=True,
|
|
435
|
-
db_index=True,
|
|
436
|
-
help_text="Próxima fecha/hora de ejecución calculada"
|
|
437
|
-
)
|
|
438
|
-
last_executed_at = models.DateTimeField(
|
|
439
|
-
null=True,
|
|
440
|
-
blank=True,
|
|
441
|
-
help_text="Última vez que se ejecutó"
|
|
442
|
-
)
|
|
443
|
-
execution_count = models.IntegerField(
|
|
444
|
-
default=0,
|
|
445
|
-
help_text="Cantidad de veces que se ha ejecutado"
|
|
446
|
-
)
|
|
447
|
-
|
|
448
|
-
# Auditoría
|
|
449
|
-
created_by = models.ForeignKey(
|
|
450
|
-
User,
|
|
451
|
-
on_delete=models.CASCADE,
|
|
452
|
-
related_name='created_events'
|
|
453
|
-
)
|
|
454
|
-
|
|
455
|
-
class Meta:
|
|
456
|
-
db_table = '"automations"."events"'
|
|
457
|
-
app_label = 'constec_db'
|
|
458
|
-
indexes = [
|
|
459
|
-
models.Index(fields=['company', 'event_type']),
|
|
460
|
-
models.Index(fields=['status', 'next_execution_at']),
|
|
461
|
-
models.Index(fields=['recurrence_type']),
|
|
462
|
-
]
|
|
463
|
-
ordering = ['next_execution_at']
|
|
464
|
-
|
|
465
|
-
def __str__(self):
|
|
466
|
-
return f"{self.event_type}: {self.title}"
|
|
485
|
+
@property
|
|
486
|
+
def creator(self):
|
|
487
|
+
"""Helper to access creator regardless of type."""
|
|
488
|
+
return self.created_by_user or self.created_by_org_user
|
|
@@ -15,6 +15,8 @@ constec/db/migrations/0003_remove_module_level.py
|
|
|
15
15
|
constec/db/migrations/0004_rename_entities_company_cuit_idx_entities_company_e2c50f_idx_and_more.py
|
|
16
16
|
constec/db/migrations/0005_event.py
|
|
17
17
|
constec/db/migrations/0006_automation_trigger_action_executionlog_notificationtemplate.py
|
|
18
|
+
constec/db/migrations/0007_add_organization_to_automations.py
|
|
19
|
+
constec/db/migrations/0008_refactor_creator_fields.py
|
|
18
20
|
constec/db/migrations/__init__.py
|
|
19
21
|
constec/db/models/__init__.py
|
|
20
22
|
constec/db/models/automation.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "constec"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "Base library for the Constec ecosystem - shared utilities, models, and namespace foundation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|