Esempio n. 1
0
    def __call__(self):
        request = self.request
        dbsession = request.dbsession
        owner = request.owner
        owner_id = owner.id

        schema = StatementUploadSchema()
        try:
            self.appstruct = appstruct = schema.deserialize(request.json)
        except colander.Invalid as e:
            handle_invalid(e, schema=schema)

        name = appstruct['name']
        pos = name.rfind('.')
        if pos >= 0:
            ext = name[pos:].lower()
        else:
            ext = ''

        content_type = appstruct['type'].split(';')[0]
        if content_type in self.excel_types or ext in self.excel_extensions:
            self.handle_excel()

        if self.statement is None:
            raise HTTPBadRequest(
                json_body={
                    'error':
                    'file_type_not_supported',
                    'error_description': ("File type not supported: %s (%s)" %
                                          (ext, appstruct['type'])),
                })

        # Auto-reconcile the statement to the extent possible.
        configure_dblog(request=request, event_type='statement_auto_reco')
        auto_reco_statement(dbsession=dbsession,
                            owner=owner,
                            period=self.context.period,
                            statement=self.statement)

        entry_count = (dbsession.query(
            func.count(1)).select_from(AccountEntry).filter(
                AccountEntry.statement_id == self.statement.id).scalar())

        dbsession.add(
            OwnerLog(
                owner_id=owner_id,
                personal_id=request.personal_id,
                event_type='statement_upload',
                remote_addr=request.remote_addr,
                user_agent=request.user_agent,
                content={
                    'statement_id': self.statement.id,
                    'filename': appstruct['name'],
                    'content_type': appstruct['type'],
                    'size': appstruct['size'],
                    'entry_count': entry_count,
                },
            ))

        return {'statement': serialize_statement(self.statement)}
Esempio n. 2
0
def add_file(context, request):
    owner = request.owner
    owner_id = owner.id
    dbsession = request.dbsession

    schema = FileAddSchema(validator=validate_file_add)
    try:
        params = schema.deserialize(request.json)
    except colander.Invalid as e:
        handle_invalid(e, schema=schema)

    file_type = params['file_type']
    has_vault = file_type in ('open_circ', 'closed_circ')
    peer_id = params['peer_id'] if file_type == 'account' else None
    auto_enable_loops = (
        params['auto_enable_loops'] if file_type == 'closed_circ' else None)

    file = File(
        owner_id=owner_id,
        file_type=file_type,
        title=params['title'],
        currency=params['currency'],
        has_vault=has_vault,
        peer_id=peer_id,
        auto_enable_loops=auto_enable_loops,
        archived=False)
    dbsession.add(file)
    dbsession.flush()  # Assign file.id

    return serialize_file(file)
Esempio n. 3
0
def file_save(context, request):
    """Change the file."""
    file = context.file

    schema = FileSaveSchema()
    try:
        appstruct = schema.deserialize(request.json)
    except colander.Invalid as e:
        handle_invalid(e, schema=schema)

    file.title = appstruct['title']

    if file.file_type == 'closed_circ':
        file.auto_enable_loops = appstruct['auto_enable_loops']

    if appstruct['reinterpret']:
        # Reinterpret all the movements in this file.
        # This may make some movements reconcileable and may make
        # reconciliation unavailable for some unreconciled movements.
        # It does not remove reconciled movements.
        dbsession = request.dbsession
        query = (
            dbsession.query(FileSync).filter(FileSync.file_id == file.id))
        query.delete(synchronize_session=False)

        dbsession.expire_all()
        sync = SyncBase(request)
        sync.sync_missing()

    request.dbsession.add(OwnerLog(
        owner_id=request.owner.id,
        personal_id=request.personal_id,
        event_type='edit_file',
        content={
            'file_id': file.id,
            'title': file.title,
            'auto_enable_loops': appstruct.get('auto_enable_loops'),
            'reinterpret': appstruct['reinterpret'],
        }))

    return serialize_file(file)
Esempio n. 4
0
def statement_add_blank(context, request):
    """Add a blank statement."""
    period = context.period
    dbsession = request.dbsession
    owner = request.owner
    owner_id = owner.id

    schema = StatementAddBlankSchema()
    try:
        appstruct = schema.deserialize(request.json)
    except colander.Invalid as e:
        handle_invalid(e, schema=schema)

    statement = Statement(
        owner_id=owner_id,
        period_id=period.id,
        file_id=period.file_id,
        source=appstruct['source'],
    )
    dbsession.add(statement)
    dbsession.flush()  # Assign statement.id

    dbsession.add(
        OwnerLog(
            owner_id=owner_id,
            personal_id=request.personal_id,
            event_type='statement_add_blank',
            remote_addr=request.remote_addr,
            user_agent=request.user_agent,
            content={
                'statement_id': statement.id,
                'source': appstruct['source'],
            },
        ))

    return {
        'statement': serialize_statement(statement),
    }
