Example #1
0
class ReferencePaymentBatchFile(models.Model):
    created = models.DateTimeField(_("created"), default=now, db_index=True, blank=True, editable=False)
    file = models.FileField(verbose_name=_("file"), upload_to="uploads")
    original_filename = SafeCharField(_("original filename"), blank=True, default="", max_length=256)
    tag = SafeCharField(_("tag"), blank=True, max_length=64, default="", db_index=True)
    errors = SafeTextField(_("errors"), max_length=4086, default="", blank=True)
    cached_total_amount = models.DecimalField(_("total amount"), max_digits=10, decimal_places=2, null=True, default=None, blank=True)

    class Meta:
        verbose_name = _("reference payment batch file")
        verbose_name_plural = _("reference payment batch files")

    def get_total_amount(self, force: bool = False) -> Decimal:
        if self.cached_total_amount is None or force:
            self.cached_total_amount = sum_queryset(ReferencePaymentRecord.objects.filter(batch__file=self))
            self.save(update_fields=["cached_total_amount"])
        return self.cached_total_amount

    @property
    def total_amount(self) -> Decimal:
        return self.get_total_amount()

    total_amount.fget.short_description = _("total amount")  # type: ignore

    @property
    def full_path(self):
        return join(settings.MEDIA_ROOT, self.file.name) if self.file else ""

    def __str__(self):
        return basename(str(self.file.name)) if self.file else ""
Example #2
0
class AccountType(models.Model):
    code = SafeCharField(verbose_name=_("code"),
                         max_length=32,
                         db_index=True,
                         unique=True)
    name = SafeCharField(verbose_name=_("name"),
                         max_length=64,
                         db_index=True,
                         unique=True)
    is_asset = models.BooleanField(verbose_name=_("asset"))
    created = models.DateTimeField(verbose_name=_("created"),
                                   default=now,
                                   db_index=True,
                                   editable=False,
                                   blank=True)
    last_modified = models.DateTimeField(verbose_name=_("last modified"),
                                         auto_now=True,
                                         db_index=True,
                                         editable=False,
                                         blank=True)

    class Meta:
        verbose_name = _("account type")
        verbose_name_plural = _("account types")

    def __str__(self):
        return str(self.name)

    @property
    def is_liability(self) -> bool:
        return not self.is_asset

    is_liability.fget.short_description = _("liability")  # type: ignore  # pytype: disable=attribute-error
Example #3
0
class PayoutParty(models.Model):
    name = SafeCharField(_("name"), max_length=128, db_index=True)
    account_number = SafeCharField(_("account number"), max_length=35, db_index=True, validators=[iban_validator])
    bic = SafeCharField(_("BIC"), max_length=16, db_index=True, blank=True)
    org_id = SafeCharField(_("organization id"), max_length=32, db_index=True, blank=True, default="")
    address = SafeTextField(_("address"), blank=True, default="")
    country_code = SafeCharField(_("country code"), max_length=2, default="FI", blank=True, db_index=True)
    payouts_account = models.ForeignKey(Account, verbose_name=_("payouts account"), null=True, default=None, blank=True, on_delete=models.PROTECT)

    class Meta:
        verbose_name = _("payout party")
        verbose_name_plural = _("payout parties")

    def __str__(self):
        return "{} ({})".format(self.name, self.account_number)

    def clean(self):
        if not self.bic:
            self.bic = iban_bic(self.account_number)

    @property
    def address_lines(self):
        out = []
        for line in self.address.split("\n"):
            line = line.strip()
            if line:
                out.append(line)
        return out
Example #4
0
class StatementRecordRemittanceInfo(models.Model):
    detail = models.ForeignKey(StatementRecordDetail, related_name="remittanceinfo_set", on_delete=models.CASCADE)
    additional_info = SafeCharField(_("additional remittance info"), max_length=256, blank=True, db_index=True)
    amount = models.DecimalField(_("amount"), decimal_places=2, max_digits=10, null=True, default=None, blank=True)
    currency_code = SafeCharField(_("currency code"), max_length=3, blank=True)
    reference = SafeCharField(_("reference"), max_length=35, blank=True, db_index=True)

    def __str__(self):
        return "{} {} ref {} ({})".format(self.amount if self.amount is not None else "", self.currency_code, self.reference, self.additional_info)

    class Meta:
        verbose_name = _("statement record remittance info")
        verbose_name_plural = _("statement record remittance info")
Example #5
0
class AccountEntrySourceFile(models.Model):
    """
    Account entry source is set for entries based on some event like payment file import
    """

    name = SafeCharField(verbose_name=_("name"),
                         max_length=255,
                         db_index=True,
                         blank=True,
                         default="")
    created = models.DateTimeField(verbose_name=_("created"),
                                   default=now,
                                   db_index=True,
                                   editable=False,
                                   blank=True)
    last_modified = models.DateTimeField(verbose_name=_("last modified"),
                                         auto_now=True,
                                         db_index=True,
                                         editable=False,
                                         blank=True)

    class Meta:
        verbose_name = _("account entry source file")
        verbose_name_plural = _("account entry source files")

    def __str__(self):
        return "[{}] {}".format(self.id, self.name)
Example #6
0
class Contract(models.Model):
    """
    Base class for contracts (e.g. rent contracts, loans, etc.)
    """

    created = models.DateTimeField(verbose_name=_("created"),
                                   default=now,
                                   db_index=True,
                                   editable=False,
                                   blank=True)
    last_modified = models.DateTimeField(verbose_name=_("last modified"),
                                         auto_now=True,
                                         db_index=True,
                                         editable=False,
                                         blank=True)
    name = SafeCharField(verbose_name=_("name"),
                         max_length=128,
                         default="",
                         blank=True,
                         db_index=True)

    class Meta:
        verbose_name = _("contract")
        verbose_name_plural = _("contracts")

    def __str__(self):
        return "[{}]".format(self.id)
