"""Accounting Period model.""" from abacus_common_logic.connectors.database import db from abacus_common_logic.models import BaseModel, NormalizedDateTime from sqlalchemy import Enum from sqlalchemy.orm import Query, backref from royalties.constants import constants from royalties.utils.strings import sanitize_s3_path class AccountingPeriod(BaseModel): """Accounting Period model.""" __tablename__ = 'accounting_period' accounting_period_id = db.Column(db.Integer, primary_key=True) statement_period_id = db.Column( db.Integer, db.ForeignKey('statement_period.statement_period_id'), nullable=False, ) accounting_period_name = db.Column(db.String(255), nullable=False) accounting_period_status = db.Column( Enum( *constants.ACCOUNTING_PERIOD_STATUSES, name='accounting_period_status', create_type=False, ), nullable=False, ) contract_type = db.Column( Enum(*constants.CONTRACT_TYPES, name='contract_type', create_type=False), nullable=False, ) closed_date = db.Column(NormalizedDateTime(), nullable=True) is_visible = db.Column(db.Boolean, nullable=False, default=True) visible_reason = db.Column(db.VARCHAR(50), nullable=True) statement_period = db.relationship( 'StatementPeriod', lazy='joined', backref=backref('accounting_period', cascade='all, delete-orphan'), ) __table_args__ = ( db.UniqueConstraint('accounting_period_name', name='accounting_period_name'), ) @classmethod def default_order(cls): """Override default ordering in BaseModel.""" return cls.created_at.desc() @classmethod def find_by_name(cls, accounting_period_name): """Override base model's find_by_name.""" return cls.query.filter_by( accounting_period_name=accounting_period_name ).first() @classmethod def get_current_period(cls, statement_period_id, contract_type): """Return the currently open accounting period if there is one.""" return cls.query.filter_by( closed_date=None, contract_type=contract_type, statement_period_id=statement_period_id, ).first() @classmethod def get_filtered_query(cls, *, is_visible: bool | None = None) -> Query: """Get accounting periods by field values. To prevent filters from being injected, they are applied to the search in an ad-hoc fashion. """ query = cls.query # Filter by visibility if is_visible is not None: query = query.filter_by(is_visible=is_visible) return query def get_s3_folder_name(self): """Get the s3 folder path for the period.""" sanitized_name = sanitize_s3_path(self.accounting_period_name) return '{}-{}'.format(self.accounting_period_id, sanitized_name) def is_new_acc_period_status_valid(self, new_acc_period_status): """Check if the accounting period can transition to the new status. To update a period's status to 'Closed', all associated accounting runs must have a run status of 'Committed', 'Invalid', or 'Skipped'. """ current_status_new = self.accounting_period_status closed = constants.ACCOUNTING_PERIOD_STATUSES.CLOSED valid_statuses = [ constants.ACCOUNTING_PERIOD_STATUSES.LOCKED, constants.ACCOUNTING_PERIOD_STATUSES.CLOSED, ] if current_status_new == new_acc_period_status: return True if new_acc_period_status not in valid_statuses: return False if new_acc_period_status == closed: valid_run_statuses = [ constants.ACCOUNTING_RUN_STATUSES.COMMITTED, constants.ACCOUNTING_RUN_STATUSES.SKIPPED, constants.ACCOUNTING_RUN_STATUSES.SENT_TO_WORKSTATION, constants.ACCOUNTING_RUN_STATUSES.EXPORT_PAYMENTS, constants.ACCOUNTING_RUN_STATUSES.INVALID, ] return all( run.run_status in valid_run_statuses for run in self.accounting_runs ) return True