amsdal_crm 0.2.1__tar.gz → 0.2.3__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.
Files changed (75) hide show
  1. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/PKG-INFO +1 -1
  2. amsdal_crm-0.2.3/amsdal_crm/__about__.py +1 -0
  3. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/migrations/0000_initial.py +12 -8
  4. amsdal_crm-0.2.3/amsdal_crm/models/common.py +45 -0
  5. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/models/deal.py +4 -35
  6. amsdal_crm-0.2.3/amsdal_crm/models/entity.py +122 -0
  7. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/pyproject.toml +5 -0
  8. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/uv.lock +362 -363
  9. amsdal_crm-0.2.1/amsdal_crm/__about__.py +0 -1
  10. amsdal_crm-0.2.1/amsdal_crm/models/entity.py +0 -326
  11. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/.amsdal/.environment +0 -0
  12. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/.amsdal-cli +0 -0
  13. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/.github/workflows/ci.yml +0 -0
  14. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/.github/workflows/release.yml +0 -0
  15. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/.github/workflows/tag_check.yml +0 -0
  16. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/.gitignore +0 -0
  17. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/CLAUDE.md +0 -0
  18. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/README.md +0 -0
  19. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/RELEASE.md +0 -0
  20. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/Third-Party Materials - AMSDAL Dependencies - License Notices.md +0 -0
  21. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/__init__.py +0 -0
  22. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/app.py +0 -0
  23. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/constants.py +0 -0
  24. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/errors.py +0 -0
  25. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/fixtures/__init__.py +0 -0
  26. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/fixtures/permissions.py +0 -0
  27. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/fixtures/pipelines.py +0 -0
  28. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/lifecycle/__init__.py +0 -0
  29. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/lifecycle/consumer.py +0 -0
  30. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/models/__init__.py +0 -0
  31. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/models/activity.py +0 -0
  32. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/models/attachment.py +0 -0
  33. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/models/custom_field_definition.py +0 -0
  34. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/models/pipeline.py +0 -0
  35. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/models/stage.py +0 -0
  36. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/models/workflow_rule.py +0 -0
  37. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/services/__init__.py +0 -0
  38. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/services/activity_service.py +0 -0
  39. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/services/custom_field_service.py +0 -0
  40. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/services/deal_service.py +0 -0
  41. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/services/email_service.py +0 -0
  42. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/services/workflow_service.py +0 -0
  43. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/amsdal_crm/settings.py +0 -0
  44. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/config.yml +0 -0
  45. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/license_check.py +0 -0
  46. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/scripts/release.sh +0 -0
  47. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/scripts/tag_check.sh +0 -0
  48. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/conftest.py +0 -0
  49. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/integration/__init__.py +0 -0
  50. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/integration/conftest.py +0 -0
  51. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/integration/services/__init__.py +0 -0
  52. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/integration/services/test_deal_service.py +0 -0
  53. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/integration/services/test_email_service.py +0 -0
  54. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/integration/services/test_workflow_service.py +0 -0
  55. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/unit/__init__.py +0 -0
  56. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/unit/conftest.py +0 -0
  57. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/unit/lifecycle/__init__.py +0 -0
  58. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/unit/lifecycle/test_consumer.py +0 -0
  59. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/unit/models/__init__.py +0 -0
  60. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/unit/models/test_activity.py +0 -0
  61. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/unit/models/test_deal.py +0 -0
  62. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/unit/models/test_entity.py +0 -0
  63. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/unit/models/test_pipeline.py +0 -0
  64. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/unit/models/test_remaining.py +0 -0
  65. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/unit/models/test_stage.py +0 -0
  66. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/unit/services/__init__.py +0 -0
  67. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/unit/services/test_activity_service.py +0 -0
  68. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/unit/services/test_custom_field_service.py +0 -0
  69. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/unit/services/test_deal_service.py +0 -0
  70. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/unit/services/test_email_service.py +0 -0
  71. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/unit/services/test_workflow_service.py +0 -0
  72. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/unit/test_app.py +0 -0
  73. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/unit/test_constants.py +0 -0
  74. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/unit/test_errors.py +0 -0
  75. {amsdal_crm-0.2.1 → amsdal_crm-0.2.3}/tests/unit/test_settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amsdal_crm
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: amsdal-crm plugin for AMSDAL Framework
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: aiohttp==3.12.15
@@ -0,0 +1 @@
1
+ __version__ = '0.2.3'
@@ -65,6 +65,7 @@ class Migration(migrations.Migration):
65
65
  "properties": {
66
66
  "created_at": {"type": "datetime", "title": "Created At", "format": "date-time"},
67
67
  "updated_at": {"type": "datetime", "title": "Updated At", "format": "date-time"},
68
+ "custom_fields": {"type": "anything", "title": "Custom Fields"},
68
69
  "name": {"type": "string", "title": "Entity Name"},
69
70
  "legal_name": {"type": "string", "title": "Legal Name"},
70
71
  "status": {
@@ -75,10 +76,9 @@ class Migration(migrations.Migration):
75
76
  "enum": ["Active", "Inactive"],
76
77
  },
77
78
  "note": {"type": "string", "title": "Note"},
78
- "custom_fields": {"type": "anything", "title": "Custom Fields"},
79
79
  "assigned_to": {"type": "User", "title": "Assigned To"},
80
80
  },