Example #7
0
class ReferencePaymentRecord(AccountEntry):
    """
    Reference payment record. See jacc.Invoice for date/time variable naming conventions.
    """

    objects = PaymentRecordManager()  # type: ignore
    batch = models.ForeignKey(ReferencePaymentBatch, verbose_name=_("batch"), related_name="record_set", on_delete=models.CASCADE)
    line_number = models.SmallIntegerField(_("line number"), default=0, blank=True)
    record_type = SafeCharField(_("record type"), max_length=1)
    account_number = SafeCharField(_("account number"), max_length=32, db_index=True)
    record_date = models.DateField(_("record date"), db_index=True)
    paid_date = models.DateField(_("paid date"), db_index=True, blank=True, null=True, default=None)
    archive_identifier = SafeCharField(_("archive identifier"), max_length=32, blank=True, default="", db_index=True)
    remittance_info = SafeCharField(_("remittance info"), max_length=32, db_index=True)
    payer_name = SafeCharField(_("payer name"), max_length=12, blank=True, default="", db_index=True)
    currency_identifier = SafeCharField(_("currency identifier"), max_length=1, choices=CURRENCY_IDENTIFIERS)
    name_source = SafeCharField(_("name source"), max_length=1, choices=NAME_SOURCES, blank=True)
    correction_identifier = SafeCharField(_("correction identifier"), max_length=1, choices=CORRECTION_IDENTIFIER)
    delivery_method = SafeCharField(_("delivery method"), max_length=1, db_index=True, choices=DELIVERY_METHOD, blank=True)
    receipt_code = SafeCharField(_("receipt code"), max_length=1, choices=RECEIPT_CODE, db_index=True, blank=True)
    manually_settled = models.BooleanField(_("manually settled"), db_index=True, default=False, blank=True)

    class Meta:
        verbose_name = _("reference payment records")
        verbose_name_plural = _("reference payment records")

    @property
    def is_settled(self) -> bool:
        """
        True if entry is either manually settled or has SUM(children)==amount.
        """
        return self.manually_settled or sum_queryset(self.child_set) == self.amount  # type: ignore

    @property
    def remittance_info_short(self) -> str:
        """
        Remittance info without preceding zeroes.
        :return: str
        """
        return re.sub(r"^0+", "", self.remittance_info)

    def clean(self):
        self.source_file = self.batch
        self.timestamp = pytz.utc.localize(datetime.combine(self.paid_date, time(0, 0)))
        self.description = "{amount} {remittance_info} {payer_name}".format(
            amount=self.amount, remittance_info=self.remittance_info, payer_name=self.payer_name
        )
Example #8
0
class StatementFile(models.Model):
    created = models.DateTimeField(_("created"), default=now, db_index=True, blank=True, editable=False)
    file = models.FileField(verbose_name=_("file"), upload_to="uploads")
    original_filename = SafeCharField(_("original filename"), blank=True, default="", max_length=256)
    tag = SafeCharField(_("tag"), blank=True, max_length=64, default="", db_index=True)
    errors = SafeTextField(_("errors"), max_length=4086, default="", blank=True)

    class Meta:
        verbose_name = _("account statement file")
        verbose_name_plural = _("account statement files")

    @property
    def full_path(self):
        return join(settings.MEDIA_ROOT, self.file.name) if self.file else ""

    def __str__(self):
        return basename(str(self.file.name)) if self.file else ""
Example #9
0
class EntryType(models.Model):
    code = SafeCharField(verbose_name=_("code"),
                         max_length=64,
                         db_index=True,
                         unique=True)
    identifier = SafeCharField(verbose_name=_("identifier"),
                               max_length=40,
                               db_index=True,
                               blank=True,
                               default="")
    name = SafeCharField(verbose_name=_("name"),
                         max_length=128,
                         db_index=True,
                         blank=True,
                         default="")
    created = models.DateTimeField(verbose_name=_("created"),
                                   default=now,
                                   db_index=True,
                                   editable=False,
                                   blank=True)
    last_modified = models.DateTimeField(verbose_name=_("last modified"),
                                         auto_now=True,
                                         db_index=True,
                                         editable=False,
                                         blank=True)
    payback_priority = models.SmallIntegerField(
        verbose_name=_("payback priority"),
        default=0,
        blank=True,
        db_index=True)
    is_settlement = models.BooleanField(verbose_name=_("is settlement"),
                                        default=False,
                                        blank=True,
                                        db_index=True)
    is_payment = models.BooleanField(verbose_name=_("is payment"),
                                     default=False,
                                     blank=True,
                                     db_index=True)

    class Meta:
        verbose_name = _("entry type")
        verbose_name_plural = _("entry types")

    def __str__(self):
        return "{} ({})".format(self.name, self.code)
Example #10
0
class CurrencyExchangeSource(models.Model):
    name = SafeCharField(_("name"), max_length=64)
    created = models.DateTimeField(_("created"), default=now, db_index=True, blank=True, editable=False)

    class Meta:
        verbose_name = _("currency exchange source")
        verbose_name_plural = _("currency exchange sources")

    def __str__(self):
        return str(self.name)
Example #11
0
class EuriborRate(models.Model):
    objects = EuriborRateManager()
    record_date = models.DateField(_("record date"), db_index=True)
    name = SafeCharField(_("interest rate name"), db_index=True, max_length=64)
    rate = models.DecimalField(_("interest rate %"), max_digits=10, decimal_places=4, db_index=True)
    created = models.DateTimeField(_("created"), default=now, db_index=True, blank=True, editable=False)

    class Meta:
        verbose_name = _("euribor rate")
        verbose_name_plural = _("euribor rates")
Example #12
0
class CurrencyExchange(models.Model):
    record_date = models.DateField(_("record date"), db_index=True)
    source_currency = SafeCharField(_("source currency"), max_length=3, blank=True)
    target_currency = SafeCharField(_("target currency"), max_length=3, blank=True)
    unit_currency = SafeCharField(_("unit currency"), max_length=3, blank=True)
    exchange_rate = models.DecimalField(_("exchange rate"), decimal_places=6, max_digits=12, null=True, default=None, blank=True)
    source = models.ForeignKey(
        CurrencyExchangeSource,
        verbose_name=_("currency exchange source"),
        blank=True,
        null=True,
        default=None,
        on_delete=models.PROTECT,
    )  # noqa

    class Meta:
        verbose_name = _("currency exchange")
        verbose_name_plural = _("currency exchanges")

    def __str__(self):
        return "{src} = {rate} {tgt}".format(src=self.source_currency, tgt=self.target_currency, rate=self.exchange_rate)
Example #13
0
class ReferencePaymentBatch(AccountEntrySourceFile):
    objects = ReferencePaymentBatchManager()
    file = models.ForeignKey("ReferencePaymentBatchFile", blank=True, default=None, null=True, on_delete=models.CASCADE)
    record_date = models.DateTimeField(_("record date"), db_index=True)
    institution_identifier = SafeCharField(_("institution identifier"), max_length=2, blank=True)
    service_identifier = SafeCharField(_("service identifier"), max_length=9, blank=True)
    currency_identifier = SafeCharField(_("currency identifier"), max_length=3, choices=CURRENCY_IDENTIFIERS)
    cached_total_amount = models.DecimalField(_("total amount"), max_digits=10, decimal_places=2, null=True, default=None, blank=True)

    class Meta:
        verbose_name = _("reference payment batch")
        verbose_name_plural = _("reference payment batches")

    def get_total_amount(self, force: bool = False) -> Decimal:
        if self.cached_total_amount is None or force:
            self.cached_total_amount = sum_queryset(ReferencePaymentRecord.objects.filter(batch=self))
            self.save(update_fields=["cached_total_amount"])
        return self.cached_total_amount

    @property
    def total_amount(self) -> Decimal:
        return self.get_total_amount()

    total_amount.fget.short_description = _("total amount")  # type: ignore
