Merge branch 'origin' of ssh://git.bigdatatech.vn:235/utopia/api into origin
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -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
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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):
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
19
app/migrations/0345_alter_co_ownership_transaction.py
Normal file
19
app/migrations/0345_alter_co_ownership_transaction.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1325,8 +1325,11 @@ class Individual(models.Model):
|
|||||||
class Organization(models.Model):
|
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()
|
||||||
website = models.CharField(max_length=200, null=True)
|
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)
|
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)
|
||||||
|
|||||||
325
app/workflows.py
325
app/workflows.py
@@ -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)
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
,kumduy,duy-pc,04.01.2026 23:48,file:///home/kumduy/.config/libreoffice/4;
|
||||||
Binary file not shown.
BIN
static/contract/1. Phiếu xác lập thỏa thuận ưu tiên tổ chức.docx
Normal file
BIN
static/contract/1. Phiếu xác lập thỏa thuận ưu tiên tổ chức.docx
Normal file
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.
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.
Reference in New Issue
Block a user