Merge branch 'origin' of ssh://git.bigdatatech.vn:235/utopia/api into origin

This commit is contained in:
Xuan Loi
2026-01-05 12:03:10 +07:00
40 changed files with 77 additions and 334 deletions

Binary file not shown.

View File

@@ -14,10 +14,9 @@ Including another URLconf
2. Add a URL to urlpatterns: re_path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: re_path('blog/', include('blog.urls'))
""" """
from django.urls import re_path from django.urls import re_path
from app import views, cob, payment, contract, cleardata, email, backup, server, workflows,api_workflow, importdata from app import views, cob, payment, contract, cleardata, email, backup, server,api_workflow, importdata
urlpatterns = [ urlpatterns = [
# New Workflow Endpoints
re_path("workflow/execute/$", api_workflow.execute_workflow), re_path("workflow/execute/$", api_workflow.execute_workflow),
# Existing Endpoints # Existing Endpoints

View File

@@ -1,7 +1,7 @@
import os import os
import subprocess import subprocess
from datetime import datetime from datetime import datetime
from django.db import models
import numpy as np import numpy as np
from docx import Document from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.enum.text import WD_ALIGN_PARAGRAPH
@@ -206,13 +206,23 @@ class DocumentGenerator:
raise ValueError(f"Could not resolve '{lookup_from}'. It is not a valid API parameter or a reference to another data source.") raise ValueError(f"Could not resolve '{lookup_from}'. It is not a valid API parameter or a reference to another data source.")
def _get_value_from_object(self, obj, field_path): def _get_value_from_object(self, obj, field_path):
if obj is None: if not obj:
return None return None
parts = field_path.split('.')
value = obj value = obj
for part in field_path.replace("__", ".").split("."): for part in parts:
if value is None: if value is None:
return None break
# Lấy thuộc tính từ object
value = getattr(value, part, None) value = getattr(value, part, None)
# KIỂM TRA NẾU LÀ QUAN HỆ NGƯỢC (ForeignKey ngược hoặc ManyToMany)
# Trong Django, các quan hệ này trả về một Manager (có method 'all')
if hasattr(value, 'all') and not isinstance(value, models.Model):
value = value.first() # Tự động lấy bản ghi đầu tiên
return value return value
def fetch_data(self): def fetch_data(self):

View File

@@ -0,0 +1,35 @@
# Generated by Django 5.1.7 on 2026-01-04 16:15
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('app', '0343_import_setting_call_api_delete_payment'),
]
operations = [
migrations.AddField(
model_name='legal_rep',
name='relation',
field=models.ForeignKey(default=10, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='app.relation'),
preserve_default=False,
),
migrations.AddField(
model_name='organization',
name='bank_account',
field=models.CharField(max_length=50, null=True),
),
migrations.AddField(
model_name='organization',
name='bank_name',
field=models.CharField(max_length=100, null=True),
),
migrations.AddField(
model_name='organization',
name='tax_code',
field=models.CharField(max_length=20, null=True),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.1.7 on 2026-01-04 17:34
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('app', '0344_legal_rep_relation_organization_bank_account_and_more'),
]
operations = [
migrations.AlterField(
model_name='co_ownership',
name='transaction',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='co_op', to='app.transaction'),
),
]

View File

@@ -1326,7 +1326,10 @@ class Organization(models.Model):
customer = models.ForeignKey(Customer, null=False, related_name='orgncust', on_delete=models.PROTECT) customer = models.ForeignKey(Customer, null=False, related_name='orgncust', on_delete=models.PROTECT)
shortname = models.CharField(max_length=50, null=False) shortname = models.CharField(max_length=50, null=False)
established_date = models.DateField() established_date = models.DateField()
tax_code = models.CharField(max_length=20, null=True)
website = models.CharField(max_length=200, null=True) 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) create_time = models.DateTimeField(null=True, auto_now_add=True)
update_time = models.DateTimeField(null=True, auto_now=True) update_time = models.DateTimeField(null=True, auto_now=True)
@@ -1338,6 +1341,7 @@ class Organization(models.Model):
class Legal_Rep(models.Model): class Legal_Rep(models.Model):
organization = models.ForeignKey(Organization, null=False, related_name='orgrep', on_delete=models.PROTECT) 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) 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) create_time = models.DateTimeField(null=True, auto_now_add=True)
class Meta: class Meta:
@@ -1961,7 +1965,7 @@ class Transaction_Discount(models.Model):
class Co_Ownership(models.Model): class Co_Ownership(models.Model):
code = models.CharField(max_length=30, null=False, unique=True) code = models.CharField(max_length=30, null=False, unique=True)
transaction = models.ForeignKey(Transaction, null=False, related_name='+', on_delete=models.PROTECT) 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) people = models.ForeignKey(People, null=False, related_name='+', on_delete=models.PROTECT)
create_time = models.DateTimeField(null=True, auto_now_add=True) create_time = models.DateTimeField(null=True, auto_now_add=True)
update_time = models.DateTimeField(null=True, auto_now=True) update_time = models.DateTimeField(null=True, auto_now=True)