Example #14
0
class StatementRecordDetail(models.Model):
    record = models.ForeignKey(StatementRecord, verbose_name=_("record"), related_name="detail_set", on_delete=models.CASCADE)
    batch_identifier = SafeCharField(_("batch message id"), max_length=64, db_index=True, blank=True, default="")
    amount = models.DecimalField(verbose_name=_("amount"), max_digits=10, decimal_places=2, blank=True, default=None, null=True, db_index=True)
    currency_code = SafeCharField(_("currency code"), max_length=3)
    instructed_amount = models.DecimalField(
        verbose_name=_("instructed amount"),
        max_digits=10,
        decimal_places=2,
        blank=True,
        default=None,
        null=True,
        db_index=True,
    )
    exchange = models.ForeignKey(
        CurrencyExchange,
        verbose_name=_("currency exchange"),
        related_name="recorddetail_set",
        on_delete=models.PROTECT,
        null=True,
        default=None,
        blank=True,
    )
    archive_identifier = SafeCharField(_("archive identifier"), max_length=64, blank=True)
    end_to_end_identifier = SafeCharField(_("end-to-end identifier"), max_length=64, blank=True)
    creditor_name = SafeCharField(_("creditor name"), max_length=128, blank=True)
    creditor_account = SafeCharField(_("creditor account"), max_length=35, blank=True)
    creditor_account_scheme = SafeCharField(_("creditor account scheme"), max_length=8, blank=True)
    debtor_name = SafeCharField(_("debtor name"), max_length=128, blank=True)
    ultimate_debtor_name = SafeCharField(_("ultimate debtor name"), max_length=128, blank=True)
    unstructured_remittance_info = SafeCharField(_("unstructured remittance info"), max_length=2048, blank=True)
    paid_date = models.DateTimeField(_("paid date"), db_index=True, blank=True, null=True, default=None)

    class Meta:
        verbose_name = _("statement record details")
        verbose_name_plural = _("statement record details")
Example #15
0
class StatementRecordSepaInfo(models.Model):
    record = models.OneToOneField(StatementRecord, verbose_name=_("record"), related_name="sepa_info", on_delete=models.CASCADE)
    reference = SafeCharField(_("reference"), max_length=35, blank=True)
    iban_account_number = SafeCharField(_("IBAN"), max_length=35, blank=True)
    bic_code = SafeCharField(_("BIC"), max_length=35, blank=True)
    recipient_name_detail = SafeCharField(_("recipient name detail"), max_length=70, blank=True)
    payer_name_detail = SafeCharField(_("payer name detail"), max_length=70, blank=True)
    identifier = SafeCharField(_("identifier"), max_length=35, blank=True)
    archive_identifier = SafeCharField(_("archive identifier"), max_length=64, blank=True)

    class Meta:
        verbose_name = _("SEPA")
        verbose_name_plural = _("SEPA")

    def __str__(self):
        return "[{}]".format(self.id)
Example #16
0
class WsEdiSoapCall(models.Model):
    connection = models.ForeignKey("WsEdiConnection", verbose_name=_("WS-EDI connection"), on_delete=models.CASCADE)
    command = SafeCharField(_("command"), max_length=64, blank=True, db_index=True)
    created = models.DateTimeField(_("created"), default=now, db_index=True, editable=False, blank=True)
    executed = models.DateTimeField(_("executed"), default=None, null=True, db_index=True, editable=False, blank=True)
    error = SafeTextField(_("error"), blank=True)

    class Meta:
        verbose_name = _("WS-EDI SOAP call")
        verbose_name_plural = _("WS-EDI SOAP calls")

    def __str__(self):
        return "WsEdiSoapCall({})".format(self.id)

    @property
    def timestamp(self) -> datetime:
        return self.created.astimezone(pytz.timezone("Europe/Helsinki"))

    @property
    def timestamp_digits(self) -> str:
        v = re.sub(r"[^\d]", "", self.created.isoformat())
        return v[:17]

    @property
    def request_identifier(self) -> str:
        return str(self.id)

    @property
    def command_camelcase(self) -> str:
        return self.command[0:1].lower() + self.command[1:]  # noqa

    def debug_get_filename(self, file_type: str) -> str:
        return "{:08}{}.xml".format(self.id, file_type)

    @property
    def debug_request_full_path(self) -> str:
        return self.debug_get_file_path(self.debug_get_filename("q"))

    @property
    def debug_response_full_path(self) -> str:
        return self.debug_get_file_path(self.debug_get_filename("s"))

    @staticmethod
    def debug_get_file_path(filename: str) -> str:
        return os.path.join(settings.WSEDI_LOG_PATH, filename) if hasattr(settings, "WSEDI_LOG_PATH") and settings.WSEDI_LOG_PATH else ""
Example #17
0
class PayoutStatus(models.Model):
    objects = PayoutStatusManager()
    payout = models.ForeignKey(
        Payout,
        verbose_name=_("payout"),
        related_name="payoutstatus_set",
        on_delete=models.PROTECT,
        null=True,
        default=None,
        blank=True,
    )
    created = models.DateTimeField(_("created"), default=now, db_index=True, editable=False, blank=True)
    timestamp = models.DateTimeField(_("timestamp"), default=now, db_index=True, editable=False, blank=True)
    file_name = SafeCharField(_("file name"), max_length=128, blank=True, db_index=True, editable=False)
    file_path = SafeCharField(_("file path"), max_length=255, blank=True, db_index=True, editable=False)
    response_code = SafeCharField(_("response code"), max_length=4, blank=True, db_index=True)
    response_text = SafeCharField(_("response text"), max_length=128, blank=True)
    msg_id = SafeCharField(_("message id"), max_length=64, blank=True, db_index=True)
    original_msg_id = SafeCharField(_("original message id"), blank=True, max_length=64, db_index=True)
    group_status = SafeCharField(_("group status"), max_length=8, blank=True, db_index=True)
    status_reason = SafeCharField(_("status reason"), max_length=255, blank=True)

    class Meta:
        verbose_name = _("payout status")
        verbose_name_plural = _("payout statuses")

    def __str__(self):
        return str(self.group_status)

    @property
    def full_path(self) -> str:
        return get_media_full_path(self.file_path) if self.file_path else ""

    @property
    def is_accepted(self):
        return self.group_status == "ACCP"

    @property
    def is_rejected(self):
        return self.group_status == "RJCT"
