constec 0.7.1__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.
Files changed (40) hide show
  1. constec/db/__init__.py +11 -0
  2. constec/db/apps.py +8 -0
  3. constec/db/migrations/0001_initial.py +551 -0
  4. constec/db/migrations/0002_module_level.py +38 -0
  5. constec/db/migrations/0003_remove_module_level.py +15 -0
  6. constec/db/migrations/0004_rename_entities_company_cuit_idx_entities_company_e2c50f_idx_and_more.py +345 -0
  7. constec/db/migrations/0005_event.py +46 -0
  8. constec/db/migrations/0006_automation_trigger_action_executionlog_notificationtemplate.py +275 -0
  9. constec/db/migrations/0007_add_organization_to_automations.py +91 -0
  10. constec/db/migrations/0008_refactor_creator_fields.py +173 -0
  11. constec/db/migrations/0009_rename_user_to_companyuser.py +40 -0
  12. constec/db/migrations/__init__.py +0 -0
  13. constec/db/models/__init__.py +110 -0
  14. constec/db/models/automation.py +488 -0
  15. constec/db/models/base.py +23 -0
  16. constec/db/models/company.py +36 -0
  17. constec/db/models/contact.py +71 -0
  18. constec/db/models/erp.py +101 -0
  19. constec/db/models/erp_entity.py +122 -0
  20. constec/db/models/flow.py +138 -0
  21. constec/db/models/group.py +36 -0
  22. constec/db/models/module.py +67 -0
  23. constec/db/models/organization.py +62 -0
  24. constec/db/models/person.py +28 -0
  25. constec/db/models/session.py +89 -0
  26. constec/db/models/tag.py +70 -0
  27. constec/db/models/user.py +74 -0
  28. constec/py.typed +0 -0
  29. constec/services/__init__.py +14 -0
  30. constec/services/encryption.py +92 -0
  31. constec/shared/__init__.py +20 -0
  32. constec/shared/exceptions.py +48 -0
  33. constec/utils/__init__.py +20 -0
  34. constec/utils/cuit.py +107 -0
  35. constec/utils/password.py +62 -0
  36. constec-0.7.1.dist-info/METADATA +94 -0
  37. constec-0.7.1.dist-info/RECORD +40 -0
  38. constec-0.7.1.dist-info/WHEEL +5 -0
  39. constec-0.7.1.dist-info/licenses/LICENSE +21 -0
  40. constec-0.7.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,101 @@
