Feature: Deletion of active accounts and employees built.

Feature: Many columns made non-nullable to better safeguard data.
Fixed: Employee save/update did not work due to misplaced date check.
This commit is contained in:
Tanshu
2013-09-30 01:09:02 +05:30
parent 9c965727df
commit 64c1820867
9 changed files with 161 additions and 42 deletions

View File

@ -174,7 +174,8 @@ class LedgerBase(Base):
def type_object(self): def type_object(self):
return LedgerType.by_id(self.type) return LedgerType.by_id(self.type)
def __init__(self, code=None, name=None, type=None, is_active=None, is_reconcilable=False, costcenter_id=None, is_fixture=False): def __init__(self, code=None, name=None, type=None, is_active=None, is_reconcilable=False, costcenter_id=None,
is_fixture=False):
self.code = code self.code = code
self.name = name self.name = name
self.type = type self.type = type
@ -216,15 +217,13 @@ class LedgerBase(Base):
DBSession.add(self) DBSession.add(self)
return self return self
def can_delete(self): def can_delete(self, advanced_delete):
if self.is_fixture: if self.is_fixture:
return False, "{0} is a fixture and cannot be edited or deleted.".format(self.name) return False, "{0} is a fixture and cannot be edited or deleted.".format(self.name)
if self.is_active: if self.is_active:
return False, 'Account is active' return False, 'Account is active'
if len(self.journals) > 0: if len(self.journals) > 0 and not advanced_delete:
return False, 'Account has journal entries' return False, 'Account has journal entries'
if len(self.products) > 0:
return False, 'Account has products'
return True, '' return True, ''
@classmethod @classmethod
@ -257,6 +256,10 @@ class LedgerBase(Base):
def esi_pf_payable(cls): def esi_pf_payable(cls):
return uuid.UUID('42277912-cc18-854b-b134-9f4b00dba419') return uuid.UUID('42277912-cc18-854b-b134-9f4b00dba419')
@classmethod
def suspense(cls):
return uuid.UUID('3854e317-6f3b-5142-ab26-9c44d4cddd08')
class Employee(LedgerBase): class Employee(LedgerBase):
__tablename__ = 'entities_employees' __tablename__ = 'entities_employees'
@ -292,6 +295,9 @@ class Employee(LedgerBase):
DBSession.add(self) DBSession.add(self)
return self return self
def can_delete(self, advanced_delete):
return super(Employee, self).can_delete(advanced_delete)
@classmethod @classmethod
def list(cls): def list(cls):
return DBSession.query(Employee).order_by(desc(Employee.is_active)).order_by(Ledger.costcenter_id).order_by( return DBSession.query(Employee).order_by(desc(Employee.is_active)).order_by(Ledger.costcenter_id).order_by(
@ -305,6 +311,11 @@ class Employee(LedgerBase):
class Ledger(LedgerBase): class Ledger(LedgerBase):
__mapper_args__ = {'polymorphic_identity': ''} __mapper_args__ = {'polymorphic_identity': ''}
def can_delete(self, advanced_delete):
if len(self.products) > 0:
return False, 'Account has products'
return super(Ledger, self).can_delete(advanced_delete)
@classmethod @classmethod
def list(cls): def list(cls):
return DBSession.query(Ledger).order_by(Ledger.type).order_by(Ledger.name).order_by(Ledger.code).all() return DBSession.query(Ledger).order_by(Ledger.type).order_by(Ledger.name).order_by(Ledger.code).all()

View File

@ -60,7 +60,7 @@ class Voucher(Base):
last_edit_date = Column('LastEditDate', DateTime(timezone=True)) last_edit_date = Column('LastEditDate', DateTime(timezone=True))
_type = Column('VoucherType', Integer) _type = Column('VoucherType', Integer)
user_id = Column('UserID', GUID(), ForeignKey('auth_users.UserID')) user_id = Column('UserID', GUID(), ForeignKey('auth_users.UserID'), nullable=False)
poster_id = Column('PosterID', GUID(), ForeignKey('auth_users.UserID')) poster_id = Column('PosterID', GUID(), ForeignKey('auth_users.UserID'))
user = relationship('User', primaryjoin="User.id==Voucher.user_id", cascade=None) user = relationship('User', primaryjoin="User.id==Voucher.user_id", cascade=None)
@ -121,9 +121,9 @@ class Journal(Base):
id = Column('JournalID', GUID(), primary_key=True, default=uuid.uuid4) id = Column('JournalID', GUID(), primary_key=True, default=uuid.uuid4)
debit = Column('Debit', Integer) debit = Column('Debit', Integer)
amount = Column('Amount', Numeric) amount = Column('Amount', Numeric)
voucher_id = Column('VoucherID', GUID(), ForeignKey('entities_vouchers.VoucherID')) voucher_id = Column('VoucherID', GUID(), ForeignKey('entities_vouchers.VoucherID'), nullable=False)
ledger_id = Column('LedgerID', GUID(), ForeignKey('entities_ledgers.LedgerID')) ledger_id = Column('LedgerID', GUID(), ForeignKey('entities_ledgers.LedgerID'), nullable=False)
cost_center_id = Column('CostCenterID', GUID(), ForeignKey('entities_costcenters.CostCenterID')) cost_center_id = Column('CostCenterID', GUID(), ForeignKey('entities_costcenters.CostCenterID'), nullable=False)
@hybrid_property @hybrid_property
def signed_amount(self): def signed_amount(self):
@ -157,8 +157,8 @@ class Journal(Base):
class SalaryDeduction(Base): class SalaryDeduction(Base):
__tablename__ = 'entities_salarydeductions' __tablename__ = 'entities_salarydeductions'
id = Column('SalaryDeductionID', GUID(), primary_key=True, default=uuid.uuid4) id = Column('SalaryDeductionID', GUID(), primary_key=True, default=uuid.uuid4)
voucher_id = Column('VoucherID', GUID(), ForeignKey('entities_vouchers.VoucherID')) voucher_id = Column('VoucherID', GUID(), ForeignKey('entities_vouchers.VoucherID'), nullable=False)
journal_id = Column('JournalID', GUID(), ForeignKey('entities_journals.JournalID')) journal_id = Column('JournalID', GUID(), ForeignKey('entities_journals.JournalID'), nullable=False)
gross_salary = Column('GrossSalary', Integer) gross_salary = Column('GrossSalary', Integer)
days_worked = Column('DaysWorked', Integer) days_worked = Column('DaysWorked', Integer)
esi_ee = Column('EsiEmployee', Integer) esi_ee = Column('EsiEmployee', Integer)
@ -188,9 +188,9 @@ class Inventory(Base):
__tablename__ = 'entities_inventories' __tablename__ = 'entities_inventories'
# __tableagrs__ = (UniqueConstraint('VoucherID', 'ProductID')) # __tableagrs__ = (UniqueConstraint('VoucherID', 'ProductID'))
id = Column('InventoryID', GUID(), primary_key=True, default=uuid.uuid4) id = Column('InventoryID', GUID(), primary_key=True, default=uuid.uuid4)
voucher_id = Column('VoucherID', GUID(), ForeignKey('entities_vouchers.VoucherID')) voucher_id = Column('VoucherID', GUID(), ForeignKey('entities_vouchers.VoucherID'), nullable=False)
product_id = Column('ProductID', GUID(), ForeignKey('entities_products.ProductID')) product_id = Column('ProductID', GUID(), ForeignKey('entities_products.ProductID'), nullable=False)
batch_id = Column('BatchID', GUID(), ForeignKey('entities_batches.BatchID')) batch_id = Column('BatchID', GUID(), ForeignKey('entities_batches.BatchID'), nullable=False)
quantity = Column('Quantity', Numeric) quantity = Column('Quantity', Numeric)
rate = Column('Rate', Numeric) rate = Column('Rate', Numeric)
tax = Column('Tax', Numeric) tax = Column('Tax', Numeric)
@ -219,7 +219,7 @@ class Batch(Base):
id = Column('BatchID', GUID(), primary_key=True, default=uuid.uuid4) id = Column('BatchID', GUID(), primary_key=True, default=uuid.uuid4)
name = Column('Name', DateTime) name = Column('Name', DateTime)
product_id = Column('ProductID', GUID(), ForeignKey('entities_products.ProductID')) product_id = Column('ProductID', GUID(), ForeignKey('entities_products.ProductID'), nullable=False)
quantity_remaining = Column('QuantityRemaining', Numeric) quantity_remaining = Column('QuantityRemaining', Numeric)
rate = Column('Rate', Numeric) rate = Column('Rate', Numeric)
tax = Column('Tax', Numeric) tax = Column('Tax', Numeric)

View File

@ -83,6 +83,7 @@
<div class="form-group"> <div class="form-group">
<div class="col-md-offset-2 col-md-10"> <div class="col-md-offset-2 col-md-10">
<button class="btn btn-primary" ng-click="save()">Save</button> <button class="btn btn-primary" ng-click="save()">Save</button>
<button class="btn btn-danger" ng-show="employee.LedgerID" ng-click="confirm()">Delete</button>
</div> </div>
</div> </div>
</form> </form>

View File

@ -14,10 +14,10 @@
</div> </div>
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<button class="btn btn-primary" ng-click="setLockDate()">Set</button> <button class="btn btn-block btn-primary" ng-click="setLockDate()">Set</button>
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<button class="btn btn-danger" ng-click="clearLockDate()">Clear</button> <button class="btn btn-block btn-danger" ng-click="clearLockDate()">Clear</button>
</div> </div>
</div> </div>
</form> </form>

View File

@ -16,7 +16,7 @@ AccountListCtrl.resolve = {
}] }]
}; };
var AccountCtrl = ['$scope', '$location', 'account', 'account_types', 'cost_centers', function ($scope, $location, account, account_types, cost_centers) { var AccountCtrl = ['$scope', '$location', '$modal', 'account', 'account_types', 'cost_centers', function ($scope, $location, $modal, account, account_types, cost_centers) {
$scope.account = account; $scope.account = account;
$scope.account_types = account_types; $scope.account_types = account_types;

View File

@ -17,19 +17,19 @@ EmployeeListCtrl.resolve = {
}] }]
}; };
var EmployeeCtrl = ['$scope', '$routeParams', '$location', 'dateFilter', 'employee', 'cost_centers', function ($scope, $routeParams, $location, dateFilter, employee, cost_centers) { var EmployeeCtrl = ['$scope', '$routeParams', '$location', 'dateFilter', '$modal', 'employee', 'cost_centers', function ($scope, $routeParams, $location, dateFilter, $modal, employee, cost_centers) {
$scope.employee = employee; $scope.employee = employee;
$scope.cost_centers = cost_centers; $scope.cost_centers = cost_centers;
$scope.save = function () { $scope.save = function () {
$scope.employee.$save(function (u, putResponseHeaders) {
if (angular.isDate($scope.employee.JoiningDate)) { if (angular.isDate($scope.employee.JoiningDate)) {
$scope.info.Date = dateFilter($scope.info.JoiningDate, 'dd-MMM-yyyy'); $scope.employee.JoiningDate = dateFilter($scope.employee.JoiningDate, 'dd-MMM-yyyy');
} }
if (angular.isDate($scope.employee.LeavingDate)) { if (angular.isDate($scope.employee.LeavingDate)) {
$scope.info.Date = dateFilter($scope.info.LeavingDate, 'dd-MMM-yyyy'); $scope.employee.LeavingDate = dateFilter($scope.employee.LeavingDate, 'dd-MMM-yyyy');
} }
$scope.employee.$save(function (u, putResponseHeaders) {
$scope.toasts.push({Type: 'Success', Message: u.Code}); $scope.toasts.push({Type: 'Success', Message: u.Code});
$location.path('/Employees'); $location.path('/Employees');
}, function (data, status) { }, function (data, status) {
@ -45,6 +45,28 @@ var EmployeeCtrl = ['$scope', '$routeParams', '$location', 'dateFilter', 'employ
$scope.toasts.push({Type: 'Danger', Message: data.data}); $scope.toasts.push({Type: 'Danger', Message: data.data});
}); });
}; };
$scope.confirm = function () {
var modalInstance = $modal.open({
backdrop: true,
templateUrl: '/template/modal/confirm.html',
controller: ['$scope', '$modalInstance', function ($scope, $modalInstance) {
$scope.title = "Delete Account";
$scope.body = "Are you sure? This cannot be undone.";
$scope.isDelete = true;
$scope.ok = function () {
$modalInstance.close();
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
}]
});
modalInstance.result.then(function () {
$scope.delete();
});
};
$('#txtName').focus(); $('#txtName').focus();
}]; }];

View File

@ -197,5 +197,5 @@ var JournalModalCtrl = ['$scope', '$modalInstance', '$q', 'edit', 'Account', fun
deferred.resolve(result); deferred.resolve(result);
}); });
return deferred.promise; return deferred.promise;
} };
}] }];