Example #18
0
class Invoice(models.Model, CachedFieldsMixin):
    """
    Invoice model. Typically used as base model for actual app-specific invoice model.

    Convention for naming date/time variables:
    1) date fields are suffixed with _date if they are either plain date fields or interpreted as such (due_date)
    2) natural datetime fields are in past tense, e.g. created, sent (instead of create_date, send_date)

    Note: It is useful sometimes to have full datetime with timezone even for plain dates like due_date,
    because this to be processing to be independent of server, client and invoice time zones.
    """

    objects: models.Manager = InvoiceManager()
    type = SafeCharField(verbose_name=_("type"),
                         max_length=2,
                         db_index=True,
                         default=INVOICE_DEFAULT,
                         blank=True,
                         choices=INVOICE_TYPE)
    number = SafeCharField(verbose_name=_("invoice number"),
                           max_length=32,
                           default="",
                           blank=True,
                           db_index=True)
    created = models.DateTimeField(verbose_name=_("created"),
                                   default=now,
                                   db_index=True,
                                   editable=False,
                                   blank=True)
    last_modified = models.DateTimeField(verbose_name=_("last modified"),
                                         auto_now=True,
                                         db_index=True,
                                         editable=False,
                                         blank=True)
    sent = models.DateTimeField(verbose_name=_("sent"),
                                db_index=True,
                                default=None,
                                blank=True,
                                null=True)
    due_date = models.DateTimeField(verbose_name=_("due date"),
                                    db_index=True,
                                    default=get_default_due_date)
    notes = SafeTextField(verbose_name=_("notes"), blank=True, default="")
    filename = SafeCharField(verbose_name=_("filename"),
                             max_length=255,
                             blank=True,
                             default="",
                             db_index=True)
    amount = models.DecimalField(verbose_name=_("amount"),
                                 max_digits=10,
                                 decimal_places=2,
                                 default=Decimal("0.00"),
                                 blank=True,
                                 help_text=_("invoice.amount.help_text"))
    paid_amount = models.DecimalField(
        verbose_name=_("paid amount"),
        max_digits=10,
        decimal_places=2,
        blank=True,
        null=True,
        default=Decimal("0.00"),
        db_index=True,
        help_text=_("invoice.paid_amount.help_text"),
    )
    unpaid_amount = models.DecimalField(
        verbose_name=_("unpaid amount"),
        max_digits=10,
        decimal_places=2,
        editable=False,
        blank=True,
        null=True,
        default=None,
        db_index=True,
        help_text=_("invoice.unpaid_amount.help_text"),
    )
    overpaid_amount = models.DecimalField(
        verbose_name=_("overpaid amount"),
        max_digits=10,
        decimal_places=2,
        editable=False,
        blank=True,
        null=True,
        default=Decimal("0.00"),
        db_index=True,
        help_text=_("invoice.overpaid_amount.help_text"),
    )
    close_date = models.DateTimeField(verbose_name=_("close date"),
                                      default=None,
                                      null=True,
                                      blank=True,
                                      db_index=True)
    late_days = models.SmallIntegerField(verbose_name=_("late days"),
                                         default=None,
                                         null=True,
                                         blank=True,
                                         db_index=True)
    state = SafeCharField(verbose_name=_("state"),
                          max_length=1,
                          blank=True,
                          default="",
                          db_index=True,
                          choices=INVOICE_STATE)
    cached_receivables_account: Optional[Account] = None
    cached_fields = [
        "amount",
        "paid_amount",
        "unpaid_amount",
        "overpaid_amount",
        "close_date",
        "late_days",
        "state",
    ]

    class Meta:
        verbose_name = _("invoice")
        verbose_name_plural = _("invoices")

    def __str__(self):
        return "[{}] {} {}".format(
            self.id,
            self.due_date.date().isoformat() if self.due_date else "",
            self.amount)

    @property
    def receivables_account(self) -> Optional[Account]:
        """
        Returns receivables account. Receivables account is assumed to be the one were invoice rows were recorded.
        :return: Account or None
        """
        if self.cached_receivables_account is None:
            row = AccountEntry.objects.filter(
                source_invoice=self).order_by("id").first()
            if row is not None:
                assert isinstance(row, AccountEntry)
                self.cached_receivables_account = row.account
        return self.cached_receivables_account

    @property
    def currency(self) -> str:
        recv = self.receivables_account
        return recv.currency if recv else ""

    def get_entries(self,
                    acc: Account,
                    cls: Type[AccountEntry] = AccountEntry) -> QuerySet:
        """
        Returns entries related to this invoice on specified account.
        :param acc: Account
        :param cls: AccountEntry class
        :return: QuerySet
        """
        return cls.objects.filter(
            Q(account=acc)
            & (Q(source_invoice=self)
               | Q(settled_invoice=self))) if acc else cls.objects.none()

    def get_balance(self, acc: Account) -> Decimal:
        """
        Returns balance of this invoice on specified account.
        :param acc: Account
        :return:
        """
        return sum_queryset(self.get_entries(acc))

    def get_item_balances(self, acc: Account) -> list:
        """
        Returns balances of items of the invoice.
        :param acc: Account
        :return: list (AccountEntry, Decimal) in item id order
        """
        items = []
        entries = self.get_entries(acc)
        for item in entries.filter(source_invoice=self).order_by("id"):
            assert isinstance(item, AccountEntry)
            settlements = sum_queryset(entries.filter(settled_item=item))
            bal = item.amount + settlements if item.amount is not None else settlements
            items.append((item, bal))
        return items

    def get_unpaid_items(self, acc: Account) -> list:
        """
        Returns unpaid items of the invoice in payback priority order.
        :param acc: Account
        :return: list (AccountEntry, Decimal) in payback priority order
        """
        unpaid_items = []
        for item, bal in self.get_item_balances(acc):
            assert isinstance(item, AccountEntry)
            priority = item.type.payback_priority if item.type is not None else 0
            if self.type == INVOICE_DEFAULT:
                if bal > Decimal(0):
                    unpaid_items.append((priority, item, bal))
            elif self.type == INVOICE_CREDIT_NOTE:
                if bal < Decimal(0):
                    unpaid_items.append((priority, item, bal))
            else:
                raise Exception(
                    "jacc.models.Invoice.get_unpaid_items() unimplemented for invoice type {}"
                    .format(self.type))
        return [i[1:] for i in sorted(unpaid_items, key=lambda x: x[0])]

    def get_amount(self) -> Decimal:
        return sum_queryset(self.items, "amount")

    @property
    def receivables(self) -> QuerySet:
        acc = self.receivables_account
        if acc is None:
            return AccountEntry.objects.none()
        return self.get_entries(acc)

    @property
    def items(self) -> QuerySet:
        return self.receivables.filter(source_invoice=self)

    def get_paid_amount(self) -> Decimal:
        return self.get_amount() - self.get_unpaid_amount()

    def get_unpaid_amount(self) -> Decimal:
        return sum_queryset(self.receivables)

    def get_overpaid_amount(self) -> Decimal:
        amt = sum_queryset(self.receivables)
        if self.type == INVOICE_CREDIT_NOTE:
            return max(Decimal("0.00"), amt)
        return max(Decimal("0.00"), -amt)

    @property
    def is_paid(self) -> bool:
        if self.unpaid_amount is None:
            return False
        return self.unpaid_amount >= Decimal(
            "0.00"
        ) if self.type == INVOICE_CREDIT_NOTE else self.unpaid_amount <= Decimal(
            "0.00")

    is_paid.fget.short_description = _("is paid")  # type: ignore  # pytype: disable=attribute-error

    @property
    def is_due(self) -> bool:
        return not self.is_paid and now() >= self.due_date

    is_due.fget.short_description = _("is due")  # type: ignore  # pytype: disable=attribute-error

    def get_close_date(self) -> Optional[datetime]:
        recv = self.receivables.order_by("-timestamp", "-id")
        first = recv.first()
        if first is None:
            return None
        total = sum_queryset(recv)
        if self.type == INVOICE_CREDIT_NOTE:
            if total >= Decimal("0.00"):
                return first.timestamp
        else:
            if total <= Decimal("0.00"):
                return first.timestamp
        return None

    def get_late_days(self, t: Optional[datetime] = None) -> int:
        t = self.close_date or t
        if t is None:
            t = now()
        return int(floor((t - self.due_date).total_seconds() / 86400.0))

    @property
    def is_late(self) -> bool:
        if self.late_days is None:
            return False
        return not self.is_paid and self.late_days >= settings.LATE_LIMIT_DAYS

    def get_state(self) -> str:
        if self.is_paid:
            return INVOICE_PAID
        t = now()
        if t - self.due_date >= timedelta(days=settings.LATE_LIMIT_DAYS):
            return INVOICE_LATE
        if t >= self.due_date:
            return INVOICE_DUE
        return INVOICE_NOT_DUE_YET

    def get_state_name(self) -> str:
        return choices_label(INVOICE_STATE, self.get_state())

    @property
    def state_name(self) -> str:
        return choices_label(INVOICE_STATE, self.state)

    state_name.fget.short_description = _("state")  # type: ignore  # pytype: disable=attribute-error
