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)}
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)
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)
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), }
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 {}
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)}
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 {}
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), }