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')