Example #19
0
class Account(models.Model):
    """
    Collects together accounting entries and provides summarizing functionality.
    """

    type = models.ForeignKey(AccountType,
                             verbose_name=_("type"),
                             related_name="+",
                             on_delete=models.PROTECT)
    name = SafeCharField(verbose_name=_("name"),
                         max_length=64,
                         blank=True,
                         default="",
                         db_index=True)
    currency = SafeCharField(verbose_name=_("currency"),
                             max_length=3,
                             default="EUR",
                             choices=CURRENCY_TYPE,
                             blank=True)
    created = models.DateTimeField(verbose_name=_("created"),
                                   default=now,
                                   db_index=True,
                                   editable=False,
                                   blank=True)
    last_modified = models.DateTimeField(verbose_name=_("last modified"),
                                         auto_now=True,
                                         db_index=True,
                                         editable=False,
                                         blank=True)
    notes = models.TextField(_("notes"), blank=True, default="")

    class Meta:
        verbose_name = _("account")
        verbose_name_plural = _("accounts")

    def __str__(self):
        return "[{}] {}".format(self.id, self.name or self.type.name)

    def is_asset(self) -> bool:
        return self.type.is_asset

    is_asset.boolean = True  # type: ignore
    is_asset.short_description = _("asset")  # type: ignore

    def is_liability(self) -> bool:
        return self.type.is_liability

    is_liability.boolean = True  # type: ignore
    is_liability.short_description = _("liability")  # type: ignore

    @property
    def balance(self) -> Decimal:
        return sum_queryset(self.accountentry_set.all())

    balance.fget.short_description = _("balance")  # type: ignore  # pytype: disable=attribute-error

    def get_balance(self, t: datetime):
        """
        Returns account balance before specified datetime (excluding entries on the datetime).
        :param t: datetime
        :return: Decimal
        """
        return sum_queryset(
            self.accountentry_set.all().filter(timestamp__lt=t))

    def needs_settling(self, e: AccountEntry) -> bool:
        """
        Returns True if all of following conditions are True:
        a) entry has valid amount set
        b) entry type is settlement
        c) entry has been recorded to this account
        d) invoice to be settled has been set
        e) entry has not been settled (=child set empty)
        :param e: AccountEntry (settlement)
        :return: bool
        """
        return bool(e.amount is not None and e.type and e.type.is_settlement
                    and e.account.id == self.id and e.settled_invoice
                    and not e.is_parent)
Example #20
0
class StatementRecord(AccountEntry):
    objects: models.Manager = PaymentRecordManager()  # type: ignore
    statement = models.ForeignKey(Statement, verbose_name=_("statement"), related_name="record_set", on_delete=models.CASCADE)
    line_number = models.SmallIntegerField(_("line number"), default=None, null=True, blank=True)
    record_number = models.IntegerField(_("record number"), default=None, null=True, blank=True)
    archive_identifier = SafeCharField(_("archive identifier"), max_length=64, blank=True, default="", db_index=True)
    record_date = models.DateField(_("record date"), db_index=True)
    value_date = models.DateField(_("value date"), db_index=True, blank=True, null=True, default=None)
    paid_date = models.DateField(_("paid date"), db_index=True, blank=True, null=True, default=None)
    entry_type = SafeCharField(_("entry type"), max_length=1, choices=RECORD_ENTRY_TYPE, db_index=True)
    record_code = SafeCharField(_("record type"), max_length=4, choices=RECORD_CODES, db_index=True, blank=True)
    record_domain = SafeCharField(_("record domain"), max_length=4, choices=RECORD_DOMAIN, db_index=True, blank=True)
    family_code = SafeCharField(_("family code"), max_length=4, db_index=True, blank=True, default="")
    sub_family_code = SafeCharField(_("sub family code"), max_length=4, db_index=True, blank=True, default="")
    record_description = SafeCharField(_("record description"), max_length=128, blank=True, default="")
    receipt_code = SafeCharField(_("receipt code"), max_length=1, choices=RECEIPT_CODE, db_index=True, blank=True)
    delivery_method = SafeCharField(_("delivery method"), max_length=1, db_index=True, choices=DELIVERY_METHOD, blank=True)
    name = SafeCharField(_("name"), max_length=128, blank=True, db_index=True)
    name_source = SafeCharField(_("name source"), max_length=1, blank=True, choices=NAME_SOURCES)
    recipient_account_number = SafeCharField(_("recipient account number"), max_length=32, blank=True, db_index=True)
    recipient_account_number_changed = SafeCharField(_("recipient account number changed"), max_length=1, blank=True)
    remittance_info = SafeCharField(_("remittance info"), max_length=35, db_index=True, blank=True)
    messages = SafeTextField(_("messages"), blank=True, default="")
    client_messages = SafeTextField(_("client messages"), blank=True, default="")
    bank_messages = SafeTextField(_("bank messages"), blank=True, default="")
    manually_settled = models.BooleanField(_("manually settled"), db_index=True, default=False, blank=True)

    class Meta:
        verbose_name = _("statement record")
        verbose_name_plural = _("statement records")

    @property
    def is_settled(self) -> bool:
        """
        True if entry is either manually settled or has SUM(children)==amount.
        """
        return self.manually_settled or sum_queryset(self.child_set) == self.amount  # type: ignore

    def clean(self):
        self.source_file = self.statement
        self.timestamp = pytz.utc.localize(datetime.combine(self.record_date, time(0, 0)))
        if self.name:
            self.description = "{name}: {record_description}".format(record_description=self.record_description, name=self.name)
        else:
            self.description = "{record_description}".format(record_description=self.record_description)