81
- "custom_code": 'from amsdal.contrib.auth.models.user import User\n\n\n@property\ndef display_name(self) -> str:\n """Return display name for the account."""\n return self.name\n\nasync def apost_update(self) -> None:\n """Async hook called after updating account."""\n from amsdal_crm.services.workflow_service import WorkflowService\n await WorkflowService.aexecute_rules(\'Entity\', \'update\', self)\n\nasync def apre_create(self) -> None:\n """Async hook called before creating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = await CustomFieldService.avalidate_custom_fields(\'Entity\', self.custom_fields)\n await super().apre_create()\n\nasync def apre_update(self) -> None:\n """Async hook called before updating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = await CustomFieldService.avalidate_custom_fields(\'Entity\', self.custom_fields)\n await super().apre_update()\n\ndef has_object_permission(self, user: \'User\', action: str) -> bool:\n """Check if user has permission to perform action on this account.\n\n Args:\n user: The user attempting the action\n action: The action being attempted (read, create, update, delete)\n\n Returns:\n True if user has permission, False otherwise\n """\n if self.assigned_to and self.assigned_to.email == user.email:\n return True\n if user.permissions:\n for permission in user.permissions:\n if permission.model == \'*\' and permission.action in (\'*\', action):\n return True\n if permission.model == \'Entity\' and permission.action in (\'*\', action):\n return True\n return False\n\ndef post_update(self) -> None:\n """Hook called after updating account."""\n from amsdal_crm.services.workflow_service import WorkflowService\n WorkflowService.execute_rules(\'Entity\', \'update\', self)\n\ndef pre_create(self) -> None:\n """Hook called before creating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'Entity\', self.custom_fields)\n super().pre_create()\n\ndef pre_update(self) -> None:\n """Hook called before updating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'Entity\', self.custom_fields)\n super().pre_update()',
81
+ "custom_code": 'from amsdal.contrib.auth.models.user import User\n\n\n@classmethod\ndef custom_fields_cell_template(cls) -> str:\n return \'JsonTemplate\'\n\n@property\ndef display_name(self) -> str:\n """Return display name for the account."""\n return self.name\n\nasync def _avalidate_custom_fields(self) -> None:\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = await CustomFieldService.avalidate_custom_fields(self.__class__.__name__, self.custom_fields)\n\nasync def apost_update(self) -> None:\n """Async hook called after updating account."""\n from amsdal_crm.services.workflow_service import WorkflowService\n await WorkflowService.aexecute_rules(\'Entity\', \'update\', self)\n\nasync def apre_create(self) -> None:\n await self._avalidate_custom_fields()\n await super().apre_create()\n\nasync def apre_update(self) -> None:\n await self._avalidate_custom_fields()\n await super().apre_update()\n\ndef _validate_custom_fields(self) -> None:\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(self.__class__.__name__, self.custom_fields)\n\ndef has_object_permission(self, user: \'User\', action: str) -> bool:\n """Check if user has permission to perform action on this account.\n\n Args:\n user: The user attempting the action\n action: The action being attempted (read, create, update, delete)\n\n Returns:\n True if user has permission, False otherwise\n """\n if self.assigned_to and self.assigned_to.email == user.email:\n return True\n if user.permissions:\n for permission in user.permissions:\n if permission.model == \'*\' and permission.action in (\'*\', action):\n return True\n if permission.model == \'Entity\' and permission.action in (\'*\', action):\n return True\n return False\n\ndef post_update(self) -> None:\n """Hook called after updating account."""\n from amsdal_crm.services.workflow_service import WorkflowService\n WorkflowService.execute_rules(\'Entity\', \'update\', self)\n\ndef pre_create(self) -> None:\n self._validate_custom_fields()\n super().pre_create()\n\ndef pre_update(self) -> None:\n self._validate_custom_fields()\n super().pre_update()',
82
82
  "storage_metadata": {
83
83
  "table_name": "Entity",
84
84
  "db_fields": {"assigned_to": ["assigned_to_partition_key"]},
@@ -100,6 +100,7 @@ class Migration(migrations.Migration):
100
100
  "properties": {
101
101
  "created_at": {"type": "datetime", "title": "Created At", "format": "date-time"},
102
102
  "updated_at": {"type": "datetime", "title": "Updated At", "format": "date-time"},
103
+ "custom_fields": {"type": "anything", "title": "Custom Fields"},
103
104
  "line1": {"type": "string", "title": "Address Line 1"},
104
105
  "line2": {"type": "string", "title": "Address Line 2"},
105
106
  "city": {"type": "string", "title": "City"},
@@ -108,7 +109,7 @@ class Migration(migrations.Migration):
108
109
  "country": {"type": "string", "title": "Country"},
109
110
  "is_primary": {"type": "boolean", "default": False, "title": "Is Primary"},
110
111
  },
111
- "custom_code": 'async def apre_create(self) -> None:\n """Async hook called before creating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = await CustomFieldService.avalidate_custom_fields(\'EntityAddress\', self.custom_fields)\n await super().apre_create()\n\nasync def apre_update(self) -> None:\n """Async hook called before updating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = await CustomFieldService.avalidate_custom_fields(\'EntityAddress\', self.custom_fields)\n await super().apre_update()\n\ndef pre_create(self) -> None:\n """Hook called before creating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'EntityAddress\', self.custom_fields)\n super().pre_create()\n\ndef pre_update(self) -> None:\n """Hook called before updating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'EntityAddress\', self.custom_fields)\n super().pre_update()',
112
+ "custom_code": "@classmethod\ndef custom_fields_cell_template(cls) -> str:\n return 'JsonTemplate'\n\nasync def _avalidate_custom_fields(self) -> None:\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = await CustomFieldService.avalidate_custom_fields(self.__class__.__name__, self.custom_fields)\n\nasync def apre_create(self) -> None:\n await self._avalidate_custom_fields()\n await super().apre_create()\n\nasync def apre_update(self) -> None:\n await self._avalidate_custom_fields()\n await super().apre_update()\n\ndef _validate_custom_fields(self) -> None:\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(self.__class__.__name__, self.custom_fields)\n\ndef pre_create(self) -> None:\n self._validate_custom_fields()\n super().pre_create()\n\ndef pre_update(self) -> None:\n self._validate_custom_fields()\n super().pre_update()",
112
113
  "storage_metadata": {
113
114
  "table_name": "EntityAddress",
114
115
  "db_fields": {},
@@ -372,6 +373,7 @@ class Migration(migrations.Migration):
372
373
  "properties": {
373
374
  "created_at": {"type": "datetime", "title": "Created At", "format": "date-time"},
374
375
  "updated_at": {"type": "datetime", "title": "Updated At", "format": "date-time"},
376
+ "custom_fields": {"type": "anything", "title": "Custom Fields"},
375
377
  "value": {"type": "string", "title": "Contact Point Value"},
376
378
  "is_primary": {"type": "boolean", "default": False, "title": "Is Primary"},
377
379
  "can_contact": {"type": "boolean", "default": True, "title": "Can Contact"},
@@ -381,7 +383,7 @@ class Migration(migrations.Migration):
381
383
  "description": "Entity (Person/Organization/Trust) model.\n\nRepresents a company or organization in the CRM system.\nOwned by individual users with permission controls.",
382
384
  },
383
385
  },
384
- "custom_code": 'async def apre_create(self) -> None:\n """Async hook called before creating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = await CustomFieldService.avalidate_custom_fields(\'EntityContactPoint\', self.custom_fields)\n await super().apre_create()\n\nasync def apre_update(self) -> None:\n """Async hook called before updating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = await CustomFieldService.avalidate_custom_fields(\'EntityContactPoint\', self.custom_fields)\n await super().apre_update()\n\ndef pre_create(self) -> None:\n """Hook called before creating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'EntityContactPoint\', self.custom_fields)\n super().pre_create()\n\ndef pre_update(self) -> None:\n """Hook called before updating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'EntityContactPoint\', self.custom_fields)\n super().pre_update()',
386
+ "custom_code": "@classmethod\ndef custom_fields_cell_template(cls) -> str:\n return 'JsonTemplate'\n\nasync def _avalidate_custom_fields(self) -> None:\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = await CustomFieldService.avalidate_custom_fields(self.__class__.__name__, self.custom_fields)\n\nasync def apre_create(self) -> None:\n await self._avalidate_custom_fields()\n await super().apre_create()\n\nasync def apre_update(self) -> None:\n await self._avalidate_custom_fields()\n await super().apre_update()\n\ndef _validate_custom_fields(self) -> None:\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(self.__class__.__name__, self.custom_fields)\n\ndef pre_create(self) -> None:\n self._validate_custom_fields()\n super().pre_create()\n\ndef pre_update(self) -> None:\n self._validate_custom_fields()\n super().pre_update()",
385
387
  "storage_metadata": {
386
388
  "table_name": "EntityContactPoint",
387
389
  "db_fields": {"entity": ["entity_partition_key"]},
@@ -399,6 +401,7 @@ class Migration(migrations.Migration):
399
401
  "properties": {
400
402
  "created_at": {"type": "datetime", "title": "Created At", "format": "date-time"},
401
403
  "updated_at": {"type": "datetime", "title": "Updated At", "format": "date-time"},
404
+ "custom_fields": {"type": "anything", "title": "Custom Fields"},
402
405
  "value": {"type": "string", "title": "Identifier Value"},
403
406
  "country": {"type": "string", "title": "Country"},
404
407
  "is_primary": {"type": "boolean", "default": False, "title": "Is Primary"},
@@ -408,7 +411,7 @@ class Migration(migrations.Migration):
408
411
  "description": "Entity (Person/Organization/Trust) model.\n\nRepresents a company or organization in the CRM system.\nOwned by individual users with permission controls.",
409
412
  },
410
413
  },
411
- "custom_code": 'async def apre_create(self) -> None:\n """Async hook called before creating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = await CustomFieldService.avalidate_custom_fields(\'EntityIdentifier\', self.custom_fields)\n await super().apre_create()\n\nasync def apre_update(self) -> None:\n """Async hook called before updating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = await CustomFieldService.avalidate_custom_fields(\'EntityIdentifier\', self.custom_fields)\n await super().apre_update()\n\ndef pre_create(self) -> None:\n """Hook called before creating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'EntityIdentifier\', self.custom_fields)\n super().pre_create()\n\ndef pre_update(self) -> None:\n """Hook called before updating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'EntityIdentifier\', self.custom_fields)\n super().pre_update()',
414
+ "custom_code": "@classmethod\ndef custom_fields_cell_template(cls) -> str:\n return 'JsonTemplate'\n\nasync def _avalidate_custom_fields(self) -> None:\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = await CustomFieldService.avalidate_custom_fields(self.__class__.__name__, self.custom_fields)\n\nasync def apre_create(self) -> None:\n await self._avalidate_custom_fields()\n await super().apre_create()\n\nasync def apre_update(self) -> None:\n await self._avalidate_custom_fields()\n await super().apre_update()\n\ndef _validate_custom_fields(self) -> None:\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(self.__class__.__name__, self.custom_fields)\n\ndef pre_create(self) -> None:\n self._validate_custom_fields()\n super().pre_create()\n\ndef pre_update(self) -> None:\n self._validate_custom_fields()\n super().pre_update()",
412
415
  "storage_metadata": {
413
416
  "table_name": "EntityIdentifier",
414
417
  "db_fields": {"entity": ["entity_partition_key"]},
@@ -426,6 +429,7 @@ class Migration(migrations.Migration):
426
429
  "properties": {
427
430
  "created_at": {"type": "datetime", "title": "Created At", "format": "date-time"},
428
431
  "updated_at": {"type": "datetime", "title": "Updated At", "format": "date-time"},
432
+ "custom_fields": {"type": "anything", "title": "Custom Fields"},
429
433
  "start_date": {"type": "string", "title": "Start Date"},
430
434
  "end_date": {"type": "string", "title": "End Date"},
431
435
  "relationship_group_name": {"type": "string", "title": "Relationship Group Name"},
@@ -440,7 +444,7 @@ class Migration(migrations.Migration):
440
444
  "description": "Entity (Person/Organization/Trust) model.\n\nRepresents a company or organization in the CRM system.\nOwned by individual users with permission controls.",
441
445
  },
442
446
  },
443
- "custom_code": 'async def apre_create(self) -> None:\n """Async hook called before creating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = await CustomFieldService.avalidate_custom_fields(\'EntityRelationship\', self.custom_fields)\n await super().apre_create()\n\nasync def apre_update(self) -> None:\n """Async hook called before updating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = await CustomFieldService.avalidate_custom_fields(\'EntityRelationship\', self.custom_fields)\n await super().apre_update()\n\ndef pre_create(self) -> None:\n """Hook called before creating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'EntityRelationship\', self.custom_fields)\n super().pre_create()\n\ndef pre_update(self) -> None:\n """Hook called before updating account."""\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(\'EntityRelationship\', self.custom_fields)\n super().pre_update()',
447
+ "custom_code": "@classmethod\ndef custom_fields_cell_template(cls) -> str:\n return 'JsonTemplate'\n\nasync def _avalidate_custom_fields(self) -> None:\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = await CustomFieldService.avalidate_custom_fields(self.__class__.__name__, self.custom_fields)\n\nasync def apre_create(self) -> None:\n await self._avalidate_custom_fields()\n await super().apre_create()\n\nasync def apre_update(self) -> None:\n await self._avalidate_custom_fields()\n await super().apre_update()\n\ndef _validate_custom_fields(self) -> None:\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(self.__class__.__name__, self.custom_fields)\n\ndef pre_create(self) -> None:\n self._validate_custom_fields()\n super().pre_create()\n\ndef pre_update(self) -> None:\n self._validate_custom_fields()\n super().pre_update()",
444
448
  "storage_metadata": {
445
449
  "table_name": "EntityRelationship",
446
450
  "db_fields": {
@@ -509,6 +513,7 @@ class Migration(migrations.Migration):
509
513
  "properties": {
510
514
  "created_at": {"type": "datetime", "title": "Created At", "format": "date-time"},
511
515
  "updated_at": {"type": "datetime", "title": "Updated At", "format": "date-time"},
516
+ "custom_fields": {"type": "anything", "title": "Custom Fields"},
512
517
  "name": {"type": "string", "title": "Deal Name"},
513
518
  "amount": {"type": "number", "title": "Amount"},
514
519
  "currency": {"type": "string", "default": "USD", "title": "Currency"},
@@ -535,10 +540,9 @@ class Migration(migrations.Migration):
535
540
  "title": "Status",
536
541
  "enum": ["open", "closed_won", "closed_lost"],
537
542
  },
538
- "custom_fields": {"type": "anything", "title": "Custom Fields"},
539
543
  "assigned_to": {"type": "User", "title": "Assigned To"},
540
544
  },
541
- "custom_code": "import datetime as _dt\n\nfrom amsdal.contrib.auth.models.user import User\nfrom amsdal_utils.models.data_models.reference import Reference\n\n\n@property\ndef display_name(self) -> str:\n \"\"\"Return display name for the deal.\"\"\"\n return self.name\n\n@property\ndef stage_name(self) -> str:\n \"\"\"Returns stage name for display.\"\"\"\n if hasattr(self.stage, 'name'):\n return self.stage.name\n return str(self.stage)\n\nasync def apost_update(self) -> None:\n \"\"\"Async hook called after updating deal.\"\"\"\n from amsdal_crm.services.workflow_service import WorkflowService\n await WorkflowService.aexecute_rules('Deal', 'update', self)\n\nasync def apre_create(self) -> None:\n \"\"\"Async hook called before creating deal.\"\"\"\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = await CustomFieldService.avalidate_custom_fields('Deal', self.custom_fields)\n await super().apre_create()\n\nasync def apre_update(self) -> None:\n \"\"\"Async hook called before updating deal.\n\n Automatically syncs is_closed and is_won status with stage,\n and sets closed_date when deal is closed.\n \"\"\"\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = await CustomFieldService.avalidate_custom_fields('Deal', self.custom_fields)\n stage = await self.stage\n if stage.status == 'open':\n self.status = 'open'\n if stage.status == 'closed_won':\n self.status = 'closed_won'\n if stage.status == 'closed_lost':\n self.status = 'closed_lost'\n if self.status in ('closed_won', 'closed_lost') and (not self.closed_date):\n self.closed_date = _dt.datetime.now(_dt.UTC)\n await super().apre_update()\n\ndef has_object_permission(self, user: 'User', action: str) -> bool:\n \"\"\"Check if user has permission to perform action on this deal.\n\n Args:\n user: The user attempting the action\n action: The action being attempted (read, create, update, delete)\n\n Returns:\n True if user has permission, False otherwise\n \"\"\"\n if self.assigned_to and self.assigned_to.email == user.email:\n return True\n if user.permissions:\n for permission in user.permissions:\n if permission.model == '*' and permission.action in ('*', action):\n return True\n if permission.model == 'Deal' and permission.action in ('*', action):\n return True\n return False\n\ndef post_update(self) -> None:\n \"\"\"Hook called after updating deal.\"\"\"\n from amsdal_crm.services.workflow_service import WorkflowService\n WorkflowService.execute_rules('Deal', 'update', self)\n\ndef pre_create(self) -> None:\n \"\"\"Hook called before creating deal.\"\"\"\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields('Deal', self.custom_fields)\n super().pre_create()\n\ndef pre_update(self) -> None:\n \"\"\"Hook called before updating deal.\n\n Automatically syncs is_closed and is_won status with stage,\n and sets closed_date when deal is closed.\n \"\"\"\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields('Deal', self.custom_fields)\n from amsdal_models.classes.helpers.reference_loader import ReferenceLoader\n stage = ReferenceLoader(self.stage).load_reference() if isinstance(self.stage, Reference) else self.stage\n if stage.status == 'open':\n self.status = 'open'\n if stage.status == 'closed_won':\n self.status = 'closed_won'\n if stage.status == 'closed_lost':\n self.status = 'closed_lost'\n if self.status in ('closed_won', 'closed_lost') and (not self.closed_date):\n self.closed_date = _dt.datetime.now(_dt.UTC)\n super().pre_update()",
545
+ "custom_code": "import datetime as _dt\n\nfrom amsdal.contrib.auth.models.user import User\nfrom amsdal_utils.models.data_models.reference import Reference\n\n\n@classmethod\ndef custom_fields_cell_template(cls) -> str:\n return 'JsonTemplate'\n\n@property\ndef display_name(self) -> str:\n \"\"\"Return display name for the deal.\"\"\"\n return self.name\n\n@property\ndef stage_name(self) -> str:\n \"\"\"Returns stage name for display.\"\"\"\n if hasattr(self.stage, 'name'):\n return self.stage.name\n return str(self.stage)\n\nasync def _avalidate_custom_fields(self) -> None:\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = await CustomFieldService.avalidate_custom_fields(self.__class__.__name__, self.custom_fields)\n\nasync def apost_update(self) -> None:\n \"\"\"Async hook called after updating deal.\"\"\"\n from amsdal_crm.services.workflow_service import WorkflowService\n await WorkflowService.aexecute_rules('Deal', 'update', self)\n\nasync def apre_create(self) -> None:\n await self._avalidate_custom_fields()\n await super().apre_create()\n\nasync def apre_update(self) -> None:\n \"\"\"Async hook called before updating deal.\n\n Automatically syncs is_closed and is_won status with stage,\n and sets closed_date when deal is closed.\n \"\"\"\n stage = await self.stage\n if stage.status == 'open':\n self.status = 'open'\n if stage.status == 'closed_won':\n self.status = 'closed_won'\n if stage.status == 'closed_lost':\n self.status = 'closed_lost'\n if self.status in ('closed_won', 'closed_lost') and (not self.closed_date):\n self.closed_date = _dt.datetime.now(_dt.UTC)\n await super().apre_update()\n\ndef _validate_custom_fields(self) -> None:\n if self.custom_fields:\n from amsdal_crm.services.custom_field_service import CustomFieldService\n self.custom_fields = CustomFieldService.validate_custom_fields(self.__class__.__name__, self.custom_fields)\n\ndef has_object_permission(self, user: 'User', action: str) -> bool:\n \"\"\"Check if user has permission to perform action on this deal.\n\n Args:\n user: The user attempting the action\n action: The action being attempted (read, create, update, delete)\n\n Returns:\n True if user has permission, False otherwise\n \"\"\"\n if self.assigned_to and self.assigned_to.email == user.email:\n return True\n if user.permissions:\n for permission in user.permissions:\n if permission.model == '*' and permission.action in ('*', action):\n return True\n if permission.model == 'Deal' and permission.action in ('*', action):\n return True\n return False\n\ndef post_update(self) -> None:\n \"\"\"Hook called after updating deal.\"\"\"\n from amsdal_crm.services.workflow_service import WorkflowService\n WorkflowService.execute_rules('Deal', 'update', self)\n\ndef pre_create(self) -> None:\n self._validate_custom_fields()\n super().pre_create()\n\ndef pre_update(self) -> None:\n \"\"\"Hook called before updating deal.\n\n Automatically syncs is_closed and is_won status with stage,\n and sets closed_date when deal is closed.\n \"\"\"\n from amsdal_models.classes.helpers.reference_loader import ReferenceLoader\n stage = ReferenceLoader(self.stage).load_reference() if isinstance(self.stage, Reference) else self.stage\n if stage.status == 'open':\n self.status = 'open'\n if stage.status == 'closed_won':\n self.status = 'closed_won'\n if stage.status == 'closed_lost':\n self.status = 'closed_lost'\n if self.status in ('closed_won', 'closed_lost') and (not self.closed_date):\n self.closed_date = _dt.datetime.now(_dt.UTC)\n super().pre_update()",
542
546
  "storage_metadata": {
543
547
  "table_name": "Deal",
544
548
  "db_fields": {
@@ -0,0 +1,45 @@
1
+ from typing import Any
2
+
3
+ from pydantic.fields import Field
4
+
5
+
6
+ class CustomFieldsMixin:
7
+ custom_fields: dict[str, Any] | None = Field(default=None, title='Custom Fields')
8
+
9
+ @classmethod
10
+ def custom_fields_cell_template(cls) -> str:
11
+ return 'JsonTemplate'
12
+
13
+ def pre_create(self) -> None:
14
+ self._validate_custom_fields()
15
+ super().pre_create()
16
+
17
+ async def apre_create(self) -> None:
18
+ await self._avalidate_custom_fields()
19
+ await super().apre_create()
20
+
21
+ def pre_update(self) -> None:
22
+ self._validate_custom_fields()
23
+ super().pre_update()
24
+
25
+ async def apre_update(self) -> None:
26
+ await self._avalidate_custom_fields()
27
+ await super().apre_update()
28
+
29
+ def _validate_custom_fields(self) -> None:
30
+ if self.custom_fields:
31
+ from amsdal_crm.services.custom_field_service import CustomFieldService
32
+
33
+ self.custom_fields = CustomFieldService.validate_custom_fields(
34
+ self.__class__.__name__,
35
+ self.custom_fields,
36
+ )
37
+
38
+ async def _avalidate_custom_fields(self) -> None:
39
+ if self.custom_fields:
40
+ from amsdal_crm.services.custom_field_service import CustomFieldService
41
+
42
+ self.custom_fields = await CustomFieldService.avalidate_custom_fields(
43
+ self.__class__.__name__,
44
+ self.custom_fields,
45
+ )
@@ -1,7 +1,6 @@
1
1
  """Deal Model."""
2
2
 
3
3
  import datetime as _dt
4
- from typing import Any
5
4
  from typing import ClassVar
6
5
  from typing import Literal
7
6
 
@@ -14,13 +13,15 @@ from amsdal_utils.models.data_models.reference import Reference
14
13
  from amsdal_utils.models.enums import ModuleType
15
14
  from pydantic.fields import Field
16
15
 
16
+ from models.common import CustomFieldsMixin
17
+
17
18
 
18
19
  class DealManager(Manager):
19
20
  def get_queryset(self) -> 'DealManager':
20
21
  return super().get_queryset().select_related('stage')
21
22
 
22
23
 
23
- class Deal(TimestampMixin, Model):
24
+ class Deal(CustomFieldsMixin, TimestampMixin, Model):
24
25
  """Deal (Sales Opportunity) model.
25
26
 
26
27
  Represents a sales opportunity linked to an account and contact,
@@ -50,9 +51,6 @@ class Deal(TimestampMixin, Model):
50
51
  # Status tracking
51
52
  status: Literal['open', 'closed_won', 'closed_lost'] = Field(default='open', title='Status')
52
53
 
53
- # Custom fields (JSON)
54
- custom_fields: dict[str, Any] | None = Field(default=None, title='Custom Fields')
55
-
56
54
  @property
57
55
  def display_name(self) -> str:
58
56
  """Return display name for the deal."""
@@ -90,34 +88,12 @@ class Deal(TimestampMixin, Model):
90
88
 
91
89
  return False
92
90
 
93
- def pre_create(self) -> None:
94
- """Hook called before creating deal."""
95
- if self.custom_fields:
96
- from amsdal_crm.services.custom_field_service import CustomFieldService
97
-
98
- self.custom_fields = CustomFieldService.validate_custom_fields('Deal', self.custom_fields)
99
- super().pre_create()
100
-
101
- async def apre_create(self) -> None:
102
- """Async hook called before creating deal."""
103
- if self.custom_fields:
104
- from amsdal_crm.services.custom_field_service import CustomFieldService
105
-
106
- self.custom_fields = await CustomFieldService.avalidate_custom_fields('Deal', self.custom_fields)
107
- await super().apre_create()
108
-
109
91
  def pre_update(self) -> None:
110
92
  """Hook called before updating deal.
111
93
 
112
94
  Automatically syncs is_closed and is_won status with stage,
113
95
  and sets closed_date when deal is closed.
114
96
  """
115
- # Validate custom fields first
116
- if self.custom_fields:
117
- from amsdal_crm.services.custom_field_service import CustomFieldService
118
-
119
- self.custom_fields = CustomFieldService.validate_custom_fields('Deal', self.custom_fields)
120
-
121
97
  # Load stage if it's a reference and sync closed status
122
98
  from amsdal_models.classes.helpers.reference_loader import ReferenceLoader
123
99
 
@@ -142,14 +118,7 @@ class Deal(TimestampMixin, Model):
142
118
 
143
119
  Automatically syncs is_closed and is_won status with stage,
144
120
  and sets closed_date when deal is closed.
145
- """
146
- # Validate custom fields first
147
- if self.custom_fields:
148
- from amsdal_crm.services.custom_field_service import CustomFieldService
149
-
150
- self.custom_fields = await CustomFieldService.avalidate_custom_fields('Deal', self.custom_fields)
151
-
152
- # Load stage if it's a reference and sync closed status
121
+ """ # Load stage if it's a reference and sync closed status
153
122
 
154
123
  stage = await self.stage
155
124
  if stage.status == 'open':
@@ -0,0 +1,122 @@
1
+ """Account Model."""
2
+
3
+ from typing import ClassVar
4
+ from typing import Literal
5
+
6
+ from amsdal.contrib.auth.models.user import User
7
+ from amsdal.models.mixins import TimestampMixin
8
+ from amsdal_models.classes.data_models.constraints import UniqueConstraint
9
+ from amsdal_models.classes.data_models.indexes import IndexInfo
10
+ from amsdal_models.classes.model import Model
11
+ from amsdal_utils.models.enums import ModuleType
12
+ from pydantic.fields import Field
13
+
14
+ from models.common import CustomFieldsMixin
15
+
16
+
17
+ class Entity(CustomFieldsMixin, TimestampMixin, Model):
18
+ """Entity (Person/Organization/Trust) model.
19
+
20
+ Represents a company or organization in the CRM system.
21
+ Owned by individual users with permission controls.
22
+ """
23
+
24
+ __module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
25
+ __constraints__: ClassVar[list[UniqueConstraint]] = [UniqueConstraint(name='unq_entity_name', fields=['name'])]
26
+ __indexes__: ClassVar[list[IndexInfo]] = [
27
+ IndexInfo(name='idx_entity_created_at', field='created_at'),
28
+ ]
29
+
30
+ # Core fields
31
+ name: str = Field(title='Entity Name')
32
+ legal_name: str | None = Field(default=None, title='Legal Name')
33
+ status: Literal['Active', 'Inactive'] = Field(default='Active', title='Status')
34
+ note: str | None = Field(default=None, title='Note')
35
+
36
+ assigned_to: User | None = Field(default=None, title='Assigned To')
37
+
38
+ @property
39
+ def display_name(self) -> str:
40
+ """Return display name for the account."""
41
+ return self.name
42
+
43
+ def has_object_permission(self, user: 'User', action: str) -> bool:
44
+ """Check if user has permission to perform action on this account.
45
+
46
+ Args:
47
+ user: The user attempting the action
48
+ action: The action being attempted (read, create, update, delete)
49
+
50
+ Returns:
51
+ True if user has permission, False otherwise
52
+ """
53
+ if self.assigned_to and self.assigned_to.email == user.email:
54
+ return True
55
+
56
+ # Check admin permissions
57
+ if user.permissions:
58
+ for permission in user.permissions:
59
+ if permission.model == '*' and permission.action in ('*', action):
60
+ return True
61
+ if permission.model == 'Entity' and permission.action in ('*', action):
62
+ return True
63
+
64
+ return False
65
+
66
+ def post_update(self) -> None:
67
+ """Hook called after updating account."""
68
+ from amsdal_crm.services.workflow_service import WorkflowService
69
+
70
+ WorkflowService.execute_rules('Entity', 'update', self)
71
+
72
+ async def apost_update(self) -> None:
73
+ """Async hook called after updating account."""
74
+ from amsdal_crm.services.workflow_service import WorkflowService
75
+
76
+ await WorkflowService.aexecute_rules('Entity', 'update', self)
77
+
78
+
79
+ class EntityRelationship(CustomFieldsMixin, TimestampMixin, Model):
80
+ __module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
81
+
82
+ from_entity: Entity = Field(title='From Entity')
83
+ to_entity: Entity = Field(title='To Entity')
84
+ start_date: str | None = Field(default=None, title='Start Date')
85
+ end_date: str | None = Field(default=None, title='End Date')
86
+ relationship_group_name: str | None = Field(default=None, title='Relationship Group Name')
87
+
88
+
89
+ class EntityIdentifier(CustomFieldsMixin, TimestampMixin, Model):
90
+ __module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
91
+
92
+ entity: Entity = Field(title='Entity')
93
+ value: str = Field(title='Identifier Value')
94
+ country: str | None = Field(default=None, title='Country')
95
+
96
+ # TODO: validate one per entity
97
+ is_primary: bool = Field(default=False, title='Is Primary')
98
+
99
+
100
+ class EntityContactPoint(CustomFieldsMixin, TimestampMixin, Model):
101
+ __module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
102
+
103
+ entity: Entity = Field(title='Entity')
104
+ value: str = Field(title='Contact Point Value')
105
+
106
+ # TODO: validate one per entity
107
+ is_primary: bool = Field(default=False, title='Is Primary')
108
+ can_contact: bool = Field(default=True, title='Can Contact')
109
+
110
+
111
+ class EntityAddress(CustomFieldsMixin, TimestampMixin, Model):
112
+ __module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
113
+
114
+ line1: str | None = Field(title='Address Line 1')
115
+ line2: str | None = Field(default=None, title='Address Line 2')
116
+ city: str | None = Field(title='City')
117
+ region: str | None = Field(default=None, title='Region/State')
118
+ postal_code: str | None = Field(default=None, title='Postal Code')
119
+ country: str | None = Field(title='Country')
120
+
121
+ # TODO: validate one per entity
122
+ is_primary: bool = Field(default=False, title='Is Primary')
@@ -8,6 +8,11 @@ include = [
8
8
  "amsdal_crm/py.typed",
9
9
  ]
10
10
 
11
+
12
+ [tool.pyright]
13
+ venvPath = "."
14
+ venv = ".venv"
15
+
11
16
  [project]
12
17
  name = "amsdal_crm"
13
18
  dynamic = ["version"]