Esempio n. 5
0
def entry_delete(context, request):
    """Delete an account entry."""
    period = context.period
    dbsession = request.dbsession
    owner = request.owner
    owner_id = owner.id

    schema = AccountEntryDeleteSchema()
    try:
        appstruct = schema.deserialize(request.json)
    except colander.Invalid as e:
        handle_invalid(e, schema=schema)

    statement = (dbsession.query(Statement).filter(
        Statement.owner_id == owner_id,
        Statement.file_id == period.file_id,
        Statement.id == appstruct['statement_id'],
    ).first())
    if statement is None:
        raise HTTPBadRequest(
            json_body={
                'error':
                'statement_not_found',
                'error_description': ("Statement %s not found." %
                                      appstruct['statement_id'])
            })

    entry = (dbsession.query(AccountEntry).filter(
        AccountEntry.owner_id == owner_id,
        AccountEntry.statement_id == statement.id,
        AccountEntry.id == appstruct['id'],
    ).first())

    if entry is None:
        raise HTTPBadRequest(
            json_body={
                'error': 'account_entry_not_found',
                'error_description': (
                    'The specified account entry is not found.'),
            })

    # Indicate that movements and other entries are being
    # changed because this entry is being deleted.
    configure_dblog(request=request, event_type='entry_delete')

    if entry.reco_id is not None:
        # Cancel the reco_id of movements reconciled with this entry.
        (dbsession.query(FileMovement).filter(
            FileMovement.reco_id == entry.reco_id, ).update(
                {
                    'reco_id': None,
                    # Also reset the surplus_delta for each movement.
                    'surplus_delta': -FileMovement.wallet_delta,
                },
                synchronize_session='fetch'))

        # Cancel the reco_id of account entries on other statements
        # reconciled with this entry.
        (dbsession.query(AccountEntry).filter(
            AccountEntry.reco_id == entry.reco_id,
            AccountEntry.statement_id != statement.id,
        ).update({
            'reco_id': None,
        }, synchronize_session='fetch'))

    dbsession.delete(entry)
    dbsession.flush()

    return {}
Esempio n. 6
0
def entry_save(context, request):
    """Save changes to an account entry."""
    period = context.period
    file = period.file
    dbsession = request.dbsession
    owner = request.owner
    owner_id = owner.id

    schema = AccountEntryEditSchema()
    try:
        appstruct = schema.deserialize(request.json)
    except colander.Invalid as e:
        handle_invalid(e, schema=schema)

    statement = (dbsession.query(Statement).filter(
        Statement.owner_id == owner_id,
        Statement.file_id == period.file_id,
        Statement.id == appstruct['statement_id'],
    ).first())
    if statement is None:
        raise HTTPBadRequest(
            json_body={
                'error':
                'statement_not_found',
                'error_description': ("Statement %s not found." %
                                      appstruct['statement_id'])
            })

    delta_input = appstruct['delta']
    try:
        appstruct['delta'] = parse_amount(delta_input, currency=file.currency)
    except Exception as e:
        raise HTTPBadRequest(
            json_body={
                'error':
                'amount_parse_error',
                'error_description': ("Unable to parse amount '%s': %s" %
                                      (delta_input, e))
            })

    date_input = appstruct['entry_date']
    try:
        appstruct['entry_date'] = dateutil.parser.parse(date_input).date()
    except Exception as e:
        raise HTTPBadRequest(
            json_body={
                'error':
                'date_parse_error',
                'error_description': ("Unable to parse date '%s': %s" %
                                      (date_input, e))
            })

    attrs = ('delta', 'entry_date', 'sheet', 'row', 'description')

    if appstruct['id']:
        configure_dblog(request=request, account_entry_event_type='entry_edit')

        entry = (dbsession.query(AccountEntry).filter(
            AccountEntry.owner_id == owner_id,
            AccountEntry.statement_id == statement.id,
            AccountEntry.id == appstruct['id'],
        ).first())

        if entry is None:
            raise HTTPBadRequest(
                json_body={
                    'error':
                    'account_entry_not_found',
                    'error_description': (
                        'The specified account entry is not found.'),
                })

        if entry.reco_id is not None and entry.delta != appstruct['delta']:
            raise HTTPBadRequest(
                json_body={
                    'error':
                    'amount_immutable_with_reco',
                    'error_description':
                    ('The amount of an account entry can not change once it '
                     'has been reconciled. If you need to change the amount, '
                     'remove the reconciliation of the entry.'),
                })

        for attr in attrs:
            setattr(entry, attr, appstruct[attr])
        dbsession.flush()

    else:
        configure_dblog(request=request, account_entry_event_type='entry_add')

        entry = AccountEntry(owner_id=owner_id,
                             file_id=period.file_id,
                             period_id=period.id,
                             statement_id=statement.id,
                             loop_id='0',
                             currency=file.currency,
                             reco_id=None,
                             **{attr: appstruct[attr]
                                for attr in attrs})
        dbsession.add(entry)
        dbsession.flush()  # Assign entry.id

    return {'entry': serialize_entry(entry)}