Example #21
0
class AccountEntry(models.Model):
    """
    Single mutation in account state.
    """

    objects: models.Manager = AccountEntryManager()
    account = models.ForeignKey(
        "Account",
        verbose_name=_("record account"),
        related_name="accountentry_set",
        db_index=True,
        on_delete=models.PROTECT,
    )
    created = models.DateTimeField(verbose_name=_("created"),
                                   default=now,
                                   db_index=True,
                                   editable=False,
                                   blank=True)
    last_modified = models.DateTimeField(verbose_name=_("last modified"),
                                         auto_now=True,
                                         editable=False,
                                         blank=True)
    timestamp = models.DateTimeField(verbose_name=_("timestamp"),
                                     default=now,
                                     db_index=True,
                                     blank=True)
    type = models.ForeignKey(
        EntryType,
        verbose_name=_("type"),
        related_name="+",
        on_delete=models.PROTECT,
        null=True,
        default=None,
        blank=True,
    )
    description = SafeCharField(verbose_name=_("description"),
                                max_length=256,
                                default="",
                                blank=True)
    amount = models.DecimalField(verbose_name=_("amount"),
                                 max_digits=10,
                                 decimal_places=2,
                                 blank=True,
                                 default=None,
                                 null=True,
                                 db_index=True)
    source_file = models.ForeignKey(
        AccountEntrySourceFile,
        verbose_name=_("account entry source file"),
        related_name="+",
        null=True,
        default=None,
        blank=True,
        on_delete=models.CASCADE,
        help_text=_("entry.source.file.help.text"),
    )
    source_invoice = models.ForeignKey(
        "Invoice",
        verbose_name=_("source invoice"),
        null=True,
        related_name="+",
        default=None,
        blank=True,
        on_delete=models.CASCADE,
        help_text=_("entry.source.invoice.help.text"),
    )
    settled_invoice = models.ForeignKey(
        "Invoice",
        verbose_name=_("settled invoice"),
        null=True,
        related_name="+",
        default=None,
        blank=True,
        on_delete=models.PROTECT,
        help_text=_("entry.settled.invoice.help.text"),
    )
    settled_item = models.ForeignKey(
        "AccountEntry",
        verbose_name=_("settled item"),
        null=True,
        related_name="settlement_set",
        default=None,
        blank=True,
        on_delete=models.PROTECT,
        help_text=_("entry.settled.item.help.text"),
    )
    parent = models.ForeignKey(
        "AccountEntry",
        verbose_name=_("account.entry.parent"),
        related_name="child_set",
        db_index=True,
        on_delete=models.CASCADE,
        null=True,
        default=None,
        blank=True,
    )
    archived = models.BooleanField(_("archived"), default=False, blank=True)

    class Meta:
        verbose_name = _("account entry")
        verbose_name_plural = _("account entries")

    def __str__(self):
        return "[{}] {} {} {}".format(
            self.id,
            self.timestamp.date().isoformat() if self.timestamp else "",
            self.type if self.type else "",
            self.amount,
        )

    def clean(self):
        if self.source_invoice and self.settled_invoice:
            raise ValidationError(
                "Both source_invoice ({}) and settled_invoice ({}) cannot be set same time for account entry ({})"
                .format(self.source_invoice, self.settled_invoice, self))

    @property
    def is_parent(self) -> bool:
        """
        True if this is a parent of some other account entry.
        :return: bool
        """
        return AccountEntry.objects.filter(parent=self).exists()

    @property
    def balance(self) -> Decimal:
        """
        Returns account balance after this entry.
        :return: Decimal
        """
        return sum_queryset(
            AccountEntry.objects.filter(account=self.account,
                                        timestamp__lte=self.timestamp).exclude(
                                            timestamp=self.timestamp,
                                            id__gt=self.id))

    balance.fget.short_description = _("balance")  # type: ignore  # pytype: disable=attribute-error
