diff --git a/front/core/components/chip/style.scss b/front/core/components/chip/style.scss index 34ee947ba..c89cf2b82 100644 --- a/front/core/components/chip/style.scss +++ b/front/core/components/chip/style.scss @@ -21,6 +21,9 @@ vn-chip { } } + &.white { + background-color: $color-bg-panel; + } &.colored { background-color: $color-main; color: $color-font-bg; diff --git a/modules/entry/back/methods/entry/addBuy.js b/modules/entry/back/methods/entry/addBuy.js new file mode 100644 index 000000000..015a6fc23 --- /dev/null +++ b/modules/entry/back/methods/entry/addBuy.js @@ -0,0 +1,136 @@ + +const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; + +module.exports = Self => { + Self.remoteMethodCtx('addBuy', { + description: 'Inserts or updates buy for the current entry', + accessType: 'WRITE', + accepts: [{ + arg: 'id', + type: 'number', + required: true, + description: 'The entry id', + http: {source: 'path'} + }, + { + arg: 'itemFk', + type: 'number', + required: true + }, + { + arg: 'quantity', + type: 'number', + required: true + }, + { + arg: 'packageFk', + type: 'string', + required: true + }], + returns: { + type: 'object', + root: true + }, + http: { + path: `/:id/addBuy`, + verb: 'POST' + } + }); + + Self.addBuy = async(ctx, options) => { + const conn = Self.dataSource.connector; + let tx; + let myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + try { + const models = Self.app.models; + + const newBuy = await models.Buy.create({ + itemFk: ctx.args.itemFk, + entryFk: ctx.args.id, + packageFk: ctx.args.packageFk, + quantity: ctx.args.quantity + }, myOptions); + + let filter = { + fields: [ + 'id', + 'itemFk', + 'stickers', + 'packing', + 'grouping', + 'quantity', + 'packageFk', + 'weight', + 'buyingValue', + 'price2', + 'price3' + ], + include: { + relation: 'item', + scope: { + fields: [ + 'id', + 'typeFk', + 'name', + 'size', + 'minPrice', + 'tag5', + 'value5', + 'tag6', + 'value6', + 'tag7', + 'value7', + 'tag8', + 'value8', + 'tag9', + 'value9', + 'tag10', + 'value10', + 'groupingMode' + ], + include: { + relation: 'itemType', + scope: { + fields: ['code', 'description'] + } + } + } + } + }; + + let stmts = []; + let stmt; + + stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.buyRecalc'); + stmt = new ParameterizedSQL( + `CREATE TEMPORARY TABLE tmp.buyRecalc + (INDEX (id)) + ENGINE = MEMORY + SELECT ? AS id`, [newBuy.id]); + + stmts.push(stmt); + stmts.push('CALL buy_recalcPrices()'); + + const sql = ParameterizedSQL.join(stmts, ';'); + await conn.executeStmt(sql, myOptions); + + const buy = await models.Buy.findById(newBuy.id, filter, myOptions); + + if (tx) await tx.commit(); + + return buy; + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + }; +}; diff --git a/modules/entry/back/methods/entry/deleteBuys.js b/modules/entry/back/methods/entry/deleteBuys.js new file mode 100644 index 000000000..de038d66e --- /dev/null +++ b/modules/entry/back/methods/entry/deleteBuys.js @@ -0,0 +1,50 @@ +module.exports = Self => { + Self.remoteMethodCtx('deleteBuys', { + description: 'Deletes the selected buys', + accessType: 'WRITE', + accepts: [{ + arg: 'buys', + type: ['object'], + required: true, + description: 'The buys to delete' + }], + returns: { + type: ['object'], + root: true + }, + http: { + path: `/deleteBuys`, + verb: 'POST' + } + }); + + Self.deleteBuys = async(ctx, options) => { + const models = Self.app.models; + let tx; + let myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + try { + let promises = []; + for (let buy of ctx.args.buys) { + const buysToDelete = models.Buy.destroyById(buy.id, myOptions); + promises.push(buysToDelete); + } + + const deleted = await Promise.all(promises); + + if (tx) await tx.commit(); + return deleted; + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + }; +}; diff --git a/modules/entry/back/methods/entry/specs/addBuy.spec.js b/modules/entry/back/methods/entry/specs/addBuy.spec.js new file mode 100644 index 000000000..ef1177ed5 --- /dev/null +++ b/modules/entry/back/methods/entry/specs/addBuy.spec.js @@ -0,0 +1,42 @@ +const app = require('vn-loopback/server/server'); +const LoopBackContext = require('loopback-context'); + +describe('entry addBuy()', () => { + const activeCtx = { + accessToken: {userId: 18}, + }; + + const ctx = { + req: activeCtx + }; + + const entryId = 2; + it('should create a new buy for the given entry', async() => { + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); + const itemId = 4; + const quantity = 10; + + ctx.args = { + id: entryId, + itemFk: itemId, + quantity: quantity, + packageFk: 3 + }; + + const tx = await app.models.Entry.beginTransaction({}); + const options = {transaction: tx}; + + try { + const newBuy = await app.models.Entry.addBuy(ctx, options); + + expect(newBuy.itemFk).toEqual(itemId); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); +}); diff --git a/modules/entry/back/methods/entry/specs/deleteBuys.spec.js b/modules/entry/back/methods/entry/specs/deleteBuys.spec.js new file mode 100644 index 000000000..10466645a --- /dev/null +++ b/modules/entry/back/methods/entry/specs/deleteBuys.spec.js @@ -0,0 +1,23 @@ +const app = require('vn-loopback/server/server'); +const LoopBackContext = require('loopback-context'); + +describe('sale deleteBuys()', () => { + const activeCtx = { + accessToken: {userId: 18}, + }; + + const ctx = { + req: activeCtx + }; + + it('should delete the buy', async() => { + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); + ctx.args = {buys: [{id: 1}]}; + + let result = await app.models.Buy.deleteBuys(ctx); + + expect(result).toEqual([{count: 1}]); + }); +}); diff --git a/modules/entry/back/models/buy.js b/modules/entry/back/models/buy.js index e110164e8..34f19e765 100644 --- a/modules/entry/back/models/buy.js +++ b/modules/entry/back/models/buy.js @@ -1,4 +1,5 @@ module.exports = Self => { require('../methods/entry/editLatestBuys')(Self); require('../methods/entry/latestBuysFilter')(Self); + require('../methods/entry/deleteBuys')(Self); }; diff --git a/modules/entry/back/models/buy.json b/modules/entry/back/models/buy.json index 65bf15fa4..8e36d0eef 100644 --- a/modules/entry/back/models/buy.json +++ b/modules/entry/back/models/buy.json @@ -57,14 +57,12 @@ "entry": { "type": "belongsTo", "model": "Entry", - "foreignKey": "entryFk", - "required": true + "foreignKey": "entryFk" }, "item": { "type": "belongsTo", "model": "Item", - "foreignKey": "itemFk", - "required": true + "foreignKey": "itemFk" }, "package": { "type": "belongsTo", diff --git a/modules/entry/back/models/entry.js b/modules/entry/back/models/entry.js index f1a22fddd..88611963f 100644 --- a/modules/entry/back/models/entry.js +++ b/modules/entry/back/models/entry.js @@ -2,6 +2,7 @@ module.exports = Self => { require('../methods/entry/filter')(Self); require('../methods/entry/getEntry')(Self); require('../methods/entry/getBuys')(Self); + require('../methods/entry/addBuy')(Self); require('../methods/entry/importBuys')(Self); require('../methods/entry/importBuysPreview')(Self); }; diff --git a/modules/entry/front/buy/index/index.html b/modules/entry/front/buy/index/index.html index 0ff11c8a6..4a1f0837d 100644 --- a/modules/entry/front/buy/index/index.html +++ b/modules/entry/front/buy/index/index.html @@ -1,9 +1,189 @@ + + + + +
+ + + + + + + +

Subtotal {{$ctrl.ticket.totalWithoutVat | currency: 'EUR':2}}

+

VAT {{$ctrl.ticket.totalWithVat - $ctrl.ticket.totalWithoutVat | currency: 'EUR':2}}

+

Total {{$ctrl.ticket.totalWithVat | currency: 'EUR':2}}

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + ItemQuantityPackageStickersWeightPackingGroupingBuying valueGrouping pricePacking priceImport
+ + + + + {{::buy.item.id | zeroFill:6}} + + + + {{::id}} - {{::name}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{buy.quantity * buy.buyingValue | currency: 'EUR':2}} + +
+ + + {{::buy.item.itemType.code}} + + + + {{::buy.item.size}} + + + + {{::buy.item.minPrice | currency: 'EUR':2}} + + + {{::buy.item.name}} + + +
+
+ + +
+
+
+ vn-bind="+"> -
\ No newline at end of file + + + + + + \ No newline at end of file diff --git a/modules/entry/front/buy/index/index.js b/modules/entry/front/buy/index/index.js index 6cb27e022..9f0b487b9 100644 --- a/modules/entry/front/buy/index/index.js +++ b/modules/entry/front/buy/index/index.js @@ -1,9 +1,65 @@ import ngModule from '../../module'; +import './style.scss'; import Section from 'salix/components/section'; +export default class Controller extends Section { + saveBuy(buy) { + const missingData = !buy.itemFk || !buy.quantity || !buy.packageFk; + if (missingData) return; + + if (buy.id) { + const query = `Buys/${buy.id}`; + this.$http.patch(query, buy).then(res => { + if (!res.data) return; + + buy = Object.assign(buy, res.data); + this.vnApp.showSuccess(this.$t('Data saved!')); + }); + } else { + const query = `Entries/${this.entry.id}/addBuy`; + this.$http.post(query, buy).then(res => { + if (!res.data) return; + + buy = Object.assign(buy, res.data); + this.vnApp.showSuccess(this.$t('Data saved!')); + }); + } + } + + /** + * Returns checked instances + * + * @return {Array} Checked instances + */ + selectedBuys() { + if (!this.buys) return; + + return this.buys.filter(buy => { + return buy.checked; + }); + } + + deleteBuys() { + const buys = this.selectedBuys(); + const actualInstances = buys.filter(buy => buy.id); + + const params = {buys: actualInstances}; + + if (actualInstances.length) { + this.$http.post(`Buys/deleteBuys`, params).then(() => { + this.vnApp.showSuccess(this.$t('Data saved!')); + }); + } + buys.forEach(buy => { + const index = this.buys.indexOf(buy); + this.buys.splice(index, 1); + }); + } +} + ngModule.vnComponent('vnEntryBuyIndex', { template: require('./index.html'), - controller: Section, + controller: Controller, bindings: { entry: '<' } diff --git a/modules/entry/front/buy/index/index.spec.js b/modules/entry/front/buy/index/index.spec.js new file mode 100644 index 000000000..9e9318d10 --- /dev/null +++ b/modules/entry/front/buy/index/index.spec.js @@ -0,0 +1,66 @@ +import './index.js'; + +describe('Entry buy', () => { + let controller; + let $httpBackend; + + beforeEach(ngModule('entry')); + + beforeEach(angular.mock.inject(($componentController, $compile, $rootScope, _$httpParamSerializer_, _$httpBackend_) => { + $httpBackend = _$httpBackend_; + let $element = $compile(' { + it(`should call the buys patch route if the received buy has an ID`, () => { + const buy = {id: 1, itemFk: 1, quantity: 1, packageFk: 1}; + + const query = `Buys/${buy.id}`; + + $httpBackend.expectPATCH(query).respond(200); + controller.saveBuy(buy); + $httpBackend.flush(); + }); + + it(`should call the entry addBuy post route if the received buy has no ID`, () => { + controller.entry = {id: 1}; + const buy = {itemFk: 1, quantity: 1, packageFk: 1}; + + const query = `Entries/${controller.entry.id}/addBuy`; + + $httpBackend.expectPOST(query).respond(200); + controller.saveBuy(buy); + $httpBackend.flush(); + }); + }); + + describe('deleteBuys()', () => { + it(`should perform no queries if all buys to delete were not actual instances`, () => { + controller.buys = [ + {checked: true}, + {checked: true}, + {checked: false}]; + + controller.deleteBuys(); + + expect(controller.buys.length).toEqual(1); + }); + + it(`should perform a query to delete as there's an actual instance at least`, () => { + controller.buys = [ + {checked: true, id: 1}, + {checked: true}, + {checked: false}]; + + const query = 'Buys/deleteBuys'; + + $httpBackend.expectPOST(query).respond(200); + controller.deleteBuys(); + $httpBackend.flush(); + + expect(controller.buys.length).toEqual(1); + }); + }); +}); diff --git a/modules/entry/front/buy/index/style.scss b/modules/entry/front/buy/index/style.scss new file mode 100644 index 000000000..836f1eddc --- /dev/null +++ b/modules/entry/front/buy/index/style.scss @@ -0,0 +1,28 @@ +@import "variables"; + + +vn-entry-buy-index vn-card { + max-width: $width-xl; + + .dark-row { + background-color: lighten($color-marginal, 10%); + } + + tbody tr:nth-child(1) { + border-top: 1px solid $color-marginal; + } + + tbody{ + border-bottom: 1px solid $color-marginal; + } + + tbody tr:nth-child(3) { + height: inherit + } + + tr { + margin-bottom: 10px; + } +} + +$color-font-link-medium: lighten($color-font-link, 20%) \ No newline at end of file diff --git a/modules/entry/front/latest-buys/index.html b/modules/entry/front/latest-buys/index.html index d34dfdbb2..a8fac7960 100644 --- a/modules/entry/front/latest-buys/index.html +++ b/modules/entry/front/latest-buys/index.html @@ -83,12 +83,12 @@ - + {{::buy.packing | dashIfEmpty}} - + {{::buy.grouping | dashIfEmpty}} diff --git a/modules/entry/front/routes.json b/modules/entry/front/routes.json index 34e6f469b..f45e00807 100644 --- a/modules/entry/front/routes.json +++ b/modules/entry/front/routes.json @@ -87,7 +87,8 @@ "url": "/buy", "state": "entry.card.buy", "abstract": true, - "component": "ui-view" + "component": "ui-view", + "acl": ["buyer"] }, { "url" : "/index", diff --git a/modules/entry/front/summary/index.html b/modules/entry/front/summary/index.html index 2843ecc46..8b31df0b2 100644 --- a/modules/entry/front/summary/index.html +++ b/modules/entry/front/summary/index.html @@ -10,67 +10,67 @@ - - - - - - + - {{$ctrl.entryData.travel.agency.name}} - - - - @@ -102,12 +102,12 @@ {{::line.packageFk | dashIfEmpty}} {{::line.weight}} - + {{::line.packing | dashIfEmpty}} - + {{::line.grouping | dashIfEmpty}} diff --git a/modules/ticket/front/sale/index.html b/modules/ticket/front/sale/index.html index 778e46001..d4dfe1a8a 100644 --- a/modules/ticket/front/sale/index.html +++ b/modules/ticket/front/sale/index.html @@ -91,81 +91,81 @@ vn-tooltip="{{::$ctrl.$t('Reserved')}}"> - - - - - - {{sale.itemFk}} - - - - {{::id}} - {{::name}} - - - - - {{sale.quantity}} - - + + + + + {{sale.itemFk}} + + - - - - - - {{sale.concept}} - -

{{::sale.subName}}

-
- - -
- - - - -
- - - {{sale.price | currency: 'EUR':2}} - - - - - {{(sale.discount / 100) | percentage}} - - - - {{$ctrl.getSaleTotal(sale) | currency: 'EUR':2}} - - + url="Items" + ng-model="sale.itemFk" + show-field="name" + value-field="id" + search-function="$ctrl.itemSearchFunc($search)" + on-change="$ctrl.changeQuantity(sale)" + order="id DESC" + tabindex="1"> + + {{::id}} - {{::name}} + + + + + {{sale.quantity}} + + + + + + + + {{sale.concept}} + +

{{::sale.subName}}

+
+ + +
+ + + + +
+ + + {{sale.price | currency: 'EUR':2}} + + + + + {{(sale.discount / 100) | percentage}} + + + + {{$ctrl.getSaleTotal(sale) | currency: 'EUR':2}} + +