Feature: Product deletion created including advanced delete.
Feature: Product list filtering created.
This commit is contained in:
parent
2c714dfe98
commit
3fe3a7f7f3
@ -85,6 +85,19 @@ class Product(Base):
|
||||
DBSession.add(self)
|
||||
return self
|
||||
|
||||
def can_delete(self, advanced_delete):
|
||||
if self.is_fixture:
|
||||
return False, "{0} is a fixture and cannot be edited or deleted.".format(self.name)
|
||||
if not self.discontinued:
|
||||
return False, 'Product is active'
|
||||
if len(self.inventories) > 0 and not advanced_delete:
|
||||
return False, 'Product has entries'
|
||||
return True, ''
|
||||
|
||||
@classmethod
|
||||
def suspense(cls):
|
||||
return uuid.UUID('aa79a643-9ddc-4790-ac7f-a41f9efb4c15')
|
||||
|
||||
|
||||
class ProductGroup(Base):
|
||||
__tablename__ = 'entities_productgroups'
|
||||
|
@ -256,6 +256,9 @@ class Batch(Base):
|
||||
def by_id(cls, id):
|
||||
return DBSession.query(cls).filter(cls.id == id).first()
|
||||
|
||||
@classmethod
|
||||
def suspense(cls):
|
||||
return uuid.UUID('a955790e-93cf-493c-a816-c7d92b127383')
|
||||
|
||||
class Attendance(Base):
|
||||
__tablename__ = 'entities_attendances'
|
||||
|
@ -9,7 +9,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
<table id="gvGrid" class="table table-condensed table-bordered table-striped">
|
||||
<table class="table table-condensed table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
|
@ -73,6 +73,7 @@
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-2 col-md-10">
|
||||
<button class="btn btn-primary" ng-click="save()">Save</button>
|
||||
<button class="btn btn-danger" ng-show="product.ProductID" ng-click="confirm()">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -1,4 +1,14 @@
|
||||
<h2>Products <a href="/Product" class="btn btn-success pull-right">Add <i class="glyphicon glyphicon-plus"></i></a></h2>
|
||||
<h2>Products
|
||||
<div class="form-group col-md-9 pull-right">
|
||||
<div class="col-md-10">
|
||||
<input type="text" class="form-control" placeholder="Search" ng-model="search">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<a href="/Product" class="btn btn-success btn-block">Add <i
|
||||
class="glyphicon glyphicon-plus"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
<table class="table table-condensed table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -10,7 +20,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="item in info">
|
||||
<tr ng-repeat="item in products">
|
||||
<td>{{item.Code}}</td>
|
||||
<td><a href="{{item.Url}}">{{item.Name}} ({{item.Units}})</a></td>
|
||||
<td>₹ {{item.Price}}</td>
|
||||
|
@ -78,7 +78,7 @@ var overlord = angular.module('overlord', ['overlord.directive', 'overlord.filte
|
||||
when('/CostCenter', {templateUrl: '/partial/cost-center-detail.html', controller: CostCenterCtrl, resolve: CostCenterCtrl.resolve}).
|
||||
when('/CostCenter/:id', {templateUrl: '/partial/cost-center-detail.html', controller: CostCenterCtrl, resolve: CostCenterCtrl.resolve}).
|
||||
|
||||
when('/Products', {templateUrl: '/partial/product-list.html', controller: ProductListCtrl, resolve: ProductListCtrl.resolve}).
|
||||
when('/Products', {templateUrl: '/partial/product-list.html', controller: ProductListCtrl, resolve: ProductListCtrl.resolve, reloadOnSearch:false}).
|
||||
when('/Product', {templateUrl: '/partial/product-detail.html', controller: ProductCtrl, resolve: ProductCtrl.resolve}).
|
||||
when('/Product/:id', {templateUrl: '/partial/product-detail.html', controller: ProductCtrl, resolve: ProductCtrl.resolve}).
|
||||
|
||||
|
@ -1,7 +1,82 @@
|
||||
'use strict';
|
||||
|
||||
var ProductListCtrl = ['$scope', 'products', function ($scope, products) {
|
||||
var ProductListCtrl = ['$scope', '$location', '$routeParams', 'products', function ($scope, $location, $routeParams, products) {
|
||||
$scope.search = $routeParams.q || '';
|
||||
$scope.info = products;
|
||||
var re = /((([^ ]+):\s*('[^':]+'|"[^":]+"|[^ ]+))|[^ ]+[^: '"]*)/g;
|
||||
|
||||
$scope.isTrue = function (value) {
|
||||
value = value.toLowerCase();
|
||||
return !_.any(['f', 'fa', 'fal', 'fals', 'false', 'n', 'no', '0'], function (item) {
|
||||
return item === value;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('search', function (newValue, oldValue) {
|
||||
$scope.filterProducts(newValue);
|
||||
}, true);
|
||||
|
||||
$scope.filterProducts = _.debounce(function (q) {
|
||||
if (q !== $scope._search) {
|
||||
$scope._search = q;
|
||||
if (angular.isUndefined(q) || q === '') {
|
||||
$location.path('/Products').search('q', null).replace();
|
||||
} else {
|
||||
$location.path('/Products').search({'q': q}).replace();
|
||||
}
|
||||
$scope.$apply(function () {
|
||||
$scope.products = $scope.doFilter(q);
|
||||
});
|
||||
}
|
||||
}, 350);
|
||||
$scope.doFilter = _.memoize(function (q) {
|
||||
var matches = [], i, len;
|
||||
if (angular.isUndefined(q) || q === '') {
|
||||
return $scope.info;
|
||||
}
|
||||
var m = q.match(re);
|
||||
_.forEach(m, function (item) {
|
||||
item = item.toLowerCase();
|
||||
if (item.indexOf(':') === -1) {
|
||||
matches.push({'key': 'n', 'value': item});
|
||||
} else {
|
||||
var key = item.substr(0, item.indexOf(':')).toLowerCase();
|
||||
var value = item.substr(item.indexOf(':') + 1, item.length).trim().toLowerCase();
|
||||
if (value.indexOf("'") === 0 || value.indexOf('"') === 0) {
|
||||
value = value.substring(1, value.length - 2);
|
||||
}
|
||||
if (key !== '' && value !== '' && _.any(['w', 'u', 'g'], function (item) {
|
||||
return item === key;
|
||||
})) {
|
||||
matches.push({'key': key, 'value': value});
|
||||
}
|
||||
}
|
||||
});
|
||||
return _.filter($scope.info, function (item) {
|
||||
len = matches.length;
|
||||
for (i = 0; i < len; i++) {
|
||||
var match = matches[i];
|
||||
if (match.key === 'n') {
|
||||
if (item.Name.toLowerCase().indexOf(match.value) === -1) {
|
||||
return false;
|
||||
}
|
||||
} else if (match.key === 'w') {
|
||||
if (item.Discontinued === $scope.isTrue(match.value)) {
|
||||
return false;
|
||||
}
|
||||
} else if (match.key === 'u') {
|
||||
if (item.Units.toLowerCase().indexOf(match.value) === -1) {
|
||||
return false;
|
||||
}
|
||||
} else if (match.key === 'g') {
|
||||
if (item.ProductGroup.toLowerCase().indexOf(match.value) === -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}];
|
||||
|
||||
ProductListCtrl.resolve = {
|
||||
@ -17,7 +92,7 @@ ProductListCtrl.resolve = {
|
||||
}]
|
||||
};
|
||||
|
||||
var ProductCtrl = ['$scope', '$location', 'product', 'product_groups', function ($scope, $location, product, product_groups) {
|
||||
var ProductCtrl = ['$scope', '$location', '$modal', 'product', 'product_groups', function ($scope, $location, $modal, product, product_groups) {
|
||||
$scope.product = product;
|
||||
$scope.product_groups = product_groups;
|
||||
|
||||
@ -38,6 +113,28 @@ var ProductCtrl = ['$scope', '$location', 'product', 'product_groups', function
|
||||
$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 Product";
|
||||
$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();
|
||||
}];
|
||||
|
||||
|
@ -150,7 +150,7 @@ def delete_with_data(account):
|
||||
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)
|
||||
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)
|
||||
@ -159,7 +159,7 @@ def delete_with_data(account):
|
||||
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,
|
||||
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')
|
||||
|
@ -1,12 +1,17 @@
|
||||
from decimal import Decimal
|
||||
import uuid
|
||||
from pyramid.response import Response
|
||||
from pyramid.security import authenticated_userid
|
||||
|
||||
from pyramid.view import view_config
|
||||
from sqlalchemy.orm import joinedload_all
|
||||
import transaction
|
||||
from brewman import groupfinder
|
||||
from brewman.models import DBSession
|
||||
|
||||
from brewman.models.master import Product, CostCenter, LedgerType, Ledger
|
||||
from brewman.models.validation_exception import TryCatchFunction, ValidationError
|
||||
from brewman.models.voucher import Voucher, Batch, Inventory, VoucherType
|
||||
|
||||
|
||||
@view_config(route_name='product_list', renderer='brewman:templates/angular_base.mako', permission='Authenticated')
|
||||
@ -52,17 +57,16 @@ def update(request):
|
||||
|
||||
@view_config(request_method='DELETE', route_name='api_product_id', renderer='json', permission='Products')
|
||||
def delete(request):
|
||||
item = request.matchdict.get('id', None)
|
||||
item = None if item is None else Product.by_id(uuid.UUID(item))
|
||||
product = Product.by_id(uuid.UUID(request.matchdict['id']))
|
||||
can_delete, reason = product.can_delete('Advanced Delete' in groupfinder(authenticated_userid(request), request))
|
||||
|
||||
if item is None:
|
||||
response = Response("Product not found")
|
||||
response.status_int = 500
|
||||
return response
|
||||
elif item.is_fixture:
|
||||
raise ValidationError("{0} is a fixture and cannot be edited or deleted.".format(item.full_name))
|
||||
if can_delete:
|
||||
delete_with_data(product)
|
||||
transaction.commit()
|
||||
return product_info(None)
|
||||
else:
|
||||
response = Response("Product deletion not implemented")
|
||||
transaction.abort()
|
||||
response = Response("Cannot delete product because {0}".format(reason))
|
||||
response.status_int = 500
|
||||
return response
|
||||
|
||||
@ -116,6 +120,42 @@ def product_info(id):
|
||||
product = {'ProductID': product.id, 'Code': product.code, 'Name': product.name, 'Units': product.units,
|
||||
'Fraction': product.fraction, 'FractionUnits': product.fraction_units, 'Yeild': product.yeild,
|
||||
'ShowForPurchase': product.show_for_purchase, 'Discontinued': product.discontinued,
|
||||
'IsFixture':product.is_fixture, 'ProductGroup': {'ProductGroupID': product.product_group_id},
|
||||
'IsFixture': product.is_fixture, 'ProductGroup': {'ProductGroupID': product.product_group_id},
|
||||
'Ledger': {'LedgerID': product.ledger_id}, 'Price': product.price}
|
||||
return product
|
||||
|
||||
|
||||
def delete_with_data(product):
|
||||
suspense_product = Product.by_id(Product.suspense())
|
||||
suspense_batch = Batch.by_id(Batch.suspense())
|
||||
query = Voucher.query().options(joinedload_all(Voucher.inventories, Inventory.product, innerjoin=True)) \
|
||||
.filter(Voucher.inventories.any(Inventory.product_id == product.id)) \
|
||||
.all()
|
||||
|
||||
for voucher in query:
|
||||
others, sus_inv, prod_inv = False, None, None
|
||||
for inventory in voucher.inventories:
|
||||
if inventory.product_id == product.id:
|
||||
prod_inv = inventory
|
||||
elif inventory.product_id == Product.suspense():
|
||||
sus_inv = inventory
|
||||
else:
|
||||
others = True
|
||||
if not others and voucher.type == VoucherType.by_id('Issue'):
|
||||
DBSession.delete(voucher)
|
||||
else:
|
||||
if sus_inv is None:
|
||||
prod_inv.product = suspense_product
|
||||
prod_inv.quantity = prod_inv.amount
|
||||
prod_inv.rate = 1
|
||||
prod_inv.tax = 0
|
||||
prod_inv.discount = 0
|
||||
prod_inv.batch = suspense_batch
|
||||
voucher.narration += '\nSuspense \u20B9{0:,.2f} is {1}'.format(prod_inv.amount, product.name)
|
||||
else:
|
||||
sus_inv.quantity += prod_inv.amount
|
||||
DBSession.delete(prod_inv)
|
||||
voucher.narration += '\nDeleted \u20B9{0:,.2f} of {1}'.format(prod_inv.amount, product.name)
|
||||
for batch in product.batches:
|
||||
DBSession.delete(batch)
|
||||
DBSession.delete(product)
|
||||
|
Loading…
x
Reference in New Issue
Block a user