Esempio n. 7
0
def statement_delete(context, request):
    """Delete a statement and the contained account entries."""
    period = context.period
    dbsession = request.dbsession
    owner = request.owner
    owner_id = owner.id

    schema = StatementDeleteSchema()
    try:
        appstruct = schema.deserialize(request.json)
    except colander.Invalid as e:
        handle_invalid(e, schema=schema)

    statement = (dbsession.query(Statement).filter(
        Statement.owner_id == owner_id,
        Statement.id == appstruct['id'],
        Statement.file_id == period.file_id,
    ).first())

    if statement is None:
        raise HTTPBadRequest(
            json_body={
                'error': 'statement_not_found',
                'error_description': ("Statement %s not found." %
                                      appstruct['id']),
            })

    delete_conflicts = get_delete_conflicts(dbsession=dbsession,
                                            statement=statement)

    if delete_conflicts:
        raise HTTPBadRequest(
            json_body={
                'error':
                'statement_delete_conflict',
                'error_description': (
                    "The statement can not be deleted for the following "
                    "reasons: %s" % delete_conflicts),
            })

    # Indicate that entries are being deleted and movements are being
    # changed because the statement is being deleted.
    configure_dblog(request=request, event_type='statement_delete')

    # reco_ids represents the list of recos to empty.
    reco_ids = (dbsession.query(AccountEntry.reco_id).filter(
        AccountEntry.statement_id == statement.id, ).distinct().subquery(
            name='reco_ids_subq'))

    # Cancel the reco_id of movements reconciled with any entry
    # in the statement.
    (dbsession.query(FileMovement).filter(
        FileMovement.reco_id.in_(reco_ids), ).update(
            {
                'reco_id': None,
                # Also reset the surplus_delta for each movement.
                'surplus_delta': -FileMovement.wallet_delta,
            },
            synchronize_session='fetch'))

    # Cancel the reco_id of account entries on other statements
    # reconciled with any entry in the statement.
    (dbsession.query(AccountEntry).filter(
        AccountEntry.reco_id.in_(reco_ids),
        AccountEntry.statement_id != statement.id,
    ).update({
        'reco_id': None,
    }, synchronize_session='fetch'))

    # Delete the account entries, but leave the account entry logs.
    (dbsession.query(AccountEntry).filter(
        AccountEntry.statement_id == statement.id, ).delete(
            synchronize_session='fetch'))

    # Delete the statement.
    (dbsession.query(Statement).filter(
        Statement.id == statement.id, ).delete(synchronize_session='fetch'))

    request.dbsession.add(
        OwnerLog(
            owner_id=owner_id,
            personal_id=request.personal_id,
            event_type='statement_delete',
            remote_addr=request.remote_addr,
            user_agent=request.user_agent,
            content=appstruct,
        ))

    return {}
Esempio n. 8
0
def statement_save(context, request):
    """Save changes to a statement."""
    period = context.period
    dbsession = request.dbsession
    owner = request.owner
    owner_id = owner.id

    schema = StatementSaveSchema()
    try:
        appstruct = schema.deserialize(request.json)
    except colander.Invalid as e:
        handle_invalid(e, schema=schema)

    statement = (dbsession.query(Statement).filter(
        Statement.owner_id == owner_id,
        Statement.id == appstruct['id'],
        Statement.file_id == period.file_id,
    ).first())

    if statement is None:
        raise HTTPBadRequest(
            json_body={
                'error': 'statement_not_found',
                'error_description': ("Statement %s not found." %
                                      appstruct['id']),
            })

    new_period = None
    if statement.period_id != appstruct['period_id']:
        new_period = (dbsession.query(Period).filter(
            Period.owner_id == owner_id,
            Period.file_id == period.file_id,
            ~Period.closed,
            Period.id == appstruct['period_id'],
        ).first())
        if new_period is None:
            raise HTTPBadRequest(
                json_body={
                    'error':
                    'invalid_period_id',
                    'error_description': (
                        "The selected period is closed or not available."),
                })

    changes = {}

    if statement.source != appstruct['source']:
        changes['source'] = appstruct['source']
        statement.source = appstruct['source']

    if new_period is not None:
        changes['period_id'] = appstruct['period_id']
        old_period_id = statement.period_id
        statement.period_id = appstruct['period_id']

        # Change the period of the statement's account entries and recos that
        # should move with the statement.
        configure_dblog(request=request,
                        event_type='reassign_statement_period')

        reassign_statement_period(dbsession=dbsession,
                                  statement=statement,
                                  old_period_id=old_period_id,
                                  new_period_id=statement.period_id)

    dbsession.add(
        OwnerLog(
            owner_id=owner_id,
            personal_id=request.personal_id,
            event_type='statement_save',
            remote_addr=request.remote_addr,
            user_agent=request.user_agent,
            content=appstruct,
        ))

    return {
        'statement': serialize_statement(statement),
    }