diff --git a/brewman/models/master.py b/brewman/models/master.py index 0fc4819e..fbb2be9a 100644 --- a/brewman/models/master.py +++ b/brewman/models/master.py @@ -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' diff --git a/brewman/models/voucher.py b/brewman/models/voucher.py index 93d215c1..e9fd64a9 100644 --- a/brewman/models/voucher.py +++ b/brewman/models/voucher.py @@ -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' diff --git a/brewman/static/partial/employee-list.html b/brewman/static/partial/employee-list.html index 41408c3a..096af5c1 100644 --- a/brewman/static/partial/employee-list.html +++ b/brewman/static/partial/employee-list.html @@ -9,7 +9,7 @@ - +
diff --git a/brewman/static/partial/product-detail.html b/brewman/static/partial/product-detail.html index 0b9b2074..4160ca9f 100644 --- a/brewman/static/partial/product-detail.html +++ b/brewman/static/partial/product-detail.html @@ -73,6 +73,7 @@
+
diff --git a/brewman/static/partial/product-list.html b/brewman/static/partial/product-list.html index 2abb1220..43d5dbf7 100644 --- a/brewman/static/partial/product-list.html +++ b/brewman/static/partial/product-list.html @@ -1,4 +1,14 @@ -

Products Add

+

Products +
+
+ +
+
+ Add +
+
+

Code
@@ -10,7 +20,7 @@ - + diff --git a/brewman/static/scripts/overlord.js b/brewman/static/scripts/overlord.js index 4c20acae..ee3e6f6b 100644 --- a/brewman/static/scripts/overlord.js +++ b/brewman/static/scripts/overlord.js @@ -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}). diff --git a/brewman/static/scripts/product.js b/brewman/static/scripts/product.js index a0ca4c82..0f145052 100644 --- a/brewman/static/scripts/product.js +++ b/brewman/static/scripts/product.js @@ -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(); }]; diff --git a/brewman/views/account.py b/brewman/views/account.py index 7a0487f9..7713c494 100644 --- a/brewman/views/account.py +++ b/brewman/views/account.py @@ -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') diff --git a/brewman/views/product.py b/brewman/views/product.py index 85588443..1924584d 100644 --- a/brewman/views/product.py +++ b/brewman/views/product.py @@ -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)
{{item.Code}} {{item.Name}} ({{item.Units}}) ₹ {{item.Price}}