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

View File

@@ -1,7 +1,7 @@
import os
import subprocess
from datetime import datetime
from django.db import models
import numpy as np
from docx import Document
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.")
def _get_value_from_object(self, obj, field_path):
if obj is None:
if not obj:
return None
parts = field_path.split('.')
value = obj
for part in field_path.replace("__", ".").split("."):
for part in parts:
if value is None:
return None
break
# Lấy thuộc tính từ object
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
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

@@ -1325,8 +1325,11 @@ class Individual(models.Model):
class Organization(models.Model):
customer = models.ForeignKey(Customer, null=False, related_name='orgncust', on_delete=models.PROTECT)
shortname = models.CharField(max_length=50, null=False)
established_date = models.DateField()
website = models.CharField(max_length=200, null=True)
established_date = models.DateField()
tax_code = models.CharField(max_length=20, 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)
create_time = models.DateTimeField(null=True, auto_now_add=True)
update_time = models.DateTimeField(null=True, auto_now=True)
@@ -1338,6 +1341,7 @@ class Organization(models.Model):
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:
@@ -1961,7 +1965,7 @@ class Transaction_Discount(models.Model):
class Co_Ownership(models.Model):
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)
create_time = models.DateTimeField(null=True, auto_now_add=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)