diff --git a/api/__pycache__/asgi.cpython-313.pyc b/api/__pycache__/asgi.cpython-313.pyc index e777b434..ccbfa8fd 100644 Binary files a/api/__pycache__/asgi.cpython-313.pyc and b/api/__pycache__/asgi.cpython-313.pyc differ diff --git a/api/__pycache__/settings.cpython-313.pyc b/api/__pycache__/settings.cpython-313.pyc index b34de7a7..7abe7486 100644 Binary files a/api/__pycache__/settings.cpython-313.pyc and b/api/__pycache__/settings.cpython-313.pyc differ diff --git a/api/__pycache__/urls.cpython-313.pyc b/api/__pycache__/urls.cpython-313.pyc index ba3af528..cae8de97 100644 Binary files a/api/__pycache__/urls.cpython-313.pyc and b/api/__pycache__/urls.cpython-313.pyc differ diff --git a/api/urls.py b/api/urls.py index eefdb841..5a033a05 100644 --- a/api/urls.py +++ b/api/urls.py @@ -14,10 +14,9 @@ Including another URLconf 2. Add a URL to urlpatterns: re_path('blog/', include('blog.urls')) """ from django.urls import re_path -from app import views, cob, payment, cleardata, email, backup, server,api_workflow, importdata +from app import views, cob, payment, cleardata, email, backup, server, importdata urlpatterns = [ - re_path("workflow/execute/$", api_workflow.execute_workflow), # Existing Endpoints re_path('get-model/$', views.get_model), @@ -49,7 +48,6 @@ urlpatterns = [ re_path('set-token-expiry/', views.set_token_expiry), re_path('download-contract/(?P.+)', views.download_contract), re_path('execute-command/$', server.execute_command), - re_path('excel-import/$', views.ExcelImportAPIView.as_view()), re_path('generate-document/$',views.generate_document), re_path('model-fields/(?P.+)/', importdata.model_fields), re_path('read-excel/', importdata.read_excel), diff --git a/app/__pycache__/consumers.cpython-313.pyc b/app/__pycache__/consumers.cpython-313.pyc index c79da143..083f0795 100644 Binary files a/app/__pycache__/consumers.cpython-313.pyc and b/app/__pycache__/consumers.cpython-313.pyc differ diff --git a/app/__pycache__/models.cpython-313.pyc b/app/__pycache__/models.cpython-313.pyc index 8904a563..33f77f99 100644 Binary files a/app/__pycache__/models.cpython-313.pyc and b/app/__pycache__/models.cpython-313.pyc differ diff --git a/app/__pycache__/routing.cpython-313.pyc b/app/__pycache__/routing.cpython-313.pyc index 63cef0ef..c73da060 100644 Binary files a/app/__pycache__/routing.cpython-313.pyc and b/app/__pycache__/routing.cpython-313.pyc differ diff --git a/app/__pycache__/signals.cpython-313.pyc b/app/__pycache__/signals.cpython-313.pyc index a880a96f..1baafbd4 100644 Binary files a/app/__pycache__/signals.cpython-313.pyc and b/app/__pycache__/signals.cpython-313.pyc differ diff --git a/app/__pycache__/views.cpython-313.pyc b/app/__pycache__/views.cpython-313.pyc index 598028a9..09fd6680 100644 Binary files a/app/__pycache__/views.cpython-313.pyc and b/app/__pycache__/views.cpython-313.pyc differ diff --git a/app/api_workflow.py b/app/api_workflow.py deleted file mode 100644 index 7c042bdf..00000000 --- a/app/api_workflow.py +++ /dev/null @@ -1,28 +0,0 @@ -from rest_framework.decorators import api_view -from rest_framework.response import Response -from rest_framework import status -from app.workflow_engine import run_workflow -from datetime import datetime # Thêm import - -@api_view(["POST"]) -def execute_workflow(request): - try: - workflow_code = request.data.get("workflow_code") - trigger = request.data.get("trigger") - - # Tạo bản sao của dữ liệu request để làm context cho workflow. - context = dict(request.data) - - # FIX: Bổ sung biến hệ thống: ngày hiện tại để Serializer có thể lấy giá trị cho field 'date' - context["current_date"] = datetime.now().strftime("%Y-%m-%d") - - if not workflow_code or not trigger: - # Sử dụng status.HTTP_400_BAD_REQUEST hoặc 400 như trong code gốc - return Response({"error": "workflow_code & trigger are required"}, status=400) - - result = run_workflow(workflow_code, trigger, context) - return Response({"success": True, "result": result}) - - except Exception as e: - # Trả về lỗi chi tiết hơn - return Response({"error": str(e)}, status=400) \ No newline at end of file diff --git a/app/migrations/0001_initial.py b/app/migrations/0001_initial.py index 36eade50..0543a763 100644 --- a/app/migrations/0001_initial.py +++ b/app/migrations/0001_initial.py @@ -1,7 +1,6 @@ -# Generated by Django 5.1.7 on 2026-03-06 03:31 +# Generated by Django 5.1.7 on 2026-03-19 04:48 import django.db.models.deletion -import uuid from django.db import migrations, models @@ -188,21 +187,6 @@ class Migration(migrations.Migration): 'db_table': 'company_type', }, ), - migrations.CreateModel( - name='Contract_Status', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=30, unique=True)), - ('name', models.CharField(max_length=100)), - ('en', models.CharField(max_length=100, null=True)), - ('index', models.IntegerField(default=1, null=True)), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ], - options={ - 'db_table': 'contract_status', - }, - ), migrations.CreateModel( name='Country', fields=[ @@ -284,42 +268,6 @@ class Migration(migrations.Migration): 'db_table': 'data_type', }, ), - migrations.CreateModel( - name='Dealer', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=20, null=True, unique=True)), - ('name', models.CharField(max_length=100)), - ('phone', models.CharField(db_index=True, max_length=20, null=True)), - ('email', models.CharField(max_length=50, null=True)), - ('address', models.CharField(max_length=255, null=True)), - ('sale_amount', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('pay_sale', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('commission_amount', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('pay_commission', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('commission_remain', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('batch_date', models.DateTimeField(null=True)), - ('count_sale', models.IntegerField(null=True)), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ], - options={ - 'db_table': 'dealer', - }, - ), - migrations.CreateModel( - name='Direction', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=30, unique=True)), - ('name', models.CharField(max_length=100)), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ], - options={ - 'db_table': 'direction', - }, - ), migrations.CreateModel( name='Discount_Method', fields=[ @@ -391,20 +339,6 @@ class Migration(migrations.Migration): 'db_table': 'document_type', }, ), - migrations.CreateModel( - name='Duration', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=30, unique=True)), - ('name', models.CharField(max_length=100)), - ('detail', models.TextField()), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ], - options={ - 'db_table': 'duration', - }, - ), migrations.CreateModel( name='Email_List', fields=[ @@ -529,21 +463,6 @@ class Migration(migrations.Migration): 'db_table': 'filter_type', }, ), - migrations.CreateModel( - name='Gift', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=30, unique=True)), - ('name', models.CharField(max_length=100)), - ('detail', models.TextField(null=True)), - ('index', models.IntegerField(default=1, null=True)), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ], - options={ - 'db_table': 'gift', - }, - ), migrations.CreateModel( name='Import_Setting', fields=[ @@ -562,6 +481,21 @@ class Migration(migrations.Migration): 'db_table': 'import_setting', }, ), + migrations.CreateModel( + name='Instance_Status', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=30, unique=True)), + ('name', models.CharField(max_length=100)), + ('en', models.CharField(max_length=100, null=True)), + ('color', models.CharField(max_length=20, null=True)), + ('index', models.IntegerField(default=1, null=True)), + ('create_time', models.DateTimeField(auto_now_add=True, null=True)), + ], + options={ + 'db_table': 'instance_status', + }, + ), migrations.CreateModel( name='Interest_Base', fields=[ @@ -578,26 +512,6 @@ class Migration(migrations.Migration): 'db_table': 'interest_base', }, ), - migrations.CreateModel( - name='Investor', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255)), - ('tax_code', models.CharField(max_length=20)), - ('address', models.TextField()), - ('phone', models.CharField(max_length=15, null=True)), - ('email', models.CharField(max_length=50, null=True)), - ('bank_account', models.CharField(max_length=20, null=True)), - ('bank_name', models.CharField(max_length=100, null=True)), - ('representative', models.CharField(max_length=100, null=True)), - ('website', models.URLField(null=True)), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ], - options={ - 'db_table': 'investor', - }, - ), migrations.CreateModel( name='Issued_Place', fields=[ @@ -611,19 +525,6 @@ class Migration(migrations.Migration): 'db_table': 'issued_place', }, ), - migrations.CreateModel( - name='Land_Type', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=30, unique=True)), - ('name', models.CharField(max_length=100)), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ], - options={ - 'db_table': 'land_type', - }, - ), migrations.CreateModel( name='Lang_Choice', fields=[ @@ -725,19 +626,6 @@ class Migration(migrations.Migration): 'db_table': 'notification_status', }, ), - migrations.CreateModel( - name='Ownership_Type', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=30, unique=True)), - ('name', models.CharField(max_length=100)), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ], - options={ - 'db_table': 'ownership_type', - }, - ), migrations.CreateModel( name='Payment_Method', fields=[ @@ -753,34 +641,6 @@ class Migration(migrations.Migration): 'db_table': 'payment_method', }, ), - migrations.CreateModel( - name='Payment_Schedule', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=30, null=True, unique=True)), - ('from_date', models.DateField()), - ('to_date', models.DateField()), - ('amount', models.DecimalField(decimal_places=2, max_digits=35)), - ('paid_amount', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('amount_remain', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('remain_amount', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('cycle', models.IntegerField()), - ('cycle_days', models.IntegerField()), - ('entry', models.JSONField(null=True)), - ('detail', models.JSONField(null=True)), - ('batch_date', models.DateField(null=True)), - ('ovd_days', models.IntegerField(null=True)), - ('penalty_amount', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('penalty_paid', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('penalty_remain', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('penalty_reduce', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ], - options={ - 'db_table': 'payment_schedule', - }, - ), migrations.CreateModel( name='Payment_Status', fields=[ @@ -824,44 +684,38 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='Product_Status', + name='Pricing_Tier', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('code', models.CharField(max_length=30, unique=True)), ('name', models.CharField(max_length=100)), - ('color', models.CharField(max_length=20, null=True)), + ('months', models.IntegerField()), + ('discount_percent', models.DecimalField(decimal_places=2, default=0, max_digits=5)), ('index', models.IntegerField(default=1, null=True)), ('create_time', models.DateTimeField(auto_now_add=True, null=True)), ('update_time', models.DateTimeField(auto_now=True, null=True)), ], options={ - 'db_table': 'product_status', + 'db_table': 'pricing_tier', }, ), migrations.CreateModel( - name='Product_Type', + name='Provider', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('code', models.CharField(max_length=30, unique=True)), ('name', models.CharField(max_length=100)), + ('website', models.URLField(null=True)), + ('api_endpoint', models.CharField(max_length=200, null=True)), + ('partner_id', models.CharField(max_length=100, null=True)), + ('contact_email', models.CharField(max_length=100, null=True)), + ('note', models.TextField(null=True)), + ('active', models.BooleanField(default=True)), ('create_time', models.DateTimeField(auto_now_add=True, null=True)), ('update_time', models.DateTimeField(auto_now=True, null=True)), ], options={ - 'db_table': 'product_type', - }, - ), - migrations.CreateModel( - name='Project_Status', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=30, unique=True)), - ('name', models.CharField(max_length=100)), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ], - options={ - 'db_table': 'project_status', + 'db_table': 'provider', }, ), migrations.CreateModel( @@ -891,21 +745,6 @@ class Migration(migrations.Migration): 'db_table': 'relation', }, ), - migrations.CreateModel( - name='Sale_Status', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=30, unique=True)), - ('name', models.CharField(max_length=100)), - ('color', models.CharField(max_length=20, null=True)), - ('index', models.IntegerField(default=1, null=True)), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ], - options={ - 'db_table': 'sale_status', - }, - ), migrations.CreateModel( name='Schedule_Type', fields=[ @@ -944,6 +783,22 @@ class Migration(migrations.Migration): 'db_table': 'send_status', }, ), + migrations.CreateModel( + name='Service_Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=30, unique=True)), + ('name', models.CharField(max_length=100)), + ('en', models.CharField(max_length=100, null=True)), + ('icon', models.CharField(max_length=100, null=True)), + ('index', models.IntegerField(default=1, null=True)), + ('create_time', models.DateTimeField(auto_now_add=True, null=True)), + ('update_time', models.DateTimeField(auto_now=True, null=True)), + ], + options={ + 'db_table': 'service_category', + }, + ), migrations.CreateModel( name='Setting_Choice', fields=[ @@ -1080,7 +935,7 @@ class Migration(migrations.Migration): ('description', models.TextField(blank=True)), ('order', models.PositiveIntegerField(default=0)), ('trigger_event', models.CharField(choices=[('create', 'Create'), ('update', 'Update'), ('approve', 'Approve'), ('advance', 'Advance'), ('confirm', 'Confirm'), ('custom', 'Custom')], max_length=50)), - ('target_model', models.CharField(blank=True, help_text="Model chính, e.g., 'app.Transaction'", max_length=100)), + ('target_model', models.CharField(blank=True, max_length=100)), ('actions', models.JSONField(blank=True, default=list)), ('config', models.JSONField(blank=True, default=dict)), ('is_active', models.BooleanField(default=True)), @@ -1088,12 +943,25 @@ class Migration(migrations.Migration): ('update_time', models.DateTimeField(auto_now=True)), ], options={ - 'verbose_name': 'Step/Action', - 'verbose_name_plural': 'Steps/Actions', 'db_table': 'step_action', 'ordering': ['order'], }, ), + migrations.CreateModel( + name='Subscription_Status', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=30, unique=True)), + ('name', models.CharField(max_length=100)), + ('en', models.CharField(max_length=100, null=True)), + ('color', models.CharField(max_length=20, null=True)), + ('index', models.IntegerField(default=1, null=True)), + ('create_time', models.DateTimeField(auto_now_add=True, null=True)), + ], + options={ + 'db_table': 'subscription_status', + }, + ), migrations.CreateModel( name='Table_Setting', fields=[ @@ -1175,45 +1043,33 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='Transaction_Phase', + name='Ticket_Priority', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=30, null=True, unique=True)), + ('code', models.CharField(max_length=30, unique=True)), ('name', models.CharField(max_length=100)), - ('color', models.IntegerField(null=True)), + ('en', models.CharField(max_length=100, null=True)), + ('color', models.CharField(max_length=20, null=True)), ('index', models.IntegerField(default=1, null=True)), ('create_time', models.DateTimeField(auto_now_add=True, null=True)), ], options={ - 'db_table': 'transaction_phase', + 'db_table': 'ticket_priority', }, ), migrations.CreateModel( - name='Transaction_Status', + name='Ticket_Status', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('code', models.CharField(max_length=30, unique=True)), ('name', models.CharField(max_length=100)), - ('detail', models.TextField()), + ('en', models.CharField(max_length=100, null=True)), + ('color', models.CharField(max_length=20, null=True)), + ('index', models.IntegerField(default=1, null=True)), ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), ], options={ - 'db_table': 'transaction_status', - }, - ), - migrations.CreateModel( - name='Transaction_Type', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=30, unique=True)), - ('name', models.CharField(max_length=100)), - ('detail', models.TextField()), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ], - options={ - 'db_table': 'transaction_type', + 'db_table': 'ticket_status', }, ), migrations.CreateModel( @@ -1240,14 +1096,12 @@ class Migration(migrations.Migration): ('utility_type', models.CharField(choices=[('email', 'Email API'), ('crud', 'Data CRUD'), ('payment', 'Payment API'), ('document', 'Document Gen'), ('notification', 'Notification'), ('custom', 'Custom')], max_length=50)), ('api_config', models.JSONField(blank=True, default=dict)), ('params_template', models.JSONField(blank=True, default=dict)), - ('integration_module', models.CharField(blank=True, help_text="e.g., 'app.email.send_email'", max_length=100)), + ('integration_module', models.CharField(blank=True, max_length=100)), ('is_active', models.BooleanField(default=True)), ('create_time', models.DateTimeField(auto_now_add=True)), ('update_time', models.DateTimeField(auto_now=True)), ], options={ - 'verbose_name': 'Utility', - 'verbose_name_plural': 'Utilities', 'db_table': 'utility', }, ), @@ -1267,16 +1121,17 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='Zone_Type', + name='Wallet_Transaction_Type', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('code', models.CharField(max_length=30, unique=True)), ('name', models.CharField(max_length=100)), + ('en', models.CharField(max_length=100, null=True)), + ('index', models.IntegerField(default=1, null=True)), ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), ], options={ - 'db_table': 'zone_type', + 'db_table': 'wallet_transaction_type', }, ), migrations.CreateModel( @@ -1405,8 +1260,8 @@ class Migration(migrations.Migration): ('create_time', models.DateTimeField(auto_now_add=True, null=True)), ('update_time', models.DateTimeField(auto_now=True, null=True)), ('country', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.country')), + ('segment', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.customer_segment')), ('type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.customer_type')), - ('dealer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.dealer')), ('issued_place', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.issued_place')), ('legal_type', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.legal_type')), ], @@ -1415,18 +1270,68 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='Cart', + name='Customer_Wallet', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=20, null=True, unique=True)), - ('name', models.CharField(max_length=255)), - ('index', models.IntegerField(default=1, null=True)), + ('billing_mode', models.CharField(choices=[('PREPAID', 'Prepaid — Nạp trước'), ('POSTPAID', 'Postpaid — Trả sau cuối tháng')], default='PREPAID', max_length=20)), + ('balance', models.DecimalField(decimal_places=2, default=0, max_digits=20)), + ('low_balance_threshold', models.DecimalField(decimal_places=2, max_digits=20, null=True)), + ('credit_limit', models.DecimalField(decimal_places=2, max_digits=20, null=True)), + ('billing_day', models.IntegerField(default=1, null=True)), ('create_time', models.DateTimeField(auto_now_add=True, null=True)), ('update_time', models.DateTimeField(auto_now=True, null=True)), - ('dealer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.dealer')), + ('currency', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.currency')), + ('customer', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='wallet', to='app.customer')), ], options={ - 'db_table': 'cart', + 'db_table': 'customer_wallet', + }, + ), + migrations.CreateModel( + name='Datacenter', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=30, unique=True)), + ('name', models.CharField(max_length=100)), + ('region', models.CharField(max_length=100, null=True)), + ('provider_location_id', models.CharField(max_length=50, null=True)), + ('active', models.BooleanField(default=True)), + ('create_time', models.DateTimeField(auto_now_add=True, null=True)), + ('update_time', models.DateTimeField(auto_now=True, null=True)), + ('country', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.country')), + ('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='datacenters', to='app.provider')), + ], + options={ + 'db_table': 'datacenter', + }, + ), + migrations.CreateModel( + name='Cloud_Instance', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(db_index=True, max_length=20, null=True, unique=True)), + ('provider_instance_id', models.CharField(db_index=True, max_length=100, null=True)), + ('provider_instance_name', models.CharField(max_length=200, null=True)), + ('ip_address', models.GenericIPAddressField(null=True)), + ('ipv6_address', models.CharField(max_length=50, null=True)), + ('hostname', models.CharField(max_length=200, null=True)), + ('cpu', models.IntegerField(null=True)), + ('ram_gb', models.IntegerField(null=True)), + ('disk_gb', models.IntegerField(null=True)), + ('provisioned_at', models.DateTimeField(null=True)), + ('terminated_at', models.DateTimeField(null=True)), + ('root_password', models.CharField(max_length=300, null=True)), + ('ssh_key', models.TextField(null=True)), + ('note', models.TextField(null=True)), + ('extra_info', models.JSONField(null=True)), + ('create_time', models.DateTimeField(auto_now_add=True, null=True)), + ('update_time', models.DateTimeField(auto_now=True, null=True)), + ('datacenter', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.datacenter')), + ('status', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.instance_status')), + ('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.provider')), + ], + options={ + 'db_table': 'cloud_instance', }, ), migrations.CreateModel( @@ -1472,7 +1377,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=200)), - ('model_name', models.CharField(help_text='e.g., app.Transaction_Detail', max_length=100)), + ('model_name', models.CharField(help_text='e.g., app.Subscription', max_length=100)), ('trigger_on_create', models.BooleanField(default=False)), ('trigger_on_update', models.BooleanField(default=False)), ('active', models.BooleanField(default=True)), @@ -1484,6 +1389,25 @@ class Migration(migrations.Migration): 'db_table': 'email_job', }, ), + migrations.CreateModel( + name='File', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=20, null=True, unique=True)), + ('name', models.CharField(max_length=200)), + ('file', models.CharField(max_length=200)), + ('hashtag', models.CharField(max_length=200, null=True)), + ('size', models.IntegerField()), + ('caption', models.CharField(max_length=200, null=True)), + ('create_time', models.DateTimeField(auto_now_add=True, null=True)), + ('update_time', models.DateTimeField(null=True)), + ('doc_type', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.document_type')), + ('type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.file_type')), + ], + options={ + 'db_table': 'file', + }, + ), migrations.CreateModel( name='Info_Setting', fields=[ @@ -1523,6 +1447,29 @@ class Migration(migrations.Migration): 'unique_together': {('branch', 'type', 'currency')}, }, ), + migrations.CreateModel( + name='Invoice', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('link', models.CharField(max_length=100, null=True)), + ('ref_code', models.CharField(max_length=30)), + ('amount', models.DecimalField(decimal_places=2, max_digits=35)), + ('note', models.CharField(max_length=100, null=True)), + ('issue_date', models.DateField(null=True)), + ('due_date', models.DateField(null=True)), + ('paid_amount', models.DecimalField(decimal_places=2, default=0, max_digits=20, null=True)), + ('remain_amount', models.DecimalField(decimal_places=2, max_digits=20, null=True)), + ('create_time', models.DateTimeField(auto_now_add=True, null=True)), + ('update_time', models.DateTimeField(auto_now=True, null=True)), + ('currency', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.currency')), + ('customer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='invoices', to='app.customer')), + ('payment_method', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.payment_method')), + ('status', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.payment_status')), + ], + options={ + 'db_table': 'invoice', + }, + ), migrations.CreateModel( name='Message', fields=[ @@ -1560,30 +1507,6 @@ class Migration(migrations.Migration): 'db_table': 'organization', }, ), - migrations.CreateModel( - name='Invoice', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('link', models.CharField(max_length=100, null=True)), - ('ref_code', models.CharField(max_length=30)), - ('amount', models.DecimalField(decimal_places=2, max_digits=35)), - ('note', models.CharField(max_length=100, null=True)), - ('payment', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='invoice', to='app.payment_schedule')), - ], - options={ - 'db_table': 'invoice', - }, - ), - migrations.AddField( - model_name='payment_schedule', - name='status', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.payment_status'), - ), - migrations.AddField( - model_name='payment_schedule', - name='type', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.payment_type'), - ), migrations.CreateModel( name='People', fields=[ @@ -1623,18 +1546,54 @@ class Migration(migrations.Migration): field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.people'), ), migrations.CreateModel( - name='Project', + name='Provider_Credential', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=20, unique=True)), - ('name', models.CharField(max_length=255)), + ('label', models.CharField(max_length=100)), + ('api_key', models.CharField(max_length=500)), + ('api_secret', models.CharField(max_length=500, null=True)), + ('active', models.BooleanField(default=True)), + ('note', models.TextField(null=True)), ('create_time', models.DateTimeField(auto_now_add=True, null=True)), ('update_time', models.DateTimeField(auto_now=True, null=True)), - ('investor', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.investor')), - ('status', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.project_status')), + ('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='credentials', to='app.provider')), ], options={ - 'db_table': 'project', + 'db_table': 'provider_credential', + }, + ), + migrations.CreateModel( + name='Provider_Invoice', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=20, null=True, unique=True)), + ('period_from', models.DateField()), + ('period_to', models.DateField()), + ('total_amount', models.DecimalField(decimal_places=2, max_digits=20)), + ('note', models.TextField(null=True)), + ('create_time', models.DateTimeField(auto_now_add=True, null=True)), + ('update_time', models.DateTimeField(auto_now=True, null=True)), + ('currency', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.currency')), + ('document', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.file')), + ('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='provider_invoices', to='app.provider')), + ('status', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.payment_status')), + ], + options={ + 'db_table': 'provider_invoice', + }, + ), + migrations.CreateModel( + name='Provider_Invoice_Line', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.CharField(max_length=300)), + ('amount', models.DecimalField(decimal_places=2, max_digits=20)), + ('create_time', models.DateTimeField(auto_now_add=True, null=True)), + ('instance', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.cloud_instance')), + ('provider_invoice', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='lines', to='app.provider_invoice')), + ], + options={ + 'db_table': 'provider_invoice_line', }, ), migrations.CreateModel( @@ -1650,65 +1609,6 @@ class Migration(migrations.Migration): 'db_table': 'legal_rep', }, ), - migrations.CreateModel( - name='Sale_Policy', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=30, unique=True)), - ('name', models.CharField(max_length=100)), - ('deposit', models.DecimalField(decimal_places=2, max_digits=35)), - ('enable', models.BooleanField(default=True)), - ('contract_allocation_percentage', models.IntegerField(blank=True, default=100, null=True)), - ('index', models.IntegerField(default=1, null=True)), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ('method', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.payment_method')), - ], - options={ - 'db_table': 'sale_policy', - }, - ), - migrations.CreateModel( - name='Product', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=20, null=True, unique=True)), - ('land_lot_code', models.CharField(max_length=255)), - ('zone_code', models.CharField(max_length=255)), - ('trade_code', models.CharField(max_length=20, null=True)), - ('lot_area', models.DecimalField(decimal_places=2, max_digits=35)), - ('building_area', models.DecimalField(decimal_places=2, max_digits=35)), - ('total_built_area', models.DecimalField(decimal_places=2, max_digits=35)), - ('number_of_floors', models.IntegerField()), - ('land_lot_size', models.CharField(max_length=255)), - ('villa_model', models.CharField(max_length=255, null=True)), - ('note', models.TextField(null=True)), - ('origin_price', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('price_excluding_vat', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('product_type', models.CharField(max_length=255, null=True)), - ('template_name', models.CharField(max_length=255, null=True)), - ('link', models.UUIDField(default=uuid.uuid4, editable=False, null=True, unique=True)), - ('locked_until', models.DateTimeField(null=True)), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ('cart', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prdcart', to='app.cart')), - ('dealer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.dealer')), - ('direction', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.direction')), - ('status', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.product_status')), - ('type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.product_type')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.project')), - ('policy', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.sale_policy')), - ('zone_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.zone_type')), - ], - options={ - 'db_table': 'product', - }, - ), - migrations.AddField( - model_name='product_status', - name='sale_status', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.sale_status'), - ), migrations.CreateModel( name='Email_Sent', fields=[ @@ -1724,6 +1624,37 @@ class Migration(migrations.Migration): 'db_table': 'email_sent', }, ), + migrations.CreateModel( + name='Service_Plan', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=30, unique=True)), + ('name', models.CharField(max_length=200)), + ('billing_type', models.CharField(choices=[('SUBSCRIPTION', 'Subscription'), ('PAYG', 'Pay-as-you-go'), ('BOTH', 'Cả hai')], default='SUBSCRIPTION', max_length=20)), + ('cpu', models.IntegerField(null=True)), + ('ram_gb', models.IntegerField(null=True)), + ('disk_gb', models.IntegerField(null=True)), + ('bandwidth_tb', models.DecimalField(decimal_places=2, max_digits=8, null=True)), + ('storage_gb', models.IntegerField(null=True)), + ('extra_specs', models.JSONField(null=True)), + ('provider_price', models.DecimalField(decimal_places=4, max_digits=20)), + ('provider_plan_id', models.CharField(max_length=100, null=True)), + ('sell_price', models.DecimalField(decimal_places=2, max_digits=20)), + ('sell_price_hourly', models.DecimalField(decimal_places=6, max_digits=20, null=True)), + ('active', models.BooleanField(default=True)), + ('index', models.IntegerField(default=1, null=True)), + ('note', models.TextField(null=True)), + ('create_time', models.DateTimeField(auto_now_add=True, null=True)), + ('update_time', models.DateTimeField(auto_now=True, null=True)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='plans', to='app.service_category')), + ('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='plans', to='app.provider')), + ('provider_currency', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.currency')), + ('sell_currency', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.currency')), + ], + options={ + 'db_table': 'service_plan', + }, + ), migrations.CreateModel( name='Individual', fields=[ @@ -1741,6 +1672,95 @@ class Migration(migrations.Migration): 'db_table': 'individual', }, ), + migrations.CreateModel( + name='Staff', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(db_index=True, max_length=20, unique=True)), + ('fullname', models.CharField(db_index=True, max_length=50)), + ('phone', models.CharField(db_index=True, max_length=20, unique=True)), + ('email', models.CharField(max_length=50, null=True, unique=True)), + ('dob', models.DateField(null=True)), + ('legal_code', models.CharField(max_length=20, null=True, unique=True)), + ('issued_place', models.CharField(max_length=200, null=True)), + ('issued_date', models.DateField(null=True)), + ('province', models.CharField(max_length=200, null=True)), + ('district', models.CharField(max_length=200, null=True)), + ('address', models.CharField(max_length=200, null=True)), + ('note', models.TextField(null=True)), + ('zalo', models.CharField(max_length=20, null=True)), + ('facebook', models.CharField(max_length=100, null=True)), + ('deleted', models.BooleanField(db_index=True, default=False)), + ('create_time', models.DateTimeField(auto_now_add=True, null=True)), + ('update_time', models.DateTimeField(auto_now=True, null=True)), + ('branch', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.branch')), + ('country', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.country')), + ('legal_type', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.legal_type')), + ('sex', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.sex')), + ('status', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.staff_status')), + ], + options={ + 'db_table': 'staff', + }, + ), + migrations.AddField( + model_name='customer', + name='sale_staff', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.staff'), + ), + migrations.CreateModel( + name='Subscription', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(db_index=True, max_length=20, null=True, unique=True)), + ('billing_type', models.CharField(choices=[('SUBSCRIPTION', 'Subscription'), ('PAYG', 'Pay-as-you-go')], default='SUBSCRIPTION', max_length=20)), + ('unit_price', models.DecimalField(decimal_places=2, max_digits=20, null=True)), + ('unit_price_hourly', models.DecimalField(decimal_places=6, max_digits=20, null=True)), + ('discount_amount', models.DecimalField(decimal_places=2, default=0, max_digits=20, null=True)), + ('final_price', models.DecimalField(decimal_places=2, max_digits=20, null=True)), + ('start_date', models.DateField()), + ('end_date', models.DateField(null=True)), + ('next_billing_date', models.DateField(null=True)), + ('auto_renew', models.BooleanField(default=True)), + ('note', models.TextField(null=True)), + ('create_time', models.DateTimeField(auto_now_add=True, null=True)), + ('update_time', models.DateTimeField(auto_now=True, null=True)), + ('currency', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.currency')), + ('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='subscriptions', to='app.customer')), + ('datacenter', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.datacenter')), + ('payment_method', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.payment_method')), + ('plan', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='subscriptions', to='app.service_plan')), + ('pricing_tier', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.pricing_tier')), + ('status', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.subscription_status')), + ], + options={ + 'db_table': 'subscription', + }, + ), + migrations.CreateModel( + name='Invoice_Line', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.CharField(max_length=300)), + ('period_from', models.DateField()), + ('period_to', models.DateField()), + ('unit_price', models.DecimalField(decimal_places=2, max_digits=20)), + ('quantity', models.DecimalField(decimal_places=2, default=1, max_digits=10)), + ('discount_amount', models.DecimalField(decimal_places=2, default=0, max_digits=20, null=True)), + ('line_total', models.DecimalField(decimal_places=2, max_digits=20)), + ('create_time', models.DateTimeField(auto_now_add=True, null=True)), + ('invoice', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='lines', to='app.invoice')), + ('subscription', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='invoice_lines', to='app.subscription')), + ], + options={ + 'db_table': 'invoice_line', + }, + ), + migrations.AddField( + model_name='cloud_instance', + name='subscription', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='instances', to='app.subscription'), + ), migrations.CreateModel( name='System_Setting', fields=[ @@ -1818,85 +1838,24 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='Transaction', + name='Support_Ticket', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=30, null=True, unique=True)), - ('date', models.DateField()), - ('origin_price', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('discount_amount', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('sale_price', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('deposit_amount', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('deposit_received', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('deposit_remaining', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('amount_received', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('amount_remain', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('penalty_amount', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('early_discount_amount', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('payment_plan', models.JSONField(null=True)), + ('code', models.CharField(db_index=True, max_length=20, null=True, unique=True)), + ('title', models.CharField(max_length=300)), + ('description', models.TextField()), + ('resolved_at', models.DateTimeField(null=True)), ('create_time', models.DateTimeField(auto_now_add=True, null=True)), ('update_time', models.DateTimeField(auto_now=True, null=True)), - ('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='txncust', to='app.customer')), - ('policy', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='txnplc', to='app.sale_policy')), - ('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='txnprd', to='app.product')), - ('phase', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.transaction_phase')), + ('assignee', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.staff')), + ('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='tickets', to='app.customer')), + ('instance', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='tickets', to='app.cloud_instance')), + ('subscription', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='tickets', to='app.subscription')), + ('priority', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.ticket_priority')), + ('status', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.ticket_status')), ], options={ - 'db_table': 'transaction', - }, - ), - migrations.CreateModel( - name='Transaction_Detail', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=30, null=True, unique=True)), - ('date', models.DateField()), - ('amount', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('amount_remaining', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('amount_received', models.DecimalField(decimal_places=2, max_digits=35, null=True)), - ('due_date', models.DateField(null=True)), - ('approve_time', models.DateTimeField(null=True)), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ('customer_new', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.customer')), - ('customer_old', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.customer')), - ('transaction', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='resvtxn', to='app.transaction')), - ('phase', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.transaction_phase')), - ('status', models.ForeignKey(default=1, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.transaction_status')), - ], - options={ - 'db_table': 'transaction_detail', - }, - ), - migrations.AddField( - model_name='payment_schedule', - name='txn_detail', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='psh', to='app.transaction_detail'), - ), - migrations.CreateModel( - name='Transaction_Gift', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ('gift', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.gift')), - ('transaction', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='txngift', to='app.transaction')), - ], - options={ - 'db_table': 'transaction_gift', - }, - ), - migrations.CreateModel( - name='Phase_Doctype', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ('doctype', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.document_type')), - ('phase', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='phasedoc', to='app.transaction_phase')), - ], - options={ - 'db_table': 'phase_doctype', + 'db_table': 'support_ticket', }, ), migrations.CreateModel( @@ -1926,16 +1885,6 @@ class Migration(migrations.Migration): 'db_table': 'user', }, ), - migrations.AddField( - model_name='transaction_detail', - name='approver', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user'), - ), - migrations.AddField( - model_name='transaction_detail', - name='creator', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user'), - ), migrations.CreateModel( name='Token', fields=[ @@ -1964,41 +1913,22 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='Staff', + name='Ticket_Reply', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(db_index=True, max_length=20, unique=True)), - ('fullname', models.CharField(db_index=True, max_length=50)), - ('phone', models.CharField(db_index=True, max_length=20, unique=True)), - ('email', models.CharField(max_length=50, null=True, unique=True)), - ('dob', models.DateField(null=True)), - ('legal_code', models.CharField(max_length=20, null=True, unique=True)), - ('issued_place', models.CharField(max_length=200, null=True)), - ('issued_date', models.DateField(null=True)), - ('province', models.CharField(max_length=200, null=True)), - ('district', models.CharField(max_length=200, null=True)), - ('address', models.CharField(max_length=200, null=True)), - ('note', models.TextField(null=True)), - ('zalo', models.CharField(max_length=20, null=True)), - ('facebook', models.CharField(max_length=100, null=True)), - ('deleted', models.BooleanField(db_index=True, default=False)), + ('content', models.TextField()), + ('is_internal', models.BooleanField(default=False)), + ('files', models.JSONField(null=True)), ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ('branch', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.branch')), - ('country', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.country')), - ('legal_type', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.legal_type')), - ('sex', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.sex')), - ('status', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.staff_status')), - ('creator', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user')), - ('updater', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user')), - ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user')), + ('ticket', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='replies', to='app.support_ticket')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user')), ], options={ - 'db_table': 'staff', + 'db_table': 'ticket_reply', }, ), migrations.CreateModel( - name='Product_Note', + name='Subscription_Note', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('detail', models.TextField()), @@ -2006,13 +1936,38 @@ class Migration(migrations.Migration): ('deleted', models.BooleanField(default=False, null=True)), ('create_time', models.DateTimeField(auto_now_add=True, null=True)), ('update_time', models.DateTimeField(auto_now=True, null=True)), - ('ref', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='prdnote', to='app.product')), + ('ref', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='subnote', to='app.subscription')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user')), ], options={ - 'db_table': 'product_note', + 'db_table': 'subscription_note', }, ), + migrations.AddField( + model_name='subscription', + name='creator', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user'), + ), + migrations.AddField( + model_name='subscription', + name='updater', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user'), + ), + migrations.AddField( + model_name='staff', + name='creator', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user'), + ), + migrations.AddField( + model_name='staff', + name='updater', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user'), + ), + migrations.AddField( + model_name='staff', + name='user', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user'), + ), migrations.AddField( model_name='people', name='creator', @@ -2023,10 +1978,29 @@ class Migration(migrations.Migration): name='updater', field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user'), ), - migrations.AddField( - model_name='payment_schedule', - name='updater', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user'), + migrations.CreateModel( + name='Payment_Receipt', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(db_index=True, max_length=20, null=True, unique=True)), + ('amount', models.DecimalField(decimal_places=2, max_digits=20)), + ('payment_date', models.DateField()), + ('ref_code', models.CharField(max_length=100, null=True)), + ('note', models.TextField(null=True)), + ('approve_time', models.DateTimeField(null=True)), + ('create_time', models.DateTimeField(auto_now_add=True, null=True)), + ('update_time', models.DateTimeField(auto_now=True, null=True)), + ('currency', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.currency')), + ('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='receipts', to='app.customer')), + ('invoice', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='receipts', to='app.invoice')), + ('payment_method', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.payment_method')), + ('status', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.payment_status')), + ('approver', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user')), + ('creator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user')), + ], + options={ + 'db_table': 'payment_receipt', + }, ), migrations.CreateModel( name='News', @@ -2067,6 +2041,11 @@ class Migration(migrations.Migration): 'db_table': 'layer_setting', }, ), + migrations.AddField( + model_name='invoice', + name='creator', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user'), + ), migrations.CreateModel( name='Internal_Entry', fields=[ @@ -2087,7 +2066,9 @@ class Migration(migrations.Migration): ('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='entrycate', to='app.entry_category')), ('customer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='entrycus', to='app.customer')), ('type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.entry_type')), - ('product', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.product')), + ('invoice', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.invoice')), + ('provider_invoice', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.provider_invoice')), + ('subscription', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.subscription')), ('approver', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user')), ('inputer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user')), ], @@ -2095,6 +2076,21 @@ class Migration(migrations.Migration): 'db_table': 'internal_entry', }, ), + migrations.CreateModel( + name='Instance_Log', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('action', models.CharField(max_length=50)), + ('status', models.CharField(max_length=30)), + ('detail', models.JSONField(null=True)), + ('create_time', models.DateTimeField(auto_now_add=True, null=True)), + ('instance', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='logs', to='app.cloud_instance')), + ('performed_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user')), + ], + options={ + 'db_table': 'instance_log', + }, + ), migrations.CreateModel( name='Group', fields=[ @@ -2110,30 +2106,10 @@ class Migration(migrations.Migration): 'unique_together': {('creator', 'name')}, }, ), - migrations.CreateModel( - name='File', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=20, null=True, unique=True)), - ('name', models.CharField(max_length=200)), - ('file', models.CharField(max_length=200)), - ('hashtag', models.CharField(max_length=200, null=True)), - ('size', models.IntegerField()), - ('caption', models.CharField(max_length=200, null=True)), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(null=True)), - ('doc_type', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.document_type')), - ('type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.file_type')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user')), - ], - options={ - 'db_table': 'file', - }, - ), migrations.AddField( - model_name='dealer', + model_name='file', name='user', - field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dealer_profile', to='app.user'), + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user'), ), migrations.CreateModel( name='Customer_Sms', @@ -2172,22 +2148,10 @@ class Migration(migrations.Migration): name='updater', field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user'), ), - migrations.CreateModel( - name='Contract', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('link', models.UUIDField(default=uuid.uuid4, editable=False, null=True)), - ('document', models.JSONField(null=True)), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ('status', models.ForeignKey(default=1, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.contract_status')), - ('signature', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.file')), - ('transaction', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.transaction')), - ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user')), - ], - options={ - 'db_table': 'contract', - }, + migrations.AddField( + model_name='customer', + name='user', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customer_profile', to='app.user'), ), migrations.AddField( model_name='company', @@ -2281,18 +2245,17 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='Transaction_Discount', + name='Subscription_Discount', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('value', models.DecimalField(decimal_places=2, max_digits=35, null=True)), + ('value', models.DecimalField(decimal_places=2, max_digits=20, null=True)), ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), ('discount', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.discount_type')), - ('transaction', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='txndiscount', to='app.transaction')), + ('subscription', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='discounts', to='app.subscription')), ('type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.value_type')), ], options={ - 'db_table': 'transaction_discount', + 'db_table': 'subscription_discount', }, ), migrations.CreateModel( @@ -2312,6 +2275,27 @@ class Migration(migrations.Migration): 'db_table': 'fee_type', }, ), + migrations.CreateModel( + name='Wallet_Transaction', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(db_index=True, max_length=20, null=True, unique=True)), + ('amount', models.DecimalField(decimal_places=2, max_digits=20)), + ('balance_before', models.DecimalField(decimal_places=2, max_digits=20)), + ('balance_after', models.DecimalField(decimal_places=2, max_digits=20)), + ('description', models.CharField(max_length=300)), + ('ref_code', models.CharField(max_length=100, null=True)), + ('create_time', models.DateTimeField(auto_now_add=True, null=True)), + ('invoice', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='wallet_txns', to='app.invoice')), + ('performed_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.user')), + ('subscription', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='wallet_txns', to='app.subscription')), + ('wallet', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transactions', to='app.customer_wallet')), + ('type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.wallet_transaction_type')), + ], + options={ + 'db_table': 'wallet_transaction', + }, + ), migrations.CreateModel( name='Workflow', fields=[ @@ -2325,8 +2309,6 @@ class Migration(migrations.Migration): ('initial_step', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='initial_workflows', to='app.stepaction')), ], options={ - 'verbose_name': 'Workflow', - 'verbose_name_plural': 'Workflows', 'db_table': 'workflow', }, ), @@ -2398,29 +2380,24 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='Product_File', + name='Postpaid_Billing_Cycle', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(db_index=True, max_length=20, null=True, unique=True)), + ('period_from', models.DateField()), + ('period_to', models.DateField()), + ('total_usage_amount', models.DecimalField(decimal_places=2, max_digits=20, null=True)), + ('status', models.CharField(choices=[('OPEN', 'Đang tích lũy usage'), ('INVOICED', 'Đã xuất Invoice'), ('PAID', 'Đã thanh toán')], default='OPEN', max_length=20)), + ('closed_at', models.DateTimeField(null=True)), ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('file', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.file')), - ('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='prdfile', to='app.product')), + ('update_time', models.DateTimeField(auto_now=True, null=True)), + ('currency', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.currency')), + ('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='billing_cycles', to='app.customer')), + ('invoice', models.OneToOneField(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='billing_cycle', to='app.invoice')), ], options={ - 'db_table': 'product_file', - 'unique_together': {('product', 'file')}, - }, - ), - migrations.CreateModel( - name='Project_File', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('file', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.file')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='prjfile', to='app.project')), - ], - options={ - 'db_table': 'project_file', - 'unique_together': {('project', 'file')}, + 'db_table': 'postpaid_billing_cycle', + 'unique_together': {('customer', 'period_from')}, }, ), migrations.CreateModel( @@ -2438,6 +2415,19 @@ class Migration(migrations.Migration): 'unique_together': {('customer', 'people')}, }, ), + migrations.CreateModel( + name='Plan_Datacenter', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_time', models.DateTimeField(auto_now_add=True, null=True)), + ('datacenter', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.datacenter')), + ('plan', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='plan_dcs', to='app.service_plan')), + ], + options={ + 'db_table': 'plan_datacenter', + 'unique_together': {('plan', 'datacenter')}, + }, + ), migrations.CreateModel( name='Staff_File', fields=[ @@ -2452,59 +2442,16 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='Product_Booked', + name='Subscription_File', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ('product', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='prdbk', to='app.product')), - ('transaction', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transbk', to='app.transaction')), - ], - options={ - 'db_table': 'product_booked', - 'unique_together': {('product', 'transaction')}, - }, - ), - migrations.CreateModel( - name='Co_Ownership', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ('people', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.people')), - ('transaction', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='co_op', to='app.transaction')), - ], - options={ - 'db_table': 'co_ownership', - 'unique_together': {('transaction', 'people')}, - }, - ), - migrations.CreateModel( - name='Transaction_Current', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ('transaction', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='txncurrent', to='app.transaction')), - ('detail', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.transaction_detail')), - ], - options={ - 'db_table': 'transaction_current', - 'unique_together': {('transaction', 'detail')}, - }, - ), - migrations.CreateModel( - name='Transaction_File', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), ('file', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.file')), - ('txn_detail', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='txnfile', to='app.transaction_detail')), + ('ref', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='subfile', to='app.subscription')), ], options={ - 'db_table': 'transaction_file', - 'unique_together': {('txn_detail', 'file')}, + 'db_table': 'subscription_file', + 'unique_together': {('ref', 'file')}, }, ), migrations.CreateModel( @@ -2608,29 +2555,30 @@ class Migration(migrations.Migration): ('utility', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='rules', to='app.utility')), ], options={ - 'verbose_name': 'Rule', - 'verbose_name_plural': 'Rules', 'db_table': 'rule', 'unique_together': {('step_action', 'rule_code')}, }, ), migrations.CreateModel( - name='Payment_Plan', + name='Usage_Record', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('cycle', models.IntegerField()), - ('value', models.IntegerField()), - ('days', models.IntegerField()), - ('payment_note', models.TextField()), - ('due_note', models.TextField()), + ('period_from', models.DateTimeField()), + ('period_to', models.DateTimeField()), + ('hours', models.DecimalField(decimal_places=4, max_digits=10)), + ('unit_price_hourly', models.DecimalField(decimal_places=6, max_digits=20)), + ('amount', models.DecimalField(decimal_places=4, max_digits=20)), + ('charged', models.BooleanField(default=False)), ('create_time', models.DateTimeField(auto_now_add=True, null=True)), - ('update_time', models.DateTimeField(auto_now=True, null=True)), - ('policy', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.sale_policy')), - ('type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.value_type')), + ('currency', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.currency')), + ('instance', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='usage_records', to='app.cloud_instance')), + ('postpaid_invoice', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='usage_records', to='app.invoice')), + ('subscription', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='usage_records', to='app.subscription')), + ('wallet_transaction', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='usage_records', to='app.wallet_transaction')), ], options={ - 'db_table': 'payment_plan', - 'unique_together': {('policy', 'cycle')}, + 'db_table': 'usage_record', + 'indexes': [models.Index(fields=['subscription', 'period_from'], name='usage_recor_subscri_b0fb0c_idx')], }, ), migrations.AlterUniqueTogether( diff --git a/app/migrations/0002_alter_dealer_rights_unique_together_and_more.py b/app/migrations/0002_alter_dealer_rights_unique_together_and_more.py new file mode 100644 index 00000000..86e1c0ad --- /dev/null +++ b/app/migrations/0002_alter_dealer_rights_unique_together_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 5.1.7 on 2026-03-19 04:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0001_initial'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='dealer_rights', + unique_together=None, + ), + migrations.RemoveField( + model_name='dealer_rights', + name='setting', + ), + migrations.RemoveField( + model_name='dealer_rights', + name='user', + ), + migrations.DeleteModel( + name='Dealer_Setting', + ), + migrations.DeleteModel( + name='Dealer_Rights', + ), + ] diff --git a/app/migrations/__pycache__/0001_initial.cpython-313.pyc b/app/migrations/__pycache__/0001_initial.cpython-313.pyc index ab4afb10..0013114c 100644 Binary files a/app/migrations/__pycache__/0001_initial.cpython-313.pyc and b/app/migrations/__pycache__/0001_initial.cpython-313.pyc differ diff --git a/app/migrations/__pycache__/0002_customer_avatar_customer_fullname_alter_customer_dob_and_more.cpython-310.pyc b/app/migrations/__pycache__/0002_customer_avatar_customer_fullname_alter_customer_dob_and_more.cpython-310.pyc deleted file mode 100644 index d1bd6b06..00000000 Binary files a/app/migrations/__pycache__/0002_customer_avatar_customer_fullname_alter_customer_dob_and_more.cpython-310.pyc and /dev/null differ diff --git a/app/migrations/__pycache__/0002_customer_avatar_customer_fullname_alter_customer_dob_and_more.cpython-312.pyc b/app/migrations/__pycache__/0002_customer_avatar_customer_fullname_alter_customer_dob_and_more.cpython-312.pyc deleted file mode 100644 index bee5bd8e..00000000 Binary files a/app/migrations/__pycache__/0002_customer_avatar_customer_fullname_alter_customer_dob_and_more.cpython-312.pyc and /dev/null differ diff --git a/app/migrations/__pycache__/0002_customer_avatar_customer_fullname_alter_customer_dob_and_more.cpython-313.pyc b/app/migrations/__pycache__/0002_customer_avatar_customer_fullname_alter_customer_dob_and_more.cpython-313.pyc deleted file mode 100644 index 9ad7026d..00000000 Binary files a/app/migrations/__pycache__/0002_customer_avatar_customer_fullname_alter_customer_dob_and_more.cpython-313.pyc and /dev/null differ diff --git a/app/models.py b/app/models.py index 86886e02..bf5251d8 100644 --- a/app/models.py +++ b/app/models.py @@ -2,16 +2,8 @@ from django.db import models import uuid from django.db.models import JSONField -# Create your models here. - -#==================================================================================== +# ==================================================================================== def generate_increment_code(model_class, prefix="CODE", padding=4, code_field="code"): - """ - model_class: Model sẽ sinh mã (ví dụ Customer, Product,...) - prefix: tiền tố (CUS, PRD...) - padding: số lượng chữ số (0001, 0010...) - code_field: tên trường lưu mã - """ last = model_class.objects.order_by('-id').first() next_id = (last.id + 1) if last else 1 return f"{prefix}{next_id:0{padding}d}" @@ -20,6 +12,7 @@ def generate_increment_code(model_class, prefix="CODE", padding=4, code_field="c class AutoCodeModel(models.Model): code_prefix = "CODE" code_padding = 5 + class Meta: abstract = True @@ -33,6 +26,10 @@ class AutoCodeModel(models.Model): super().save(*args, **kwargs) +# ==================================================================================== +# GIỮ NGUYÊN TỪ FILE GỐC — lookup tables không liên quan BĐS +# ==================================================================================== + class Money_Unit(models.Model): code = models.CharField(max_length=30, null=False, unique=True) name = models.CharField(max_length=100, null=False) @@ -91,83 +88,6 @@ class Register_Method(models.Model): db_table = 'register_method' -class Duration(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - detail = models.TextField(null=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'duration' - - -class Ownership_Type(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'ownership_type' - - -class Transaction_Type(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - detail = models.TextField(null=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'transaction_type' - - -class Transaction_Status(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - detail = models.TextField(null=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'transaction_status' - - -class Project_Status(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'project_status' - - -class Sale_Status(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - color = models.CharField(max_length=20, null=True) - index = models.IntegerField(null=True, default=1) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'sale_status' - - -class Product_Status(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - color = models.CharField(max_length=20, null=True) - index = models.IntegerField(null=True, default=1) - sale_status = models.ForeignKey(Sale_Status, null=True, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'product_status' - class Customer_Type(models.Model): code = models.CharField(max_length=30, null=False, unique=True) name = models.CharField(max_length=100, null=False) @@ -200,85 +120,6 @@ class Payment_Method(models.Model): db_table = 'payment_method' -class Investor(models.Model): - name = models.CharField(max_length=255) - tax_code = models.CharField(max_length=20, null=False) - address = models.TextField(null=False) - phone = models.CharField(max_length=15, null=True) - email = models.CharField(max_length=50, null=True) - bank_account = models.CharField(max_length=20, null=True) - bank_name = models.CharField(max_length=100, null=True) - representative = models.CharField(max_length=100, null=True) - website = models.URLField(null=True) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'investor' - - -class Project(models.Model): - code = models.CharField(max_length=20, unique=True) - name = models.CharField(max_length=255) - investor = models.ForeignKey(Investor, null=False, related_name='+', on_delete=models.PROTECT) - status = models.ForeignKey(Project_Status, null=False, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'project' - - -class Zone_Type(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'zone_type' - - -class Product_Type(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'product_type' - - -class Land_Type(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'land_type' - - -class Company_Type(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'company_type' - - -class Direction(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'direction' - - class Value_Type(models.Model): code = models.CharField(max_length=30, null=False, unique=True) name = models.CharField(max_length=100, null=False) @@ -315,49 +156,6 @@ class Discount_Type(models.Model): db_table = 'discount_type' -class Gift(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - detail = models.TextField(null=True) - index = models.IntegerField(null=True, default=1) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'gift' - - -class Sale_Policy(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - deposit = models.DecimalField(max_digits=35, decimal_places=2) - method = models.ForeignKey(Payment_Method, null=False, related_name='+', on_delete=models.PROTECT) - enable = models.BooleanField(default=True) - contract_allocation_percentage = models.IntegerField(null=True,blank=True,default=100) - index = models.IntegerField(null=True, default=1) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'sale_policy' - - -class Payment_Plan(models.Model): - policy = models.ForeignKey(Sale_Policy, null=False, related_name='+', on_delete=models.PROTECT) - cycle = models.IntegerField() - value = models.IntegerField() - type = models.ForeignKey(Value_Type, null=False, related_name='+', on_delete=models.PROTECT) - days = models.IntegerField() - payment_note = models.TextField() - due_note = models.TextField() - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'payment_plan' - unique_together = ('policy', 'cycle') - - class User(models.Model): username = models.CharField(max_length=50, null=False, unique=True) password = models.CharField(max_length=100, null=False) @@ -388,7 +186,7 @@ class Token(models.Model): browser = models.TextField(null=False) browser_version = models.CharField(max_length=100, null=False) os = models.CharField(max_length=100, null=False) - ip = models.CharField(max_length=100, null=False) + ip = models.CharField(max_length=100, null=False) platform = models.CharField(max_length=100, null=False) expiry = models.BooleanField(default=False) city = models.CharField(max_length=100, null=True) @@ -415,6 +213,7 @@ class Setting_Type(models.Model): db_table = 'setting_type' +# GIỮ NGUYÊN — không thêm field 'type' vào Setting_Choice (file gốc không có) class Setting_Choice(models.Model): code = models.CharField(max_length=30, null=False, unique=True) name = models.CharField(max_length=100, null=False) @@ -604,16 +403,16 @@ class Lang_Choice(models.Model): class Common(models.Model): category = models.CharField(max_length=100, null=False) classify = models.CharField(max_length=100, null=False) - code = models.CharField(max_length=100, null = False) + code = models.CharField(max_length=100, null=False) vi = models.TextField(null=False) en = models.TextField(null=True) image = models.TextField(null=True) icon = models.TextField(null=True) link = models.TextField(null=True) detail = models.JSONField(null=True) - index = models.IntegerField(null=True, default=0) + index = models.IntegerField(null=True, default=0) create_time = models.DateTimeField(null=True, auto_now_add=True) - + class Meta: db_table = 'common' unique_together = ('category', 'classify', 'code') @@ -622,7 +421,7 @@ class Common(models.Model): class System_Setting(models.Model): category = models.CharField(max_length=100, null=False) classify = models.CharField(max_length=100, null=False) - code = models.CharField(max_length=100, null = False) + code = models.CharField(max_length=100, null=False) vi = models.TextField(null=False) en = models.TextField(null=True) image = models.TextField(null=True) @@ -630,9 +429,9 @@ class System_Setting(models.Model): link = models.TextField(null=True) detail = models.JSONField(null=True) detail_en = models.JSONField(null=True) - index = models.IntegerField(null=True, default=0) + index = models.IntegerField(null=True, default=0) create_time = models.DateTimeField(null=True, auto_now_add=True) - + class Meta: db_table = 'system_setting' unique_together = ('category', 'classify', 'code') @@ -689,7 +488,7 @@ class File(AutoCodeModel): doc_type = models.ForeignKey(Document_Type, null=True, related_name='+', on_delete=models.PROTECT) name = models.CharField(max_length=200, null=False) file = models.CharField(max_length=200, null=False) - hashtag= models.CharField(max_length=200, null=True) + hashtag = models.CharField(max_length=200, null=True) size = models.IntegerField(null=False) caption = models.CharField(max_length=200, null=True) create_time = models.DateTimeField(null=True, auto_now_add=True) @@ -775,111 +574,6 @@ class Payment_Status(models.Model): db_table = 'payment_status' -class Dealer(AutoCodeModel): - code_prefix = "DL" - code_padding = 2 - code = models.CharField(max_length=20, null=True, unique=True) - user = models.OneToOneField(User, null=True, related_name='dealer_profile', on_delete=models.SET_NULL) - name = models.CharField(max_length=100, null=False) - phone = models.CharField(max_length=20, null=True, db_index=True) - email = models.CharField(max_length=50, null=True) - address = models.CharField(max_length=255, null=True) - sale_amount = models.DecimalField(max_digits=35, decimal_places=2,null=True) - pay_sale = models.DecimalField(max_digits=35, decimal_places=2,null=True) - commission_amount = models.DecimalField(max_digits=35, decimal_places=2,null=True) - pay_commission = models.DecimalField(max_digits=35, decimal_places=2,null=True) - commission_remain = models.DecimalField(max_digits=35, decimal_places=2,null=True) - batch_date = models.DateTimeField(null=True) - count_sale = models.IntegerField(null=True) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'dealer' - - -class Cart(AutoCodeModel): - code_prefix = "GH" - code_padding = 3 - code = models.CharField(max_length=20, null=True, unique=True) - name = models.CharField(max_length=255) - dealer = models.ForeignKey(Dealer, null=True, related_name='+', on_delete=models.PROTECT) - index = models.IntegerField(null=True, default=1) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'cart' - - -class Product(AutoCodeModel): - code_prefix = "SP" - code_padding = 5 - code = models.CharField(max_length=20, null=True, unique=True) - land_lot_code = models.CharField(max_length=255) - zone_code = models.CharField(max_length=255) - trade_code = models.CharField(max_length=20, null=True) - zone_type = models.ForeignKey(Zone_Type, null=False, related_name='+', on_delete=models.PROTECT) - lot_area = models.DecimalField(max_digits=35, decimal_places=2) - building_area = models.DecimalField(max_digits=35, decimal_places=2) - total_built_area = models.DecimalField(max_digits=35, decimal_places=2) - number_of_floors = models.IntegerField() - land_lot_size = models.CharField(max_length=255) - direction = models.ForeignKey(Direction, null=False, related_name='+', on_delete=models.PROTECT) - villa_model = models.CharField(max_length=255, null=True) - type = models.ForeignKey(Product_Type, null=False, related_name='+', on_delete=models.PROTECT) - project = models.ForeignKey(Project, null=False, related_name='+', on_delete=models.PROTECT) - status = models.ForeignKey(Product_Status, null=False, related_name='+', on_delete=models.PROTECT) - cart = models.ForeignKey(Cart, null=True, related_name='prdcart', on_delete=models.PROTECT) - dealer = models.ForeignKey(Dealer, null=True, related_name='+', on_delete=models.PROTECT) - policy = models.ForeignKey(Sale_Policy, null=True, related_name='+', on_delete=models.PROTECT) - note = models.TextField(null=True) - origin_price = models.DecimalField(max_digits=35, decimal_places=2, null=True) - price_excluding_vat = models.DecimalField(max_digits=35, decimal_places=2, null=True) - product_type = models.CharField(max_length=255, null=True) - template_name = models.CharField(max_length=255, null=True) - link = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, null=True) - locked_until = models.DateTimeField(null=True) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'product' - - -class Product_File(models.Model): - product = models.ForeignKey(Product, null=False, related_name='prdfile', on_delete=models.PROTECT) - file = models.ForeignKey(File, null=False, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - - class Meta: - db_table = 'product_file' - unique_together = ('product', 'file') - - -class Project_File(models.Model): - project = models.ForeignKey(Project, null=False, related_name='prjfile', on_delete=models.PROTECT) - file = models.ForeignKey(File, null=False, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - - class Meta: - db_table = 'project_file' - unique_together = ('project', 'file') - - -class Product_Note(models.Model): - ref = models.ForeignKey(Product, null=False, related_name='prdnote', on_delete=models.PROTECT) - detail = models.TextField() - files = models.JSONField(null=True) - user = models.ForeignKey(User, null=False, related_name='+', on_delete=models.PROTECT) - deleted = models.BooleanField(null=True, default=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'product_note' - - class News(models.Model): title = models.CharField(max_length=200, null=False) subtitle = models.CharField(max_length=500, null=False) @@ -901,6 +595,7 @@ class News(models.Model): db_table = 'news' +# GIỮ NGUYÊN — hệ thống messaging/notification class Message_Type(models.Model): code = models.CharField(max_length=30, null=False, unique=True) title = models.CharField(max_length=200, null=True) @@ -946,6 +641,7 @@ class Message_Receiver(models.Model): unique_together = ('message', 'user') +# GIỮ NGUYÊN — task/schedule/alert class Schedule_Type(models.Model): code = models.CharField(max_length=30, null=False, unique=True) name = models.CharField(max_length=100, null=False) @@ -1027,7 +723,7 @@ class Group(models.Model): note = models.TextField(null=True) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - + class Meta: db_table = 'group' unique_together = ('creator', 'name') @@ -1039,33 +735,31 @@ class User_Group(models.Model): deleted = models.BooleanField(default=False) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - + class Meta: db_table = 'user_group' unique_together = ('group', 'user') -#=================================================================== class User_Session(models.Model): token = models.ForeignKey(Token, null=False, related_name='userlog', on_delete=models.PROTECT) - session = models.BigIntegerField(null=False) + session = models.BigIntegerField(null=False) start_time = models.DateTimeField(null=False) end_time = models.DateTimeField(null=True) duration = models.IntegerField(null=True) click_count = models.IntegerField(null=True) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - + class Meta: db_table = 'user_session' -#=================================================================== class User_Log(models.Model): - session = models.ForeignKey(User_Session, null=False, related_name='+', on_delete=models.PROTECT) + session = models.ForeignKey(User_Session, null=False, related_name='+', on_delete=models.PROTECT) link = models.TextField(null=False) create_time = models.DateTimeField(null=True, auto_now_add=True) - + class Meta: db_table = 'user_log' @@ -1073,7 +767,7 @@ class User_Log(models.Model): class Biz_Setting(models.Model): category = models.CharField(max_length=100, null=False) classify = models.CharField(max_length=100, null=False) - code = models.CharField(max_length=100, null = False) + code = models.CharField(max_length=100, null=False) vi = models.TextField(null=False) en = models.TextField(null=True) image = models.TextField(null=True) @@ -1083,34 +777,16 @@ class Biz_Setting(models.Model): detail_en = models.JSONField(null=True) index = models.IntegerField(null=True, default=0) create_time = models.DateTimeField(null=True, auto_now_add=True) - + class Meta: db_table = 'biz_setting' unique_together = ('category', 'classify', 'code') - -class Dealer_Setting(models.Model): - category = models.CharField(max_length=100, null=False) - classify = models.CharField(max_length=100, null=False) - code = models.CharField(max_length=100, null = False) - vi = models.TextField(null=False) - en = models.TextField(null=True) - image = models.TextField(null=True) - icon = models.TextField(null=True) - link = models.TextField(null=True) - detail = models.JSONField(null=True) - detail_en = models.JSONField(null=True) - index = models.IntegerField(null=True, default=0) - create_time = models.DateTimeField(null=True, auto_now_add=True) - - class Meta: - db_table = 'dealer_setting' - unique_together = ('category', 'classify', 'code') - + class Info_Setting(models.Model): category = models.CharField(max_length=100, null=False) classify = models.CharField(max_length=100, null=False) - code = models.CharField(max_length=100, null = False) + code = models.CharField(max_length=100, null=False) vi = models.TextField(null=False) en = models.TextField(null=True) image = models.TextField(null=True) @@ -1120,7 +796,7 @@ class Info_Setting(models.Model): detail_en = models.JSONField(null=True) index = models.IntegerField(null=True, default=0) create_time = models.DateTimeField(null=True, auto_now_add=True) - + class Meta: db_table = 'info_setting' unique_together = ('category', 'classify', 'code') @@ -1130,28 +806,17 @@ class Biz_Rights(models.Model): setting = models.ForeignKey(Biz_Setting, null=False, related_name='+', on_delete=models.PROTECT) user = models.ForeignKey(User, null=False, related_name='+', on_delete=models.PROTECT) create_time = models.DateTimeField(null=True, auto_now_add=True) - + class Meta: db_table = 'biz_rights' unique_together = ('setting', 'user') - -class Dealer_Rights(models.Model): - setting = models.ForeignKey(Dealer_Setting, null=False, related_name='+', on_delete=models.PROTECT) - user = models.ForeignKey(User, null=False, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - - class Meta: - db_table = 'dealer_rights' - unique_together = ('setting', 'user') - - class Group_Rights(models.Model): setting = models.ForeignKey(Biz_Setting, null=False, related_name='+', on_delete=models.PROTECT) group = models.ForeignKey(User_Type, null=False, related_name='+', on_delete=models.PROTECT) is_edit = models.BooleanField(null=True, default=False) create_time = models.DateTimeField(null=True, auto_now_add=True) - + class Meta: db_table = 'group_rights' unique_together = ('setting', 'group') @@ -1170,7 +835,7 @@ class Account_Setting(models.Model): detail_en = models.JSONField(null=True) index = models.IntegerField(null=True, default=0) create_time = models.DateTimeField(null=True, auto_now_add=True) - + class Meta: db_table = 'account_setting' unique_together = ('category', 'classify', 'code') @@ -1238,8 +903,8 @@ class Company(AutoCodeModel): deleted = models.BooleanField(null=False, default=False, db_index=True) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: + + class Meta: db_table = 'company' @@ -1267,8 +932,8 @@ class People(AutoCodeModel): deleted = models.BooleanField(null=False, default=False) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: + + class Meta: db_table = 'people' @@ -1304,27 +969,6 @@ class Document_Audit(models.Model): db_table = 'document_audit' -class Transaction_Phase(models.Model): - code = models.CharField(max_length=30, null=True, unique=True) - name = models.CharField(max_length=100, null=False) - color = models.IntegerField(null=True) - index = models.IntegerField(null=True, default=1) - create_time = models.DateTimeField(null=True, auto_now_add=True) - - class Meta: - db_table = 'transaction_phase' - - -class Phase_Doctype(models.Model): - phase = models.ForeignKey(Transaction_Phase, null=False, related_name='phasedoc', on_delete=models.PROTECT) - doctype = models.ForeignKey(Document_Type, null=False, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'phase_doctype' - - class Customer(AutoCodeModel): code_prefix = "KH" code_padding = 5 @@ -1341,14 +985,17 @@ class Customer(AutoCodeModel): contact_address = models.CharField(max_length=200, null=True) note = models.TextField(null=True) type = models.ForeignKey(Customer_Type, null=False, related_name='+', on_delete=models.PROTECT) - dealer = models.ForeignKey(Dealer, null=True, related_name='+', on_delete=models.PROTECT) + segment = models.ForeignKey(Customer_Segment, null=True, related_name='+', on_delete=models.PROTECT) + # Field mới cho cloud: tài khoản portal + nhân viên phụ trách + user = models.OneToOneField(User, null=True, related_name='customer_profile', on_delete=models.SET_NULL) + sale_staff = models.ForeignKey('Staff', null=True, related_name='+', on_delete=models.PROTECT) creator = models.ForeignKey(User, null=False, related_name='+', on_delete=models.PROTECT) updater = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) deleted = models.BooleanField(null=False, default=False, db_index=True) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: + + class Meta: db_table = 'customer' @@ -1357,12 +1004,12 @@ class Individual(models.Model): dob = models.DateField(null=True) sex = models.ForeignKey(Sex, null=True, related_name='+', on_delete=models.PROTECT) zalo = models.CharField(max_length=20, null=True) - facebook = models.CharField(max_length=200, null=True) + facebook = models.CharField(max_length=200, null=True) company = models.ForeignKey(Company, null=True, related_name='+', on_delete=models.PROTECT) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: + + class Meta: db_table = 'individual' @@ -1373,141 +1020,41 @@ class Organization(models.Model): website = models.CharField(max_length=200, null=True) bank_account = models.CharField(max_length=50, null=True) bank_name = models.CharField(max_length=100, null=True) - type = models.ForeignKey(Company_Type, null=True, related_name='+', on_delete=models.PROTECT) + type = models.ForeignKey('Company_Type', null=True, related_name='+', on_delete=models.PROTECT) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - class Meta: + class Meta: db_table = 'organization' +class Company_Type(models.Model): + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=100, null=False) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'company_type' + + class Legal_Rep(models.Model): organization = models.ForeignKey(Organization, null=False, related_name='orgrep', on_delete=models.PROTECT) people = models.ForeignKey(People, null=False, related_name='+', on_delete=models.PROTECT) relation = models.ForeignKey(Relation, null=False, related_name='+', on_delete=models.PROTECT) create_time = models.DateTimeField(null=True, auto_now_add=True) - class Meta: + class Meta: db_table = 'legal_rep' -class Transaction(AutoCodeModel): - code_prefix = "GD" - code_padding = 5 - code = models.CharField(max_length=30, null=True, unique=True) - customer = models.ForeignKey(Customer, null=False, related_name='txncust', on_delete=models.PROTECT) - product = models.ForeignKey(Product, null=False, related_name='txnprd', on_delete=models.PROTECT) - policy = models.ForeignKey(Sale_Policy, null=False, related_name='txnplc', on_delete=models.PROTECT) - phase = models.ForeignKey(Transaction_Phase, null=False, related_name='+', on_delete=models.PROTECT) - date = models.DateField() - origin_price = models.DecimalField(max_digits=35, decimal_places=2, null=True) - discount_amount = models.DecimalField(max_digits=35, decimal_places=2, null=True) - sale_price = models.DecimalField(max_digits=35, decimal_places=2, null=True) - deposit_amount = models.DecimalField(max_digits=35, decimal_places=2, null=True) - deposit_received = models.DecimalField(max_digits=35, decimal_places=2, null=True) - deposit_remaining = models.DecimalField(max_digits=35, decimal_places=2, null=True) - amount_received = models.DecimalField(max_digits=35, decimal_places=2, null=True) - amount_remain = models.DecimalField(max_digits=35, decimal_places=2, null=True) - penalty_amount = models.DecimalField(null=True, max_digits=35, decimal_places=2) - early_discount_amount = models.DecimalField(max_digits=35, decimal_places=2, null=True) - payment_plan = models.JSONField(null=True) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'transaction' - - -class Transaction_Detail(AutoCodeModel): - code_prefix = "CT" - code_padding = 5 - code = models.CharField(max_length=30, null=True, unique=True) - date = models.DateField() - amount = models.DecimalField(max_digits=35, decimal_places=2, null=True) - amount_remaining = models.DecimalField(max_digits=35, decimal_places=2, null=True) - amount_received = models.DecimalField(max_digits=35, decimal_places=2, null=True) - due_date = models.DateField(null=True) - customer_old = models.ForeignKey(Customer, null=True, related_name='+', on_delete=models.PROTECT) - customer_new = models.ForeignKey(Customer, null=True, related_name='+', on_delete=models.PROTECT) - transaction = models.ForeignKey(Transaction, null=False, related_name='resvtxn', on_delete=models.PROTECT) - phase = models.ForeignKey(Transaction_Phase, null=False, related_name='+', on_delete=models.PROTECT) - creator = models.ForeignKey(User, null=False, related_name='+', on_delete=models.PROTECT) - status = models.ForeignKey(Transaction_Status, null=True, related_name='+', on_delete=models.PROTECT, default=1) - approver = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) - approve_time = models.DateTimeField(null=True) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'transaction_detail' - - -class Transaction_Current(models.Model): - transaction = models.OneToOneField(Transaction, null=False, related_name='txncurrent', on_delete=models.PROTECT) - detail = models.ForeignKey(Transaction_Detail, null=False, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'transaction_current' - unique_together = ('transaction', 'detail') - - -class Transaction_File(models.Model): - txn_detail = models.ForeignKey(Transaction_Detail, null=False, related_name='txnfile', on_delete=models.PROTECT) - file = models.ForeignKey(File, null=False, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'transaction_file' - unique_together = ('txn_detail', 'file') - - -class Product_Booked(models.Model): - product = models.OneToOneField(Product, null=False, related_name='prdbk', on_delete=models.PROTECT) - transaction = models.ForeignKey(Transaction, null=False, related_name='transbk', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'product_booked' - unique_together = ('product', 'transaction') - - -class Contract_Status(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - en = models.CharField(max_length=100, null=True) - index = models.IntegerField(null=True, default=1) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'contract_status' - - -class Contract(models.Model): - transaction = models.ForeignKey(Transaction, null=False, related_name='+', on_delete=models.PROTECT) - signature = models.ForeignKey(File, null=True, related_name='+', on_delete=models.PROTECT) - status = models.ForeignKey(Contract_Status, null=True, related_name='+', on_delete=models.PROTECT, default=1) - user = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) - link = models.UUIDField(default=uuid.uuid4, editable=False, unique=False, null=True) - document = models.JSONField(null=True) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'contract' - - class Customer_File(models.Model): ref = models.ForeignKey(Customer, null=False, related_name='custfile', on_delete=models.PROTECT) file = models.ForeignKey(File, null=False, related_name='+', on_delete=models.PROTECT) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - class Meta: + class Meta: db_table = 'customer_file' unique_together = ('ref', 'file') @@ -1519,7 +1066,7 @@ class Customer_Note(models.Model): create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - class Meta: + class Meta: db_table = 'customer_note' @@ -1529,11 +1076,23 @@ class People_File(models.Model): create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - class Meta: + class Meta: db_table = 'people_file' unique_together = ('ref', 'file') +class Customer_People(models.Model): + customer = models.ForeignKey(Customer, null=False, related_name='custpeople', on_delete=models.PROTECT) + people = models.ForeignKey(People, null=False, related_name='+', on_delete=models.PROTECT) + relation = models.ForeignKey(Relation, null=False, related_name='+', on_delete=models.PROTECT) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'customer_people' + unique_together = ('customer', 'people') + + class Payment_Type(models.Model): code = models.CharField(max_length=30, null=False, unique=True) name = models.CharField(max_length=100, null=False) @@ -1611,7 +1170,7 @@ class Account_Book(models.Model): create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - class Meta: + class Meta: db_table = 'account_book' unique_together = ('system_date', 'account') @@ -1621,7 +1180,7 @@ class Interest_Base(models.Model): name = models.CharField(max_length=200, null=True) en = models.CharField(max_length=100, null=True) index = models.IntegerField(null=True, default=1) - detail = models.TextField(null=False) + detail = models.TextField(null=False) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) @@ -1669,7 +1228,10 @@ class Internal_Entry(AutoCodeModel): date = models.DateField(null=False) ref = models.CharField(max_length=30, null=True, unique=True) customer = models.ForeignKey(Customer, null=True, related_name='entrycus', on_delete=models.PROTECT) - product = models.ForeignKey(Product, null=True, related_name='+', on_delete=models.PROTECT) + # Cloud: thay product → subscription + subscription = models.ForeignKey('Subscription', null=True, related_name='+', on_delete=models.PROTECT) + invoice = models.ForeignKey('Invoice', null=True, related_name='+', on_delete=models.PROTECT) + provider_invoice = models.ForeignKey('Provider_Invoice', null=True, related_name='+', on_delete=models.PROTECT) allocation_amount = models.DecimalField(null=True, max_digits=35, decimal_places=2) allocation_remain = models.DecimalField(null=True, max_digits=35, decimal_places=2) allocation_detail = models.JSONField(null=True) @@ -1685,52 +1247,33 @@ class Entry_File(models.Model): file = models.ForeignKey(File, null=False, related_name='+', on_delete=models.PROTECT) create_time = models.DateTimeField(null=True, auto_now_add=True) - class Meta: + class Meta: db_table = 'entry_file' unique_together = ('ref', 'file') -class Payment_Schedule(AutoCodeModel): - code_prefix = "SH" - code_padding = 5 - code = models.CharField(max_length=30, null=True, unique=True) - from_date = models.DateField(null=False) - to_date = models.DateField(null=False) - amount = models.DecimalField(max_digits=35, decimal_places=2) - paid_amount = models.DecimalField(null=True, max_digits=35, decimal_places=2) - amount_remain = models.DecimalField(null=True, max_digits=35, decimal_places=2) - remain_amount = models.DecimalField(null=True, max_digits=35, decimal_places=2) - cycle = models.IntegerField(null=False) - cycle_days = models.IntegerField(null=False) - txn_detail = models.ForeignKey(Transaction_Detail, null=False, related_name='psh', on_delete=models.PROTECT) - type = models.ForeignKey(Payment_Type, null=False, related_name='+', on_delete=models.PROTECT) - status = models.ForeignKey(Payment_Status, null=False, related_name='+', on_delete=models.PROTECT) - updater = models.ForeignKey(User, null=False, related_name='+', on_delete=models.PROTECT) - entry = models.JSONField(null=True) - detail = models.JSONField(null=True) - batch_date = models.DateField(null=True) - ovd_days = models.IntegerField(null=True) - penalty_amount = models.DecimalField(null=True, max_digits=35, decimal_places=2) - penalty_paid = models.DecimalField(null=True, max_digits=35, decimal_places=2) - penalty_remain = models.DecimalField(null=True, max_digits=35, decimal_places=2) - penalty_reduce = models.DecimalField(null=True, max_digits=35, decimal_places=2) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'payment_schedule' - +# GIỮ NGUYÊN — Invoice gốc (liên kết với payment_schedule BĐS, giữ để không break data cũ) class Invoice(models.Model): link = models.CharField(max_length=100, null=True) ref_code = models.CharField(max_length=30, null=False) amount = models.DecimalField(max_digits=35, decimal_places=2) - payment = models.ForeignKey(Payment_Schedule, null=False, related_name='invoice', on_delete=models.PROTECT) note = models.CharField(max_length=100, null=True) - + # Cloud: thêm fields mới (nullable để không break data cũ) + customer = models.ForeignKey(Customer, null=True, related_name='invoices', on_delete=models.PROTECT) + issue_date = models.DateField(null=True) + due_date = models.DateField(null=True) + paid_amount = models.DecimalField(max_digits=20, decimal_places=2, null=True, default=0) + remain_amount = models.DecimalField(max_digits=20, decimal_places=2, null=True) + currency = models.ForeignKey(Currency, null=True, related_name='+', on_delete=models.PROTECT) + payment_method = models.ForeignKey(Payment_Method, null=True, related_name='+', on_delete=models.PROTECT) + status = models.ForeignKey(Payment_Status, null=True, related_name='+', on_delete=models.PROTECT) + creator = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + class Meta: db_table = 'invoice' - - + class Phone_Otp(models.Model): code = models.CharField(max_length=30, null=False, unique=True) @@ -1785,7 +1328,7 @@ class Staff_Status(models.Model): class Meta: db_table = 'staff_status' - + class Staff(models.Model): code = models.CharField(max_length=20, null=False, unique=True, db_index=True) @@ -1813,29 +1356,17 @@ class Staff(models.Model): user = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: + + class Meta: db_table = 'staff' -class Customer_People(models.Model): - customer = models.ForeignKey(Customer, null=False, related_name='custpeople', on_delete=models.PROTECT) - people = models.ForeignKey(People, null=False, related_name='+', on_delete=models.PROTECT) - relation = models.ForeignKey(Relation, null=False, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'customer_people' - unique_together = ('customer', 'people') - - class Staff_File(models.Model): ref = models.ForeignKey(Staff, null=False, related_name='stafffile', on_delete=models.PROTECT) file = models.ForeignKey(File, null=False, related_name='+', on_delete=models.PROTECT) create_time = models.DateTimeField(null=True, auto_now_add=True) - class Meta: + class Meta: db_table = 'staff_file' unique_together = ('ref', 'file') @@ -1860,7 +1391,7 @@ class Batch_Log(models.Model): status = models.ForeignKey(Task_Status, null=False, related_name='+', on_delete=models.PROTECT) create_time = models.DateTimeField(null=True, auto_now_add=True) - class Meta: + class Meta: db_table = 'batch_log' @@ -1888,7 +1419,7 @@ class Customer_Sms(models.Model): user = models.ForeignKey(User, null=False, related_name='+', on_delete=models.PROTECT) create_time = models.DateTimeField(null=True, auto_now_add=True) - class Meta: + class Meta: db_table = 'customer_sms' @@ -1919,13 +1450,15 @@ class Ssh(models.Model): class Meta: db_table = 'ssh' - + + class Document_Configuration(models.Model): code = models.CharField(max_length=30, null=False, unique=True) name = models.CharField(max_length=200, null=False) template_path = models.CharField(max_length=200, null=False) - mappings = models.JSONField (default=list) + mappings = models.JSONField(default=list) update_time = models.DateTimeField(null=True, auto_now=True) + class Meta: db_table = 'document_configuration' @@ -1959,7 +1492,6 @@ class Layer_Setting(AutoCodeModel): db_table = 'layer_setting' -#========================================================================== class Send_Status(models.Model): code = models.CharField(max_length=30, null=False, unique=True) name = models.CharField(max_length=100, null=False) @@ -1968,6 +1500,7 @@ class Send_Status(models.Model): class Meta: db_table = 'send_status' + class Email_Setup(models.Model): email = models.CharField(max_length=100, null=False, unique=True) password = models.CharField(max_length=30, null=False) @@ -1992,6 +1525,7 @@ class Email_Sent(models.Model): class Meta: db_table = 'email_sent' + class Email_List(models.Model): name = models.CharField(max_length=200, null=False, unique=True) email = models.TextField(null=False) @@ -2001,6 +1535,7 @@ class Email_List(models.Model): class Meta: db_table = 'email_list' + class Email_Template(models.Model): name = models.CharField(max_length=200, null=False, unique=True) content = models.JSONField(null=False) @@ -2013,7 +1548,7 @@ class Email_Template(models.Model): class Email_Job(models.Model): name = models.CharField(max_length=200, null=False) - model_name = models.CharField(max_length=100, null=False, help_text="e.g., app.Transaction_Detail") + model_name = models.CharField(max_length=100, null=False, help_text="e.g., app.Subscription") template = models.ForeignKey(Email_Template, null=False, related_name='+', on_delete=models.PROTECT) trigger_on_create = models.BooleanField(default=False) trigger_on_update = models.BooleanField(default=False) @@ -2021,80 +1556,37 @@ class Email_Job(models.Model): create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - class Meta: db_table = 'email_job' - -class Transaction_Gift(models.Model): - transaction = models.ForeignKey(Transaction, null=False, related_name='txngift', on_delete=models.PROTECT) - gift = models.ForeignKey(Gift, null=False, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'transaction_gift' - -class Transaction_Discount(models.Model): - transaction = models.ForeignKey(Transaction, null=False, related_name='txndiscount', on_delete=models.PROTECT) - discount = models.ForeignKey(Discount_Type, null=False, related_name='+', on_delete=models.PROTECT) - type = models.ForeignKey(Value_Type, null=False, related_name='+', on_delete=models.PROTECT) - value = models.DecimalField(max_digits=35, decimal_places=2, null=True) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'transaction_discount' -class Co_Ownership(models.Model): - transaction = models.ForeignKey(Transaction, null=False, related_name='co_op', on_delete=models.PROTECT) - people = models.ForeignKey(People, null=False, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - class Meta: - db_table = 'co_ownership' - unique_together = ('transaction', 'people') - +# GIỮ NGUYÊN — Workflow engine class Workflow(models.Model): - """ - Bảng Workflow: Quản lý các luồng chính (multi-flow cho dự án). - Ví dụ: 'RESERVATION' cho giữ chỗ, 'LOAN_APPROVAL' cho duyệt vay. - """ - code = models.CharField(max_length=50, unique=True) # e.g., 'RESERVATION' + code = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=200) description = models.TextField(blank=True) is_active = models.BooleanField(default=True) - initial_step = models.ForeignKey('StepAction', null=True, blank=True, on_delete=models.SET_NULL, related_name='initial_workflows') + initial_step = models.ForeignKey('StepAction', null=True, blank=True, on_delete=models.SET_NULL, + related_name='initial_workflows') create_time = models.DateTimeField(auto_now_add=True) update_time = models.DateTimeField(auto_now=True) class Meta: db_table = 'workflow' - verbose_name = 'Workflow' - verbose_name_plural = 'Workflows' - def __str__(self): - return f"{self.name} ({self.code})" class StepAction(models.Model): - """ - Bảng Step/Action: Định nghĩa các bước/hành động trong Workflow. - Liên kết với Workflow, lưu chi tiết actions (JSON: list of dicts). - Ví dụ: Step 'create_reservation' có actions: [{'type': 'create_record', 'model': 'app.Transaction', 'fields': {...}}] - """ workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, related_name='steps') - step_code = models.CharField(max_length=50, unique=True) # e.g., 'create_reservation', 'approve_detail' + step_code = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=200) description = models.TextField(blank=True) - order = models.PositiveIntegerField(default=0) # Thứ tự chạy trong workflow + order = models.PositiveIntegerField(default=0) trigger_event = models.CharField(max_length=50, choices=[ ('create', 'Create'), ('update', 'Update'), ('approve', 'Approve'), ('advance', 'Advance'), ('confirm', 'Confirm'), ('custom', 'Custom') ]) - target_model = models.CharField(max_length=100, blank=True, help_text="Model chính, e.g., 'app.Transaction'") - # Actions: List chi tiết hành động (dynamic, multi-model) - actions = JSONField(default=list, blank=True) # e.g., [{'type': 'create_record', 'model': 'app.Product', 'fields': {'status': 'reserved'}}] - # Config extra: e.g., {'auto_advance': True, 'required_fields': ['customer_id']} + target_model = models.CharField(max_length=100, blank=True) + actions = JSONField(default=list, blank=True) config = JSONField(default=dict, blank=True) is_active = models.BooleanField(default=True) create_time = models.DateTimeField(auto_now_add=True) @@ -2104,33 +1596,35 @@ class StepAction(models.Model): db_table = 'step_action' ordering = ['order'] unique_together = ('workflow', 'step_code') - verbose_name = 'Step/Action' - verbose_name_plural = 'Steps/Actions' - def __str__(self): - return f"{self.workflow.name} - {self.name} ({self.step_code})" -class Rule(models.Model): - """ - Bảng Rule: Định nghĩa điều kiện (conditions) và quy luật ràng buộc cho Step/Action. - Liên kết với StepAction, dùng cho validation/constraints. - Ví dụ: Condition: {'field': 'a_a', 'operator': '==', 'value': 'specific_value'} → Chỉ thực hiện nếu match. - Constraint: {'after_action': 'create_record', 'must_update': {'model': 'app.Product', 'field': 'status', 'to': 'reserved'}} - """ - step_action = models.ForeignKey(StepAction, on_delete=models.CASCADE, related_name='rules') - rule_code = models.CharField(max_length=50, unique=True) # e.g., 'validate_customer_vip' +class Utility(models.Model): + code = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=200) description = models.TextField(blank=True) - # Conditions: List conditions để check trước action - conditions = JSONField(default=list, blank=True) # e.g., [ - # {'field': 'customer__segment__code', 'operator': '==', 'value': 'VIP', 'related_model': 'app.Customer'}, - # {'field': 'amount', 'operator': '>=', 'value': 1000000}] - # Constraints: Quy luật sau/before action (ràng buộc) - constraints = JSONField(default=list, blank=True) # e.g., [ - # {'trigger': 'after_create', 'type': 'must_update', 'model': 'app.Transaction_Detail', 'fields': {'status': 'new'}}, - # {'trigger': 'before_approve', 'type': 'require_approval', 'min_count': 2}] - # Utility linkage: Liên kết với Utility nếu cần reuse - utility = models.ForeignKey('Utility', null=True, blank=True, on_delete=models.SET_NULL, related_name='rules') + utility_type = models.CharField(max_length=50, choices=[ + ('email', 'Email API'), ('crud', 'Data CRUD'), ('payment', 'Payment API'), + ('document', 'Document Gen'), ('notification', 'Notification'), ('custom', 'Custom') + ]) + api_config = JSONField(default=dict, blank=True) + params_template = JSONField(default=dict, blank=True) + integration_module = models.CharField(max_length=100, blank=True) + is_active = models.BooleanField(default=True) + create_time = models.DateTimeField(auto_now_add=True) + update_time = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'utility' + + +class Rule(models.Model): + step_action = models.ForeignKey(StepAction, on_delete=models.CASCADE, related_name='rules') + rule_code = models.CharField(max_length=50, unique=True) + name = models.CharField(max_length=200) + description = models.TextField(blank=True) + conditions = JSONField(default=list, blank=True) + constraints = JSONField(default=list, blank=True) + utility = models.ForeignKey(Utility, null=True, blank=True, on_delete=models.SET_NULL, related_name='rules') is_active = models.BooleanField(default=True) create_time = models.DateTimeField(auto_now_add=True) update_time = models.DateTimeField(auto_now=True) @@ -2138,39 +1632,531 @@ class Rule(models.Model): class Meta: db_table = 'rule' unique_together = ('step_action', 'rule_code') - verbose_name = 'Rule' - verbose_name_plural = 'Rules' - def __str__(self): - return f"{self.step_action.name} - {self.name} ({self.rule_code})" -class Utility(models.Model): - """ - Bảng Utility: Chứa các action có sẵn, liên kết với hệ thống khác (e.g., API mail, CRUD data). - Dùng để reuse trong StepAction/Rule (e.g., gửi mail → gọi send_email API; create record → gọi /data/ endpoint). - """ - code = models.CharField(max_length=50, unique=True) # e.g., 'SEND_EMAIL', 'CRUD_DATA' - name = models.CharField(max_length=200) - description = models.TextField(blank=True) - # Type: Phân loại utility (e.g., 'email', 'crud', 'payment') - utility_type = models.CharField(max_length=50, choices=[ - ('email', 'Email API'), ('crud', 'Data CRUD'), ('payment', 'Payment API'), - ('document', 'Document Gen'), ('notification', 'Notification'), ('custom', 'Custom') - ]) - # Endpoint/API linkage: e.g., {'url': '/send-email/', 'method': 'POST', 'params': {'template': '[template]'}} - api_config = JSONField(default=dict, blank=True) # Config gọi API (dynamic placeholders như [user_id]) - # Params template: Mẫu params khi gọi - params_template = JSONField(default=dict, blank=True) # e.g., {'receiver': '[customer_email]', 'subject': 'Approval'} - # External integration: e.g., liên kết với Email_Job hoặc account_entry_api - integration_module = models.CharField(max_length=100, blank=True, help_text="e.g., 'app.email.send_email'") - is_active = models.BooleanField(default=True) - create_time = models.DateTimeField(auto_now_add=True) - update_time = models.DateTimeField(auto_now=True) +# ==================================================================================== +# CLOUD DOMAIN — PROVIDER (NHÀ CUNG CẤP HẠ TẦNG) +# ==================================================================================== + +class Provider(models.Model): + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=100, null=False) + website = models.URLField(null=True) + api_endpoint = models.CharField(max_length=200, null=True) + partner_id = models.CharField(max_length=100, null=True) + contact_email = models.CharField(max_length=100, null=True) + note = models.TextField(null=True) + active = models.BooleanField(default=True) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) class Meta: - db_table = 'utility' - verbose_name = 'Utility' - verbose_name_plural = 'Utilities' + db_table = 'provider' - def __str__(self): - return f"{self.name} ({self.code}) - Type: {self.utility_type}" + +class Provider_Credential(models.Model): + provider = models.ForeignKey(Provider, null=False, related_name='credentials', on_delete=models.PROTECT) + label = models.CharField(max_length=100, null=False) + api_key = models.CharField(max_length=500, null=False) + api_secret = models.CharField(max_length=500, null=True) + active = models.BooleanField(default=True) + note = models.TextField(null=True) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'provider_credential' + + +class Datacenter(models.Model): + provider = models.ForeignKey(Provider, null=False, related_name='datacenters', on_delete=models.PROTECT) + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=100, null=False) + region = models.CharField(max_length=100, null=True) + country = models.ForeignKey(Country, null=True, related_name='+', on_delete=models.PROTECT) + provider_location_id = models.CharField(max_length=50, null=True) + active = models.BooleanField(default=True) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'datacenter' + + +# ==================================================================================== +# CLOUD DOMAIN — SERVICE CATALOG +# ==================================================================================== + +class Service_Category(models.Model): + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=100, null=False) + en = models.CharField(max_length=100, null=True) + icon = models.CharField(max_length=100, null=True) + index = models.IntegerField(null=True, default=1) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'service_category' + + +class Service_Plan(models.Model): + BILLING_SUBSCRIPTION = 'SUBSCRIPTION' + BILLING_PAYG = 'PAYG' + BILLING_BOTH = 'BOTH' + BILLING_TYPE_CHOICES = [ + (BILLING_SUBSCRIPTION, 'Subscription'), + (BILLING_PAYG, 'Pay-as-you-go'), + (BILLING_BOTH, 'Cả hai'), + ] + + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=200, null=False) + category = models.ForeignKey(Service_Category, null=False, related_name='plans', on_delete=models.PROTECT) + provider = models.ForeignKey(Provider, null=False, related_name='plans', on_delete=models.PROTECT) + billing_type = models.CharField(max_length=20, choices=BILLING_TYPE_CHOICES, default=BILLING_SUBSCRIPTION) + cpu = models.IntegerField(null=True) + ram_gb = models.IntegerField(null=True) + disk_gb = models.IntegerField(null=True) + bandwidth_tb = models.DecimalField(max_digits=8, decimal_places=2, null=True) + storage_gb = models.IntegerField(null=True) + extra_specs = models.JSONField(null=True) + provider_price = models.DecimalField(max_digits=20, decimal_places=4, null=False) + provider_currency = models.ForeignKey(Currency, null=False, related_name='+', on_delete=models.PROTECT) + provider_plan_id = models.CharField(max_length=100, null=True) + sell_price = models.DecimalField(max_digits=20, decimal_places=2, null=False) + sell_price_hourly = models.DecimalField(max_digits=20, decimal_places=6, null=True) + sell_currency = models.ForeignKey(Currency, null=False, related_name='+', on_delete=models.PROTECT) + active = models.BooleanField(default=True) + index = models.IntegerField(null=True, default=1) + note = models.TextField(null=True) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'service_plan' + + +class Plan_Datacenter(models.Model): + plan = models.ForeignKey(Service_Plan, null=False, related_name='plan_dcs', on_delete=models.PROTECT) + datacenter = models.ForeignKey(Datacenter, null=False, related_name='+', on_delete=models.PROTECT) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'plan_datacenter' + unique_together = ('plan', 'datacenter') + + +class Pricing_Tier(models.Model): + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=100, null=False) + months = models.IntegerField(null=False) + discount_percent = models.DecimalField(max_digits=5, decimal_places=2, default=0) + index = models.IntegerField(null=True, default=1) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'pricing_tier' + + +# ==================================================================================== +# CLOUD DOMAIN — SUBSCRIPTION +# ==================================================================================== + +class Subscription_Status(models.Model): + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=100, null=False) + en = models.CharField(max_length=100, null=True) + color = models.CharField(max_length=20, null=True) + index = models.IntegerField(null=True, default=1) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'subscription_status' + + +class Subscription(AutoCodeModel): + BILLING_SUBSCRIPTION = 'SUBSCRIPTION' + BILLING_PAYG = 'PAYG' + BILLING_TYPE_CHOICES = [ + (BILLING_SUBSCRIPTION, 'Subscription'), + (BILLING_PAYG, 'Pay-as-you-go'), + ] + + code_prefix = "SUB" + code_padding = 6 + code = models.CharField(max_length=20, null=True, unique=True, db_index=True) + customer = models.ForeignKey(Customer, null=False, related_name='subscriptions', on_delete=models.PROTECT) + plan = models.ForeignKey(Service_Plan, null=False, related_name='subscriptions', on_delete=models.PROTECT) + datacenter = models.ForeignKey(Datacenter, null=False, related_name='+', on_delete=models.PROTECT) + billing_type = models.CharField(max_length=20, choices=BILLING_TYPE_CHOICES, default=BILLING_SUBSCRIPTION) + pricing_tier = models.ForeignKey(Pricing_Tier, null=True, related_name='+', on_delete=models.PROTECT) + payment_method = models.ForeignKey(Payment_Method, null=True, related_name='+', on_delete=models.PROTECT) + unit_price = models.DecimalField(max_digits=20, decimal_places=2, null=True) + unit_price_hourly = models.DecimalField(max_digits=20, decimal_places=6, null=True) + discount_amount = models.DecimalField(max_digits=20, decimal_places=2, null=True, default=0) + final_price = models.DecimalField(max_digits=20, decimal_places=2, null=True) + currency = models.ForeignKey(Currency, null=False, related_name='+', on_delete=models.PROTECT) + start_date = models.DateField(null=False) + end_date = models.DateField(null=True) + next_billing_date = models.DateField(null=True) + auto_renew = models.BooleanField(default=True) + status = models.ForeignKey(Subscription_Status, null=False, related_name='+', on_delete=models.PROTECT) + note = models.TextField(null=True) + creator = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) + updater = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'subscription' + + +class Subscription_Discount(models.Model): + subscription = models.ForeignKey(Subscription, null=False, related_name='discounts', on_delete=models.PROTECT) + discount = models.ForeignKey(Discount_Type, null=False, related_name='+', on_delete=models.PROTECT) + type = models.ForeignKey(Value_Type, null=False, related_name='+', on_delete=models.PROTECT) + value = models.DecimalField(max_digits=20, decimal_places=2, null=True) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'subscription_discount' + + +class Subscription_File(models.Model): + ref = models.ForeignKey(Subscription, null=False, related_name='subfile', on_delete=models.PROTECT) + file = models.ForeignKey(File, null=False, related_name='+', on_delete=models.PROTECT) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'subscription_file' + unique_together = ('ref', 'file') + + +class Subscription_Note(models.Model): + ref = models.ForeignKey(Subscription, null=False, related_name='subnote', on_delete=models.PROTECT) + detail = models.TextField(null=False) + files = models.JSONField(null=True) + user = models.ForeignKey(User, null=False, related_name='+', on_delete=models.PROTECT) + deleted = models.BooleanField(null=True, default=False) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'subscription_note' + + +# ==================================================================================== +# CLOUD DOMAIN — WALLET (PAYG) +# ==================================================================================== + +class Wallet_Transaction_Type(models.Model): + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=100, null=False) + en = models.CharField(max_length=100, null=True) + index = models.IntegerField(null=True, default=1) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'wallet_transaction_type' + + +class Customer_Wallet(models.Model): + PREPAID = 'PREPAID' + POSTPAID = 'POSTPAID' + BILLING_MODE_CHOICES = [ + (PREPAID, 'Prepaid — Nạp trước'), + (POSTPAID, 'Postpaid — Trả sau cuối tháng'), + ] + + customer = models.OneToOneField(Customer, null=False, related_name='wallet', on_delete=models.PROTECT) + billing_mode = models.CharField(max_length=20, choices=BILLING_MODE_CHOICES, default=PREPAID) + balance = models.DecimalField(max_digits=20, decimal_places=2, null=False, default=0) + currency = models.ForeignKey(Currency, null=False, related_name='+', on_delete=models.PROTECT) + low_balance_threshold = models.DecimalField(max_digits=20, decimal_places=2, null=True) + credit_limit = models.DecimalField(max_digits=20, decimal_places=2, null=True) + billing_day = models.IntegerField(null=True, default=1) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'customer_wallet' + + +class Wallet_Transaction(AutoCodeModel): + code_prefix = "WT" + code_padding = 7 + code = models.CharField(max_length=20, null=True, unique=True, db_index=True) + wallet = models.ForeignKey(Customer_Wallet, null=False, related_name='transactions', on_delete=models.PROTECT) + type = models.ForeignKey(Wallet_Transaction_Type, null=False, related_name='+', on_delete=models.PROTECT) + amount = models.DecimalField(max_digits=20, decimal_places=2, null=False) + balance_before = models.DecimalField(max_digits=20, decimal_places=2, null=False) + balance_after = models.DecimalField(max_digits=20, decimal_places=2, null=False) + description = models.CharField(max_length=300, null=False) + subscription = models.ForeignKey(Subscription, null=True, related_name='wallet_txns', on_delete=models.PROTECT) + invoice = models.ForeignKey(Invoice, null=True, related_name='wallet_txns', on_delete=models.PROTECT) + ref_code = models.CharField(max_length=100, null=True) + performed_by = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'wallet_transaction' + + +# ==================================================================================== +# CLOUD DOMAIN — CLOUD INSTANCE +# ==================================================================================== + +class Instance_Status(models.Model): + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=100, null=False) + en = models.CharField(max_length=100, null=True) + color = models.CharField(max_length=20, null=True) + index = models.IntegerField(null=True, default=1) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'instance_status' + + +class Cloud_Instance(AutoCodeModel): + code_prefix = "INS" + code_padding = 6 + code = models.CharField(max_length=20, null=True, unique=True, db_index=True) + subscription = models.ForeignKey(Subscription, null=False, related_name='instances', on_delete=models.PROTECT) + provider = models.ForeignKey(Provider, null=False, related_name='+', on_delete=models.PROTECT) + datacenter = models.ForeignKey(Datacenter, null=False, related_name='+', on_delete=models.PROTECT) + provider_instance_id = models.CharField(max_length=100, null=True, db_index=True) + provider_instance_name = models.CharField(max_length=200, null=True) + ip_address = models.GenericIPAddressField(null=True) + ipv6_address = models.CharField(max_length=50, null=True) + hostname = models.CharField(max_length=200, null=True) + cpu = models.IntegerField(null=True) + ram_gb = models.IntegerField(null=True) + disk_gb = models.IntegerField(null=True) + status = models.ForeignKey(Instance_Status, null=False, related_name='+', on_delete=models.PROTECT) + provisioned_at = models.DateTimeField(null=True) + terminated_at = models.DateTimeField(null=True) + root_password = models.CharField(max_length=300, null=True) + ssh_key = models.TextField(null=True) + note = models.TextField(null=True) + extra_info = models.JSONField(null=True) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'cloud_instance' + + +class Instance_Log(models.Model): + instance = models.ForeignKey(Cloud_Instance, null=False, related_name='logs', on_delete=models.PROTECT) + action = models.CharField(max_length=50, null=False) + status = models.CharField(max_length=30, null=False) + performed_by = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) + detail = models.JSONField(null=True) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'instance_log' + + +# ==================================================================================== +# CLOUD DOMAIN — USAGE RECORD (PAYG) +# ==================================================================================== + +class Usage_Record(models.Model): + subscription = models.ForeignKey(Subscription, null=False, related_name='usage_records', + on_delete=models.PROTECT) + instance = models.ForeignKey(Cloud_Instance, null=True, related_name='usage_records', + on_delete=models.PROTECT) + period_from = models.DateTimeField(null=False) + period_to = models.DateTimeField(null=False) + hours = models.DecimalField(max_digits=10, decimal_places=4, null=False) + unit_price_hourly = models.DecimalField(max_digits=20, decimal_places=6, null=False) + amount = models.DecimalField(max_digits=20, decimal_places=4, null=False) + currency = models.ForeignKey(Currency, null=False, related_name='+', on_delete=models.PROTECT) + charged = models.BooleanField(default=False) + wallet_transaction = models.ForeignKey(Wallet_Transaction, null=True, related_name='usage_records', + on_delete=models.PROTECT) + postpaid_invoice = models.ForeignKey(Invoice, null=True, related_name='usage_records', + on_delete=models.PROTECT) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'usage_record' + indexes = [ + models.Index(fields=['subscription', 'period_from']), + ] + + +# ==================================================================================== +# CLOUD DOMAIN — INVOICE LINES & RECEIPTS +# ==================================================================================== + +class Invoice_Line(models.Model): + invoice = models.ForeignKey(Invoice, null=False, related_name='lines', on_delete=models.PROTECT) + subscription = models.ForeignKey(Subscription, null=False, related_name='invoice_lines', + on_delete=models.PROTECT) + description = models.CharField(max_length=300, null=False) + period_from = models.DateField(null=False) + period_to = models.DateField(null=False) + unit_price = models.DecimalField(max_digits=20, decimal_places=2, null=False) + quantity = models.DecimalField(max_digits=10, decimal_places=2, default=1) + discount_amount = models.DecimalField(max_digits=20, decimal_places=2, null=True, default=0) + line_total = models.DecimalField(max_digits=20, decimal_places=2, null=False) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'invoice_line' + + +class Payment_Receipt(AutoCodeModel): + code_prefix = "PT" + code_padding = 6 + code = models.CharField(max_length=20, null=True, unique=True, db_index=True) + invoice = models.ForeignKey(Invoice, null=False, related_name='receipts', on_delete=models.PROTECT) + customer = models.ForeignKey(Customer, null=False, related_name='receipts', on_delete=models.PROTECT) + amount = models.DecimalField(max_digits=20, decimal_places=2, null=False) + currency = models.ForeignKey(Currency, null=False, related_name='+', on_delete=models.PROTECT) + payment_method = models.ForeignKey(Payment_Method, null=False, related_name='+', on_delete=models.PROTECT) + payment_date = models.DateField(null=False) + ref_code = models.CharField(max_length=100, null=True) + note = models.TextField(null=True) + status = models.ForeignKey(Payment_Status, null=False, related_name='+', on_delete=models.PROTECT) + approver = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) + approve_time = models.DateTimeField(null=True) + creator = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'payment_receipt' + + +# ==================================================================================== +# CLOUD DOMAIN — POSTPAID BILLING CYCLE +# ==================================================================================== + +class Postpaid_Billing_Cycle(AutoCodeModel): + OPEN = 'OPEN' + INVOICED = 'INVOICED' + PAID = 'PAID' + STATUS_CHOICES = [ + (OPEN, 'Đang tích lũy usage'), + (INVOICED, 'Đã xuất Invoice'), + (PAID, 'Đã thanh toán'), + ] + + code_prefix = "PBC" + code_padding = 6 + code = models.CharField(max_length=20, null=True, unique=True, db_index=True) + customer = models.ForeignKey(Customer, null=False, related_name='billing_cycles', on_delete=models.PROTECT) + period_from = models.DateField(null=False) + period_to = models.DateField(null=False) + total_usage_amount = models.DecimalField(max_digits=20, decimal_places=2, null=True) + currency = models.ForeignKey(Currency, null=False, related_name='+', on_delete=models.PROTECT) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=OPEN) + invoice = models.OneToOneField(Invoice, null=True, related_name='billing_cycle', on_delete=models.PROTECT) + closed_at = models.DateTimeField(null=True) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'postpaid_billing_cycle' + unique_together = ('customer', 'period_from') + + +# ==================================================================================== +# CLOUD DOMAIN — PROVIDER COST +# ==================================================================================== + +class Provider_Invoice(AutoCodeModel): + code_prefix = "PVI" + code_padding = 5 + code = models.CharField(max_length=20, null=True, unique=True) + provider = models.ForeignKey(Provider, null=False, related_name='provider_invoices', on_delete=models.PROTECT) + period_from = models.DateField(null=False) + period_to = models.DateField(null=False) + total_amount = models.DecimalField(max_digits=20, decimal_places=2, null=False) + currency = models.ForeignKey(Currency, null=False, related_name='+', on_delete=models.PROTECT) + document = models.ForeignKey(File, null=True, related_name='+', on_delete=models.PROTECT) + note = models.TextField(null=True) + status = models.ForeignKey(Payment_Status, null=False, related_name='+', on_delete=models.PROTECT) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'provider_invoice' + + +class Provider_Invoice_Line(models.Model): + provider_invoice = models.ForeignKey(Provider_Invoice, null=False, related_name='lines', + on_delete=models.PROTECT) + instance = models.ForeignKey(Cloud_Instance, null=True, related_name='+', on_delete=models.PROTECT) + description = models.CharField(max_length=300, null=False) + amount = models.DecimalField(max_digits=20, decimal_places=2, null=False) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'provider_invoice_line' + + +# ==================================================================================== +# CLOUD DOMAIN — SUPPORT TICKET +# ==================================================================================== + +class Ticket_Priority(models.Model): + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=100, null=False) + en = models.CharField(max_length=100, null=True) + color = models.CharField(max_length=20, null=True) + index = models.IntegerField(null=True, default=1) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'ticket_priority' + + +class Ticket_Status(models.Model): + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=100, null=False) + en = models.CharField(max_length=100, null=True) + color = models.CharField(max_length=20, null=True) + index = models.IntegerField(null=True, default=1) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'ticket_status' + + +class Support_Ticket(AutoCodeModel): + code_prefix = "TK" + code_padding = 6 + code = models.CharField(max_length=20, null=True, unique=True, db_index=True) + customer = models.ForeignKey(Customer, null=False, related_name='tickets', on_delete=models.PROTECT) + subscription = models.ForeignKey(Subscription, null=True, related_name='tickets', on_delete=models.PROTECT) + instance = models.ForeignKey(Cloud_Instance, null=True, related_name='tickets', on_delete=models.PROTECT) + title = models.CharField(max_length=300, null=False) + description = models.TextField(null=False) + priority = models.ForeignKey(Ticket_Priority, null=False, related_name='+', on_delete=models.PROTECT) + status = models.ForeignKey(Ticket_Status, null=False, related_name='+', on_delete=models.PROTECT) + assignee = models.ForeignKey(Staff, null=True, related_name='+', on_delete=models.PROTECT) + resolved_at = models.DateTimeField(null=True) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'support_ticket' + + +class Ticket_Reply(models.Model): + ticket = models.ForeignKey(Support_Ticket, null=False, related_name='replies', on_delete=models.PROTECT) + content = models.TextField(null=False) + is_internal = models.BooleanField(default=False) + user = models.ForeignKey(User, null=False, related_name='+', on_delete=models.PROTECT) + files = models.JSONField(null=True) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'ticket_reply' \ No newline at end of file diff --git a/app/signals.py b/app/signals.py index c9576130..d0106b06 100644 --- a/app/signals.py +++ b/app/signals.py @@ -4,12 +4,8 @@ from django.apps import apps from channels.layers import get_channel_layer from asgiref.sync import async_to_sync from django.db import transaction - -# Import hàm get_serializer đã có from .views import get_serializer -# Danh sách các model không muốn theo dõi để tránh "nhiễu" -# Ví dụ: các model của admin, session, hoặc các model log không cần real-time MODELS_TO_IGNORE = ['LogEntry', 'Session', 'ContentType', 'AdminLog', 'Permission', 'Group', 'Token', 'Phone_Otp'] def send_model_update(instance, change_type): @@ -19,35 +15,29 @@ def send_model_update(instance, change_type): model_class = instance.__class__ model_name = model_class._meta.model_name - # Bỏ qua các model trong danh sách ignore if model_class.__name__ in MODELS_TO_IGNORE: return - # Lấy serializer một cách linh động _model, serializer_class = get_serializer(model_name) if not serializer_class: print(f"Warning: No serializer found for model {model_name}. Cannot send update.") return - # Serialize instance đã thay đổi - # Đối với 'delete', instance vẫn còn tồn tại trong bộ nhớ tại thời điểm này serializer = serializer_class(instance) - # Chuẩn bị payload để gửi đi payload = { "name": model_name, "change_type": change_type, "record": serializer.data } - # Gửi tin nhắn đến group tương ứng channel_layer = get_channel_layer() group_name = f"model_{model_name}_updates" async_to_sync(channel_layer.group_send)( group_name, { - "type": "realtime.update", # Khớp với tên phương thức trong DataConsumer + "type": "realtime.update", "payload": payload } ) @@ -59,14 +49,10 @@ def generic_post_save_handler(sender, instance, created, **kwargs): def send_update_after_commit(): change_type = "created" if created else "updated" try: - # Re-fetch the instance to ensure we have the committed data refreshed_instance = sender.objects.get(pk=instance.pk) send_model_update(refreshed_instance, change_type) except sender.DoesNotExist: - # Object đã bị xóa (ví dụ: delete_entry vừa xóa Internal_Entry) - # Bỏ qua việc gửi update, hoặc gửi thông báo "deleted" nếu cần print(f"Object {sender.__name__} {instance.pk} đã bị xóa, bỏ qua gửi update.") - # Optional: vẫn gửi "deleted" để frontend biết object không còn send_model_update(instance, "deleted") except Exception as exc: print(f"Lỗi trong send_update_after_commit: {exc}") @@ -77,9 +63,7 @@ def generic_post_delete_handler(sender, instance, **kwargs): """ Hàm xử lý chung cho tín hiệu post_delete từ BẤT KỲ model nào. """ - # For delete, the action happens immediately, so on_commit is not strictly necessary - # unless the delete is part of a larger transaction that could be rolled back. - # It's safer to use it anyway. + def send_delete_after_commit(): send_model_update(instance, "deleted") @@ -95,4 +79,3 @@ def connect_signals(): print("Connected generic signals for real-time updates.") -# File apps.py của bạn đã gọi hàm connect_signals() này rồi, nên mọi thứ sẽ tự động hoạt động. \ No newline at end of file diff --git a/app/views.py b/app/views.py index b74f7325..59120c27 100644 --- a/app/views.py +++ b/app/views.py @@ -1041,277 +1041,6 @@ def set_token_expiry(request): return Response(status = status.HTTP_200_OK) -#============================================================================= -class ExcelImportAPIView(APIView): - parser_classes = (MultiPartParser, FormParser) - - def post(self, request, format=None): - excel_file = request.FILES.get('file') - if not excel_file: - return Response({'error': 'No Excel file provided (key "file" not found)'}, status=status.HTTP_400_BAD_REQUEST) - - config_str = request.data.get('config') - if not config_str: - return Response({'error': 'No configuration provided (key "config" not found)'}, status=status.HTTP_400_BAD_REQUEST) - - try: - config = json.loads(config_str) - except json.JSONDecodeError: - return Response({'error': 'Invalid JSON configuration'}, status=status.HTTP_400_BAD_REQUEST) - - model_name = config.get('model_name') - mappings = config.get('mappings', []) - import_mode = config.get('import_mode', 'insert_only') - - header_row_excel = config.get('header_row_index', 1) - header_index = max(0, header_row_excel - 1) - - # LẤY VÀ PHÂN TÍCH TRƯỜNG UNIQUE KEY - unique_fields_config = config.get('unique_fields', 'code') - if isinstance(unique_fields_config, str): - UNIQUE_KEY_FIELDS = [unique_fields_config] - elif isinstance(unique_fields_config, list): - UNIQUE_KEY_FIELDS = unique_fields_config - else: - return Response({'error': 'Invalid format for unique_fields. Must be a string or a list of strings.'}, status=status.HTTP_400_BAD_REQUEST) - - if not model_name or not mappings: - return Response({'error': 'model_name or mappings missing in configuration'}, status=status.HTTP_400_BAD_REQUEST) - - try: - TargetModel = apps.get_model('app', model_name) - except LookupError: - return Response({'error': f'Model "{model_name}" not found in app'}, status=status.HTTP_400_BAD_REQUEST) - - related_models_cache = {} - for mapping in mappings: - if 'foreign_key' in mapping: - fk_config = mapping['foreign_key'] - related_model_name = fk_config.get('model_name') - if related_model_name: - try: - related_models_cache[related_model_name] = apps.get_model('app', related_model_name) - except LookupError: - return Response({'error': f"Related model '{related_model_name}' not found for mapping '{mapping.get('excel_column')}'"}, status=status.HTTP_400_BAD_REQUEST) - - try: - file_stream = io.BytesIO(excel_file.read()) - if excel_file.name.lower().endswith(('.xlsx', '.xls')): - df = pd.read_excel(file_stream, header=header_index) - else: - df = pd.read_csv(file_stream, header=header_index) - except Exception as e: - return Response({'error': f'Error reading file: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST) - - cleaned_columns = [] - for col in df.columns: - col_str = str(col).strip() - col_str = col_str.replace('\n', ' ').strip() - col_str = re.sub(r'\s*\([^)]*\)', '', col_str).strip() - col_str = ' '.join(col_str.split()) - cleaned_columns.append(col_str) - - df.columns = cleaned_columns - df.reset_index(drop=True, inplace=True) - - # Caching Foreign Key objects - related_obj_cache = {} - for related_name, RelatedModel in related_models_cache.items(): - lookup_field = next((m['foreign_key']['lookup_field'] for m in mappings if 'foreign_key' in m and m['foreign_key']['model_name'] == related_name), None) - if lookup_field: - try: - related_obj_cache[related_name] = { - str(getattr(obj, lookup_field)).strip().lower(): obj - for obj in RelatedModel.objects.all() - } - if 'pk' not in related_obj_cache[related_name]: - related_obj_cache[related_name].update({ - str(obj.pk): obj for obj in RelatedModel.objects.all() - }) - except Exception as e: - return Response({'error': f"Error caching related model {related_name}: {e}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - objects_to_create = [] - errors = [] - - for index, row in df.iterrows(): - instance_data = {} - row_errors = [] - is_valid_for_db = True - - for mapping in mappings: - excel_column = mapping.get('excel_column') - model_field = mapping.get('model_field') - default_value = mapping.get('default_value') - - excel_value = None - is_static_default = False - - # 1. XÁC ĐỊNH NGUỒN GIÁ TRỊ (STATIC DEFAULT HOẶC EXCEL) - if not excel_column and default_value is not None: - # Trường hợp 1: Không có cột Excel, luôn dùng giá trị mặc định tĩnh - excel_value = default_value - is_static_default = True - elif excel_column and excel_column in row: - # Trường hợp 2: Có cột Excel - excel_value = row[excel_column] - is_static_default = False - - # === BỔ SUNG: KIỂM TRA VÀ SỬ DỤNG default_value NẾU CELL RỖNG === - # Nếu giá trị từ Excel rỗng VÀ có default_value được cung cấp trong mapping - if (pd.isna(excel_value) or (isinstance(excel_value, str) and str(excel_value).strip() == '')) and default_value is not None: - excel_value = default_value - is_static_default = True # Coi như giá trị tĩnh để bypass Section 2 (kiểm tra NULL) - # === KẾT THÚC BỔ SUNG === - - elif excel_column and excel_column not in row: - row_errors.append(f"Excel column '{excel_column}' not found (Header index: {header_row_excel})") - is_valid_for_db = False - continue - elif excel_column is None and default_value is None: - continue - else: - row_errors.append(f"Invalid mapping entry: {mapping} - requires excel_column or default_value") - is_valid_for_db = False - continue - - # 2. XỬ LÝ NULL/EMPTY VALUES (Chỉ khi giá trị đến từ Excel và KHÔNG phải giá trị tĩnh) - if not is_static_default and (pd.isna(excel_value) or (isinstance(excel_value, str) and str(excel_value).strip() == '')): - try: - field_obj = TargetModel._meta.get_field(model_field) - except FieldDoesNotExist: - row_errors.append(f"Model field '{model_field}' not found in model '{model_name}'") - is_valid_for_db = False - continue - - # Trường cho phép NULL - if field_obj.null: - instance_data[model_field] = None - continue - # Trường có Default Value (từ Model) - elif field_obj.default is not models_fields.NOT_PROVIDED: - instance_data[model_field] = field_obj.default - continue - # Trường KHÔNG cho phép NULL (Non-nullable field) - else: - # === START: LOGIC BỔ SUNG CHO allow_empty_excel_non_nullable === - allow_empty_non_nullable = mapping.get('allow_empty_excel_non_nullable', False) - - # Chỉ áp dụng bypass nếu là CharField/TextField (có thể lưu "" để thỏa mãn NOT NULL) - if allow_empty_non_nullable and isinstance(field_obj, (CharField, TextField)): - instance_data[model_field] = "" - continue # Chấp nhận chuỗi rỗng và đi tiếp - - # Nếu không được phép bypass HOẶC không phải CharField/TextField - row_errors.append(f"Non-nullable field '{model_field}' has empty value in row {index + 1}") - is_valid_for_db = False - instance_data[model_field] = "" if isinstance(field_obj, (CharField, TextField)) else None - continue - # === END: LOGIC BỔ SUNG CHO allow_empty_excel_non_nullable === - - # 3. XỬ LÝ FOREIGN KEY - if 'foreign_key' in mapping: - fk_config = mapping['foreign_key'] - related_model_name = fk_config.get('model_name') - key_to_lookup = str(excel_value).strip().lower() - RelatedModelCache = related_obj_cache.get(related_model_name, {}) - related_obj = RelatedModelCache.get(key_to_lookup) - - # Logic dự phòng để tìm theo ID nếu là giá trị tĩnh và là số - if not related_obj and is_static_default and str(excel_value).isdigit(): - related_obj = RelatedModelCache.get(str(excel_value)) - - if related_obj: - instance_data[model_field] = related_obj - else: - # Kiểm tra lại trường hợp giá trị lookup là rỗng/0 khi model field cho phép NULL - if (pd.isna(excel_value) or str(excel_value).strip() == '' or str(excel_value).strip() == '0') and TargetModel._meta.get_field(model_field).null: - instance_data[model_field] = None - continue - - # Báo lỗi và không hợp lệ nếu không tìm thấy object - row_errors.append(f"Related object for '{model_field}' with value '{excel_value}' not found in model '{related_model_name}' (row {index + 1})") - - if not TargetModel._meta.get_field(model_field).null: - is_valid_for_db = False - - instance_data[model_field] = None - continue - else: - instance_data[model_field] = excel_value - - if row_errors: - errors.append({'row': index + 1, 'messages': row_errors}) - - if is_valid_for_db: - try: - objects_to_create.append(TargetModel(**instance_data)) - except Exception as e: - errors.append({'row': index + 1, 'messages': [f"Critical error creating model instance: {str(e)}"]}) - - successful_row_count = len(objects_to_create) - - try: - with transaction.atomic(): - - # === LOGIC XỬ LÝ CÁC CHẾ ĐỘ NHẬP DỮ LIỆU === - if import_mode == 'overwrite': - TargetModel.objects.all().delete() - TargetModel.objects.bulk_create(objects_to_create) - message = f'{successful_row_count} records imported successfully after full **overwrite**.' - - elif import_mode == 'upsert': - for field in UNIQUE_KEY_FIELDS: - try: - TargetModel._meta.get_field(field) - except FieldDoesNotExist: - return Response({'error': f"Unique field '{field}' not found in model '{model_name}'. Cannot perform upsert."}, status=status.HTTP_400_BAD_REQUEST) - - existing_objects_query = TargetModel.objects.only('pk', *UNIQUE_KEY_FIELDS) - existing_map = {} - for obj in existing_objects_query: - key_tuple = tuple(getattr(obj, field) for field in UNIQUE_KEY_FIELDS) - existing_map[key_tuple] = obj - - to_update = [] - to_insert = [] - - for new_instance in objects_to_create: - try: - lookup_key = tuple(getattr(new_instance, field) for field in UNIQUE_KEY_FIELDS) - except AttributeError: - continue - - if lookup_key in existing_map: - new_instance.pk = existing_map[lookup_key].pk - to_update.append(new_instance) - else: - to_insert.append(new_instance) - - update_fields = [ - m['model_field'] - for m in mappings - if m['model_field'] not in ['pk'] and m['model_field'] not in UNIQUE_KEY_FIELDS - ] - - TargetModel.objects.bulk_update(to_update, update_fields) - TargetModel.objects.bulk_create(to_insert) - message = f'{len(to_insert)} records inserted, {len(to_update)} records updated successfully (Upsert mode).' - - elif import_mode == 'insert_only': - TargetModel.objects.bulk_create(objects_to_create) - message = f'{successful_row_count} records imported successfully (Insert Only mode).' - - else: - return Response({'error': f"Invalid import_mode specified: {import_mode}"}, status=status.HTTP_400_BAD_REQUEST) - - except Exception as e: - return Response({'error': f'Database error during bulk operation (Rollback occurred): {str(e)}', 'rows_attempted': successful_row_count}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - if errors: - return Response({'status': 'partial_success', 'message': f'{message} Invalid rows were skipped.', 'errors': errors}, status=status.HTTP_207_MULTI_STATUS) - - return Response({'status': 'success', 'message': message}, status=status.HTTP_201_CREATED) #============================================================================= executor = ThreadPoolExecutor(max_workers=10) def background_generate(doc_code, context_pks, output_filename, uid): @@ -1545,29 +1274,6 @@ class EmailTemplatePreview: @api_view(['POST']) def preview_email_template(request): - """ - API để preview email template - trả về nội dung đã thay thế placeholders - - POST /api/email/preview/ - Body: { - "template_id": 1, - "context_pks": { - "contract_id": 456, - "customer_id": 789 - } - } - - Response: { - "subject": "Thông báo hợp đồng #HD-001", - "content": "
Nội dung y nguyên đã thay thế...
", - "recipient_email": "customer@example.com", - "replacements": { - "[contract.code]": "HD-001", - "[customer.name]": "Nguyễn Văn A", - ... - } - } - """ try: # Validate input template_id = request.data.get('template_id') diff --git a/app/workflow_actions.py b/app/workflow_actions.py deleted file mode 100644 index 8f521607..00000000 --- a/app/workflow_actions.py +++ /dev/null @@ -1,375 +0,0 @@ -from django.test import Client -from app.workflow_registry import register_action -from app.workflow_utils import resolve_value -from app.document_generator import DocumentGenerator -from app.jobemail import EmailJobRunner -from app.payment import account_entry_api -from django.apps import apps -import re -import datetime - -client = Client() -# ============================ -# Logic xử lý Map Expression ($map) -# ============================ -def handle_map_expression(expression, context): - """ - Xử lý biểu thức đặc biệt để biến đổi danh sách dữ liệu. - Cú pháp: $map(data.installments, {amount: amount, due_date: $add_days(created_at, gap)}) - """ - # Regex tách nguồn dữ liệu và template - match = re.match(r"^\$map\(([^,]+),\s*\{(.*)\}\)$", expression.strip()) - if not match: - return [] - - source_path = match.group(1).strip() - template_content = match.group(2).strip() - - # Lấy danh sách dữ liệu gốc từ context (ví dụ: data.installments) - source_data = resolve_value(source_path, context) - if not isinstance(source_data, list): - return [] - - # Tìm các cặp key: value trong template định nghĩa - # Hỗ trợ cả trường hợp value là một hàm lồng như $add_days - pairs = re.findall(r"(\w+):\s*(\$add_days\([^)]+\)|[^{},]+)", template_content) - - results = [] - for index, item in enumerate(source_data): - # Tạo context riêng cho từng item để resolve - item_context = {**context, "item": item, "index": index} - processed_row = {} - - for key, val_expr in pairs: - val_expr = val_expr.strip() - - # 1. Xử lý biến chỉ mục ($index) - if val_expr == "$index": - processed_row[key] = index - - # 2. Xử lý hàm cộng ngày ($add_days) - elif "$add_days" in val_expr: - m = re.search(r"\$add_days\(([^,]+),\s*([^)]+)\)", val_expr) - if m: - base_key = m.group(1).strip() - days_key = m.group(2).strip() - - # Tìm giá trị ngày gốc và số ngày cần cộng - base_date = item.get(base_key) if base_key in item else resolve_value(base_key, item_context) - days = item.get(days_key) if days_key in item else resolve_value(days_key, item_context) - - try: - # Chuyển đổi string sang date object - if isinstance(base_date, str): - base_date = datetime.datetime.strptime(base_date[:10], "%Y-%m-%d").date() - - new_date = base_date + datetime.timedelta(days=int(days or 0)) - processed_row[key] = new_date.isoformat() - except Exception: - processed_row[key] = str(base_date) # Trả về bản gốc nếu lỗi - - # 3. Xử lý lấy giá trị từ item hiện tại hoặc context chung - else: - if val_expr == "$item": - processed_row[key] = item - elif val_expr == "$index": - processed_row[key] = index - elif val_expr in item: - processed_row[key] = item[val_expr] - else: - processed_row[key] = resolve_value(val_expr, item_context) - - results.append(processed_row) - - return results - -# ============================ -# CRUD thông qua API có sẵn -# ============================ -def deep_resolve_values(data, context): - if isinstance(data, dict): - return {k: deep_resolve_values(v, context) for k, v in data.items()} - elif isinstance(data, list): - return [deep_resolve_values(item, context) for item in data] - elif isinstance(data, str): - # Workaround for resolver bug: handle strings that are only a placeholder - match = re.fullmatch(r"\{([^}]+)\}", data) - if match: - # The path is the content inside the braces, e.g., "transaction_detail.id" - path = match.group(1) - # resolve_value works on raw paths, so call it directly - return resolve_value(path, context) - else: - # This handles complex strings like "/prefix/{path}/" or normal strings - return resolve_value(data, context) - else: - return data - -@register_action("API_CALL", schema={"required": ["method", "url"]}) -def api_call_action(params, context): - """Thực hiện gọi API nội bộ bằng Django Test Client""" - method = params["method"].upper() - url = resolve_value(params["url"], context) - save_as = params.get("save_as") - - raw_body = params.get("body") - - # ============================ - # Resolve body - # ============================ - if isinstance(raw_body, str) and raw_body.startswith("$map"): - body = handle_map_expression(raw_body, context) - elif isinstance(raw_body, dict): - body = deep_resolve_values(raw_body, context) - elif raw_body is None: - body = None - else: - body = resolve_value(raw_body, context) - - print(f" [API_CALL] {method} {url}") - print(f" [API_CALL] Resolved Body: {body}") - - # ============================ - # Execute request - # ============================ - if method == "POST": - resp = client.post(url, body, content_type="application/json") - elif method == "PATCH": - resp = client.patch(url, body, content_type="application/json") - elif method == "PUT": - resp = client.put(url, body, content_type="application/json") - elif method == "DELETE": - resp = client.delete(url) - else: - resp = client.get(url) - - print(f" [API_CALL] Status Code: {resp.status_code}") - - # ============================ - # Handle error - # ============================ - if resp.status_code >= 400: - error_content = resp.content.decode("utf-8") if resp.content else "" - print(f" [API_CALL] Error: {error_content}") - raise Exception(f"API Call failed: {error_content}") - - # ============================ - # Handle response safely - # ============================ - if resp.status_code == 204 or not resp.content: - # DELETE / No Content - result = {"deleted": True} - else: - try: - result = resp.json() - except ValueError: - # Fallback nếu response không phải JSON - result = resp.content.decode("utf-8") - - print(f" [API_CALL] Result: {result}") - - if save_as: - context[save_as] = result - - return result - - -# ============================ -# Gọi Utility / API bên ngoài -# ============================ -@register_action("CALL_UTILITY", schema={"required": ["utility_code"]}) -def call_utility_action(params, context): - Utility = apps.get_model("app", "Utility") - util = Utility.objects.get(code=params["utility_code"]) - - module_path = util.integration_module - if not module_path: - return {"error": "utility has no module"} - - from django.utils.module_loading import import_string - func = import_string(module_path) - - resolved_params = {k: resolve_value(v, context) for k,v in params.get("params", {}).items()} - return func(**resolved_params) - - -@register_action("SEND_EMAIL", schema={"required": ["template"]}) -def send_email_action(params, context): - tpl_name = params["template"] - tpl_pks = {k: resolve_value(v, context) for k,v in params.get("context_pks", {}).items()} - - Email_Template = apps.get_model("app", "Email_Template") - try: - template_obj = Email_Template.objects.get(name=tpl_name) - except Email_Template.DoesNotExist: - raise Exception(f"Email template '{tpl_name}' not found") - - runner = EmailJobRunner(template=template_obj, context_pks=tpl_pks) - success = runner.run() - - return {"sent": success} - - -# ============================ -# Tạo Document -# ============================ - -@register_action("GENERATE_DOCUMENT", schema={"required": ["document_code"]}) -def generate_document_action(params, context): - code = resolve_value(params["document_code"], context) - pks = {k: str(resolve_value(v, context)) for k, v in params.get("context_pks", {}).items()} - save_as = params.get("save_as") - - print(f" [GEN_DOC] Generating for code: {code}") - - gen = DocumentGenerator(document_code=code, context_pks=pks) - result = gen.generate() - - formatted_result = [{ - "pdf": result.get("pdf"), - "file": result.get("file"), - "name": result.get("name"), - "code": code - }] - - if save_as: - context[save_as] = formatted_result - print(f" [GEN_DOC] Success: Saved to context as '{save_as}'") - - return formatted_result - - -# ============================ -# Hạch toán -# ============================ -@register_action("ACCOUNT_ENTRY", schema={"required": ["amount", "category_code"]}) -def account_entry_action(params, context): - amount = resolve_value(params["amount"], context) - content = params.get("content", "") - userid = resolve_value(params.get("userid"), context) - - return account_entry_api( - code=params.get("internal_account", "HOAC02VND"), - amount=amount, - content=content, - type=params.get("type", "CR"), - category=apps.get_model("app", "Entry_Category").objects.get(code=params["category_code"]).id, - userid=userid, - ref=params.get("ref") - ) - -# ============================ -# Tìm bản ghi -# ============================ -@register_action("LOOKUP_DATA", schema={"required": ["model_name", "lookup_field", "lookup_value"]}) -def lookup_data_action(params, context): - model_name = params["model_name"] - field = params["lookup_field"] - save_as = params.get("save_as") - - # Lấy giá trị thực tế (ví dụ: "reserved") - value = resolve_value(params["lookup_value"], context) - - print(f" [LOOKUP] Searching {model_name} where {field} = '{value}'") - - try: - Model = apps.get_model("app", model_name) - obj = Model.objects.filter(**{field: value}).first() - - if not obj: - print(f" [LOOKUP] ERROR: Not found!") - raise Exception(f"Lookup failed: {model_name} with {field}={value} not found.") - - if save_as: - context[save_as] = obj - print(f" [LOOKUP] Success: Found ID {obj.id}, saved to context as '{save_as}'") - - return obj.id - except Exception as e: - print(f" [LOOKUP] EXCEPTION: {str(e)}") - raise e - - -# ============================ -# Quét và phân bổ toàn bộ bút toán còn phần dư -# ============================ -@register_action("ALLOCATE_ALL_PENDING", schema={}) -def allocate_all_pending_action(params, context): - """ - Quét toàn bộ Internal_Entry có allocation_remain > 0 (type CR), - group by product_id, gọi phân bổ cho từng product cho đến khi hết. - """ - from app.payment import allocate_payment_to_schedules, allocate_penalty_reduction - from decimal import Decimal - - Internal_Entry = apps.get_model("app", "Internal_Entry") - Payment_Schedule = apps.get_model("app", "Payment_Schedule") - Product_Booked = apps.get_model("app", "Product_Booked") - Transaction_Current = apps.get_model("app", "Transaction_Current") - Transaction_Detail = apps.get_model("app", "Transaction_Detail") - - # ---------- Lấy toàn bộ product_id còn entry chưa phân bổ hết ---------- - product_ids = list( - Internal_Entry.objects.filter( - type__code="CR", - allocation_remain__gt=0, - product__isnull=False - ) - .values_list("product_id", flat=True) - .distinct() - ) - - print(f" [ALLOCATE_ALL] Tìm được {len(product_ids)} product có entry còn phần dư") - - if not product_ids: - return {"total_products": 0, "results": []} - - # ---------- DEBUG: dump trạng thái trước khi phân bổ ---------- - for pid in product_ids: - print(f"\n [DEBUG] ===== Product {pid} — trạng thái TRƯỚC phân bổ =====") - - # Entries - entries = Internal_Entry.objects.filter( - product_id=pid, type__code="CR", allocation_remain__gt=0 - ).order_by("date", "create_time") - for e in entries: - print(f" Entry id={e.id} | account_id={e.account_id} | amount={e.amount} | allocation_remain={e.allocation_remain} | date={e.date}") - - # Lấy txn_detail của product - booked = Product_Booked.objects.filter(product_id=pid).first() - if not booked or not booked.transaction: - print(f" !! Không có Product_Booked / Transaction") - continue - - txn = booked.transaction - txn_detail = None - try: - current = Transaction_Current.objects.get(transaction=txn) - txn_detail = current.detail - except Exception: - txn_detail = Transaction_Detail.objects.filter(transaction=txn).order_by("-create_time").first() - - if not txn_detail: - print(f" !! Không có Transaction_Detail") - continue - - # Schedules - all_schedules = Payment_Schedule.objects.filter(txn_detail=txn_detail).order_by("cycle", "from_date") - unpaid = all_schedules.filter(status__id=1) - print(f" Tổng schedule: {all_schedules.count()} | Chưa thanh toán (status=1): {unpaid.count()}") - for s in all_schedules: - print(f" Schedule id={s.id} | cycle={s.cycle} | status_id={s.status_id} | amount_remain={s.amount_remain} | penalty_remain={s.penalty_remain} | remain_amount={s.remain_amount}") - - # ---------- Chạy phân bổ ---------- - results = [] - for product_id in product_ids: - try: - normal = allocate_payment_to_schedules(product_id) - reduction = allocate_penalty_reduction(product_id) - results.append({"product_id": product_id, "normal": normal, "reduction": reduction}) - print(f" [ALLOCATE_ALL] Product {product_id}: OK — normal={normal}") - except Exception as e: - print(f" [ALLOCATE_ALL] Product {product_id}: ERROR - {str(e)}") - results.append({"product_id": product_id, "error": str(e)}) - - return {"total_products": len(product_ids), "results": results} \ No newline at end of file diff --git a/app/workflow_engine.py b/app/workflow_engine.py deleted file mode 100644 index 4a10aeb6..00000000 --- a/app/workflow_engine.py +++ /dev/null @@ -1,84 +0,0 @@ -from django.db import transaction -from app.models import Workflow, StepAction, Rule -from app.workflow_registry import ACTION_REGISTRY, validate_action_schema -from app.workflow_utils import resolve_value - -@transaction.atomic -def execute_step(step: StepAction, context: dict): - #print(f"\n>>> EXECUTING STEP: {step.step_code} (Order: {step.order})") - - # Evaluate rules first - for rule in step.rules.filter(is_active=True): - if not evaluate_rule(rule, context): - #print(f"Step {step.step_code} skipped due to rule failure.") - return {"step": step.step_code, "skipped": True, "reason": "rule_failed"} - - results = [] - # Lưu ý: step.actions thường là một list các dict - actions_list = step.actions if isinstance(step.actions, list) else [] - - for action in actions_list: - action_type = action.get("type") - params = action.get("params", {}) - - #print(f" - Action Type: {action_type}") - - if action_type not in ACTION_REGISTRY: - #print(f" - ERROR: Action type '{action_type}' not registered!") - continue - - try: - validate_action_schema(action_type, params) - handler = ACTION_REGISTRY[action_type] - - # Thực thi handler - output = handler(params, context) - - results.append({"action": action_type, "result": output}) - - # Lưu output cuối cùng vào context - context["last_result"] = output - except Exception as e: - #print(f" - ERROR in action {action_type}: {str(e)}") - # Raise để transaction.atomic rollback nếu cần, hoặc xử lý tùy ý - raise e - - return {"step": step.step_code, "executed": True, "results": results} - - -def evaluate_rule(rule: Rule, context: dict): - for condition in (rule.conditions or []): - left = resolve_value(condition.get("left"), context) - right = resolve_value(condition.get("right"), context) - op = condition.get("operator", "==") - - #print(f" Evaluating Rule: {left} {op} {right}") - - if op == "IN" and left not in right: return False - if op == "==" and left != right: return False - if op == "!=" and left == right: return False - if op == ">" and not (left > right): return False - if op == "<" and not (left < right): return False - - return True - - -def run_workflow(workflow_code: str, trigger: str, context: dict): - #print(f"\n================ START WORKFLOW: {workflow_code} ================") - #print(f"Trigger: {trigger} | Initial Context: {context}") - - workflow = Workflow.objects.filter(code=workflow_code, is_active=True).first() - if not workflow: - #print(f"Workflow '{workflow_code}' not found or inactive.") - raise Exception(f"Workflow '{workflow_code}' not found") - - steps = workflow.steps.filter(trigger_event=trigger, is_active=True).order_by("order") - #print(f"Found {steps.count()} active steps.") - - outputs = [] - for step in steps: - res = execute_step(step, context) - outputs.append(res) - - #print(f"================ FINISH WORKFLOW: {workflow_code} ================\n") - return outputs \ No newline at end of file diff --git a/app/workflow_registry.py b/app/workflow_registry.py deleted file mode 100644 index 80ab6ec3..00000000 --- a/app/workflow_registry.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Callable, Dict - -ACTION_REGISTRY: Dict[str, Callable] = {} -ACTION_SCHEMAS: Dict[str, dict] = {} - -def register_action(name: str, schema=None): - def decorator(func): - ACTION_REGISTRY[name] = func - ACTION_SCHEMAS[name] = schema or {} - return func - return decorator - -def validate_action_schema(action_name, params): - schema = ACTION_SCHEMAS.get(action_name, {}) - required = schema.get("required", []) - - for key in required: - if key not in params: - raise Exception(f"Action '{action_name}' missing required param: {key}") - - return True diff --git a/app/workflow_utils.py b/app/workflow_utils.py deleted file mode 100644 index d08ca2f1..00000000 --- a/app/workflow_utils.py +++ /dev/null @@ -1,652 +0,0 @@ -import re -import math -from datetime import datetime, date, timedelta -from decimal import Decimal -from django.db import models -from django.apps import apps - -# ============================================= -# CORE RESOLVER -# ============================================= -def resolve_value(expr, context): - """ - Universal expression resolver with support for: - - Literals (int, float, bool, string) - - Template strings: {key}, "text {key} text" - - Dotted paths: customer.address.city - - Math functions: $add, $sub, $multiply, $divide, $mod, $power, $round, $abs, $min, $max - - Date functions: $now, $today, $date_diff, $date_add, $date_format, $date_parse - - String functions: $concat, $upper, $lower, $trim, $replace, $substring, $split, $length - - Logic functions: $if, $switch, $and, $or, $not - - List functions: $append, $agg, $filter, $map, $first, $last, $count, $sum - - Lookup functions: $vlookup, $lookup, $get - - Nested functions support - """ - if expr is None: - return None - - # Direct literal types - if isinstance(expr, (int, float, bool, Decimal)): - return expr - - if not isinstance(expr, str): - return expr - - expr = expr.strip() - - # ============================================= - # 1. SYSTEM VARIABLES - # ============================================= - if expr == "$now": - return context.get("now", datetime.now()) - - if expr == "$today": - if "today" in context: - return context["today"] - now_in_context = context.get("now") - if isinstance(now_in_context, datetime): - return now_in_context.date() - return date.today() - - if expr == "$today_str": - now_val = context.get("now", datetime.now()) - if isinstance(now_val, datetime): - return now_val.date().isoformat() - elif isinstance(now_val, date): - return now_val.isoformat() - return date.today().isoformat() - - if expr == "$now_iso": - return datetime.now().isoformat(timespec='seconds') - - if expr == "$timestamp": - return int(datetime.now().timestamp()) - - # ============================================= - # 2. MATH FUNCTIONS (Support Nested) - # ============================================= - math_functions = { - 'add': lambda a, b: a + b, - 'sub': lambda a, b: a - b, - 'subtract': lambda a, b: a - b, - 'multiply': lambda a, b: a * b, - 'mul': lambda a, b: a * b, - 'divide': lambda a, b: a / b if b != 0 else 0, - 'div': lambda a, b: a / b if b != 0 else 0, - 'mod': lambda a, b: a % b if b != 0 else 0, - 'power': lambda a, b: a ** b, - 'pow': lambda a, b: a ** b, - } - - for func_name, func in math_functions.items(): - pattern = rf'^\${func_name}\((.*)\)$' - match = re.match(pattern, expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) == 2: - a = to_number(resolve_value(args[0], context)) - b = to_number(resolve_value(args[1], context)) - return func(a, b) - - # Single-argument math functions - single_math = { - 'round': lambda x, d=0: round(x, int(d)), - 'abs': lambda x: abs(x), - 'ceil': lambda x: math.ceil(x), - 'floor': lambda x: math.floor(x), - 'sqrt': lambda x: math.sqrt(x) if x >= 0 else 0, - } - - for func_name, func in single_math.items(): - pattern = rf'^\${func_name}\((.*)\)$' - match = re.match(pattern, expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) >= 1: - val = to_number(resolve_value(args[0], context)) - if len(args) == 2 and func_name == 'round': - decimals = to_number(resolve_value(args[1], context)) - return func(val, decimals) - return func(val) - - # Multi-argument math - if re.match(r'^\$(min|max)\(', expr, re.IGNORECASE): - match = re.match(r'^\$(min|max)\((.*)\)$', expr, re.IGNORECASE) - if match: - func_name = match.group(1).lower() - args = split_args(match.group(2)) - values = [to_number(resolve_value(arg, context)) for arg in args] - return min(values) if func_name == 'min' else max(values) - - # ============================================= - # 3. DATE FUNCTIONS - # ============================================= - # $date_diff(date1, date2, unit?) - if re.match(r'^\$date_diff\(', expr, re.IGNORECASE): - match = re.match(r'^\$date_diff\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) >= 2: - raw_d1 = resolve_value(args[0], context) - raw_d2 = resolve_value(args[1], context) - - d1 = to_date(raw_d1) - d2 = to_date(raw_d2) - - unit = resolve_value(args[2], context).lower() if len(args) > 2 else 'days' - - #print(f"[DEBUG date_diff] raw_d1: {raw_d1}, raw_d2: {raw_d2}") # DEBUG - #print(f"[DEBUG date_diff] d1 (datetime): {d1}, d2 (datetime): {d2}") # DEBUG - #print(f"[DEBUG date_diff] unit: {unit}") # DEBUG - - if not (d1 and d2): - #print("[DEBUG date_diff] One or both dates are invalid. Returning 0.") # DEBUG - return 0 - - # Ensure we are comparing date objects, ignoring time - d1_date_only = d1.date() - d2_date_only = d2.date() - - #print(f"[DEBUG date_diff] d1_date_only: {d1_date_only}, d2_date_only: {d2_date_only}") # DEBUG - - if unit == 'days': - delta_days = (d1_date_only - d2_date_only).days - #print(f"[DEBUG date_diff] Calculated delta_days: {delta_days}. Returning {delta_days}.") # DEBUG - return delta_days - elif unit == 'months': - delta_months = (d1_date_only.year - d2_date_only.year) * 12 + d1_date_only.month - d2_date_only.month - #print(f"[DEBUG date_diff] Calculated delta_months: {delta_months}. Returning {delta_months}.") # DEBUG - return delta_months - elif unit == 'years': - delta_years = d1_date_only.year - d2_date_only.year - #print(f"[DEBUG date_diff] Calculated delta_years: {delta_years}. Returning {delta_years}.") # DEBUG - return delta_years - - #print(f"[DEBUG date_diff] Unit '{unit}' not recognized. Returning 0.") # DEBUG - return 0 - - # $date_add(date, amount, unit?) - if re.match(r'^\$date_add\(', expr, re.IGNORECASE): - match = re.match(r'^\$date_add\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) >= 2: - base_date = to_date(resolve_value(args[0], context)) - amount = to_number(resolve_value(args[1], context)) - unit = resolve_value(args[2], context).lower() if len(args) > 2 else 'days' - - if base_date: - # Ensure base_date is datetime - if isinstance(base_date, date) and not isinstance(base_date, datetime): - base_date = datetime.combine(base_date, datetime.min.time()) - - if unit == 'days': - result = base_date + timedelta(days=int(amount)) - elif unit == 'months': - month = base_date.month + int(amount) - year = base_date.year + (month - 1) // 12 - month = ((month - 1) % 12) + 1 - result = base_date.replace(year=year, month=month) - elif unit == 'years': - result = base_date.replace(year=base_date.year + int(amount)) - elif unit == 'hours': - result = base_date + timedelta(hours=int(amount)) - else: - result = base_date + timedelta(days=int(amount)) - - return result.isoformat() if isinstance(result, datetime) else result.strftime("%Y-%m-%d") - - # $date_format(date, format) - if re.match(r'^\$date_format\(', expr, re.IGNORECASE): - match = re.match(r'^\$date_format\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) == 2: - dt = to_date(resolve_value(args[0], context)) - fmt = resolve_value(args[1], context).strip('\'"') - if dt: - return dt.strftime(fmt) - - # $date_parse(string, format) - if re.match(r'^\$date_parse\(', expr, re.IGNORECASE): - match = re.match(r'^\$date_parse\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) >= 1: - date_str = str(resolve_value(args[0], context)) - fmt = resolve_value(args[1], context).strip('\'"') if len(args) > 1 else "%Y-%m-%d" - try: - return datetime.strptime(date_str, fmt).strftime("%Y-%m-%d") - except: - return None - - # ============================================= - # 4. STRING FUNCTIONS - # ============================================= - # $concat(str1, str2, ...) - if re.match(r'^\$concat\(', expr, re.IGNORECASE): - match = re.match(r'^\$concat\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - return ''.join(str(resolve_value(arg, context) or '') for arg in args) - - # $upper, $lower, $trim - string_single = { - 'upper': lambda s: str(s).upper(), - 'lower': lambda s: str(s).lower(), - 'trim': lambda s: str(s).strip(), - 'length': lambda s: len(str(s)), - } - - for func_name, func in string_single.items(): - pattern = rf'^\${func_name}\((.*)\)$' - match = re.match(pattern, expr, re.IGNORECASE) - if match: - arg = resolve_value(match.group(1).strip(), context) - return func(arg) - - # $replace(text, old, new) - if re.match(r'^\$replace\(', expr, re.IGNORECASE): - match = re.match(r'^\$replace\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) == 3: - text = str(resolve_value(args[0], context)) - old = str(resolve_value(args[1], context)).strip('\'"') - new = str(resolve_value(args[2], context)).strip('\'"') - return text.replace(old, new) - - # $substring(text, start, length?) - if re.match(r'^\$substring\(', expr, re.IGNORECASE): - match = re.match(r'^\$substring\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) >= 2: - text = str(resolve_value(args[0], context)) - start = int(to_number(resolve_value(args[1], context))) - length = int(to_number(resolve_value(args[2], context))) if len(args) > 2 else None - return text[start:start+length] if length else text[start:] - - # $split(text, delimiter) - if re.match(r'^\$split\(', expr, re.IGNORECASE): - match = re.match(r'^\$split\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) == 2: - text = str(resolve_value(args[0], context)) - delimiter = str(resolve_value(args[1], context)).strip('\'"') - return text.split(delimiter) - - # ============================================= - # 5. LOGIC FUNCTIONS - # ============================================= - # $if(condition, true_value, false_value) - if re.match(r'^\$if\(', expr, re.IGNORECASE): - match = re.match(r'^\$if\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) == 3: - condition = resolve_value(args[0], context) - return resolve_value(args[1], context) if condition else resolve_value(args[2], context) - - # $switch(value, case1, result1, case2, result2, ..., default) - if re.match(r'^\$switch\(', expr, re.IGNORECASE): - match = re.match(r'^\$switch\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) >= 2: - value = resolve_value(args[0], context) - for i in range(1, len(args) - 1, 2): - if i + 1 < len(args): - case = resolve_value(args[i], context) - if value == case: - return resolve_value(args[i + 1], context) - # Default value is last arg if odd number of args - if len(args) % 2 == 0: - return resolve_value(args[-1], context) - - # $and, $or, $not - if re.match(r'^\$and\(', expr, re.IGNORECASE): - match = re.match(r'^\$and\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - return all(resolve_value(arg, context) for arg in args) - - if re.match(r'^\$or\(', expr, re.IGNORECASE): - match = re.match(r'^\$or\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - return any(resolve_value(arg, context) for arg in args) - - if re.match(r'^\$not\(', expr, re.IGNORECASE): - match = re.match(r'^\$not\((.*)\)$', expr, re.IGNORECASE) - if match: - arg = resolve_value(match.group(1).strip(), context) - return not arg - - # ============================================= - # 6. LIST/ARRAY FUNCTIONS - # ============================================= - # $append(list, element) - if re.match(r'^\$append\(', expr, re.IGNORECASE): - match = re.match(r"^\$append\(([^,]+),\s*(.+)\)$", expr, re.DOTALL) - if match: - list_expr = match.group(1).strip() - element_expr = match.group(2).strip() - - # 1. Resolve the list - target_list = resolve_value(list_expr, context) - if target_list is None: - target_list = [] - - # Ensure it's a copy so we don't modify the original context variable directly - target_list = list(target_list) - - # 2. Resolve the element - resolved_element = resolve_value(element_expr, context) - - if isinstance(resolved_element, str): - try: - import json - element_to_append = json.loads(resolved_element) - except json.JSONDecodeError: - element_to_append = resolved_element - else: - element_to_append = resolved_element - - target_list.append(element_to_append) - return target_list - - - # $first(list), $last(list) - if re.match(r'^\$(first|last)\(', expr, re.IGNORECASE): - match = re.match(r'^\$(first|last)\((.*)\)$', expr, re.IGNORECASE) - if match: - func_name = match.group(1).lower() - lst = resolve_value(match.group(2).strip(), context) - if isinstance(lst, list) and len(lst) > 0: - return lst[0] if func_name == 'first' else lst[-1] - - # $count(list) - if re.match(r'^\$count\(', expr, re.IGNORECASE): - match = re.match(r'^\$count\((.*)\)$', expr, re.IGNORECASE) - if match: - lst = resolve_value(match.group(1).strip(), context) - return len(lst) if isinstance(lst, list) else 0 - - # $agg(list, operation, field?) - if re.match(r'^\$agg\(', expr, re.IGNORECASE): - match = re.match(r'^\$agg\(([^,]+),\s*[\'"]([^\'\"]+)[\'"](?:,\s*[\'"]?([^\'\")]+)[\'"]?)?\)$', expr) - if match: - list_expr = match.group(1).strip() - operation = match.group(2).strip() - field_expr = match.group(3).strip() if match.group(3) else None - - target_list = resolve_value(list_expr, context) - if not isinstance(target_list, list): - return 0 - - if operation == 'count': - return len(target_list) - - if operation == 'sum': - if not field_expr: - return sum(to_number(item) for item in target_list) - total = 0 - for item in target_list: - value = item.get(field_expr) if isinstance(item, dict) else getattr(item, field_expr, 0) - total += to_number(value) - return total - - if operation in ['min', 'max', 'avg']: - values = [] - for item in target_list: - if field_expr: - value = item.get(field_expr) if isinstance(item, dict) else getattr(item, field_expr, 0) - else: - value = item - values.append(to_number(value)) - - if not values: - return 0 - if operation == 'min': - return min(values) - elif operation == 'max': - return max(values) - elif operation == 'avg': - return sum(values) / len(values) - - # ============================================= - # 7. LOOKUP FUNCTIONS - # ============================================= - # $vlookup(lookup_value, model_name, lookup_field, return_field) - if re.match(r'^\$vlookup\(', expr, re.IGNORECASE): - match = re.match(r'^\$vlookup\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) == 4: - lookup_value = resolve_value(args[0], context) - model_name = resolve_value(args[1], context).strip('\'"') - lookup_field = resolve_value(args[2], context).strip('\'"') - return_field = resolve_value(args[3], context).strip('\'"') - - try: - Model = apps.get_model('app', model_name) - obj = Model.objects.filter(**{lookup_field: lookup_value}).first() - if obj: - return getattr(obj, return_field, None) - except: - pass - - # $lookup(model_name, field, value) - if re.match(r'^\$lookup\(', expr, re.IGNORECASE): - match = re.match(r'^\$lookup\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) == 3: - model_name = resolve_value(args[0], context).strip('\'"') - field = resolve_value(args[1], context).strip('\'"') - value = resolve_value(args[2], context) - - try: - Model = apps.get_model('app', model_name) - return Model.objects.filter(**{field: value}).first() - except: - pass - - # $get(dict_or_object, key, default?) - if re.match(r'^\$get\(', expr, re.IGNORECASE): - match = re.match(r'^\$get\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) >= 2: - obj = resolve_value(args[0], context) - key = resolve_value(args[1], context) - default = resolve_value(args[2], context) if len(args) > 2 else None - - if isinstance(obj, dict): - return obj.get(key, default) - else: - return getattr(obj, key, default) - - # ============================================= - # 8. COMPARISON OPERATORS - # ============================================= - # $eq, $ne, $gt, $gte, $lt, $lte, $in, $contains - comparisons = { - 'eq': lambda a, b: a == b, - 'ne': lambda a, b: a != b, - 'gt': lambda a, b: a > b, - 'gte': lambda a, b: a >= b, - 'lt': lambda a, b: a < b, - 'lte': lambda a, b: a <= b, - 'in': lambda a, b: a in b, - 'contains': lambda a, b: b in a, - } - - for op_name, op_func in comparisons.items(): - pattern = rf'^\${op_name}\(' - if re.match(pattern, expr, re.IGNORECASE): - match = re.match(rf'^\${op_name}\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) == 2: - a = resolve_value(args[0], context) - b = resolve_value(args[1], context) - return op_func(a, b) - - # ============================================= - # 9. HELPER: Get context value (dotted path) - # ============================================= - def get_context_value(key_path): - if not key_path: - return None - - # Check if numeric literal - if re.match(r"^-?\d+(\.\d+)?$", key_path): - return float(key_path) - - # Simple key - if "." not in key_path: - val = context.get(key_path) - if isinstance(val, Decimal): - return float(val) - return val - - # Dotted path - root, *rest = key_path.split(".") - val = context.get(root) - - for r in rest: - if val is None: - return None - - # Array notation: field[0] - array_match = re.match(r"(\w+)\[(\d+)\]", r) - if array_match: - attr_name = array_match.group(1) - index = int(array_match.group(2)) - val = getattr(val, attr_name, None) if not isinstance(val, dict) else val.get(attr_name) - try: - val = val[index] - except: - return None - else: - if isinstance(val, dict): - val = val.get(r) - else: - val = getattr(val, r, None) - - # Auto-fetch first() for QuerySet - if hasattr(val, 'all') and not isinstance(val, models.Model): - val = val.first() - - if isinstance(val, Decimal): - return float(val) - return val - - # ============================================= - # 10. TEMPLATE STRING PROCESSING - # ============================================= - pattern = re.compile(r"\{(\w+(\.\w+)*)\}") - if pattern.search(expr): - single_match = pattern.fullmatch(expr) - if single_match: - return get_context_value(single_match.group(1)) - - def replace_match(match): - val = get_context_value(match.group(1)) - return str(val) if val is not None else "" - return pattern.sub(replace_match, expr) - - # ============================================= - # 11. SUPPORT $last_result - # ============================================= - if expr.startswith("$last_result"): - _, _, field = expr.partition(".") - last_res = context.get("last_result") - if not field: - return last_res - if last_res is None: - return None - return getattr(last_res, field, None) if not isinstance(last_res, dict) else last_res.get(field) - - # ============================================= - # 12. DOTTED PATH OR DIRECT CONTEXT KEY - # ============================================= - if re.match(r"^-?\d+(\.\d+)?$", expr): - return float(expr) - - if "." in expr or expr in context: - return get_context_value(expr) - - return expr - - -# ============================================= -# HELPER FUNCTIONS -# ============================================= -def split_args(content): - """ - Split function arguments respecting nested parentheses and quotes. - Example: "a, $add(b, c), 'd'" -> ["a", "$add(b, c)", "'d'"] - """ - args = [] - current = [] - depth = 0 - in_quote = None - - for char in content: - if char in ('"', "'") and (not in_quote or in_quote == char): - in_quote = None if in_quote else char - current.append(char) - elif in_quote: - current.append(char) - elif char == '(': - depth += 1 - current.append(char) - elif char == ')': - depth -= 1 - current.append(char) - elif char == ',' and depth == 0: - args.append(''.join(current).strip()) - current = [] - else: - current.append(char) - - if current: - args.append(''.join(current).strip()) - - return args - - -def to_number(value, default=0): - """Convert value to number, return default if fails.""" - if value is None or value == '': - return default - try: - return float(value) - except (ValueError, TypeError): - return default - - -def to_date(value): - """Convert value to datetime object.""" - #print(f"[DEBUG to_date] Input value: {value} (type: {type(value)})") # DEBUG - if isinstance(value, datetime): - #print(f"[DEBUG to_date] Output (datetime): {value}") # DEBUG - return value - if isinstance(value, date): - result = datetime.combine(value, datetime.min.time()) - #print(f"[DEBUG to_date] Output (date -> datetime): {result}") # DEBUG - return result - if isinstance(value, str): - for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d", "%d/%m/%Y", "%Y-%m-%dT%H:%M:%S"): - try: - result = datetime.strptime(value.split('.')[0], fmt) - #print(f"[DEBUG to_date] Output (str -> datetime): {result}") # DEBUG - return result - except: - continue - #print(f"[DEBUG to_date] Output (None): None") # DEBUG - return None \ No newline at end of file diff --git a/prefect-ui.log b/prefect-ui.log new file mode 100644 index 00000000..f8d6b34c --- /dev/null +++ b/prefect-ui.log @@ -0,0 +1,17 @@ + + ___ ___ ___ ___ ___ ___ _____ +| _ \ _ \ __| __| __/ __|_ _| +| _/ / _|| _|| _| (__ | | +|_| |_|_\___|_| |___\___| |_| + +Configure Prefect to communicate with the server with: + + prefect config set PREFECT_API_URL=http://127.0.0.1:4200/api + +View the API reference documentation at http://127.0.0.1:4200/docs + +Check out the dashboard at http://127.0.0.1:4200 + + + +Server stopped! diff --git a/requirements.txt b/requirements.txt index f330c3bc..47d97af9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,5 +20,6 @@ num2words mammoth paramiko channels +prefect croniter uvicorn[standard] \ No newline at end of file diff --git a/rundev.sh b/rundev.sh index 0ad8820e..c481feda 100644 --- a/rundev.sh +++ b/rundev.sh @@ -1,11 +1,30 @@ +#!/usr/bin/env bash + +# Chạy Prefect UI (background, port 4200) +if ! lsof -i:4200 > /dev/null 2>&1; then + echo "Port 4200 trống → Khởi động Prefect server background..." + nohup prefect server start --host 127.0.0.1 --port 4200 > prefect-ui.log 2>&1 & + sleep 3 # chờ 3 giây để server khởi động ổn định + echo "Prefect UI đã khởi động (truy cập: http://localhost:4200)" + echo "Logs: tail -f prefect-ui.log" +else + echo "Port 4200 đã có Prefect server chạy rồi → bỏ qua" +fi + +# ======================== +# Chạy Django API (gunicorn + uvicorn) +# ======================== python3 envdev.py + sudo kill -9 $(lsof -i:8000 -t) 2> /dev/null + +echo "Khởi động Gunicorn..." gunicorn api.asgi:application \ - -k uvicorn.workers.UvicornWorker \ - -w 3 \ - --worker-connections 2000 \ - --max-requests 10000 \ - --max-requests-jitter 1000 \ - --timeout 1000 \ - --log-level info \ - -b 0.0.0.0:8000 \ No newline at end of file + -k uvicorn.workers.UvicornWorker \ + -w 3 \ + --worker-connections 2000 \ + --max-requests 10000 \ + --max-requests-jitter 1000 \ + --timeout 1000 \ + --log-level info \ + -b 0.0.0.0:8000 \ No newline at end of file