constec 0.1.0__py3-none-any.whl → 0.2.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.
- constec/db/__init__.py +11 -0
- constec/db/apps.py +8 -0
- constec/db/migrations/0001_initial.py +539 -0
- constec/db/migrations/__init__.py +0 -0
- constec/db/models/__init__.py +91 -0
- constec/db/models/base.py +14 -0
- constec/db/models/company.py +36 -0
- constec/db/models/contact.py +68 -0
- constec/db/models/erp.py +109 -0
- constec/db/models/erp_entity.py +113 -0
- constec/db/models/flow.py +145 -0
- constec/db/models/group.py +36 -0
- constec/db/models/module.py +43 -0
- constec/db/models/organization.py +62 -0
- constec/db/models/person.py +28 -0
- constec/db/models/session.py +94 -0
- constec/db/models/tag.py +68 -0
- constec/db/models/user.py +72 -0
- constec/py.typed +0 -0
- constec/services/__init__.py +14 -0
- constec/services/encryption.py +92 -0
- constec/utils/__init__.py +20 -0
- constec/utils/cuit.py +107 -0
- constec/utils/password.py +62 -0
- {constec-0.1.0.dist-info → constec-0.2.1.dist-info}/METADATA +12 -2
- constec-0.2.1.dist-info/RECORD +31 -0
- {constec-0.1.0.dist-info → constec-0.2.1.dist-info}/WHEEL +1 -1
- constec-0.1.0.dist-info/RECORD +0 -7
- {constec-0.1.0.dist-info → constec-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {constec-0.1.0.dist-info → constec-0.2.1.dist-info}/top_level.txt +0 -0
|
@@ -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)
|
constec/db/models/erp.py
ADDED
|
@@ -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}"
|
|
@@ -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)
|