def _validate_linking(routing_slip: RoutingSlipModel, parent_rs_slip: RoutingSlipModel) -> None: """Validate the linking. 1). child already has a parent/already linked. 2). its already a parent. 3). parent_rs_slip has a parent.ie parent_rs_slip shouldn't already be linked 4). one of them has transactions """ if RoutingSlip._is_linked_already(routing_slip): raise BusinessException(Error.RS_ALREADY_LINKED) children = RoutingSlipModel.find_children(routing_slip.number) if children and len(children) > 0: raise BusinessException(Error.RS_ALREADY_A_PARENT) if RoutingSlip._is_linked_already(parent_rs_slip): raise BusinessException(Error.RS_PARENT_ALREADY_LINKED) # prevent self linking if routing_slip.number == parent_rs_slip.number: raise BusinessException(Error.RS_CANT_LINK_TO_SAME) # has one of these has pending if routing_slip.invoices: raise BusinessException(Error.RS_CHILD_HAS_TRANSACTIONS) # Stop the user from linking NSF. NSF can only be a parent. if routing_slip.status == RoutingSlipStatus.NSF.value: raise BusinessException(Error.RS_CANT_LINK_NSF) RoutingSlip._validate_status(parent_rs_slip, routing_slip)
def apply_routing_slips_to_invoice(cls, # pylint: disable = too-many-arguments, too-many-locals routing_slip_payment_account: PaymentAccountModel, active_cfs_account: CfsAccountModel, parent_routing_slip: RoutingSlipModel, invoice: InvoiceModel, invoice_number: str) -> bool: """Apply routing slips (receipts in CFS) to invoice.""" has_errors = False child_routing_slips: List[RoutingSlipModel] = RoutingSlipModel.find_children(parent_routing_slip.number) # an invoice has to be applied to multiple receipts (incl. all linked RS); apply till the balance is zero for routing_slip in (parent_routing_slip, *child_routing_slips): try: # apply receipt now current_app.logger.debug(f'Apply receipt {routing_slip.number} on invoice {invoice_number} ' f'for routing slip {routing_slip.number}') receipt_number = routing_slip.number # For linked routing slips, new receipt numbers ends with 'L' if routing_slip.status == RoutingSlipStatus.LINKED.value: receipt_number = f'{routing_slip.number}L' # If balance of receipt is zero, continue to next receipt. receipt_balance_before_apply = float( CFSService.get_receipt(active_cfs_account, receipt_number).get('unapplied_amount') ) current_app.logger.debug(f'Current balance on {receipt_number} = {receipt_balance_before_apply}') if receipt_balance_before_apply == 0: continue current_app.logger.debug(f'Applying receipt {receipt_number} to {invoice_number}') receipt_response = CFSService.apply_receipt(active_cfs_account, receipt_number, invoice_number) # Create receipt. receipt = Receipt() receipt.receipt_number = receipt_response.json().get('receipt_number', None) receipt_amount = receipt_balance_before_apply - float(receipt_response.json().get('unapplied_amount')) receipt.receipt_amount = receipt_amount receipt.invoice_id = invoice.id receipt.receipt_date = datetime.now() receipt.flush() invoice_from_cfs = CFSService.get_invoice(active_cfs_account, invoice_number) if invoice_from_cfs.get('amount_due') == 0: break except Exception as e: # NOQA # pylint: disable=broad-except capture_message( f'Error on creating Routing Slip invoice: account id={routing_slip_payment_account.id}, ' f'routing slip : {routing_slip.id}, ERROR : {str(e)}', level='error') current_app.logger.error(e) has_errors = True continue return has_errors
def get_links(cls, rs_number: str) -> Dict[str, any]: """Find dependents/links of a routing slips.""" links: Dict[str, any] = None routing_slip: RoutingSlipModel = RoutingSlipModel.find_by_number(rs_number) if routing_slip: routing_slip_schema = RoutingSlipSchema() children = RoutingSlipModel.find_children(rs_number) links = { 'parent': routing_slip_schema.dump(routing_slip.parent), 'children': routing_slip_schema.dump(children, many=True) } return links
def adjust_routing_slips(cls): """Adjust routing slips. Steps: 1. Adjust routing slip receipts for any Write off routing slips. 2. Adjust routing slip receipts for any Refund approved routing slips. """ current_app.logger.info('<<adjust_routing_slips') adjust_statuses = [RoutingSlipStatus.REFUND_AUTHORIZED.value, RoutingSlipStatus.WRITE_OFF_AUTHORIZED.value] # For any pending refund/write off balance should be more than $0 routing_slips = db.session.query(RoutingSlipModel) \ .filter(RoutingSlipModel.status.in_(adjust_statuses), RoutingSlipModel.remaining_amount > 0).all() current_app.logger.info(f'Found {len(routing_slips)} to write off or refund authorized.') for routing_slip in routing_slips: try: # 1.Adjust the routing slip and it's child routing slips for the remaining balance. current_app.logger.debug(f'Adjusting routing slip {routing_slip.number}') payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(routing_slip.payment_account_id) cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id(payment_account.id) # reverse routing slip receipt # Find all child routing slip and reverse it, as all linked routing slips are also considered as NSF. child_routing_slips: List[RoutingSlipModel] = RoutingSlipModel.find_children(routing_slip.number) for rs in (routing_slip, *child_routing_slips): receipt_number = rs.number is_refund = routing_slip.status == RoutingSlipStatus.REFUND_AUTHORIZED.value if rs.parent_number: receipt_number = f'{receipt_number}L' # Adjust the receipt to zero in CFS CFSService.adjust_receipt_to_zero(cfs_account, receipt_number, is_refund) routing_slip.refund_amount = routing_slip.remaining_amount routing_slip.remaining_amount = 0 routing_slip.save() except Exception as e: # NOQA # pylint: disable=broad-except capture_message( f'Error on Adjusting Routing Slip for :={routing_slip.number}, ' f'routing slip : {routing_slip.id}, ERROR : {str(e)}', level='error') current_app.logger.error(e) continue
def process_nsf(cls): """Process NSF routing slips. Steps: 1. Find all routing slips with NSF status. 2. Reverse the receipt for the NSF routing slips. 3. Add an invoice for NSF fees. """ routing_slips: List[RoutingSlipModel] = db.session.query(RoutingSlipModel) \ .join(PaymentAccountModel, PaymentAccountModel.id == RoutingSlipModel.payment_account_id) \ .join(CfsAccountModel, CfsAccountModel.account_id == PaymentAccountModel.id) \ .filter(RoutingSlipModel.status == RoutingSlipStatus.NSF.value) \ .filter(CfsAccountModel.status == CfsAccountStatus.ACTIVE.value).all() current_app.logger.info(f'Found {len(routing_slips)} to process NSF.') for routing_slip in routing_slips: # 1. Reverse the routing slip receipt. # 2. Reverse all the child receipts. # 3. Change the CFS Account status to FREEZE. try: current_app.logger.debug(f'Reverse receipt {routing_slip.number}') payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(routing_slip.payment_account_id) cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id(payment_account.id) # Find all child routing slip and reverse it, as all linked routing slips are also considered as NSF. child_routing_slips: List[RoutingSlipModel] = RoutingSlipModel.find_children(routing_slip.number) for rs in (routing_slip, *child_routing_slips): receipt_number = rs.number if rs.parent_number: receipt_number = f'{receipt_number}L' CFSService.reverse_rs_receipt_in_cfs(cfs_account, receipt_number, is_nsf=True) for payment in db.session.query(PaymentModel) \ .filter(PaymentModel.receipt_number == receipt_number).all(): payment.payment_status_code = PaymentStatus.FAILED.value # Update the CFS Account status to FREEZE. cfs_account.status = CfsAccountStatus.FREEZE.value # Update all invoice status to CREATED. invoices: List[InvoiceModel] = db.session.query(InvoiceModel) \ .filter(InvoiceModel.routing_slip == routing_slip.number) \ .filter(InvoiceModel.invoice_status_code == InvoiceStatus.PAID.value) \ .all() for inv in invoices: # Reset the statuses inv.invoice_status_code = InvoiceStatus.CREATED.value inv_ref = InvoiceReferenceModel.find_reference_by_invoice_id_and_status( inv.id, InvoiceReferenceStatus.COMPLETED.value ) inv_ref.status_code = InvoiceReferenceStatus.ACTIVE.value # Delete receipts as receipts are reversed in CFS. for receipt in ReceiptModel.find_all_receipts_for_invoice(inv.id): db.session.delete(receipt) inv = cls._create_nsf_invoice(cfs_account, routing_slip.number, payment_account) # Reduce the NSF fee from remaining amount. routing_slip.remaining_amount = float(routing_slip.remaining_amount) - inv.total routing_slip.save() except Exception as e: # NOQA # pylint: disable=broad-except capture_message( f'Error on Processing NSF for :={routing_slip.number}, ' f'routing slip : {routing_slip.id}, ERROR : {str(e)}', level='error') current_app.logger.error(e) continue
class RoutingSlip: # pylint: disable=too-many-instance-attributes, too-many-public-methods """Service to manage Routing slip related operations.""" def __init__(self): """Initialize the service.""" self.__dao = None self._id: int = None self._number: str = None self._payment_account_id: int = None self._status_code: str = None self._total: Decimal = None self._remaining_amount: Decimal = None self._total_usd: Decimal = None @property def _dao(self): if not self.__dao: self.__dao = RoutingSlipModel() return self.__dao @_dao.setter def _dao(self, value): self.__dao: RoutingSlipModel = value self.id: int = self._dao.id self.number: str = self._dao.number self.status_code: str = self._dao.status_code self.payment_account_id: int = self._dao.payment_account_id self.total: Decimal = self._dao.total self.remaining_amount: Decimal = self._dao.remaining_amount self._total_usd: Decimal = self._dao.total_usd @property def id(self): """Return the _id.""" return self._id @id.setter def id(self, value: int): """Set the id.""" self._id = value self._dao.id = value @property def number(self): """Return the number.""" return self._number @number.setter def number(self, value: str): """Set the number.""" self._number = value self._dao.number = value @property def status_code(self): """Return the status_code.""" return self._status_code @status_code.setter def status_code(self, value: str): """Set the status_code.""" self._status_code = value self._dao.status_code = value @property def payment_account_id(self): """Return the payment_account_id.""" return self._payment_account_id @payment_account_id.setter def payment_account_id(self, value: int): """Set the payment_account_id.""" self._payment_account_id = value self._dao.payment_account_id = value @property def total(self): """Return the total.""" return self._total @total.setter def total(self, value: Decimal): """Set the total.""" self._total = value self._dao.total = value @property def remaining_amount(self): """Return the remaining_amount.""" return self._remaining_amount @remaining_amount.setter def remaining_amount(self, value: Decimal): """Set the amount.""" self._remaining_amount = value self._dao.remaining_amount = value @property def total_usd(self): """Return the usd total.""" return self._total_usd @total_usd.setter def total_usd(self, value: Decimal): """Set the usd total.""" self._total_usd = value self._dao.total_usd = value def commit(self): """Save the information to the DB.""" return self._dao.commit() def flush(self): """Save the information to the DB.""" return self._dao.flush() def rollback(self): """Rollback.""" return self._dao.rollback() def save(self): """Save the information to the DB.""" return self._dao.save() def asdict(self) -> Dict[str]: """Return the routing slip as a python dict.""" routing_slip_schema = RoutingSlipSchema() d = routing_slip_schema.dump(self._dao) return d @classmethod def search(cls, search_filter: Dict, page: int, limit: int, return_all: bool = False): """Search for routing slip.""" routing_slips, total = RoutingSlipModel.search(search_filter, page, limit, return_all) data = { 'total': total, 'page': page, 'limit': limit, # We need these fields, to populate the UI. 'items': RoutingSlipSchema(only=('number', 'payments.receipt_number', 'payment_account.name', 'created_name', 'routing_slip_date', 'status', 'invoices.business_identifier', 'payments.cheque_receipt_number', 'remaining_amount', 'total', 'invoices.corp_type_code', 'payments.payment_method_code', 'payments.payment_status_code', 'payment_account.payment_method' ) ).dump(routing_slips, many=True) } return data @classmethod @user_context def create_daily_reports(cls, date: str, **kwargs): """Create and return daily report for the day provided.""" routing_slips: List[RoutingSlipModel] = RoutingSlipModel.search( dict( dateFilter=dict( endDate=date, startDate=date, target='created_on' ) ), page=1, limit=0, return_all=True )[0] total: float = 0 no_of_cash: int = 0 no_of_cheque: int = 0 total_cash_usd: float = 0 total_cheque_usd: float = 0 total_cash_cad: float = 0 total_cheque_cad: float = 0 # TODO Only CAD supported now, so just add up the total. for routing_slip in routing_slips: total += float(routing_slip.total) if routing_slip.payment_account.payment_method == PaymentMethod.CASH.value: no_of_cash += 1 # TODO check if the payment is CAD or USD. total_cash_cad += float(routing_slip.total) if routing_slip.total_usd is not None: total_cash_usd += float(routing_slip.total_usd) else: no_of_cheque += 1 total_cheque_cad += float(routing_slip.total) if routing_slip.total_usd is not None: total_cheque_usd += float(routing_slip.total_usd) report_dict = dict( templateName='routing_slip_report', reportName=f'Routing-Slip-Daily-Report-{date}', templateVars=dict( day=date, reportDay=str(get_local_time(datetime.now())), total=total, numberOfCashReceipts=no_of_cash, numberOfChequeReceipts=no_of_cheque, totalCashInUsd=total_cash_usd, totalChequeInUsd=total_cheque_usd, totalCashInCad=total_cash_cad, totalChequeInCad=total_cheque_cad ) ) pdf_response = OAuthService.post(current_app.config.get('REPORT_API_BASE_URL'), kwargs['user'].bearer_token, AuthHeaderType.BEARER, ContentType.JSON, report_dict) return pdf_response, report_dict.get('reportName') @classmethod def validate_and_find_by_number(cls, rs_number: str) -> Dict[str, any]: """Validate digits before finding by routing slip number.""" if not current_app.config.get('ALLOW_LEGACY_ROUTING_SLIPS'): RoutingSlip._validate_routing_slip_number_digits(rs_number) return cls.find_by_number(rs_number) @classmethod def find_by_number(cls, rs_number: str) -> Dict[str, any]: """Find by routing slip number.""" routing_slip_dict: Dict[str, any] = None routing_slip: RoutingSlipModel = RoutingSlipModel.find_by_number(rs_number) if routing_slip: routing_slip_schema = RoutingSlipSchema() routing_slip_dict = routing_slip_schema.dump(routing_slip) routing_slip_dict['allowedStatuses'] = RoutingSlipStatusTransitionService. \ get_possible_transitions(routing_slip) return routing_slip_dict @classmethod def get_links(cls, rs_number: str) -> Dict[str, any]: """Find dependents/links of a routing slips.""" links: Dict[str, any] = None routing_slip: RoutingSlipModel = RoutingSlipModel.find_by_number(rs_number) if routing_slip: routing_slip_schema = RoutingSlipSchema() children = RoutingSlipModel.find_children(rs_number) links = { 'parent': routing_slip_schema.dump(routing_slip.parent), 'children': routing_slip_schema.dump(children, many=True) } return links @classmethod @user_context def create(cls, request_json: Dict[str, any], **kwargs): """Search for routing slip.""" # 1. Create customer profile in CFS and store it in payment_account and cfs_accounts # 2. Create receipt in CFS # 3. Create routing slip and payment records. rs_number = request_json.get('number') # Validate Routing slip digits and if slip number is unique. if cls.validate_and_find_by_number(rs_number): raise BusinessException(Error.FAS_INVALID_ROUTING_SLIP_NUMBER) payment_methods: [str] = [payment.get('paymentMethod') for payment in request_json.get('payments')] # all the payment should have the same payment method if len(set(payment_methods)) != 1: raise BusinessException(Error.FAS_INVALID_PAYMENT_METHOD) # If payment method is cheque and then there is no payment date then raise error if payment_methods[0] == PaymentMethod.CHEQUE.value: for payment in request_json.get('payments'): if payment.get('paymentDate') is None: raise BusinessException(Error.INVALID_REQUEST) pay_account: PaymentAccountModel = PaymentAccountModel( name=request_json.get('paymentAccount').get('accountName'), payment_method=payment_methods[0], ).flush() CfsAccountModel( account_id=pay_account.id, status=CfsAccountStatus.PENDING.value ).flush() total = get_quantized(sum(float(payment.get('paidAmount')) for payment in request_json.get('payments'))) # Calculate Total USD total_usd = get_quantized(sum(float(payment.get('paidUsdAmount', 0)) for payment in request_json.get('payments'))) # Create a routing slip record. routing_slip: RoutingSlipModel = RoutingSlipModel( number=rs_number, payment_account_id=pay_account.id, status=RoutingSlipStatus.ACTIVE.value, total=total, remaining_amount=total, routing_slip_date=string_to_date(request_json.get('routingSlipDate')), total_usd=total_usd ).flush() for payment in request_json.get('payments'): PaymentModel( payment_system_code=PaymentSystem.FAS.value, payment_account_id=pay_account.id, payment_method_code=payment.get('paymentMethod'), payment_status_code=PaymentStatus.COMPLETED.value, receipt_number=rs_number, cheque_receipt_number=payment.get('chequeReceiptNumber'), is_routing_slip=True, paid_amount=payment.get('paidAmount'), payment_date=string_to_date(payment.get('paymentDate')) if payment.get('paymentDate') else None, created_by=kwargs['user'].user_name, paid_usd_amount=payment.get('paidUsdAmount', None) ).flush() routing_slip.commit() return cls.find_by_number(rs_number) @classmethod def do_link(cls, rs_number: str, parent_rs_number: str) -> Dict[str, any]: """Link routing slip to parent routing slip.""" routing_slip: RoutingSlipModel = RoutingSlipModel.find_by_number(rs_number) parent_routing_slip: RoutingSlipModel = RoutingSlipModel.find_by_number(parent_rs_number) if routing_slip is None or parent_routing_slip is None: raise BusinessException(Error.FAS_INVALID_ROUTING_SLIP_NUMBER) # do validations if its linkable RoutingSlip._validate_linking(routing_slip=routing_slip, parent_rs_slip=parent_routing_slip) routing_slip.parent_number = parent_routing_slip.number routing_slip.status = RoutingSlipStatus.LINKED.value # transfer the amount to parent. # we keep the total amount as such and transfer only the remaining amount. parent_routing_slip.remaining_amount += routing_slip.remaining_amount routing_slip.remaining_amount = 0 routing_slip.commit() return cls.find_by_number(rs_number) @classmethod @user_context def update(cls, rs_number: str, action: str, request_json: Dict[str, any], **kwargs) -> Dict[str, any]: """Update routing slip.""" user: UserContext = kwargs['user'] if (patch_action := PatchActions.from_value(action)) is None: raise BusinessException(Error.PATCH_INVALID_ACTION) routing_slip: RoutingSlipModel = RoutingSlipModel.find_by_number(rs_number) if routing_slip is None: raise BusinessException(Error.FAS_INVALID_ROUTING_SLIP_NUMBER) if patch_action == PatchActions.UPDATE_STATUS: # Update the remaining amount as negative total of sum of all totals for that routing slip. status = request_json.get('status') RoutingSlipStatusTransitionService.validate_possible_transitions(routing_slip, status) status = RoutingSlipStatusTransitionService.get_actual_status(status) # Our routing_slips job will create an invoice (under transactions in the UI). if status == RoutingSlipStatus.NSF.value: total_paid_to_reverse: float = 0 for rs in (routing_slip, *RoutingSlipModel.find_children(routing_slip.number)): total_paid_to_reverse += rs.total routing_slip.remaining_amount += -total_paid_to_reverse elif status in (RoutingSlipStatus.WRITE_OFF_AUTHORIZED.value, RoutingSlipStatus.REFUND_AUTHORIZED.value) \ and not user.has_role(Role.FAS_REFUND_APPROVER.value): abort(403) routing_slip.status = status routing_slip.save() return cls.find_by_number(rs_number)