View File

@ -1,12 +1,16 @@
import uuid import uuid
from pyramid.response import Response from pyramid.response import Response
from pyramid.security import authenticated_userid
from pyramid.view import view_config from pyramid.view import view_config
from sqlalchemy.orm import joinedload_all
import transaction import transaction
from brewman import groupfinder
from brewman.models import DBSession from brewman.models import DBSession
from brewman.models.master import CostCenter, Ledger, LedgerType, LedgerBase from brewman.models.master import CostCenter, Ledger, LedgerType, LedgerBase
from brewman.models.validation_exception import ValidationError, TryCatchFunction from brewman.models.validation_exception import ValidationError, TryCatchFunction
from brewman.models.voucher import Voucher, Journal, VoucherType
@view_config(route_name='account_list', renderer='brewman:templates/angular_base.mako', permission='Accounts') @view_config(route_name='account_list', renderer='brewman:templates/angular_base.mako', permission='Accounts')
@ -49,9 +53,9 @@ def update(request):
@view_config(request_method='DELETE', route_name='api_account_id', renderer='json', permission='Accounts') @view_config(request_method='DELETE', route_name='api_account_id', renderer='json', permission='Accounts')
def delete(request): def delete(request):
account = Ledger.by_id(uuid.UUID(request.matchdict['id'])) account = Ledger.by_id(uuid.UUID(request.matchdict['id']))
can_delete, reason = account.can_delete() can_delete, reason = account.can_delete('Advanced Delete' in groupfinder(authenticated_userid(request), request))
if can_delete: if can_delete:
DBSession.delete(account) delete_with_data(account)
transaction.commit() transaction.commit()
return account_info(None) return account_info(None)
else: else:
@ -125,3 +129,37 @@ def account_info(id):
return account return account
def delete_with_data(account):
suspense_ledger = Ledger.by_id(Ledger.suspense())
query = Voucher.query().options(joinedload_all(Voucher.journals, Journal.ledger, innerjoin=True)) \
.filter(Voucher.journals.any(Journal.ledger_id == account.id)) \
.all()
for voucher in query:
others, sus_jnl, acc_jnl = False, None, None
for journal in voucher.journals:
if journal.ledger_id == account.id:
acc_jnl = journal
elif journal.ledger_id == Ledger.suspense():
sus_jnl = journal
else:
others = True
if not others:
DBSession.delete(voucher)
else:
if sus_jnl is None:
acc_jnl.ledger = suspense_ledger
voucher.narration += '\nSuspense \u20B9 {0:,.2f} is {1}'.format(acc_jnl.amount, account.name)
else:
amount = (sus_jnl.debit * sus_jnl.amount) + (acc_jnl.debit * acc_jnl.amount)
DBSession.delete(acc_jnl)
if amount == 0:
DBSession.delete(sus_jnl)
else:
sus_jnl.amount = abs(amount)
sus_jnl.debit = -1 if amount < 0 else 1
voucher.narration += '\nDeleted \u20B9 {0:,.2f} of {1}'.format(acc_jnl.amount * acc_jnl.debit,
account.name)
if voucher.type in (VoucherType.by_name('Payment').id, VoucherType.by_name('Receipt').id):
voucher.type = VoucherType.by_name('Journal')
DBSession.delete(account)