Example #22
0
class WsEdiConnection(models.Model):
    objects = WsEdiConnectionManager()
    name = SafeCharField(_("name"), max_length=64)
    enabled = models.BooleanField(_("enabled"), blank=True, default=True)
    sender_identifier = SafeCharField(_("sender identifier"), max_length=32)
    receiver_identifier = SafeCharField(_("receiver identifier"), max_length=32)
    target_identifier = SafeCharField(_("target identifier"), max_length=32)
    environment = SafeCharField(_("environment"), max_length=32, default="PRODUCTION")
    pin = SafeCharField("PIN", max_length=64, default="", blank=True)
    pki_endpoint = models.URLField(_("PKI endpoint"), blank=True, default="")
    bank_root_cert_file = models.FileField(verbose_name=_("bank root certificate file"), blank=True, upload_to="certs")
    soap_endpoint = models.URLField(_("EDI endpoint"))
    signing_cert_file = models.FileField(verbose_name=_("signing certificate file"), blank=True, upload_to="certs")
    signing_key_file = models.FileField(verbose_name=_("signing key file"), blank=True, upload_to="certs")
    encryption_cert_file = models.FileField(verbose_name=_("encryption certificate file"), blank=True, upload_to="certs")
    encryption_key_file = models.FileField(verbose_name=_("encryption key file"), blank=True, upload_to="certs")
    bank_encryption_cert_file = models.FileField(verbose_name=_("bank encryption cert file"), blank=True, upload_to="certs")
    bank_signing_cert_file = models.FileField(verbose_name=_("bank signing cert file"), blank=True, upload_to="certs")
    ca_cert_file = models.FileField(verbose_name=_("CA certificate file"), blank=True, upload_to="certs")
    debug_commands = SafeTextField(_("debug commands"), blank=True, help_text=_("wsedi.connection.debug.commands.help.text"))
    created = models.DateTimeField(_("created"), default=now, db_index=True, editable=False, blank=True)
    _signing_cert = None

    class Meta:
        verbose_name = _("WS-EDI connection")
        verbose_name_plural = _("WS-EDI connections")

    def __str__(self):
        return "{} / {}".format(self.name, self.receiver_identifier)

    @property
    def is_test(self) -> bool:
        return str(self.environment).lower() in ["customertest", "test"]

    @property
    def signing_cert_full_path(self) -> str:
        return get_media_full_path(self.signing_cert_file.file.name) if self.signing_cert_file else ""

    @property
    def signing_key_full_path(self) -> str:
        return get_media_full_path(self.signing_key_file.file.name) if self.signing_key_file else ""

    @property
    def encryption_cert_full_path(self) -> str:
        return get_media_full_path(self.encryption_cert_file.file.name) if self.encryption_cert_file else ""

    @property
    def encryption_key_full_path(self) -> str:
        return get_media_full_path(self.encryption_key_file.file.name) if self.encryption_key_file else ""

    @property
    def bank_encryption_cert_full_path(self) -> str:
        return get_media_full_path(self.bank_encryption_cert_file.file.name) if self.bank_encryption_cert_file else ""

    @property
    def bank_root_cert_full_path(self) -> str:
        return get_media_full_path(self.bank_root_cert_file.file.name) if self.bank_root_cert_file else ""

    @property
    def ca_cert_full_path(self) -> str:
        return get_media_full_path(self.ca_cert_file.file.name) if self.ca_cert_file else ""

    @property
    def signing_cert_with_public_key_full_path(self) -> str:
        src_file = self.signing_cert_full_path
        file = src_file[:-4] + "-with-pubkey.pem"
        if not os.path.isfile(file):
            cmd = [
                settings.OPENSSL_PATH,
                "x509",
                "-pubkey",
                "-in",
                src_file,
            ]
            logger.info(" ".join(cmd))
            out = subprocess.check_output(cmd)
            with open(file, "wb") as fp:
                fp.write(out)
        return file

    @property
    def bank_encryption_cert_with_public_key_full_path(self) -> str:
        src_file = self.bank_encryption_cert_full_path
        file = src_file[:-4] + "-with-pubkey.pem"
        if not os.path.isfile(file):
            cmd = [
                settings.OPENSSL_PATH,
                "x509",
                "-pubkey",
                "-in",
                src_file,
            ]
            # logger.info(' '.join(cmd))
            out = subprocess.check_output(cmd)
            with open(file, "wb") as fp:
                fp.write(out)
        return file

    @property
    def signing_cert(self):
        if hasattr(self, "_signing_cert") and self._signing_cert:
            return self._signing_cert
        self._signing_cert = get_x509_cert_from_file(self.signing_cert_full_path)
        return self._signing_cert

    def get_pki_template(self, template_name: str, soap_call: WsEdiSoapCall, **kwargs) -> bytes:
        return format_xml(
            get_template(template_name).render(
                {
                    "ws": soap_call.connection,
                    "soap_call": soap_call,
                    "command": soap_call.command,
                    "timestamp": now().astimezone(pytz.timezone("Europe/Helsinki")).isoformat(),
                    **kwargs,
                }
            )
        ).encode()

    def get_application_request(self, command: str, **kwargs) -> bytes:
        return format_xml(
            get_template("jbank/application_request_template.xml").render(
                {
                    "ws": self,
                    "command": command,
                    "timestamp": now().astimezone(pytz.timezone("Europe/Helsinki")).isoformat(),
                    **kwargs,
                }
            )
        ).encode()

    @classmethod
    def verify_signature(cls, content: bytes, signing_key_full_path: str):
        with tempfile.NamedTemporaryFile() as fp:
            fp.write(content)
            fp.flush()
            cmd = [settings.XMLSEC1_PATH, "--verify", "--pubkey-pem", signing_key_full_path, fp.name]
            # logger.info(' '.join(cmd))
            subprocess.check_output(cmd)

    def sign_pki_request(self, content: bytes, signing_key_full_path: str, signing_cert_full_path: str) -> bytes:
        return self._sign_request(content, signing_key_full_path, signing_cert_full_path)

    def sign_application_request(self, content: bytes) -> bytes:
        return self._sign_request(content, self.signing_key_full_path, self.signing_cert_full_path)

    @classmethod
    def _sign_request(cls, content: bytes, signing_key_full_path: str, signing_cert_full_path: str) -> bytes:
        """
        Sign a request.
        See https://users.dcc.uchile.cl/~pcamacho/tutorial/web/xmlsec/xmlsec.html
        :param content: XML application request
        :param signing_key_full_path: Override signing key full path (if not use self.signing_key_full_path)
        :param signing_cert_full_path: Override signing key full path (if not use self.signing_cert_full_path)
        :return: str
        """
        with tempfile.NamedTemporaryFile() as fp:
            fp.write(content)
            fp.flush()
            cmd = [
                settings.XMLSEC1_PATH,
                "--sign",
                "--privkey-pem",
                "{},{}".format(signing_key_full_path, signing_cert_full_path),
                fp.name,
            ]
            # logger.info(' '.join(cmd))
            out = subprocess.check_output(cmd)
        cls.verify_signature(out, signing_key_full_path)
        return out

    def encrypt_pki_request(self, content: bytes) -> bytes:
        return self._encrypt_request(content)

    def encrypt_application_request(self, content: bytes) -> bytes:
        return self._encrypt_request(content)

    def _encrypt_request(self, content: bytes) -> bytes:
        with tempfile.NamedTemporaryFile() as fp:
            fp.write(content)
            fp.flush()
            cmd = [
                self._xmlsec1_example_bin("encrypt3"),
                fp.name,
                self.bank_encryption_cert_with_public_key_full_path,
                self.bank_encryption_cert_full_path,
            ]
            # logger.info(' '.join(cmd))
            out = subprocess.check_output(cmd)
        return out

    def encode_application_request(self, content: bytes) -> bytes:
        lines = content.split(b"\n")
        if lines and lines[0].startswith(b"<?xml"):
            lines = lines[1:]
        content_without_xml_tag = b"\n".join(lines)
        return base64.b64encode(content_without_xml_tag)

    def decode_application_response(self, content: bytes) -> bytes:
        return base64.b64decode(content)

    def decrypt_application_response(self, content: bytes) -> bytes:
        with tempfile.NamedTemporaryFile() as fp:
            fp.write(content)
            fp.flush()
            cmd = [
                self._xmlsec1_example_bin("decrypt3"),
                fp.name,
                self.encryption_key_full_path,
            ]
            # logger.info(' '.join(cmd))
            out = subprocess.check_output(cmd)
        return out

    @property
    def debug_command_list(self) -> List[str]:
        return [x for x in re.sub(r"[^\w]+", " ", self.debug_commands).strip().split(" ") if x]

    @staticmethod
    def _xmlsec1_example_bin(file: str) -> str:
        if hasattr(settings, "XMLSEC1_EXAMPLES_PATH") and settings.XMLSEC1_EXAMPLES_PATH:
            xmlsec1_examples_path = settings.XMLSEC1_EXAMPLES_PATH
        else:
            xmlsec1_examples_path = os.path.join(str(os.getenv("HOME") or ""), "bin/xmlsec1-examples")
        return str(os.path.join(xmlsec1_examples_path, file))
