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()
Example #3
0
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
Example #4
0
 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
Example #5
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()
Example #6
0
    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
Example #7
0
 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)
Example #8
0
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)
Example #9
0
 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
Example #10
0
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()
Example #11
0
    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&nbsp;Kč', formatted)
Example #12
0
    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
Example #13
0
    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
Example #14
0
    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, '')
Example #15
0
    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)
Example #16
0
 def _get_form(self, *args, **kwargs):
     form = BankPaymentForm(*args, **kwargs)
     form.fields['processor'].choices = BankPayment.objective_choices()
     return form
Example #17
0
        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()
Example #18
0
 def test_objective_choices(self):
     self.assertEqual(BankPayment.objective_choices(), BLANK_CHOICE_DASH + [
         ('dummy', 'Dummy objective'),
     ])