View File

@ -1,13 +1,18 @@
import datetime import datetime
import uuid import uuid
from pyramid.response import Response from pyramid.response import Response
from pyramid.security import authenticated_userid
from pyramid.view import view_config from pyramid.view import view_config
from sqlalchemy.orm import joinedload_all
import transaction import transaction
from brewman import groupfinder
from brewman.models import DBSession from brewman.models import DBSession
from brewman.models.master import CostCenter, Employee, LedgerBase from brewman.models.master import CostCenter, Employee, LedgerBase, Ledger
from brewman.models.validation_exception import ValidationError, TryCatchFunction from brewman.models.validation_exception import ValidationError, TryCatchFunction
from brewman.models.voucher import Voucher, Journal, VoucherType
@view_config(route_name='employee_list', renderer='brewman:templates/angular_base.mako', permission='Authenticated') @view_config(route_name='employee_list', renderer='brewman:templates/angular_base.mako', permission='Authenticated')
@view_config(request_method='GET', route_name='employee_id', renderer='brewman:templates/angular_base.mako', @view_config(request_method='GET', route_name='employee_id', renderer='brewman:templates/angular_base.mako',
@ -59,9 +64,9 @@ def update(request):
@view_config(request_method='DELETE', route_name='api_employee_id', renderer='json', permission='Employees') @view_config(request_method='DELETE', route_name='api_employee_id', renderer='json', permission='Employees')
def delete(request): def delete(request):
employee = Employee.by_id(uuid.UUID(request.matchdict['id'])) employee = Employee.by_id(uuid.UUID(request.matchdict['id']))
can_delete, reason = employee.can_delete() can_delete, reason = employee.can_delete('Advanced Delete' in groupfinder(authenticated_userid(request), request))
if can_delete: if can_delete:
DBSession.delete(employee) delete_with_data(employee)
transaction.commit() transaction.commit()
return employee_info(None) return employee_info(None)
else: else:
@ -124,3 +129,45 @@ def employee_info(id):
'LeavingDate': None if employee.is_active else employee.leaving_date.strftime('%d-%b-%Y'), 'LeavingDate': None if employee.is_active else employee.leaving_date.strftime('%d-%b-%Y'),
'CostCenter': {'CostCenterID': employee.costcenter_id, 'Name': employee.costcenter.name}} 'CostCenter': {'CostCenterID': employee.costcenter_id, 'Name': employee.costcenter.name}}
return employee return employee
def delete_with_data(employee):
suspense_ledger = Ledger.by_id(Ledger.suspense())
query = Voucher.query().options(joinedload_all(Voucher.journals, Journal.ledger, innerjoin=True))\
.filter(Voucher.journals.any(Journal.ledger_id == employee.id))\
.all()
for voucher in query:
others, sus_jnl, acc_jnl = False, None, None
for journal in voucher.journals:
if journal.ledger_id == employee.id:
acc_jnl = journal
elif journal.ledger_id == Ledger.suspense():
sus_jnl = journal
else:
others = True
if not others:
DBSession.delete(voucher)
else:
if sus_jnl is None:
acc_jnl.ledger = suspense_ledger
voucher.narration += '\nSuspense \u20B9 {0:,.2f} is {1}'.format(acc_jnl.amount, employee.name)
else:
amount = (sus_jnl.debit * sus_jnl.amount) + (acc_jnl.debit * acc_jnl.amount)
if acc_jnl.salary_deduction is not None:
DBSession.delete(acc_jnl.salary_deduction)
DBSession.delete(acc_jnl)
if amount == 0:
DBSession.delete(sus_jnl)
else:
sus_jnl.amount = abs(amount)
sus_jnl.debit = -1 if amount < 0 else 1
voucher.narration += '\nDeleted \u20B9 {0:,.2f} of {1}'.format(acc_jnl.amount * acc_jnl.debit,
employee.name)
if voucher.type in (VoucherType.by_name('Payment').id, VoucherType.by_name('Receipt').id):
voucher.type = VoucherType.by_name('Journal')
for fingerprint in employee.fingerprints:
DBSession.delete(fingerprint)
for attendance in employee.attendances:
DBSession.delete(attendance)
DBSession.delete(employee)