def test_constraint_success(self): """Test clean method with not violated constraint.""" account = BankAccount(account_number='123', currency='USD') payment = BankPayment(identifier='PAYMENT', account=account, amount=Money('999.00', 'USD')) payment.clean()
def test_constraint_error(self): """Test clean method with violated constraint.""" account = BankAccount(account_number='123', currency='USD') payment = BankPayment(identifier='PAYMENT', account=account, amount=Money('999.00', 'CZK')) with self.assertRaises( ValidationError, msg='Bank payment PAYMENT is in different currency (CZK) ' 'than bank account 123 (USD).'): payment.clean()
def ignore_negative_payments(payment: BankPayment) -> BankPayment: """ Process negative bank payments by IgnorePaymentProcessor. This function can be used as pain import callback. It expects that there is the IgnorePaymentProcessor among PAIN_PROCESSORS. """ if (payment.amount.amount < 0): payment.state = PaymentState.PROCESSED payment.processor = _get_ignore_processor_name() return payment
def _save_if_not_exists(self, payment: BankPayment) -> int: """Return value is the number of skipped payments.""" with transaction.atomic(): if self._payment_exists(payment): LOGGER.info('Payment ID %s already exists - skipping.', payment) return 1 else: payment.full_clean() for callback in SETTINGS.import_callbacks: payment = callback(payment) payment.save() return 0
def process_payment(payment: BankPayment, data: dict) -> None: """Create client and invoices and assign processor.""" if data['registrar_handle']: # Payment is from registrar. # We need to create client and invoices. payment.state = PaymentState.PROCESSED payment.processor = 'fred' payment.save() Client.objects.create( handle=data['registrar_handle'], remote_id=data['registrar_id'], payment=payment, ) if data['advance_invoice']: for invoice_id, invoice_number in data['advance_invoice'].items(): invoice, created = Invoice.objects.get_or_create(number=invoice_number, defaults={ 'remote_id': invoice_id, 'invoice_type': InvoiceType.ADVANCE}) invoice.payments.add(payment) if data['account_invoices']: for invoice_id, invoice_number in data['account_invoices'].items(): invoice, created = Invoice.objects.get_or_create(number=invoice_number, defaults={ 'remote_id': invoice_id, 'invoice_type': InvoiceType.ACCOUNT}) invoice.payments.add(payment) elif data['type'] == 5: # Payment is academy-related. payment.state = PaymentState.PROCESSED payment.processor = 'payments' payment.save()
def parse(self, bank_statement: IO[bytes]) -> Iterator[BankPayment]: """Parse XML input.""" parser = etree.XMLParser(resolve_entities=False) tree = etree.parse(bank_statement, parser) account_number = self.compose_account_number(tree.find('//*/account_number').text, tree.find('//*/account_bank_code').text) try: account = BankAccount.objects.get(account_number=account_number) except BankAccount.DoesNotExist: raise BankAccount.DoesNotExist('Bank account %s does not exist.' % account_number) for item in tree.findall('//*/*/item'): attrs = dict((el.tag, el.text) for el in item.iterchildren()) payment = BankPayment( identifier=attrs['ident'], account=account, transaction_date=datetime.strptime(attrs['date'], '%Y-%m-%d'), counter_account_number=self.compose_account_number(attrs['account_number'], attrs['account_bank_code']), counter_account_name=none_to_str(attrs['name']), amount=Money(attrs['price'], account.currency), description=none_to_str(attrs['memo']), constant_symbol=none_to_str(attrs['const_symbol']), variable_symbol=none_to_str(attrs['var_symbol']), specific_symbol=none_to_str(attrs['spec_symbol']), ) yield payment
def update_payments_state(self, payment: BankPayment) -> None: """Update status of the payment form CSOB Gateway and if newly paid, process the payment.""" try: gateway_result = self.client.payment_status(payment.identifier).payload except requests.ConnectionError: raise PaymentHandlerConnectionError('Gateway connection error') if gateway_result['resultCode'] == CSOB.RETURN_CODE_OK: payment.card_payment_state = CSOB.PAYMENT_STATUSES[gateway_result['paymentStatus']] # `state` attribute must not be updated unless it's INITIALIZED, as we would easily go from # PROCESSED to READY_TO_PROCESS again. if payment.state == PaymentState.INITIALIZED: payment.state = CSOB_GATEWAY_TO_PAYMENT_STATE_MAPPING[gateway_result['paymentStatus']] payment.save() else: LOGGER.error('payment_status resultCode != OK: %s', gateway_result) raise PaymentHandlerError('payment_status resultCode != OK', gateway_result)
def get_payment(**kwargs: Any) -> BankPayment: """Create payment object.""" default = { 'identifier': 'PAYMENT1', 'account': None, 'transaction_date': date(2018, 5, 9), 'counter_account_number': '098765/4321', 'amount': Money('42.00', 'CZK'), } default.update(kwargs) return BankPayment(**default)
def _convert_to_models(self, statement: BankStatement) -> Iterable[BankPayment]: account_number = statement.account_number try: account = BankAccount.objects.get(account_number=account_number) except BankAccount.DoesNotExist: raise CommandError('Bank account {} does not exist.'.format(account_number)) payments = [] for payment_data in statement: payment = BankPayment.from_payment_data_class(payment_data) payment.account = account payments.append(payment) return payments
def format_payment(payment: BankPayment) -> str: """Return formatted payment row.""" row = ('{ident:10} {create:32} amount: {amount:>13} ' 'account_memo: {memo:40} account_name: {account:26}').format( ident=payment.identifier, create=payment.create_time.isoformat(), amount=str(payment.amount), memo=payment.description.strip(), account=payment.counter_account_name, ) if payment.processing_error: row += ' processing_error: {}'.format( payment.get_processing_error_display()) return row.strip()
def test_unbreakable_amount_en(self): modeladmin = BankPaymentAdmin(BankPayment, admin.site) payment = BankPayment(amount=10000.445) original_definition = _FORMATTER.formatting_definitions.get('EN', None) try: _FORMATTER.add_formatting_definition('en', group_size=3, group_separator=',', decimal_point='.', positive_sign='', trailing_positive_sign='', negative_sign='-', trailing_negative_sign='', rounding_method=ROUND_HALF_UP) formatted = modeladmin.unbreakable_amount(payment) finally: _FORMATTER.formatting_definitions['EN'] = original_definition self.assertEquals('10,000.45 Kč', formatted)
def parse(self, bank_statement: IO[bytes]) -> Iterator[BankPayment]: """Parse XML input.""" parser = etree.XMLParser(resolve_entities=False) tree = etree.parse(bank_statement, parser) account_number = self.compose_account_number( tree.find('//*/account_number').text, tree.find('//*/account_bank_code').text) try: account = BankAccount.objects.get(account_number=account_number) except BankAccount.DoesNotExist: raise BankAccount.DoesNotExist( 'Bank account {} does not exist.'.format(account_number)) for item in tree.findall('//*/*/item'): attrs = dict((el.tag, el.text) for el in item.iterchildren()) if attrs.get('status', '1') == '1' and attrs.get( 'code', '1') == '1' and attrs.get('type', '1') == '1': # Only import payments with code==1 (normal transaction) and status==1 (realized transfer) if SETTINGS.trim_varsym: variable_symbol = none_to_str( attrs['var_symbol']).lstrip('0') else: variable_symbol = none_to_str(attrs['var_symbol']) payment = BankPayment( identifier=attrs['ident'], account=account, transaction_date=datetime.strptime(attrs['date'], '%Y-%m-%d'), counter_account_number=self.compose_account_number( attrs['account_number'], attrs['account_bank_code']), counter_account_name=none_to_str(attrs['name']), amount=Money(attrs['price'], account.currency), description=none_to_str(attrs['memo']), constant_symbol=none_to_str(attrs['const_symbol']), variable_symbol=variable_symbol, specific_symbol=none_to_str(attrs['spec_symbol']), ) yield payment
def get_form(self, request, obj=None, **kwargs): """Filter allowed processors for manual assignment of payments.""" form = super().get_form(request, obj, **kwargs) allowed_choices = [] for processor, label in BankPayment.objective_choices(): if not processor or request.user.has_perm( 'django_pain.can_manually_assign_to_{}'.format(processor)): allowed_choices.append((processor, label)) processor_field = deepcopy(form.base_fields['processor']) processor_field.choices = allowed_choices form.base_fields['processor'] = processor_field if obj is not None: initial_tax_date = self._get_initial_tax_date(obj.transaction_date) if initial_tax_date is not None: tax_date_field = deepcopy(form.base_fields['tax_date']) tax_date_field.initial = initial_tax_date form.base_fields['tax_date'] = tax_date_field return form
def test_from_payment_data_class_blank_values(self): payment_data = Payment() payment_data.identifier = 'abc123' payment_data.transaction_date = date(2020, 9, 1) payment_data.counter_account = None payment_data.name = None payment_data.amount = Money(1, 'CZK') payment_data.description = None payment_data.constant_symbol = None payment_data.variable_symbol = None payment_data.specific_symbol = None model = BankPayment.from_payment_data_class(payment_data) self.assertEqual(model.identifier, payment_data.identifier) self.assertEqual(model.transaction_date, payment_data.transaction_date) self.assertEqual(model.counter_account_number, '') self.assertEqual(model.counter_account_name, '') self.assertEqual(model.amount, payment_data.amount) self.assertEqual(model.description, '') self.assertEqual(model.constant_symbol, '') self.assertEqual(model.variable_symbol, '') self.assertEqual(model.specific_symbol, '')
def test_from_payment_data_class(self): payment_data = Payment() payment_data.identifier = 'abc123' payment_data.transaction_date = date(2020, 9, 1) payment_data.counter_account = '12345/678' payment_data.name = 'John Doe' payment_data.amount = Money(1, 'CZK') payment_data.description = 'Hello!' payment_data.constant_symbol = '123' payment_data.variable_symbol = '456' payment_data.specific_symbol = '789' model = BankPayment.from_payment_data_class(payment_data) self.assertEqual(model.identifier, payment_data.identifier) self.assertEqual(model.transaction_date, payment_data.transaction_date) self.assertEqual(model.counter_account_number, payment_data.counter_account) self.assertEqual(model.counter_account_name, payment_data.name) self.assertEqual(model.amount, payment_data.amount) self.assertEqual(model.description, payment_data.description) self.assertEqual(model.constant_symbol, payment_data.constant_symbol) self.assertEqual(model.variable_symbol, payment_data.variable_symbol) self.assertEqual(model.specific_symbol, payment_data.specific_symbol)
def _get_form(self, *args, **kwargs): form = BankPaymentForm(*args, **kwargs) form.fields['processor'].choices = BankPayment.objective_choices() return form
continue try: with transaction.atomic(): if BankPayment.objects.filter(uuid=data['uuid']).count() > 0: logging.info('Payment %s has already been imported. Skipping.', data['uuid']) stats['skipped'] += 1 continue payment = BankPayment( uuid=data['uuid'], identifier=data['account_payment_ident'], account=accounts[account_number], transaction_date=parse_date(data['date']), counter_account_number=compose_account_number(data['counter_account_number'], data['counter_account_bank_code']), counter_account_name=data['counter_account_name'] or '', amount=Money(data['price'], 'CZK'), description=data['memo'] or '', constant_symbol=data['constant_symbol'] or '', variable_symbol=data['variable_symbol'] or '', specific_symbol=data['specific_symbol'] or '', ) try: payment.full_clean() except ValidationError as err: logging.error('Payment %s has invalid data: %s', data['uuid'], err) stats['errors'] += 1 continue else: payment.save()
def test_objective_choices(self): self.assertEqual(BankPayment.objective_choices(), BLANK_CHOICE_DASH + [ ('dummy', 'Dummy objective'), ])