1
+ from django.db import models
2
+ from .base import UUIDModel
3
+ from .company import Company
4
+
5
+
6
+ class System(UUIDModel):
7
+ """ERP system definition - independent of any company.
8
+
9
+ An ERP system can be shared across multiple companies via CompanySystem.
10
+ """
11
+ name = models.CharField(max_length=100, unique=True)
12
+ description = models.TextField(blank=True, null=True)
13
+ api_endpoint = models.URLField(max_length=200, blank=True)
14
+ metadata = models.JSONField(default=dict, blank=True)
15
+
16
+ class Meta:
17
+ app_label = 'constec_db'
18
+ db_table = 'erp"."systems'
19
+
20
+ def __str__(self):
21
+ return f"{self.name}"
22
+
23
+
24
+ class CompanySystem(UUIDModel):
25
+ """Junction table linking companies to ERP systems.
26
+
27
+ Allows many-to-many relationship:
28
+ - A company can use multiple ERP systems
29
+ - An ERP system can be used by multiple companies
30
+ """
31
+ company = models.ForeignKey(
32
+ Company,
33
+ on_delete=models.CASCADE,
34
+ related_name="erp_systems",
35
+ )
36
+ system = models.ForeignKey(
37
+ System,
38
+ on_delete=models.CASCADE,
39
+ related_name="company_assignments",
40
+ )
41
+ is_primary = models.BooleanField(
42
+ default=False,
43
+ help_text="Whether this is the primary ERP system for the company"
44
+ )
45
+ metadata = models.JSONField(default=dict, blank=True)
46
+
47
+ class Meta:
48
+ app_label = 'constec_db'
49
+ db_table = 'erp"."company_systems'
50
+ unique_together = [['company', 'system']]
51
+ indexes = [
52
+ models.Index(fields=['company', 'is_primary']),
53
+ models.Index(fields=['system', 'is_active']),
54
+ ]
55
+
56
+ def __str__(self):
57
+ primary = " (Primary)" if self.is_primary else ""
58
+ return f"{self.company.name} → {self.system.name}{primary}"
59
+
60
+
61
+ class Connection(UUIDModel):
62
+ """Database connection credentials for an ERP system."""
63
+
64
+ DB_TYPE_CHOICES = [
65
+ ('mssql', 'SQL Server'),
66
+ ('postgresql', 'PostgreSQL'),
67
+ ('mysql', 'MySQL'),
68
+ ]
69
+
70
+ company = models.ForeignKey(
71
+ Company,
72
+ on_delete=models.CASCADE,
73
+ related_name="erp_connections",
74
+ )
75
+ system = models.ForeignKey(
76
+ System,
77
+ on_delete=models.CASCADE,
78
+ related_name="connections",
79
+ )
80
+ name = models.CharField(
81
+ max_length=100,
82
+ blank=True,
83
+ default='',
84
+ help_text="Descriptive name for this connection"
85
+ )
86
+ slug = models.CharField(max_length=50)
87
+ db_type = models.CharField(max_length=20, choices=DB_TYPE_CHOICES, default='mssql')
88
+ host = models.CharField(max_length=255)
89
+ port = models.IntegerField()
90
+ database = models.CharField(max_length=100)
91
+ username = models.CharField(max_length=100)
92
+ encrypted_password = models.TextField()
93
+ last_tested_at = models.DateTimeField(null=True, blank=True)
94
+
95
+ class Meta:
96
+ app_label = 'constec_db'
97
+ db_table = 'erp"."connections'
98
+ unique_together = [['system', 'slug']]
99
+
100
+ def __str__(self):
101
+ return f"{self.name}" if self.name else f"{self.system.name} ({self.slug})"
@@ -0,0 +1,122 @@
1
+ from django.core.exceptions import ValidationError
2
+ from django.db import models
3
+ from .base import UUIDModel
4
+ from .company import Company
5
+ from .erp import System
6
+ from .person import Person
7
+
8
+
9
+ class Role(UUIDModel):
10
+ """Roles within the ERP (customer, supplier, seller)."""
11
+ system = models.ForeignKey(
12
+ System,
13
+ on_delete=models.CASCADE,
14
+ related_name="roles",
15
+ )
16
+ name = models.CharField(max_length=100)
17
+ description = models.TextField(blank=True, null=True)
18
+
19
+ class Meta:
20
+ app_label = 'constec_db'
21
+ db_table = 'erp"."roles'
22
+ unique_together = [['system', 'name']]
23
+
24
+ def __str__(self):
25
+ return f"{self.name}"
26
+
27
+
28
+ class Entity(UUIDModel):
29
+ """Link between ERP external entities and Core persons.
30
+
31
+ An Entity can exist without a Person (when someone has ERP access
32
+ but no Core account yet). When they authenticate via CUIT, we can
33
+ optionally link them to a Person later.
34
+ """
35
+ company = models.ForeignKey(
36
+ Company,
37
+ on_delete=models.CASCADE,
38
+ related_name="erp_entities",
39
+ )
40
+ person = models.ForeignKey(
41
+ Person,
42
+ on_delete=models.CASCADE,
43
+ related_name="erp_entities",
44
+ null=True,
45
+ blank=True,
46
+ help_text="Optional link to Core person"
47
+ )
48
+ system = models.ForeignKey(
49
+ System,
50
+ on_delete=models.CASCADE,
51
+ related_name="entities",
52
+ )
53
+ role = models.ForeignKey(
54
+ Role,
55
+ on_delete=models.PROTECT,
56
+ related_name="entities",
57
+ )
58
+ external_id = models.CharField(
59
+ max_length=255,
60
+ help_text="Entity ID in external ERP system (e.g., cli_Cod, pro_Cod)"
61
+ )
62
+ cuit = models.CharField(
63
+ max_length=13,
64
+ blank=True,
65
+ default='',
66
+ help_text="CUIT without dashes (for authentication lookup)"
67
+ )
68
+ metadata = models.JSONField(default=dict, blank=True)
69
+
70
+ class Meta:
71
+ app_label = 'constec_db'
72
+ db_table = 'erp"."entities'
73
+ verbose_name_plural = 'Entities'
74
+ unique_together = [['company', 'role', 'external_id']]
75
+ indexes = [
76
+ models.Index(fields=['cuit']),
77
+ models.Index(fields=['company', 'cuit']),
78
+ ]
79
+
80
+ def clean(self):
81
+ from .erp import CompanySystem
82
+ if not CompanySystem.objects.filter(company=self.company, system=self.system).exists():
83
+ raise ValidationError(
84
+ f"Company {self.company} is not associated with System {self.system}"
85
+ )
86
+
87
+ def __str__(self):
88
+ person_info = f"({self.person.full_name})" if self.person else "(no person linked)"
89
+ return f"[{self.system.name}] {self.role.name} {self.external_id} {person_info}"
90
+
91
+
92
+ class EntityAuth(UUIDModel):
93
+ """Authentication credentials for ERP entities.
94
+
95
+ Stores hashed passwords for entities that can authenticate via Constancia AI.
96
+ Linked to Entity (which may or may not have a Person linked).
97
+ """
98
+ entity = models.OneToOneField(
99
+ Entity,
100
+ on_delete=models.CASCADE,
101
+ related_name="auth",
102
+ help_text="The ERP entity these credentials belong to"
103
+ )
104
+ password_hash = models.CharField(
105
+ max_length=255,
106
+ help_text="Bcrypt hashed password"
107
+ )
108
+ last_login = models.DateTimeField(
109
+ null=True,
110
+ blank=True,
111
+ help_text="Last successful login timestamp"
112
+ )
113
+
114
+ class Meta:
115
+ app_label = 'constec_db'
116
+ db_table = 'erp"."entity_auth'
117
+ verbose_name = "Entity Authentication"
118
+ verbose_name_plural = "Entity Authentications"
119
+
120
+ def __str__(self):
121
+ status = "active" if self.is_active else "inactive"
122
+ return f"Auth for {self.entity} ({status})"
@@ -0,0 +1,138 @@
1
+ from django.db import models
2
+ from .base import UUIDModel
3
+ from .organization import Organization, OrganizationUser
4
+ from .company import Company
5
+
6
+
7
+ class FlowTemplate(UUIDModel):
8
+ """Global flow templates available to all companies.
9
+
10
+ Created by organization admins. Companies can use these
11
+ templates as-is or customize them.
12
+ """
13
+
14
+ organization = models.ForeignKey(
15
+ Organization,
16
+ on_delete=models.CASCADE,
17
+ related_name="flow_templates",
18
+ null=True,
19
+ blank=True,
20
+ help_text="Organization that owns this template (null for global)"
21
+ )
22
+
23
+ name = models.CharField(max_length=100)
24
+ description = models.TextField()
25
+ category = models.CharField(
26
+ max_length=50,
27
+ choices=[
28
+ ('finance', 'Finance & Accounting'),
29
+ ('customer_service', 'Customer Service'),
30
+ ('sales', 'Sales & CRM'),
31
+ ('general', 'General Purpose'),
32
+ ('custom', 'Custom'),
33
+ ]
34
+ )
35
+
36
+ graph_definition = models.JSONField(
37
+ help_text="Graph structure in JSON format with nodes and edges"
38
+ )
39
+
40
+ default_config = models.JSONField(
41
+ default=dict,
42
+ help_text="Default configuration for this template (LLM, tools, prompts)"
43
+ )
44
+
45
+ is_global = models.BooleanField(
46
+ default=False,
47
+ help_text="If true, all organizations can use this template"
48
+ )
49
+
50
+ version = models.CharField(max_length=20, default="1.0.0")
51
+ created_by = models.ForeignKey(
52
+ OrganizationUser,
53
+ on_delete=models.SET_NULL,
54
+ null=True,
55
+ related_name="created_templates"
56
+ )
57
+
58
+ class Meta:
59
+ app_label = 'constec_db'
60
+ db_table = 'constancia"."flow_templates'
61
+ unique_together = [['organization', 'name', 'version']]
62
+ indexes = [
63
+ models.Index(fields=['organization', 'is_global']),
64
+ models.Index(fields=['category', 'is_active']),
65
+ ]
66
+
67
+ def __str__(self):
68
+ global_tag = " [GLOBAL]" if self.is_global else ""
69
+ return f"{self.name} v{self.version}{global_tag}"
70
+
71
+
72
+ class Flow(UUIDModel):
73
+ """Company-specific flow configuration.
74
+
75
+ Can either use a template or define custom graph.
76
+ Defines how the AI agent behaves for a specific company.
77
+ """
78
+
79
+ company = models.ForeignKey(
80
+ Company,
81
+ on_delete=models.CASCADE,
82
+ related_name="flows",
83
+ )
84
+
85
+ name = models.CharField(max_length=100)
86
+ description = models.TextField(blank=True, null=True)
87
+
88
+ template = models.ForeignKey(
89
+ FlowTemplate,
90
+ null=True,
91
+ blank=True,
92
+ on_delete=models.SET_NULL,
93
+ related_name="flows",
94
+ help_text="If set, uses template's graph. config overrides template defaults."
95
+ )
96
+
97
+ graph_definition = models.JSONField(
98
+ null=True,
99
+ blank=True,
100
+ help_text="Custom graph structure (only if not using template)"
101
+ )
102
+
103
+ config = models.JSONField(
104
+ default=dict,
105
+ help_text="Configuration that merges with template.default_config"
106
+ )
107
+
108
+ is_default = models.BooleanField(
109
+ default=False,
110
+ help_text="Whether this is the default flow for the company"
111
+ )
112
+
113
+ class Meta:
114
+ app_label = 'constec_db'
115
+ db_table = 'constancia"."flows'
116
+ unique_together = [['company', 'name']]
117
+ indexes = [
118
+ models.Index(fields=['company', 'is_default']),
119
+ models.Index(fields=['template', 'is_active']),
120
+ ]
121
+
122
+ def get_graph_definition(self):
123
+ """Returns the graph definition (from template or custom)."""
124
+ if self.template:
125
+ return self.template.graph_definition
126
+ return self.graph_definition
127
+
128
+ def get_merged_config(self):
129
+ """Merges template defaults with flow config."""
130
+ if self.template:
131
+ merged = {**self.template.default_config}
132
+ merged.update(self.config)
133
+ return merged
134
+ return self.config
135
+
136
+ def __str__(self):
137
+ default = " (Default)" if self.is_default else ""
138
+ return f"{self.company.name} - {self.name}{default}"
@@ -0,0 +1,36 @@
1
+ from django.db import models
2
+ from .base import UUIDModel
3
+ from .company import Company
4
+ from .user import CompanyUser
5
+
6
+
7
+ class UserGroup(UUIDModel):
8
+ """Hierarchical user groups within a company."""
9
+ company = models.ForeignKey(
10
+ Company,
11
+ on_delete=models.CASCADE,
12
+ related_name="user_groups",
13
+ null=True,
14
+ blank=True
15
+ )
16
+ name = models.CharField(max_length=100)
17
+ description = models.TextField(blank=True, null=True)
18
+ parent = models.ForeignKey(
19
+ 'self',
20
+ on_delete=models.CASCADE,
21
+ related_name="sub_groups",
22
+ null=True,
23
+ blank=True
24
+ )
25
+ users = models.ManyToManyField(
26
+ CompanyUser,
27
+ related_name="groups"
28
+ )
29
+
30
+ class Meta:
31
+ app_label = 'constec_db'
32
+ db_table = 'core"."user_groups'
33
+ unique_together = [['company', 'name']]
34
+
35
+ def __str__(self):
36
+ return f"{self.name}"
@@ -0,0 +1,67 @@
1
+ from django.db import models
2
+ from .base import UUIDModel
3
+ from .company import Company
4
+ from .organization import Organization
5
+
6
+
7
+ class Module(UUIDModel):
8
+ """Available modules in the platform."""
9
+ name = models.CharField(max_length=100)
10
+ slug = models.SlugField(max_length=100, null=True)
11
+ description = models.TextField(blank=True, null=True)
12
+ version = models.CharField(max_length=20)
13
+
14
+ class Meta:
15
+ app_label = 'constec_db'
16
+ db_table = 'core"."modules'
17
+
18
+ def __str__(self):
19
+ return f"{self.name}"
20
+
21
+
22
+ class CompanyModule(UUIDModel):
23
+ """Modules enabled per company."""
24
+ company = models.ForeignKey(
25
+ Company,
26
+ on_delete=models.CASCADE,
27
+ related_name="company_modules",
28
+ )
29
+ module = models.ForeignKey(
30
+ Module,
31
+ on_delete=models.CASCADE,
32
+ related_name="company_modules",
33
+ )
34
+ is_enabled = models.BooleanField(default=True)
35
+ settings = models.JSONField(default=dict, blank=True)
36
+
37
+ class Meta:
38
+ app_label = 'constec_db'
39
+ db_table = 'core"."company_modules'
40
+ unique_together = [['company', 'module']]
41
+
42
+ def __str__(self):
43
+ return f"{self.company.name} - {self.module.name}"
44
+
45
+
46
+ class OrganizationModule(UUIDModel):
47
+ """Modules enabled per organization."""
48
+ organization = models.ForeignKey(
49
+ Organization,
50
+ on_delete=models.CASCADE,
51
+ related_name="organization_modules",
52
+ )
53
+ module = models.ForeignKey(
54
+ Module,
55
+ on_delete=models.CASCADE,
56
+ related_name="organization_modules",
57
+ )
58
+ is_enabled = models.BooleanField(default=True)
59
+ settings = models.JSONField(default=dict, blank=True)
60
+
61
+ class Meta:
62
+ app_label = 'constec_db'
63
+ db_table = 'core"."organization_modules'
64
+ unique_together = [['organization', 'module']]
65
+
66
+ def __str__(self):
67
+ return f"{self.organization.name} - {self.module.name}"
@@ -0,0 +1,62 @@
1
+ from django.db import models
2
+ from .base import UUIDModel
3
+
4
+
5
+ class Organization(UUIDModel):
6
+ """Platform-level tenant (e.g., Constec itself)."""
7
+ name = models.CharField(max_length=255)
8
+ slug = models.SlugField(max_length=100, unique=True)
9
+ description = models.TextField(blank=True, null=True)
10
+
11
+ class Meta:
12
+ app_label = 'constec_db'
13
+ db_table = 'core"."organizations'
14
+
15
+ def __str__(self):
16
+ return self.name
17
+
18
+
19
+ class OrganizationRole(UUIDModel):
20
+ """Roles within an organization."""
21
+ organization = models.ForeignKey(
22
+ Organization,
23
+ on_delete=models.CASCADE,
24
+ related_name="organization_roles",
25
+ null=True,
26
+ blank=True
27
+ )
28
+ name = models.CharField(max_length=100)
29
+ description = models.TextField(blank=True, null=True)
30
+ permissions = models.JSONField(default=dict)
31
+ is_system_role = models.BooleanField(default=False)
32
+
33
+ class Meta:
34
+ app_label = 'constec_db'
35
+ db_table = 'core"."organization_roles'
36
+ unique_together = [['organization', 'name']]
37
+
38
+ def __str__(self):
39
+ return self.name
40
+
41
+
42
+ class OrganizationUser(UUIDModel):
43
+ """Administrator users at organization level."""
44
+ organization = models.ForeignKey(
45
+ Organization,
46
+ on_delete=models.CASCADE,
47
+ related_name="users",
48
+ )
49
+ name = models.CharField(max_length=255)
50
+ email = models.EmailField(unique=True)
51
+ password_hash = models.CharField(max_length=255)
52
+ role = models.ForeignKey(
53
+ OrganizationRole,
54
+ on_delete=models.PROTECT
55
+ )
56
+
57
+ class Meta:
58
+ app_label = 'constec_db'
59
+ db_table = 'core"."organization_users'
60
+
61
+ def __str__(self):
62
+ return f"{self.name} ({self.email})"
@@ -0,0 +1,28 @@
1
+ from django.db import models
2
+ from .base import UUIDModel
3
+ from .company import Company
4
+
5
+
6
+ class Person(UUIDModel):
7
+ """Real person (customer, supplier, employee)."""
8
+ company = models.ForeignKey(
9
+ Company,
10
+ on_delete=models.CASCADE,
11
+ related_name="persons",
12
+ )
13
+ first_name = models.CharField(max_length=100)
14
+ last_name = models.CharField(max_length=100)
15
+ full_name = models.CharField(max_length=255)
16
+ metadata = models.JSONField(default=dict, blank=True)
17
+
18
+ class Meta:
19
+ app_label = 'constec_db'
20
+ db_table = 'core"."persons'
21
+
22
+ def __str__(self):
23
+ return f"{self.full_name}"
24
+
25
+ def save(self, *args, **kwargs):
26
+ """Auto-generate full_name from first_name and last_name."""
27
+ self.full_name = f"{self.first_name} {self.last_name}"
28
+ super().save(*args, **kwargs)
@@ -0,0 +1,89 @@
1
+ from django.db import models
2
+ from .base import UUIDModel
3
+ from .company import Company
4
+ from .person import Person
5
+ from .flow import Flow
6
+
7
+
8
+ class Session(UUIDModel):
9
+ """Conversation session between a user and the AI agent.
10
+
11
+ Tracks the conversation context including company, user, and permissions.
12
+ All MCP tools receive the session_id to know who is making the request.
13
+ """
14
+
15
+ company = models.ForeignKey(
16
+ Company,
17
+ on_delete=models.CASCADE,
18
+ related_name="sessions",
19
+ help_text="The company this session belongs to"
20
+ )
21
+ user = models.ForeignKey(
22
+ Person,
23
+ on_delete=models.CASCADE,
24
+ related_name="sessions",
25
+ help_text="The person chatting (can be employee or client)"
26
+ )
27
+ flow = models.ForeignKey(
28
+ Flow,
29
+ on_delete=models.PROTECT,
30
+ related_name="sessions",
31
+ help_text="AI flow configuration to use"
32
+ )
33
+
34
+ metadata = models.JSONField(
35
+ default=dict,
36
+ help_text="Session context (connection, role, etc.)"
37
+ )
38
+
39
+ ended_at = models.DateTimeField(null=True, blank=True)
40
+
41
+ class Meta:
42
+ app_label = 'constec_db'
43
+ db_table = 'constancia"."sessions'
44
+ indexes = [
45
+ models.Index(fields=['company', 'is_active']),
46
+ models.Index(fields=['user', 'is_active']),
47
+ models.Index(fields=['-created_at']),
48
+ ]
49
+
50
+ def __str__(self):
51
+ return f"Session {self.id} - {self.user} @ {self.company.name}"
52
+
53
+
54
+ class Message(UUIDModel):
55
+ """Message in a conversation session.
56
+
57
+ Stores the conversation history between user and AI assistant.
58
+ """
59
+
60
+ ROLE_CHOICES = [
61
+ ('user', 'User'),
62
+ ('assistant', 'Assistant'),
63
+ ]
64
+
65
+ session = models.ForeignKey(
66
+ Session,
67
+ on_delete=models.CASCADE,
68
+ related_name="messages",
69
+ )
70
+ role = models.CharField(max_length=20, choices=ROLE_CHOICES)
71
+ content = models.TextField()
72
+
73
+ metadata = models.JSONField(
74
+ default=dict,
75
+ blank=True,
76
+ help_text="Additional message data: tool calls, errors, context, etc."
77
+ )
78
+
79
+ class Meta:
80
+ app_label = 'constec_db'
81
+ db_table = 'constancia"."messages'
82
+ ordering = ['created_at']
83
+ indexes = [
84
+ models.Index(fields=['session', 'created_at']),
85
+ ]
86
+
87
+ def __str__(self):
88
+ preview = self.content[:50] + "..." if len(self.content) > 50 else self.content
89
+ return f"{self.role}: {preview}"