production-evyos-systems-an.../ServicesBank/Finder/Payment/draft/second.py

349 lines
16 KiB
Python

import arrow
from decimal import Decimal
from datetime import datetime, timedelta
from Schemas import BuildDecisionBookPayments, AccountRecords, ApiEnumDropdown
from time import perf_counter
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
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:
print(f'No record of master payment is found for :{process_date.strftime("%Y-%m-%d %H:%M:%S")} ')
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])
old_balance = account_record.remainder_balance
account_record.update(remainder_balance=total_payment)
account_record.save()
# Only print if there's a change in balance
if old_balance != total_payment:
print(f"Account {account_record.id}: Updated remainder_balance {old_balance}{total_payment}")
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()
ApiEnumDropdown.set_session(session)
order_pay = ApiEnumDropdown.query.filter(ApiEnumDropdown.enum_type == 'order_pay').order_by(ApiEnumDropdown.id.asc()).all()
# 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
# Log account status with debt information
if remaining_debt <= 0 and account_record.remainder_balance == 0:
# Skip accounts with no debt and zero remainder balance
continue
if not available_fund > 0.0:
# Skip accounts with no available funds
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)
print(f"Account {account_record.id}: {available_fund} funds remaining after payment")
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')
# print(f'not_closed_records : {not_closed_records}')
# print(f'error_records : {error_records}')
# print(f'records_to_close : {records_to_close}')
# print(f"Total: {not_closed_records + error_records + records_to_close} / {len(shallow_copy_list)}")