347 lines
15 KiB
Python
347 lines
15 KiB
Python
import arrow
|
|
from decimal import Decimal
|
|
from datetime import datetime, timedelta
|
|
from Schemas import BuildDecisionBookPayments, AccountRecords, ApiEnumDropdown
|
|
|
|
from time import perf_counter
|
|
import time
|
|
from sqlalchemy import cast, Date, String
|
|
from Controllers.Postgres.engine import get_session_factory
|
|
# from ServicesApi.Schemas.account.account import AccountRecords
|
|
# from ServicesApi.Schemas.building.decision_book import BuildDecisionBookPayments
|
|
|
|
# Helper function to calculate available funds
|
|
def df_fund(account_income, total_paid):
|
|
return abs(account_income) - abs(total_paid)
|
|
|
|
class BuildDuesTypes:
|
|
|
|
def __init__(self):
|
|
self.debit: ApiEnumDropdownShallowCopy = None
|
|
self.add_debit: ApiEnumDropdownShallowCopy = None
|
|
self.renovation: ApiEnumDropdownShallowCopy = None
|
|
self.lawyer_expence: ApiEnumDropdownShallowCopy = None
|
|
self.service_fee: ApiEnumDropdownShallowCopy = None
|
|
self.information: ApiEnumDropdownShallowCopy = None
|
|
|
|
|
|
class ApiEnumDropdownShallowCopy:
|
|
id: int
|
|
uuid: str
|
|
enum_class: str
|
|
key: str
|
|
value: str
|
|
|
|
def __init__(self, id: int, uuid: str, enum_class: str, key: str, value: str):
|
|
self.id = id
|
|
self.uuid = uuid
|
|
self.enum_class = enum_class
|
|
self.key = key
|
|
self.value = value
|
|
|
|
|
|
def find_master_payment_value(build_parts_id: int, process_date: datetime, session, debit_type):
|
|
BuildDecisionBookPayments.set_session(session)
|
|
book_payments_query = (
|
|
BuildDecisionBookPayments.process_date_m == process_date.month, BuildDecisionBookPayments.process_date_y == process_date.year,
|
|
BuildDecisionBookPayments.build_parts_id == build_parts_id, BuildDecisionBookPayments.account_records_id.is_(None),
|
|
BuildDecisionBookPayments.account_is_debit == True, BuildDecisionBookPayments.payment_types_id == debit_type.id,
|
|
)
|
|
debit_row = BuildDecisionBookPayments.query.filter(*book_payments_query).order_by(BuildDecisionBookPayments.process_date.desc()).first()
|
|
if not debit_row:
|
|
return 0, None, None
|
|
return abs(debit_row.payment_amount), debit_row, str(debit_row.ref_id)
|
|
|
|
|
|
def calculate_paid_amount_for_master(ref_id: str, session, debit_amount):
|
|
"""Calculate how much has been paid for a given payment reference.
|
|
|
|
Args:
|
|
ref_id: The reference ID to check payments for
|
|
session: Database session
|
|
debit_amount: Original debit amount
|
|
|
|
Returns:
|
|
float: Remaining amount to pay (debit_amount - total_paid)
|
|
"""
|
|
BuildDecisionBookPayments.set_session(session)
|
|
paid_rows = BuildDecisionBookPayments.query.filter(
|
|
BuildDecisionBookPayments.ref_id == ref_id,
|
|
BuildDecisionBookPayments.account_records_id.isnot(None),
|
|
BuildDecisionBookPayments.account_is_debit == False
|
|
).order_by(BuildDecisionBookPayments.process_date.desc()).all()
|
|
|
|
if not paid_rows:
|
|
return debit_amount
|
|
|
|
total_paid = sum([abs(paid_row.payment_amount) for paid_row in paid_rows])
|
|
remaining = abs(debit_amount) - abs(total_paid)
|
|
return remaining
|
|
|
|
|
|
def find_master_payment_value_previous(build_parts_id: int, process_date: datetime, session, debit_type):
|
|
BuildDecisionBookPayments.set_session(session)
|
|
parse_process_date = datetime(process_date.year, process_date.month, 1) - timedelta(days=1)
|
|
book_payments_query = (
|
|
BuildDecisionBookPayments.process_date < parse_process_date,
|
|
BuildDecisionBookPayments.build_parts_id == build_parts_id, BuildDecisionBookPayments.account_records_id.is_(None),
|
|
BuildDecisionBookPayments.account_is_debit == True, BuildDecisionBookPayments.payment_types_id == debit_type.id,
|
|
)
|
|
debit_rows = BuildDecisionBookPayments.query.filter(*book_payments_query).order_by(BuildDecisionBookPayments.process_date.desc()).all()
|
|
return debit_rows
|
|
|
|
|
|
def find_amount_to_pay(build_parts_id: int, process_date: datetime, session, debit_type):
|
|
# debit -negative value that need to be pay
|
|
debit, debit_row, debit_row_ref_id = find_master_payment_value(build_parts_id=build_parts_id, process_date=process_date, session=session, debit_type=debit_type)
|
|
# Is there any payment done for this ref_id ?
|
|
return calculate_paid_amount_for_master(ref_id=debit_row_ref_id, session=session, debit_amount=debit), debit_row
|
|
|
|
|
|
def calculate_total_debt_for_account(build_parts_id: int, session):
|
|
"""Calculate the total debt and total paid amount for an account regardless of process date."""
|
|
BuildDecisionBookPayments.set_session(session)
|
|
|
|
# Get all debits for this account
|
|
all_debits = BuildDecisionBookPayments.query.filter(
|
|
BuildDecisionBookPayments.build_parts_id == build_parts_id,
|
|
BuildDecisionBookPayments.account_records_id.is_(None),
|
|
BuildDecisionBookPayments.account_is_debit == True
|
|
).all()
|
|
|
|
total_debt = sum([abs(debit.payment_amount) for debit in all_debits])
|
|
|
|
# Get all payments for this account's debits
|
|
total_paid = 0
|
|
for debit in all_debits:
|
|
payments = BuildDecisionBookPayments.query.filter(
|
|
BuildDecisionBookPayments.ref_id == debit.ref_id,
|
|
BuildDecisionBookPayments.account_is_debit == False
|
|
).all()
|
|
|
|
if payments:
|
|
total_paid += sum([abs(payment.payment_amount) for payment in payments])
|
|
|
|
return total_debt, total_paid
|
|
|
|
|
|
def refresh_book_payment(account_record: AccountRecords):
|
|
"""Update the remainder_balance of an account record based on attached payments.
|
|
|
|
This function calculates the total of all payments attached to an account record
|
|
and updates the remainder_balance field accordingly. The remainder_balance represents
|
|
funds that have been received but not yet allocated to specific debits.
|
|
|
|
Args:
|
|
account_record: The account record to update
|
|
|
|
Returns:
|
|
float: The total payment amount
|
|
"""
|
|
total_payment = 0
|
|
all_payment_attached = BuildDecisionBookPayments.query.filter(
|
|
BuildDecisionBookPayments.account_records_id == account_record.id,
|
|
BuildDecisionBookPayments.account_is_debit == False,
|
|
).all()
|
|
|
|
if all_payment_attached:
|
|
total_payment = sum([abs(row.payment_amount) for row in all_payment_attached])
|
|
|
|
# Always update the remainder_balance, even if no payments are attached
|
|
# This ensures we track unallocated funds properly
|
|
old_balance = account_record.remainder_balance
|
|
account_record.update(remainder_balance=total_payment)
|
|
account_record.save()
|
|
|
|
return total_payment
|
|
|
|
|
|
def close_payment_book(payment_row_book, account_record, value, session):
|
|
BuildDecisionBookPayments.set_session(session)
|
|
new_row = BuildDecisionBookPayments.create(
|
|
ref_id=str(payment_row_book.uu_id),
|
|
payment_plan_time_periods=payment_row_book.payment_plan_time_periods,
|
|
period_time=payment_row_book.period_time,
|
|
currency=payment_row_book.currency,
|
|
account_records_id=account_record.id,
|
|
account_records_uu_id=str(account_record.uu_id),
|
|
build_parts_id=payment_row_book.build_parts_id,
|
|
build_parts_uu_id=str(payment_row_book.build_parts_uu_id),
|
|
payment_amount=value,
|
|
payment_types_id=payment_row_book.payment_types_id,
|
|
payment_types_uu_id=str(payment_row_book.payment_types_uu_id),
|
|
process_date_m=payment_row_book.process_date.month,
|
|
process_date_y=payment_row_book.process_date.year,
|
|
process_date=payment_row_book.process_date,
|
|
build_decision_book_item_id=payment_row_book.build_decision_book_item_id,
|
|
build_decision_book_item_uu_id=str(payment_row_book.build_decision_book_item_uu_id),
|
|
decision_book_project_id=payment_row_book.decision_book_project_id,
|
|
decision_book_project_uu_id=str(payment_row_book.decision_book_project_uu_id),
|
|
is_confirmed=True,
|
|
account_is_debit=False,
|
|
)
|
|
return new_row.save()
|
|
|
|
|
|
def get_enums_from_database():
|
|
build_dues_types = BuildDuesTypes()
|
|
with ApiEnumDropdown.new_session() as session:
|
|
|
|
ApiEnumDropdown.set_session(session)
|
|
|
|
debit_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-D").first() # Debit
|
|
add_debit_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-A").first() # Add Debit
|
|
renovation_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-R").first() # Renovation
|
|
late_payment_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-L").first() # Lawyer expence
|
|
service_fee_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-S").first() # Service fee
|
|
information_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-I").first() # Information
|
|
|
|
build_dues_types.debit = ApiEnumDropdownShallowCopy(
|
|
debit_enum_shallow.id, str(debit_enum_shallow.uu_id), debit_enum_shallow.enum_class, debit_enum_shallow.key, debit_enum_shallow.value
|
|
)
|
|
build_dues_types.add_debit = ApiEnumDropdownShallowCopy(
|
|
add_debit_enum_shallow.id, str(add_debit_enum_shallow.uu_id), add_debit_enum_shallow.enum_class, add_debit_enum_shallow.key, add_debit_enum_shallow.value
|
|
)
|
|
build_dues_types.renovation = ApiEnumDropdownShallowCopy(
|
|
renovation_enum_shallow.id, str(renovation_enum_shallow.uu_id), renovation_enum_shallow.enum_class, renovation_enum_shallow.key, renovation_enum_shallow.value
|
|
)
|
|
build_dues_types.lawyer_expence = ApiEnumDropdownShallowCopy(
|
|
late_payment_enum_shallow.id, str(late_payment_enum_shallow.uu_id), late_payment_enum_shallow.enum_class, late_payment_enum_shallow.key, late_payment_enum_shallow.value
|
|
)
|
|
build_dues_types.service_fee = ApiEnumDropdownShallowCopy(
|
|
service_fee_enum_shallow.id, str(service_fee_enum_shallow.uu_id), service_fee_enum_shallow.enum_class, service_fee_enum_shallow.key, service_fee_enum_shallow.value
|
|
)
|
|
build_dues_types.information = ApiEnumDropdownShallowCopy(
|
|
information_enum_shallow.id, str(information_enum_shallow.uu_id), information_enum_shallow.enum_class, information_enum_shallow.key, information_enum_shallow.value
|
|
)
|
|
return [build_dues_types.debit, build_dues_types.lawyer_expence, build_dues_types.add_debit, build_dues_types.renovation, build_dues_types.service_fee, build_dues_types.information]
|
|
|
|
|
|
def payment_function():
|
|
session_factory = get_session_factory()
|
|
session = session_factory()
|
|
# Set session for all models
|
|
AccountRecords.set_session(session)
|
|
BuildDecisionBookPayments.set_session(session)
|
|
order_pay = get_enums_from_database()
|
|
|
|
# Get account records with positive currency_value regardless of remainder_balance
|
|
# This ensures accounts with unallocated funds are processed
|
|
account_records = AccountRecords.query.filter(
|
|
AccountRecords.build_parts_id.isnot(None),
|
|
AccountRecords.currency_value > 0,
|
|
AccountRecords.bank_date >= '2022-01-01'
|
|
).order_by(AccountRecords.build_parts_id.desc()).all()
|
|
|
|
start_time = time.time()
|
|
|
|
for account_record in account_records:
|
|
incoming_total_money = abs(account_record.currency_value)
|
|
total_paid = refresh_book_payment(account_record)
|
|
available_fund = df_fund(incoming_total_money, total_paid)
|
|
|
|
|
|
# Calculate total debt and payment status for this account
|
|
total_debt, already_paid = calculate_total_debt_for_account(account_record.build_parts_id, session)
|
|
remaining_debt = total_debt - already_paid
|
|
|
|
# Skip accounts with no debt and zero remainder balance
|
|
if remaining_debt <= 0 and account_record.remainder_balance == 0:
|
|
continue
|
|
|
|
# Skip accounts with no available funds
|
|
if not available_fund > 0.0:
|
|
continue
|
|
|
|
|
|
process_date = datetime.now()
|
|
|
|
# Try to pay current month first
|
|
for debit_type in order_pay:
|
|
amount_to_pay, debit_row = find_amount_to_pay(
|
|
build_parts_id=account_record.build_parts_id,
|
|
process_date=process_date,
|
|
session=session,
|
|
debit_type=debit_type
|
|
)
|
|
|
|
if amount_to_pay > 0 and debit_row:
|
|
if amount_to_pay >= available_fund:
|
|
close_payment_book(
|
|
payment_row_book=debit_row,
|
|
account_record=account_record,
|
|
value=available_fund,
|
|
session=session
|
|
)
|
|
total_paid = refresh_book_payment(account_record)
|
|
available_fund = df_fund(incoming_total_money, total_paid)
|
|
else:
|
|
close_payment_book(
|
|
payment_row_book=debit_row,
|
|
account_record=account_record,
|
|
value=amount_to_pay,
|
|
session=session
|
|
)
|
|
total_paid = refresh_book_payment(account_record)
|
|
available_fund = df_fund(incoming_total_money, total_paid)
|
|
|
|
if not available_fund > 0.0:
|
|
continue
|
|
|
|
# Try to pay previous unpaid debts
|
|
should_continue = False
|
|
for debit_type in order_pay:
|
|
debit_rows = find_master_payment_value_previous(
|
|
build_parts_id=account_record.build_parts_id,
|
|
process_date=process_date,
|
|
session=session,
|
|
debit_type=debit_type
|
|
)
|
|
|
|
if not debit_rows:
|
|
continue
|
|
|
|
for debit_row in debit_rows:
|
|
amount_to_pay = calculate_paid_amount_for_master(
|
|
ref_id=debit_row.ref_id,
|
|
session=session,
|
|
debit_amount=debit_row.payment_amount
|
|
)
|
|
|
|
# Skip if already fully paid
|
|
if not amount_to_pay > 0:
|
|
continue
|
|
|
|
if amount_to_pay >= available_fund:
|
|
close_payment_book(
|
|
payment_row_book=debit_row,
|
|
account_record=account_record,
|
|
value=available_fund,
|
|
session=session
|
|
)
|
|
total_paid = refresh_book_payment(account_record)
|
|
available_fund = df_fund(incoming_total_money, total_paid)
|
|
should_continue = True
|
|
break
|
|
else:
|
|
close_payment_book(
|
|
payment_row_book=debit_row,
|
|
account_record=account_record,
|
|
value=amount_to_pay,
|
|
session=session
|
|
)
|
|
total_paid = refresh_book_payment(account_record)
|
|
available_fund = df_fund(incoming_total_money, total_paid)
|
|
if should_continue or not available_fund > 0.0:
|
|
break
|
|
if not available_fund > 0.0:
|
|
continue # Changed from break to continue to process next account record
|
|
|
|
|
|
if __name__ == "__main__":
|
|
start_time = perf_counter()
|
|
payment_function()
|
|
end_time = perf_counter()
|
|
elapsed = end_time - start_time
|
|
print(f'{elapsed:.3f} : seconds')
|