Example #23
0
class Payout(AccountEntry):
    connection = models.ForeignKey(
        "WsEdiConnection",
        verbose_name=_("WS-EDI connection"),
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
    )
    payer = models.ForeignKey(PayoutParty, verbose_name=_("payer"), related_name="+", on_delete=models.PROTECT)
    recipient = models.ForeignKey(PayoutParty, verbose_name=_("recipient"), related_name="+", on_delete=models.PROTECT)
    messages = SafeTextField(_("recipient messages"), blank=True, default="")
    reference = SafeCharField(_("recipient reference"), blank=True, default="", max_length=32)
    msg_id = SafeCharField(_("message id"), max_length=64, blank=True, db_index=True, editable=False)
    file_name = SafeCharField(_("file name"), max_length=255, blank=True, db_index=True, editable=False)
    full_path = SafeTextField(_("full path"), blank=True, editable=False)
    file_reference = SafeCharField(_("file reference"), max_length=255, blank=True, db_index=True, editable=False)
    due_date = models.DateField(_("due date"), db_index=True, blank=True, null=True, default=None)
    paid_date = models.DateTimeField(_("paid date"), db_index=True, blank=True, null=True, default=None)
    state = SafeCharField(_("state"), max_length=1, blank=True, default=PAYOUT_WAITING_PROCESSING, choices=PAYOUT_STATE, db_index=True)

    class Meta:
        verbose_name = _("payout")
        verbose_name_plural = _("payouts")

    def clean(self):
        if self.parent and not self.amount:
            self.amount = self.parent.amount

        # prevent defining both reference and messages
        if self.messages and self.reference or not self.messages and not self.reference:
            raise ValidationError(_("payment.must.have.reference.or.messages"))

        # validate reference if any
        if self.reference:
            if self.reference[:2] == "RF":  # noqa
                iso_payment_reference_validator(self.reference)
            else:
                fi_payment_reference_validator(self.reference)

        # prevent canceling payouts which have been uploaded successfully
        if self.state == PAYOUT_CANCELED:
            if self.is_upload_done:
                group_status = self.group_status
                if group_status != "RJCT":
                    raise ValidationError(_("File already uploaded") + " ({})".format(group_status))

        # save paid time if marking payout as paid manually
        if self.state == PAYOUT_PAID and not self.paid_date:
            self.paid_date = now()
            status = self.payoutstatus_set.order_by("-created").first()
            if status:
                assert isinstance(status, PayoutStatus)
                self.paid_date = status.created

        # always require amount
        if self.amount is None or self.amount <= Decimal("0.00"):
            raise ValidationError({"amount": _("value > 0 required")})

    def generate_msg_id(self, commit: bool = True):
        msg_id_base = re.sub(r"[^\d]", "", now().isoformat())[:-4]
        self.msg_id = msg_id_base + "P" + str(self.id)
        if commit:
            self.save(update_fields=["msg_id"])

    @property
    def state_name(self):
        return choices_label(PAYOUT_STATE, self.state)

    @property
    def is_upload_done(self):
        return PayoutStatus.objects.filter(payout=self, response_code="00").first() is not None

    @property
    def is_accepted(self):
        return self.has_group_status("ACCP")

    @property
    def is_rejected(self):
        return self.has_group_status("RJCT")

    def has_group_status(self, group_status: str) -> bool:
        return PayoutStatus.objects.filter(payout=self, group_status=group_status).exists()

    @property
    def group_status(self):
        status = PayoutStatus.objects.filter(payout=self).order_by("-timestamp", "-id").first()
        return status.group_status if status else ""

    group_status.fget.short_description = _("payment.group.status")  # type: ignore  # pytype: disable=attribute-error
Example #24
0
class Statement(AccountEntrySourceFile):
    file = models.ForeignKey("StatementFile", blank=True, default=None, null=True, on_delete=models.CASCADE)
    account = models.ForeignKey(Account, related_name="+", on_delete=models.PROTECT)
    account_number = SafeCharField(_("account number"), max_length=32, db_index=True)
    statement_identifier = SafeCharField(_("statement identifier"), max_length=48, db_index=True, blank=True, default="")
    statement_number = models.SmallIntegerField(_("statement number"), db_index=True)
    begin_date = models.DateField(_("begin date"), db_index=True)
    end_date = models.DateField(_("end date"), db_index=True)
    record_date = models.DateTimeField(_("record date"), db_index=True)
    customer_identifier = SafeCharField(_("customer identifier"), max_length=64, blank=True, default="")
    begin_balance_date = models.DateField(_("begin balance date"), null=True, blank=True, default=None)
    begin_balance = models.DecimalField(_("begin balance"), max_digits=10, decimal_places=2)
    record_count = models.IntegerField(_("record count"), null=True, default=None)
    currency_code = SafeCharField(_("currency code"), max_length=3)
    account_name = SafeCharField(_("account name"), max_length=32, blank=True, default="")
    account_limit = models.DecimalField(_("account limit"), max_digits=10, decimal_places=2, blank=True, default=None, null=True)
    owner_name = SafeCharField(_("owner name"), max_length=64)
    contact_info_1 = SafeCharField(_("contact info (1)"), max_length=64, blank=True, default="")
    contact_info_2 = SafeCharField(_("contact info (2)"), max_length=64, blank=True, default="")
    bank_specific_info_1 = SafeCharField(_("bank specific info (1)"), max_length=1024, blank=True, default="")
    iban = SafeCharField(_("IBAN"), max_length=32, db_index=True)
    bic = SafeCharField(_("BIC"), max_length=11, db_index=True)

    class Meta:
        verbose_name = _("statement")
        verbose_name_plural = _("statements")