From cdd09a3af5fd7622ad47eca339f7cbec770111a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihan=20=C5=9Eent=C3=BCrk?= <53110792+CihanSenturk@users.noreply.github.com> Date: Wed, 22 Jan 2025 13:25:01 +0300 Subject: [PATCH] added document item quantity mathematical operations --- app/Http/Requests/Document/Document.php | 4 + app/Jobs/Document/CreateDocumentItem.php | 4 +- app/Utilities/helpers.php | 19 ++++ package-lock.json | 89 ++++++++++++++++++- package.json | 13 +-- resources/assets/js/plugins/functions.js | 9 +- resources/assets/js/views/common/documents.js | 23 +++-- .../documents/form/line-item.blade.php | 2 +- 8 files changed, 145 insertions(+), 18 deletions(-) diff --git a/app/Http/Requests/Document/Document.php b/app/Http/Requests/Document/Document.php index 516dbfa8e..00472c17b 100644 --- a/app/Http/Requests/Document/Document.php +++ b/app/Http/Requests/Document/Document.php @@ -93,6 +93,8 @@ class Document extends FormRequest foreach ($items as $key => $item) { $size = 10; + $items[$key]['quantity'] = calculation_to_quantity($item['quantity']); + if (Str::contains($item['quantity'], ['.', ','])) { $size = 12; } @@ -101,6 +103,8 @@ class Document extends FormRequest $this->items_quantity_size[$key] = $size; } + + $this->request->set('items', $items); } return $rules; diff --git a/app/Jobs/Document/CreateDocumentItem.php b/app/Jobs/Document/CreateDocumentItem.php index c2a257e63..fc108bdbf 100644 --- a/app/Jobs/Document/CreateDocumentItem.php +++ b/app/Jobs/Document/CreateDocumentItem.php @@ -31,7 +31,7 @@ class CreateDocumentItem extends Job implements HasOwner, HasSource, ShouldCreat $item_id = ! empty($this->request['item_id']) ? $this->request['item_id'] : 0; $precision = currency($this->document->currency_code)->getPrecision(); - $item_amount = (double) $this->request['price'] * (double) $this->request['quantity']; + $item_amount = (double) $this->request['price'] * (double) calculation_to_quantity($this->request['quantity']); $item_discounted_amount = $item_amount; @@ -159,7 +159,7 @@ class CreateDocumentItem extends Job implements HasOwner, HasSource, ShouldCreat $this->request['item_id'] = $item_id; $this->request['name'] = Str::limit($this->request['name'], 180, ''); $this->request['description'] = ! empty($this->request['description']) ? $this->request['description'] : ''; - $this->request['quantity'] = (double) $this->request['quantity']; + $this->request['quantity'] = (double) calculation_to_quantity($this->request['quantity']); $this->request['price'] = round($this->request['price'], $precision); $this->request['tax'] = round($item_tax_total, $precision); $this->request['discount_type'] = ! empty($this->request['discount_type']) ? $this->request['discount_type'] : 'percent'; diff --git a/app/Utilities/helpers.php b/app/Utilities/helpers.php index 524a43527..44fdb88ee 100644 --- a/app/Utilities/helpers.php +++ b/app/Utilities/helpers.php @@ -429,3 +429,22 @@ if (! function_exists('request_is_portal')) { return $r->is($company_id . '/portal') || $r->is($company_id . '/portal/*'); } } + +if (! function_exists('calculation_to_quantity')) { + function calculation_to_quantity($quantity) + { + if (! preg_match('/^[0-9+\-x*\/().\s]+$/', $quantity)) { + throw new \InvalidArgumentException('Invalid mathematical expression.'); + } + + $quantity = Str::replace('x', '*', $quantity); + + try { + $result = eval('return ' . $quantity . ';'); + } catch (\Throwable $e) { + throw new \InvalidArgumentException('Error evaluating the expression: ' . $e->getMessage()); + } + + return $result; + } +} diff --git a/package-lock.json b/package-lock.json index 52b0304ad..d28b7f162 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "jsonwebtoken": "^9.0.2", "laravel-mix-tailwind": "^0.1.2", "lodash": "^4.17.21", + "mathjs": "^14.0.1", "moment": ">=2.29.4", "nprogress": "^0.2.0", "popper.js": "^1.16.1", @@ -2213,9 +2214,9 @@ "license": "MIT" }, "node_modules/@babel/runtime": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", - "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" @@ -7672,6 +7673,19 @@ "integrity": "sha512-Ue/Zd9aOucHzHXwaCe4yeHR7jypp7TKrIBZ5yls35nPNiVXlW14npmNVKM1ZaLlQTKZ6/4ewA//gYKHHIwCpOw==", "license": "MIT" }, + "node_modules/complex.js": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz", + "integrity": "sha512-qtx7HRhPGSCBtGiST4/WGHuW+zeaND/6Ld+db6PbrulIB1i2Ev/2UPiqcmpQNPSyfBKraC0EOvOKCB5dGZKt3g==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -8628,6 +8642,12 @@ "node": ">=0.10.0" } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "license": "MIT" + }, "node_modules/deep-equal": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", @@ -9510,6 +9530,12 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escape-latex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", + "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -12416,6 +12442,12 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "license": "MIT" + }, "node_modules/javascript-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz", @@ -13807,6 +13839,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mathjs": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.0.1.tgz", + "integrity": "sha512-yyJgLwC6UXuve724np8tHRMYaTtb5UqiOGQkjwbSXgH8y1C/LcJ0pvdNDZLI2LT7r+iExh2Y5HwfAY+oZFtGIQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.25.7", + "complex.js": "^2.2.5", + "decimal.js": "^10.4.3", + "escape-latex": "^1.2.0", + "fraction.js": "^5.2.1", + "javascript-natural-sort": "^0.7.1", + "seedrandom": "^3.0.5", + "tiny-emitter": "^2.1.0", + "typed-function": "^4.2.1" + }, + "bin": { + "mathjs": "bin/cli.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mathjs/node_modules/fraction.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.2.1.tgz", + "integrity": "sha512-Ah6t/7YCYjrPUFUFsOsRLMXAdnYM+aQwmojD2Ayb/Ezr82SwES0vuyQ8qZ3QO8n9j7W14VJuVZZet8U3bhSdQQ==", + "license": "MIT", + "engines": { + "node": ">= 12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -18385,6 +18453,12 @@ "license": "MIT", "peer": true }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "license": "MIT" + }, "node_modules/select": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", @@ -20443,6 +20517,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-function": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz", + "integrity": "sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index 21eada2d5..8f46f4a5a 100644 --- a/package.json +++ b/package.json @@ -23,19 +23,25 @@ "@sentry/vue": "^7.47.0", "@tailwindcss/forms": "^0.5.3", "@themesberg/flowbite": "^1.2.0", + "ajv": "^6.12.6", "axios": "^1.3.5", "date-fns": "^2.29.3", "dropzone": "^5.9.3", "element-ui": "^2.15.13", "es6-promise": "^4.2.8", + "express": "^4.19.2", "flatpickr": "^4.6.13", "fuse.js": "^6.6.2", "glightbox": "^3.2.0", "json-schema": ">=0.4.0", + "jsonwebtoken": "^9.0.2", "laravel-mix-tailwind": "^0.1.2", "lodash": "^4.17.21", + "mathjs": "^14.0.1", + "moment": ">=2.29.4", "nprogress": "^0.2.0", "popper.js": "^1.16.1", + "qs": "^6.11.1", "swiper": "^9.2.0", "tailwind": "^4.0.0", "tailwindcss": "^3.3.6", @@ -48,12 +54,7 @@ "vue2-editor": "^2.10.3", "vue2-transitions": "^0.3.0", "vuedraggable": "^2.24.3", - "moment": ">=2.29.4", - "qs": "^6.11.1", - "jsonwebtoken": "^9.0.2", - "ws": "^8.14.2", - "ajv": "^6.12.6", - "express": "^4.19.2" + "ws": "^8.14.2" }, "devDependencies": { "@babel/core": "^7.23.5", diff --git a/resources/assets/js/plugins/functions.js b/resources/assets/js/plugins/functions.js index 4dd8c7118..18d1645e4 100644 --- a/resources/assets/js/plugins/functions.js +++ b/resources/assets/js/plugins/functions.js @@ -15,6 +15,13 @@ function getQueryVariable(variable) { return(false); } +const { evaluate } = require('mathjs'); + +// use the evaluate function to evaluate the expression +function calculationToQuantity(quantity) { + return evaluate(quantity); +} + //This function wraps setTimeout function in a promise in order to display dom manipulations on root components asynchronously & fast const setPromiseTimeout = time => new Promise(resolve => @@ -23,4 +30,4 @@ const setPromiseTimeout = time => , time) ); -export {getQueryVariable, setPromiseTimeout} +export {getQueryVariable, calculationToQuantity, setPromiseTimeout} diff --git a/resources/assets/js/views/common/documents.js b/resources/assets/js/views/common/documents.js index ba7911480..8a8088ab9 100644 --- a/resources/assets/js/views/common/documents.js +++ b/resources/assets/js/views/common/documents.js @@ -8,7 +8,7 @@ import Vue from 'vue'; import DashboardPlugin from './../../plugins/dashboard-plugin'; import { addDays, format } from 'date-fns'; -import { setPromiseTimeout, getQueryVariable } from './../../plugins/functions'; +import { setPromiseTimeout, calculationToQuantity, getQueryVariable } from './../../plugins/functions'; import Global from './../../mixins/global'; @@ -318,7 +318,7 @@ const app = new Vue({ // items calculate this.items.forEach(function(item, index) { - item.total = item.grand_total = item.price * item.quantity; + item.total = item.grand_total = item.price * calculationToQuantity(item.quantity); let item_discounted_total = items_amount[index]; @@ -346,7 +346,7 @@ const app = new Vue({ this.calculateItemTax(item, totals_taxes, total_discount + line_discount_amount); - item.total = item.price * item.quantity; + item.total = item.price * calculationToQuantity(item.quantity); // calculate sub, tax, discount all items. line_item_discount_total += line_discount_amount; @@ -474,7 +474,7 @@ const app = new Vue({ if (fixed.length) { fixed.forEach(function(fixed) { item.tax_ids[fixed.tax_index].name = fixed.tax_name; - item.tax_ids[fixed.tax_index].price = fixed.tax_rate * item.quantity; + item.tax_ids[fixed.tax_index].price = fixed.tax_rate * calculationToQuantity(item.quantity); total_tax_amount += item.tax_ids[fixed.tax_index].price; @@ -536,7 +536,7 @@ const app = new Vue({ this.items.forEach(function(item, index) { let item_total = 0; - item_total = item.price * item.quantity; + item_total = item.price * calculationToQuantity(item.quantity); // item discount calculate. if (item.discount) { @@ -1055,6 +1055,19 @@ const app = new Vue({ this.form.discount.replace(',', '.'); }, + 'items': { + handler(newItems, oldItems) { + const regex = /^[0-9+\-x*\/().\s]+$/; + + newItems.forEach((item, index) => { + if (item.quantity && ! regex.test(item.quantity)) { + item.quantity = item.quantity.replace(/[^0-9+\-x*\/().\s]/g, ""); + } + }); + }, + deep: true, + }, + 'form.loading': function (newVal, oldVal) { if (! newVal) { this.send_to = false; diff --git a/resources/views/components/documents/form/line-item.blade.php b/resources/views/components/documents/form/line-item.blade.php index 8284a90a9..7c46c1ae2 100644 --- a/resources/views/components/documents/form/line-item.blade.php +++ b/resources/views/components/documents/form/line-item.blade.php @@ -98,7 +98,7 @@ @stack('quantity_input_start')