View File

@@ -1,325 +0,0 @@
# workflows.py
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from django.db import transaction
from django.db.models import F
from decimal import Decimal
from app.models import (
Transaction,User, Transaction_Detail, Product, Product_Status, Payment_Schedule,
Transaction_Phase, Transaction_Status, Payment_Status, Entry_Category,
Payment_Plan,Product_Booked
)
from app.payment import account_entry_api
from datetime import datetime, timedelta # Corrected: Import timedelta
@api_view(['POST'])
@transaction.atomic
def create_reservation(request):
"""
Tạo một giao dịch giữ chỗ (24h hoặc giữ chỗ thường).
"""
data = request.data
try:
phase_code = data.get('phase_code')
deposit_received = data.get('deposit_received')
customer_id = data.get('customer_id')
product_id = data.get('product_id')
policy_id = data.get('policy_id')
user_id = data.get('user_id')
origin_price = data.get('origin_price')
discount_amount = data.get('discount_amount', 0)
sale_price = data.get('sale_price')
deposit_amount = data.get('deposit_amount')
installments = data.get('installments', [])
payment_plan = data.get('payment_plan', None)
reservation_phase = Transaction_Phase.objects.get(code=phase_code)
initial_detail_status = Transaction_Status.objects.get(code='new')
# Correction: Use 'unpaid' for unconfirmed payment status
unconfirmed_payment_status = Payment_Status.objects.get(code='unpaid')
if phase_code == 'reserved24H':
product_status_code = 'resvered24H'
elif phase_code == 'deposit':
product_status_code = 'deposit'
elif phase_code == 'fulfillwish':
product_status_code = 'deposit'
else:
product_status_code = 'resvered'
product_status_reserved = Product_Status.objects.get(code=product_status_code)
transaction_obj = Transaction.objects.create(
customer_id=customer_id, product_id=product_id, policy_id=policy_id,
phase=reservation_phase, date=datetime.now().strftime('%Y-%m-%d'),
origin_price=origin_price, discount_amount=discount_amount, sale_price=sale_price,
deposit_amount=deposit_amount, deposit_received=0,
deposit_remaining=deposit_amount, amount_received=0,
payment_plan=payment_plan
)
amount_recived = installments[0].get('amount') if installments else 0
transaction_detail_obj = Transaction_Detail.objects.create(
transaction=transaction_obj, phase=reservation_phase, status=initial_detail_status,
date=datetime.now().strftime('%Y-%m-%d'),amount=deposit_amount,amount_recived=amount_recived,
due_date=datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=2),
creator_id=user_id
)
product_obj = Product.objects.get(id=product_id)
product_obj.status = product_status_reserved
product_obj.save()
product_booked_obj = Product_Booked.objects.create(
transaction=transaction_obj,
product=product_obj
)
schedules = []
for i, installment in enumerate(installments):
schedule = Payment_Schedule.objects.create(
txn_detail=transaction_detail_obj,
from_date=datetime.now().strftime('%Y-%m-%d'),
to_date=datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=installment.get('due_days', 0)),
amount=installment.get('amount'), cycle=i + 1,
cycle_days=installment.get('due_days', 0), type_id=1,
status=unconfirmed_payment_status, updater_id=user_id,
detail={'note': f"Thanh toán cọc đợt {i+1}"}
)
schedules.append(schedule.id)
return Response({
'message': f'Giao dịch "{reservation_phase.name}" đã được tạo thành công.',
'transaction_id': transaction_obj.id,
'transaction_code': transaction_obj.code,
'transaction_detail_id': transaction_detail_obj.id,
'product_status': product_obj.status.name,
'payment_schedule_ids': schedules
}, status=status.HTTP_201_CREATED)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
@api_view(['POST'])
@transaction.atomic
def confirm_payment_schedule(request):
"""
Kế toán xác nhận một công nợ (Payment_Schedule).
Job chạy ngầm sẽ lo việc chuyển trạng thái detail.
"""
data = request.data
try:
payment_schedule_id = data.get('payment_schedule_id')
accountant_id = data.get('user_id')
internal_account_code = data.get('internal_account_code', 'HOAC02VND')
# Correction: Use 'paid' for confirmed payment status
confirmed_payment_status = Payment_Status.objects.get(code='paid')
schedule_obj = Payment_Schedule.objects.select_related('txn_detail', 'txn_detail__transaction__customer', 'status').get(id=payment_schedule_id)
if schedule_obj.status.code == 'paid':
return Response({'message': 'Công nợ này đã được xác nhận trước đó.'}, status=status.HTTP_200_OK)
schedule_obj.status = confirmed_payment_status
schedule_obj.updater_id = accountant_id
entry_content = f"Xác nhận thanh toán đợt {schedule_obj.cycle} cho GD {schedule_obj.txn_detail.code} của KH {schedule_obj.txn_detail.transaction.customer.code}"
deposit_category = Entry_Category.objects.get(code='THU_COC')
entry_data = account_entry_api(
code=internal_account_code, amount=schedule_obj.amount, content=entry_content,
type='CR', category=deposit_category.id, userid=accountant_id,
ref=schedule_obj.txn_detail.code
)
if 'error' in entry_data:
raise Exception(entry_data['error'])
schedule_obj.entry_id = entry_data['id']
schedule_obj.save()
transaction_obj = schedule_obj.txn_detail.transaction
schedule_amount = Decimal(str(schedule_obj.amount))
transaction_obj.amount_received = (transaction_obj.amount_received or Decimal('0')) + schedule_amount
transaction_obj.deposit_received = (transaction_obj.deposit_received or Decimal('0')) + schedule_amount
transaction_obj.deposit_remaining = (transaction_obj.deposit_remaining or Decimal('0')) - schedule_amount
transaction_obj.save()
return Response({
'message': 'Công nợ đã được xác nhận thành công.'
}, status=status.HTTP_200_OK)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
@api_view(['POST'])
@transaction.atomic
def approve_transaction_detail(request):
"""
Duyệt một Transaction_Detail.
Nếu phase là 'reserved24H', sẽ tự động chuyển Transaction sang phase 'reserved'.
"""
data = request.data
try:
transaction_detail_id = data.get('transaction_detail_id')
approver_id = data.get('user_id')
detail_obj = Transaction_Detail.objects.select_related('status', 'phase', 'transaction').get(id=transaction_detail_id)
user_obj = User.objects.get(id=approver_id)
if detail_obj.status.code != 'pending':
return Response(
{'error': f'Giao dịch không ở trạng thái "Chờ duyệt". Trạng thái hiện tại: {detail_obj.status.name}'},
status=status.HTTP_400_BAD_REQUEST
)
# Correction: Use 'approved' for approved status
approved_status = Transaction_Status.objects.get(code='approved')
detail_obj.status = approved_status
detail_obj.approver = user_obj
detail_obj.approve_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
detail_obj.save()
if detail_obj.phase.code == 'reserved24H':
transaction_obj = detail_obj.transaction
next_phase_obj = Transaction_Phase.objects.get(code='reserved')
transaction_obj.phase = next_phase_obj
transaction_obj.save()
productId = transaction_obj.product_id
product_obj = Product.objects.get(id=productId)
product_obj.status = Product_Status.objects.get(code='resvered')
product_obj.save()
new_detail_obj = Transaction_Detail.objects.create(
transaction=transaction_obj, phase=next_phase_obj,
status=Transaction_Status.objects.get(code='new'),
date=datetime.now().strftime('%Y-%m-%d'),
amount=0, creator_id=approver_id
)
return Response({
'message': 'Giao dịch đã được duyệt và tự động chuyển sang giai đoạn "Giữ chỗ".',
'transaction_id': transaction_obj.id,
'new_transaction_phase': next_phase_obj.name,
'new_transaction_detail_id': new_detail_obj.id
}, status=status.HTTP_200_OK)
else:
return Response({
'message': 'Giao dịch đã được duyệt thành công.',
'transaction_detail_id': detail_obj.id,
'new_status': approved_status.name
}, status=status.HTTP_200_OK)
except Exception as e:
return Response({'error ': str(e)}, status=status.HTTP_400_BAD_REQUEST)
@api_view(['POST'])
@transaction.atomic
def advance_transaction_phase(request):
"""
Chuyển một Transaction sang một giai đoạn mới và tự động tạo công nợ nếu cần.
"""
data = request.data
try:
transaction_id = data.get('transaction_id')
next_phase_code = data.get('next_phase_code')
user_id = data.get('user_id')
product_status = data.get('product_status')
if not all([transaction_id, next_phase_code, user_id]):
return Response({'error': 'transaction_id, next_phase_code, và user_id là bắt buộc.'}, status=status.HTTP_400_BAD_REQUEST)
transaction_obj = Transaction.objects.select_related('phase', 'product', 'policy').get(id=transaction_id)
next_phase_obj = Transaction_Phase.objects.get(code=next_phase_code)
initial_detail_status = Transaction_Status.objects.get(code='new')
next_product_status = Product_Status.objects.get(code=product_status)
transaction_obj.phase = next_phase_obj
transaction_obj.save()
productId = transaction_obj.product_id
product_obj = Product.objects.get(id=productId)
product_obj.status = next_product_status
product_obj.save()
new_detail_obj = Transaction_Detail.objects.create(
transaction=transaction_obj, phase=next_phase_obj, status=initial_detail_status,
date=datetime.now().strftime('%Y-%m-%d'),
amount=0, creator_id=user_id
)
if next_phase_code == 'pertrade':
unconfirmed_payment_status = Payment_Status.objects.get(code='unpaid')
total_plan_amount = 0
# Ưu tiên kế hoạch thanh toán riêng trong giao dịch
if transaction_obj.payment_plan and isinstance(transaction_obj.payment_plan, list):
for i, plan_item in enumerate(transaction_obj.payment_plan):
plan_amount = Decimal(str(plan_item.get('amount', 0)))
due_days = plan_item.get('due_days', 0)
total_plan_amount += plan_amount
Payment_Schedule.objects.create(
txn_detail=new_detail_obj,
from_date=datetime.now().strftime('%Y-%m-%d'),
to_date=datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=due_days),
amount=plan_amount,
cycle=i + 1,
cycle_days=due_days,
type_id=2, # Giả sử type_id=2 cho thanh toán mua bán
status=unconfirmed_payment_status,
updater_id=user_id,
detail={'note': f"Thanh toán đợt {i + 1} theo kế hoạch riêng"}
)
new_detail_obj.amount = total_plan_amount
new_detail_obj.save()
# Nếu không có kế hoạch riêng, dùng chính sách bán hàng
elif transaction_obj.policy:
payment_plans_from_policy = Payment_Plan.objects.filter(policy=transaction_obj.policy).select_related('type').order_by('cycle')
if payment_plans_from_policy.exists():
for plan in payment_plans_from_policy:
plan_amount = 0
if plan.type.code == 'percentage':
plan_amount = (transaction_obj.sale_price * plan.value) / 100
elif plan.type.code == 'money':
plan_amount = plan.value
total_plan_amount += Decimal(str(plan_amount))
Payment_Schedule.objects.create(
txn_detail=new_detail_obj, from_date=datetime.now().strftime('%Y-%m-%d'),
to_date=datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=plan.days),
amount=plan_amount, cycle=plan.cycle, cycle_days=plan.days,
type_id=2, status=unconfirmed_payment_status, updater_id=user_id,
detail={'note': f"Thanh toán đợt {plan.cycle} theo chính sách {transaction_obj.policy.name}"}
)
new_detail_obj.amount = total_plan_amount
new_detail_obj.save()
else:
raise Exception("Giao dịch không có chính sách bán hàng hoặc kế hoạch thanh toán riêng để tạo công nợ.")
return Response({
'message': f'Giao dịch đã được chuyển thành công sang giai đoạn "{next_phase_obj.name}".',
'transaction_id': transaction_obj.id,
'new_transaction_detail_id': new_detail_obj.id,
'new_transaction_detail_code': new_detail_obj.code,
'product_id': productId,
'customer':transaction_obj.customer.id,
'new_product_status': next_product_status.name
}, status=status.HTTP_200_OK)
except Transaction.DoesNotExist:
return Response({'error': 'Giao dịch không tồn tại.'}, status=status.HTTP_404_NOT_FOUND)
except Transaction_Phase.DoesNotExist:
return Response({'error': f'Giai đoạn mới "{next_phase_code}" không hợp lệ.'}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)

View File

@@ -0,0 +1 @@
,kumduy,duy-pc,04.01.2026 23:48,file:///home/kumduy/.config/libreoffice/4;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.