constec 0.1.0__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.
@@ -0,0 +1,91 @@
1
+ """
2
+ constec.db.models - All Django models for the Constec platform.
3
+
4
+ Usage:
5
+ from constec.db.models import Company, User, Session, Message
6
+
7
+ # ERP models (from erp schema, no prefix needed):
8
+ from constec.db.models import System, Connection, Entity
9
+
10
+ # Or import from specific modules:
11
+ from constec.db.models.organization import Organization
12
+ from constec.db.models.erp import System, Connection
13
+ from constec.db.models.erp_entity import Role, Entity, EntityAuth
14
+ """
15
+
16
+ # Base
17
+ from .base import UUIDModel
18
+
19
+ # Core models (core schema)
20
+ from .organization import Organization, OrganizationRole, OrganizationUser
21
+ from .company import Company
22
+ from .user import User, UserRole, UserCompanyAccess
23
+ from .person import Person
24
+ from .group import UserGroup
25
+ from .contact import ContactType, Contact, PersonContact
26
+ from .tag import TagCategory, PersonTag, PersonTagged
27
+ from .module import Module, CompanyModule
28
+
29
+ # ERP models (erp schema)
30
+ from .erp import System, CompanySystem, Connection
31
+ from .erp_entity import Role, Entity, EntityAuth
32
+
33
+ # Aliases for backward compatibility
34
+ ErpSystem = System
35
+ CompanyErpSystem = CompanySystem
36
+ ErpConnection = Connection
37
+ ErpRole = Role
38
+ ErpEntity = Entity
39
+
40
+ # Constancia models (constancia schema)
41
+ from .flow import FlowTemplate, Flow
42
+ from .session import Session, Message
43
+
44
+
45
+ __all__ = [
46
+ # Base
47
+ 'UUIDModel',
48
+ # Organization
49
+ 'Organization',
50
+ 'OrganizationRole',
51
+ 'OrganizationUser',
52
+ # Company
53
+ 'Company',
54
+ # User
55
+ 'User',
56
+ 'UserRole',
57
+ 'UserCompanyAccess',
58
+ # Person
59
+ 'Person',
60
+ # Group
61
+ 'UserGroup',
62
+ # Contact
63
+ 'ContactType',
64
+ 'Contact',
65
+ 'PersonContact',
66
+ # Tag
67
+ 'TagCategory',
68
+ 'PersonTag',
69
+ 'PersonTagged',
70
+ # Module
71
+ 'Module',
72
+ 'CompanyModule',
73
+ # ERP (erp schema)
74
+ 'System',
75
+ 'CompanySystem',
76
+ 'Connection',
77
+ 'Role',
78
+ 'Entity',
79
+ 'EntityAuth',
80
+ # ERP aliases (backward compatibility)
81
+ 'ErpSystem',
82
+ 'CompanyErpSystem',
83
+ 'ErpConnection',
84
+ 'ErpRole',
85
+ 'ErpEntity',
86
+ # Constancia (constancia schema)
87
+ 'FlowTemplate',
88
+ 'Flow',
89
+ 'Session',
90
+ 'Message',
91
+ ]
@@ -0,0 +1,14 @@
1
+ import uuid
2
+ from django.db import models
3
+
4
+
5
+ class UUIDModel(models.Model):
6
+ """Base model with UUID as primary key.
7
+
8
+ All Constec models inherit from this to ensure consistent
9
+ UUID-based primary keys across the platform.
10
+ """
11
+ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
12
+
13
+ class Meta:
14
+ abstract = True
@@ -0,0 +1,36 @@
1
+ from django.db import models
2
+ from django.core.exceptions import ValidationError
3
+ from .base import UUIDModel
4
+ from .organization import Organization
5
+
6
+
7
+ class Company(UUIDModel):
8
+ """Client company, belongs to an Organization."""
9
+ organization = models.ForeignKey(
10
+ Organization,
11
+ on_delete=models.CASCADE,
12
+ related_name="companies",
13
+ )
14
+ name = models.CharField(max_length=255)
15
+ legal_name = models.CharField(max_length=255)
16
+ slug = models.SlugField(max_length=100, unique=True)
17
+ parent_company = models.ForeignKey(
18
+ 'self',
19
+ null=True,
20
+ blank=True,
21
+ on_delete=models.CASCADE,
22
+ related_name='children'
23
+ )
24
+ website = models.URLField(blank=True, null=True)
25
+ is_active = models.BooleanField(default=True)
26
+
27
+ class Meta:
28
+ app_label = 'constec_db'
29
+ db_table = 'core"."companies'
30
+
31
+ def __str__(self):
32
+ return self.name
33
+
34
+ def clean(self):
35
+ if self.parent_company == self:
36
+ raise ValidationError('A company cannot be its own parent')
@@ -0,0 +1,68 @@
1
+ from django.db import models
2
+ from .base import UUIDModel
3
+ from .person import Person
4
+
5
+
6
+ class ContactType(UUIDModel):
7
+ """Types of contact (email, phone, etc)."""
8
+ name = models.CharField(max_length=50)
9
+ description = models.TextField(blank=True, null=True)
10
+
11
+ class Meta:
12
+ app_label = 'constec_db'
13
+ db_table = 'core"."contact_types'
14
+
15
+ def __str__(self):
16
+ return f"{self.name}"
17
+
18
+
19
+ class Contact(UUIDModel):
20
+ """Contact value."""
21
+ type = models.ForeignKey(
22
+ ContactType,
23
+ on_delete=models.CASCADE,
24
+ related_name="contacts",
25
+ )
26
+ value = models.CharField(max_length=255)
27
+ metadata = models.JSONField(default=dict, blank=True)
28
+
29
+ class Meta:
30
+ app_label = 'constec_db'
31
+ db_table = 'core"."contacts'
32
+
33
+ def __str__(self):
34
+ return f"{self.value} ({self.type.name})"
35
+
36
+
37
+ class PersonContact(UUIDModel):
38
+ """Person-contact relationship."""
39
+ person = models.ForeignKey(
40
+ Person,
41
+ on_delete=models.CASCADE,
42
+ related_name="person_contacts",
43
+ )
44
+ contact = models.ForeignKey(
45
+ Contact,
46
+ on_delete=models.CASCADE,
47
+ related_name="person_contacts",
48
+ )
49
+ is_primary = models.BooleanField(default=False)
50
+
51
+ class Meta:
52
+ app_label = 'constec_db'
53
+ db_table = 'core"."person_contacts'
54
+ unique_together = [['person', 'contact']]
55
+
56
+ def __str__(self):
57
+ return f"{self.person.full_name} - {self.contact.value} ({self.contact.type.name})"
58
+
59
+ def save(self, *args, **kwargs):
60
+ """Remove is_primary from other contacts of the same type for this person."""
61
+ if self.is_primary:
62
+ PersonContact.objects.filter(
63
+ person=self.person,
64
+ contact__type=self.contact.type,
65
+ is_primary=True
66
+ ).exclude(pk=self.pk).update(is_primary=False)
67
+
68
+ super().save(*args, **kwargs)
@@ -0,0 +1,109 @@
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
+ created_at = models.DateTimeField(auto_now_add=True, null=True)
16
+ updated_at = models.DateTimeField(auto_now=True, null=True)
17
+
18
+ class Meta:
19
+ app_label = 'constec_db'
20
+ db_table = '"erp"."systems"'
21
+
22
+ def __str__(self):
23
+ return f"{self.name}"
24
+
25
+
26
+ class CompanySystem(UUIDModel):
27
+ """Junction table linking companies to ERP systems.
28
+
29
+ Allows many-to-many relationship:
30
+ - A company can use multiple ERP systems
31
+ - An ERP system can be used by multiple companies
32
+ """
33
+ company = models.ForeignKey(
34
+ Company,
35
+ on_delete=models.CASCADE,
36
+ related_name="erp_systems",
37
+ )
38
+ system = models.ForeignKey(
39
+ System,
40
+ on_delete=models.CASCADE,
41
+ related_name="company_assignments",
42
+ )
43
+ is_primary = models.BooleanField(
44
+ default=False,
45
+ help_text="Whether this is the primary ERP system for the company"
46
+ )
47
+ is_active = models.BooleanField(default=True)
48
+ metadata = models.JSONField(default=dict, blank=True)
49
+ created_at = models.DateTimeField(auto_now_add=True)
50
+ updated_at = models.DateTimeField(auto_now=True)
51
+
52
+ class Meta:
53
+ app_label = 'constec_db'
54
+ db_table = '"erp"."company_systems"'
55
+ unique_together = [['company', 'system']]
56
+ indexes = [
57
+ models.Index(fields=['company', 'is_primary']),
58
+ models.Index(fields=['system', 'is_active']),
59
+ ]
60
+
61
+ def __str__(self):
62
+ primary = " (Primary)" if self.is_primary else ""
63
+ return f"{self.company.name} → {self.system.name}{primary}"
64
+
65
+
66
+ class Connection(UUIDModel):
67
+ """Database connection credentials for an ERP system."""
68
+
69
+ DB_TYPE_CHOICES = [
70
+ ('mssql', 'SQL Server'),
71
+ ('postgresql', 'PostgreSQL'),
72
+ ('mysql', 'MySQL'),
73
+ ]
74
+
75
+ company = models.ForeignKey(
76
+ Company,
77
+ on_delete=models.CASCADE,
78
+ related_name="erp_connections",
79
+ )
80
+ system = models.ForeignKey(
81
+ System,
82
+ on_delete=models.CASCADE,
83
+ related_name="connections",
84
+ )
85
+ name = models.CharField(
86
+ max_length=100,
87
+ blank=True,
88
+ default='',
89
+ help_text="Descriptive name for this connection"
90
+ )
91
+ slug = models.CharField(max_length=50)
92
+ db_type = models.CharField(max_length=20, choices=DB_TYPE_CHOICES, default='mssql')
93
+ host = models.CharField(max_length=255)
94
+ port = models.IntegerField()
95
+ database = models.CharField(max_length=100)
96
+ username = models.CharField(max_length=100)
97
+ encrypted_password = models.TextField()
98
+ is_active = models.BooleanField(default=True)
99
+ last_tested_at = models.DateTimeField(null=True, blank=True)
100
+ created_at = models.DateTimeField(auto_now_add=True)
101
+ updated_at = models.DateTimeField(auto_now=True)
102
+
103
+ class Meta:
104
+ app_label = 'constec_db'
105
+ db_table = '"erp"."connections"'
106
+ unique_together = [['system', 'slug']]
107
+
108
+ def __str__(self):
109
+ return f"{self.name}" if self.name else f"{self.system.name} ({self.slug})"
@@ -0,0 +1,113 @@
1
+ from django.db import models
2
+ from .base import UUIDModel
3
+ from .erp import System
4
+ from .person import Person
5
+
6
+
7
+ class Role(UUIDModel):
8
+ """Roles within the ERP (customer, supplier, seller)."""
9
+ system = models.ForeignKey(
10
+ System,
11
+ on_delete=models.CASCADE,
12
+ related_name="roles",
13
+ )
14
+ name = models.CharField(max_length=100)
15
+ description = models.TextField(blank=True, null=True)
16
+
17
+ class Meta:
18
+ app_label = 'constec_db'
19
+ db_table = '"erp"."roles"'
20
+ unique_together = [['system', 'name']]
21
+
22
+ def __str__(self):
23
+ return f"{self.name}"
24
+
25
+
26
+ class Entity(UUIDModel):
27
+ """Link between ERP external entities and Core persons.
28
+
29
+ An Entity can exist without a Person (when someone has ERP access
30
+ but no Core account yet). When they authenticate via CUIT, we can
31
+ optionally link them to a Person later.
32
+ """
33
+ person = models.ForeignKey(
34
+ Person,
35
+ on_delete=models.CASCADE,
36
+ related_name="erp_entities",
37
+ null=True,
38
+ blank=True,
39
+ help_text="Optional link to Core person"
40
+ )
41
+ system = models.ForeignKey(
42
+ System,
43
+ on_delete=models.CASCADE,
44
+ related_name="entities",
45
+ )
46
+ role = models.ForeignKey(
47
+ Role,
48
+ on_delete=models.PROTECT,
49
+ related_name="entities",
50
+ )
51
+ external_id = models.CharField(
52
+ max_length=255,
53
+ help_text="Entity ID in external ERP system (e.g., cli_Cod, pro_Cod)"
54
+ )
55
+ cuit = models.CharField(
56
+ max_length=13,
57
+ blank=True,
58
+ default='',
59
+ help_text="CUIT without dashes (for authentication lookup)"
60
+ )
61
+ metadata = models.JSONField(default=dict, blank=True)
62
+
63
+ class Meta:
64
+ app_label = 'constec_db'
65
+ db_table = '"erp"."entities"'
66
+ unique_together = [['system', 'role', 'external_id']]
67
+ indexes = [
68
+ models.Index(fields=['cuit']),
69
+ models.Index(fields=['system', 'cuit']),
70
+ ]
71
+
72
+ def __str__(self):
73
+ person_info = f"({self.person.full_name})" if self.person else "(no person linked)"
74
+ return f"[{self.system.name}] {self.role.name} {self.external_id} {person_info}"
75
+
76
+
77
+ class EntityAuth(UUIDModel):
78
+ """Authentication credentials for ERP entities.
79
+
80
+ Stores hashed passwords for entities that can authenticate via Constancia AI.
81
+ Linked to Entity (which may or may not have a Person linked).
82
+ """
83
+ entity = models.OneToOneField(
84
+ Entity,
85
+ on_delete=models.CASCADE,
86
+ related_name="auth",
87
+ help_text="The ERP entity these credentials belong to"
88
+ )
89
+ password_hash = models.CharField(
90
+ max_length=255,
91
+ help_text="Bcrypt hashed password"
92
+ )
93
+ is_active = models.BooleanField(
94
+ default=True,
95
+ help_text="Whether this account can authenticate"
96
+ )
97
+ created_at = models.DateTimeField(auto_now_add=True)
98
+ updated_at = models.DateTimeField(auto_now=True)
99
+ last_login = models.DateTimeField(
100
+ null=True,
101
+ blank=True,
102
+ help_text="Last successful login timestamp"
103
+ )
104
+
105
+ class Meta:
106
+ app_label = 'constec_db'
107
+ db_table = '"erp"."entity_auth"'
108
+ verbose_name = "Entity Authentication"
109
+ verbose_name_plural = "Entity Authentications"
110
+
111
+ def __str__(self):
112
+ status = "active" if self.is_active else "inactive"
113
+ return f"Auth for {self.entity} ({status})"
@@ -0,0 +1,145 @@
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
+ is_active = models.BooleanField(default=True)
50
+
51
+ version = models.CharField(max_length=20, default="1.0.0")
52
+ created_by = models.ForeignKey(
53
+ OrganizationUser,
54
+ on_delete=models.SET_NULL,
55
+ null=True,
56
+ related_name="created_templates"
57
+ )
58
+ created_at = models.DateTimeField(auto_now_add=True)
59
+ updated_at = models.DateTimeField(auto_now=True)
60
+
61
+ class Meta:
62
+ app_label = 'constec_db'
63
+ db_table = 'constancia"."flow_templates'
64
+ unique_together = [['organization', 'name', 'version']]
65
+ indexes = [
66
+ models.Index(fields=['organization', 'is_global']),
67
+ models.Index(fields=['category', 'is_active']),
68
+ ]
69
+
70
+ def __str__(self):
71
+ global_tag = " [GLOBAL]" if self.is_global else ""
72
+ return f"{self.name} v{self.version}{global_tag}"
73
+
74
+
75
+ class Flow(UUIDModel):
76
+ """Company-specific flow configuration.
77
+
78
+ Can either use a template or define custom graph.
79
+ Defines how the AI agent behaves for a specific company.
80
+ """
81
+
82
+ company = models.ForeignKey(
83
+ Company,
84
+ on_delete=models.CASCADE,
85
+ related_name="flows",
86
+ )
87
+
88
+ name = models.CharField(max_length=100)
89
+ description = models.TextField(blank=True, null=True)
90
+
91
+ template = models.ForeignKey(
92
+ FlowTemplate,
93
+ null=True,
94
+ blank=True,
95
+ on_delete=models.SET_NULL,
96
+ related_name="flows",
97
+ help_text="If set, uses template's graph. config overrides template defaults."
98
+ )
99
+
100
+ graph_definition = models.JSONField(
101
+ null=True,
102
+ blank=True,
103
+ help_text="Custom graph structure (only if not using template)"
104
+ )
105
+
106
+ config = models.JSONField(
107
+ default=dict,
108
+ help_text="Configuration that merges with template.default_config"
109
+ )
110
+
111
+ is_active = models.BooleanField(default=True)
112
+ is_default = models.BooleanField(
113
+ default=False,
114
+ help_text="Whether this is the default flow for the company"
115
+ )
116
+
117
+ created_at = models.DateTimeField(auto_now_add=True)
118
+ updated_at = models.DateTimeField(auto_now=True)
119
+
120
+ class Meta:
121
+ app_label = 'constec_db'
122
+ db_table = 'constancia"."flows'
123
+ unique_together = [['company', 'name']]
124
+ indexes = [
125
+ models.Index(fields=['company', 'is_default']),
126
+ models.Index(fields=['template', 'is_active']),
127
+ ]
128
+
129
+ def get_graph_definition(self):
130
+ """Returns the graph definition (from template or custom)."""
131
+ if self.template:
132
+ return self.template.graph_definition
133
+ return self.graph_definition
134
+
135
+ def get_merged_config(self):
136
+ """Merges template defaults with flow config."""
137
+ if self.template:
138
+ merged = {**self.template.default_config}
139
+ merged.update(self.config)
140
+ return merged
141
+ return self.config
142
+
143
+ def __str__(self):
144
+ default = " (Default)" if self.is_default else ""
145
+ 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 User
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
+ User,
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,43 @@
1
+ from django.db import models
2
+ from .base import UUIDModel
3
+ from .company import Company
4
+
5
+
6
+ class Module(UUIDModel):
7
+ """Available modules in the platform."""
8
+ name = models.CharField(max_length=100)
9
+ slug = models.SlugField(max_length=100, null=True)
10
+ description = models.TextField(blank=True, null=True)
11
+ version = models.CharField(max_length=20)
12
+ is_active = models.BooleanField(default=True)
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}"