Merge scheduling and entries in operation.
This commit is contained in:
@ -1,69 +0,0 @@
|
||||
"""
|
||||
This file is part of Accountant.
|
||||
|
||||
Accountant is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Accountant is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from sqlalchemy import distinct, func, case, cast, extract
|
||||
|
||||
from accountant import db
|
||||
|
||||
|
||||
class Entry(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
pointed = db.Column(db.Boolean, nullable=False, default=False)
|
||||
operation_date = db.Column(db.Date, nullable=False)
|
||||
label = db.Column(db.String(500), nullable=False)
|
||||
value = db.Column(db.Numeric(15, 2), nullable=False)
|
||||
account_id = db.Column(db.Integer, db.ForeignKey('account.id'))
|
||||
scheduled_operation_id = db.Column(db.Integer,
|
||||
db.ForeignKey('scheduled_operation.id'))
|
||||
|
||||
account = db.relationship("Account", backref=db.backref('entry',
|
||||
lazy="dynamic"))
|
||||
scheduled_operation = db.relationship("ScheduledOperation",
|
||||
backref=db.backref('entry',
|
||||
lazy="dynamic"))
|
||||
|
||||
category = db.Column(db.String(100), nullable=True)
|
||||
|
||||
def __init__(self, pointed, label, value, account_id, operation_date=None,
|
||||
category=None, scheduled_operation_id=None):
|
||||
self.pointed = pointed
|
||||
self.operation_date = operation_date
|
||||
self.label = label
|
||||
self.value = value
|
||||
self.account_id = account_id
|
||||
self.category = category
|
||||
self.scheduled_operation_id = scheduled_operation_id
|
||||
|
||||
@classmethod
|
||||
def get_months_for_account(cls, session, account):
|
||||
if isinstance(account, int) or isinstance(account, str):
|
||||
account_id = account
|
||||
else:
|
||||
account_id = account.id
|
||||
|
||||
query = session.query(
|
||||
distinct(func.lpad(cast(extract("year", cls.operation_date),
|
||||
db.String), 4, '0')).label("year"),
|
||||
func.lpad(cast(extract("month", cls.operation_date),
|
||||
db.String), 2, '0').label("month")
|
||||
).filter(cls.account_id == account_id).order_by("year", "month")
|
||||
|
||||
return query
|
||||
|
||||
@classmethod
|
||||
def get(cls, session, id):
|
||||
return session.query(cls).filter(cls.id == id).one()
|
@ -16,36 +16,68 @@
|
||||
"""
|
||||
import arrow
|
||||
|
||||
from sqlalchemy import func, case, desc, false
|
||||
from sqlalchemy import func, case, desc, true, false
|
||||
|
||||
from accountant import db
|
||||
|
||||
|
||||
class Operation(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
pointed = db.Column(db.Boolean, nullable=False, default=False)
|
||||
|
||||
operation_date = db.Column(db.Date, nullable=False)
|
||||
label = db.Column(db.String(500), nullable=False)
|
||||
value = db.Column(db.Numeric(15, 2), nullable=False)
|
||||
account_id = db.Column(db.Integer, db.ForeignKey('account.id'))
|
||||
scheduled_operation_id = db.Column(db.Integer,
|
||||
db.ForeignKey('scheduled_operation.id'))
|
||||
|
||||
account = db.relationship('Account', backref=db.backref('operation',
|
||||
lazy="dynamic"))
|
||||
scheduled_operation_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('scheduled_operation.id')
|
||||
)
|
||||
|
||||
scheduled_operation = db.relationship(
|
||||
"ScheduledOperation", backref=db.backref('entry', lazy="dynamic")
|
||||
)
|
||||
|
||||
account_id = db.Column(db.Integer, db.ForeignKey('account.id'))
|
||||
|
||||
account = db.relationship(
|
||||
'Account', backref=db.backref('operation', lazy="dynamic")
|
||||
)
|
||||
|
||||
category = db.Column(db.String(100), nullable=True)
|
||||
|
||||
canceled = db.Column(db.Boolean, nullable=False)
|
||||
pointed = db.Column(
|
||||
db.Boolean,
|
||||
nullable=False,
|
||||
default=False,
|
||||
server_default=false()
|
||||
)
|
||||
|
||||
def __init__(self, pointed, label, value, account_id, operation_date=None,
|
||||
category=None):
|
||||
self.pointed = pointed
|
||||
confirmed = db.Column(
|
||||
db.Boolean,
|
||||
nullable=False,
|
||||
default=True,
|
||||
server_default=true()
|
||||
)
|
||||
|
||||
canceled = db.Column(
|
||||
db.Boolean,
|
||||
nullable=False,
|
||||
default=False,
|
||||
server_default=false()
|
||||
)
|
||||
|
||||
def __init__(self, label, value, account_id, operation_date=None,
|
||||
category=None, pointed=False, confirmed=True, canceled=False,
|
||||
scheduled_operation_id=None):
|
||||
self.operation_date = operation_date
|
||||
self.label = label
|
||||
self.value = value
|
||||
self.account_id = account_id
|
||||
self.category = category
|
||||
self.account_id = account_id
|
||||
self.scheduled_operation_id = scheduled_operation_id
|
||||
self.pointed = pointed
|
||||
self.confirmed = confirmed
|
||||
self.canceled = canceled
|
||||
|
||||
@classmethod
|
||||
def get_for_account_and_range(cls, session, account, begin, end):
|
||||
|
@ -14,28 +14,42 @@
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from sqlalchemy import desc
|
||||
from calendar import monthrange
|
||||
|
||||
from sqlalchemy import desc, false
|
||||
|
||||
import arrow
|
||||
|
||||
from accountant import db
|
||||
|
||||
from .accounts import Account
|
||||
from .operations import Operation
|
||||
|
||||
|
||||
class ScheduledOperation(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
start_date = db.Column(db.Date, nullable=False)
|
||||
stop_date = db.Column(db.Date, nullable=False)
|
||||
|
||||
day = db.Column(db.Integer, nullable=False)
|
||||
frequency = db.Column(db.Integer, nullable=False)
|
||||
|
||||
label = db.Column(db.String(500), nullable=False)
|
||||
value = db.Column(db.Numeric(15, 2), nullable=False)
|
||||
account_id = db.Column(db.Integer, db.ForeignKey('account.id'))
|
||||
|
||||
account = db.relationship(Account, backref=db.backref('scheduled_operation',
|
||||
lazy="dynamic"))
|
||||
|
||||
category = db.Column(db.String(100), nullable=True)
|
||||
|
||||
account_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('account.id')
|
||||
)
|
||||
|
||||
account = db.relationship(
|
||||
Account,
|
||||
backref=db.backref('scheduled_operation', lazy="dynamic")
|
||||
)
|
||||
|
||||
def __init__(self, start_date, stop_date, day, frequency, label, value,
|
||||
account_id, category=None):
|
||||
self.start_date = start_date
|
||||
@ -56,3 +70,50 @@ class ScheduledOperation(db.Model):
|
||||
cls.value,
|
||||
cls.label,
|
||||
)
|
||||
|
||||
def reschedule(self, session):
|
||||
# 1) delete unconfirmed operations for this account.
|
||||
session.query(
|
||||
Operation
|
||||
).filter(
|
||||
Operation.scheduled_operation_id == self.id,
|
||||
Operation.confirmed == false()
|
||||
).delete()
|
||||
|
||||
# 2) schedule remaining operations.
|
||||
start_date = arrow.get(self.start_date)
|
||||
stop_date = arrow.get(self.stop_date)
|
||||
|
||||
for r in arrow.Arrow.range(
|
||||
"month", start_date, stop_date
|
||||
)[::self.frequency]:
|
||||
current_monthrange = monthrange(r.year, r.month)
|
||||
|
||||
day = max(self.day, current_monthrange[0])
|
||||
day = min(self.day, current_monthrange[1])
|
||||
|
||||
r.replace(day=day)
|
||||
|
||||
# Search if a close operation exists.
|
||||
if not session.query(
|
||||
Operation
|
||||
).filter(
|
||||
Operation.account_id == self.account_id,
|
||||
Operation.scheduled_operation_id == self.id,
|
||||
Operation.operation_date >= r.clone().replace(weeks=-2).date(),
|
||||
Operation.operation_date <= r.clone().replace(weeks=+2).date()
|
||||
).count():
|
||||
# Create not confirmed operation if not found.
|
||||
operation = Operation(
|
||||
operation_date=r.date(),
|
||||
label=self.label,
|
||||
value=self.value,
|
||||
category=self.category,
|
||||
account_id=self.account_id,
|
||||
scheduled_operation_id=self.id,
|
||||
pointed=False,
|
||||
confirmed=False,
|
||||
canceled=False
|
||||
)
|
||||
|
||||
session.add(operation)
|
||||
|
@ -24,7 +24,6 @@ from accountant import session_aware
|
||||
|
||||
from .. import api_api
|
||||
|
||||
from ..models.entries import Entry
|
||||
from ..models.operations import Operation
|
||||
|
||||
from ..fields import Object
|
||||
@ -40,6 +39,7 @@ resource_fields = {
|
||||
'account_id': fields.Integer,
|
||||
'scheduled_operation_id': fields.Integer(default=None),
|
||||
'sold': fields.Float,
|
||||
'confirmed': fields.Boolean,
|
||||
'canceled': fields.Boolean,
|
||||
}
|
||||
|
||||
@ -53,6 +53,8 @@ parser.add_argument('pointed', type=bool)
|
||||
parser.add_argument('category', type=str)
|
||||
parser.add_argument('account_id', type=int)
|
||||
parser.add_argument('scheduled_operation_id', type=int)
|
||||
parser.add_argument('confirmed', type=bool)
|
||||
parser.add_argument('canceled', type=bool)
|
||||
|
||||
|
||||
range_parser = reqparse.RequestParser()
|
||||
@ -61,7 +63,7 @@ range_parser.add_argument('begin', type=lambda a: dateutil.parser.parse(a))
|
||||
range_parser.add_argument('end', type=lambda a: dateutil.parser.parse(a))
|
||||
|
||||
|
||||
class EntryListResource(Resource):
|
||||
class OperationListResource(Resource):
|
||||
@session_aware
|
||||
@marshal_with_field(fields.List(Object(resource_fields)))
|
||||
def get(self, session):
|
||||
@ -77,57 +79,57 @@ class EntryListResource(Resource):
|
||||
def post(self, session):
|
||||
kwargs = parser.parse_args()
|
||||
|
||||
entry = Entry(**kwargs)
|
||||
operation = Operation(**kwargs)
|
||||
|
||||
session.add(entry)
|
||||
session.add(operation)
|
||||
|
||||
return entry
|
||||
return operation
|
||||
|
||||
|
||||
class EntryResource(Resource):
|
||||
class OperationResource(Resource):
|
||||
@session_aware
|
||||
@marshal_with_field(Object(resource_fields))
|
||||
def get(self, entry_id, session):
|
||||
def get(self, operation_id, session):
|
||||
"""
|
||||
Get entry.
|
||||
Get operation.
|
||||
"""
|
||||
try:
|
||||
return Entry.get(session, entry_id)
|
||||
return Operation.get(session, operation_id)
|
||||
except NoResultFound:
|
||||
return None, 404
|
||||
|
||||
@session_aware
|
||||
@marshal_with_field(Object(resource_fields))
|
||||
def post(self, entry_id, session):
|
||||
def post(self, operation_id, session):
|
||||
kwargs = parser.parse_args()
|
||||
|
||||
assert (id not in kwargs or kwargs.id is None
|
||||
or kwargs.id == entry_id)
|
||||
or kwargs.id == operation_id)
|
||||
|
||||
try:
|
||||
entry = Entry.get(session, entry_id)
|
||||
operation = Operation.get(session, operation_id)
|
||||
except NoResultFound:
|
||||
return None, 404
|
||||
|
||||
# SQLAlchemy objects ignore __dict__.update() with merge.
|
||||
for k, v in kwargs.items():
|
||||
setattr(entry, k, v)
|
||||
setattr(operation, k, v)
|
||||
|
||||
session.merge(entry)
|
||||
session.merge(operation)
|
||||
|
||||
return entry
|
||||
return operation
|
||||
|
||||
@session_aware
|
||||
@marshal_with_field(Object(resource_fields))
|
||||
def delete(self, entry_id, session):
|
||||
def delete(self, operation_id, session):
|
||||
try:
|
||||
entry = Entry.get(session, entry_id)
|
||||
operation = Operation.get(session, operation_id)
|
||||
except NoResultFound:
|
||||
return None, 404
|
||||
|
||||
session.delete(entry)
|
||||
session.delete(operation)
|
||||
|
||||
return entry
|
||||
return operation
|
||||
|
||||
|
||||
category_resource_fields = {
|
||||
@ -164,7 +166,7 @@ class SoldsResource(Resource):
|
||||
return Operation.get_ohlc_per_day_for_range(session, **kwargs).all()
|
||||
|
||||
|
||||
api_api.add_resource(EntryListResource, "/entries")
|
||||
api_api.add_resource(EntryResource, "/entries/<int:entry_id>")
|
||||
api_api.add_resource(OperationListResource, "/entries")
|
||||
api_api.add_resource(OperationResource, "/entries/<int:operation_id>")
|
||||
api_api.add_resource(CategoriesResource, "/categories")
|
||||
api_api.add_resource(SoldsResource, "/solds")
|
||||
|
@ -80,11 +80,13 @@ class ScheduledOperationListResource(Resource):
|
||||
"""
|
||||
kwargs = parser.parse_args()
|
||||
|
||||
scheduledOperation = ScheduledOperation(**kwargs)
|
||||
scheduled_operation = ScheduledOperation(**kwargs)
|
||||
|
||||
session.add(scheduledOperation)
|
||||
session.add(scheduled_operation)
|
||||
|
||||
return scheduledOperation, 201
|
||||
scheduled_operation.reschedule(session)
|
||||
|
||||
return scheduled_operation, 201
|
||||
|
||||
|
||||
class ScheduledOperationResource(Resource):
|
||||
@ -109,6 +111,9 @@ class ScheduledOperationResource(Resource):
|
||||
"""
|
||||
Delete a scheduled operation.
|
||||
"""
|
||||
|
||||
raise NotImplementedError("Must be fixed.")
|
||||
|
||||
try:
|
||||
scheduled_operation = ScheduledOperation.query(
|
||||
session
|
||||
@ -148,6 +153,8 @@ class ScheduledOperationResource(Resource):
|
||||
|
||||
session.merge(scheduled_operation)
|
||||
|
||||
scheduled_operation.reschedule(session)
|
||||
|
||||
return scheduled_operation
|
||||
|
||||
|
||||
|
@ -396,7 +396,7 @@ accountantApp
|
||||
|
||||
// Returns true if the entry is a scheduled one.
|
||||
$scope.isSaved = function(entry) {
|
||||
return entry.id !== null;
|
||||
return entry.confirmed;
|
||||
};
|
||||
|
||||
// Cancel current editing entry or clears field if a new one.
|
||||
@ -426,11 +426,19 @@ accountantApp
|
||||
|
||||
// Points an entry.
|
||||
$scope.pointEntry = function(entry) {
|
||||
entry.confirmed = true;
|
||||
entry.pointed = !entry.pointed;
|
||||
|
||||
$scope.saveEntry(entry);
|
||||
};
|
||||
|
||||
// Confirm an entry.
|
||||
$scope.confirmEntry = function(entry) {
|
||||
entry.confirmed = true;
|
||||
|
||||
$scope.saveEntry(entry);
|
||||
};
|
||||
|
||||
// Create an new entry.
|
||||
$scope.createEntry = function(entry) {
|
||||
entry.account_id = $scope.account.id;
|
||||
|
@ -171,7 +171,7 @@
|
||||
|
||||
<!-- Button group for an unsaved (scheduled) entry. -->
|
||||
<div class="btn-group" ng-if="!isSaved(entry)">
|
||||
<button class="btn btn-xs btn-success" ng-click="saveEntry(entry)" title="Save">
|
||||
<button class="btn btn-xs btn-success" ng-click="confirmEntry(entry)" title="Save">
|
||||
<span class="fa fa-plus"></span>
|
||||
</button>
|
||||
|
||||
|
Reference in New Issue
Block a user