diff --git a/db/changes/10271-wisemen/00-ACL.sql b/db/changes/10271-wisemen/00-ACL.sql
index 5c81ab91b..1c1a64a30 100644
--- a/db/changes/10271-wisemen/00-ACL.sql
+++ b/db/changes/10271-wisemen/00-ACL.sql
@@ -1,2 +1,3 @@
INSERT INTO salix.ACL (model, property, accessType, permission, principalType, principalId)
VALUES ('printServerQueue', '*', 'WRITE', 'ALLOW', 'ROLE', 'employee');
+INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('FixedPrice', '*', '*', 'ALLOW', 'ROLE', 'buyer');
diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js
index 72e048772..ad81b9b3a 100644
--- a/e2e/helpers/selectors.js
+++ b/e2e/helpers/selectors.js
@@ -250,6 +250,18 @@ export default {
taxClassCheckbox: '.vn-popover.shown vn-horizontal:nth-child(11) > vn-check',
saveFieldsButton: '.vn-popover.shown vn-button[label="Save"] > button'
},
+ itemFixedPrice: {
+ add: 'vn-fixed-price vn-icon[icon="add_circle"]',
+ fourthFixedPrice: 'vn-fixed-price vn-tr:nth-child(4)',
+ fourthItemID: 'vn-fixed-price vn-tr:nth-child(4) vn-autocomplete[ng-model="price.itemFk"]',
+ fourthWarehouse: 'vn-fixed-price vn-tr:nth-child(4) vn-autocomplete[ng-model="price.warehouseFk"]',
+ fourthPPU: 'vn-fixed-price vn-tr:nth-child(4) > vn-td-editable:nth-child(4)',
+ fourthPPP: 'vn-fixed-price vn-tr:nth-child(4) > vn-td-editable:nth-child(5)',
+ fourthMinPrice: 'vn-fixed-price vn-tr:nth-child(4) > vn-td-editable:nth-child(6)',
+ fourthStarted: 'vn-fixed-price vn-tr:nth-child(4) vn-date-picker[ng-model="price.started"]',
+ fourthEnded: 'vn-fixed-price vn-tr:nth-child(4) vn-date-picker[ng-model="price.ended"]',
+ fourthDeleteIcon: 'vn-fixed-price vn-tr:nth-child(4) > vn-td:nth-child(9) > vn-icon-button[icon="delete"]'
+ },
itemCreateView: {
temporalName: 'vn-item-create vn-textfield[ng-model="$ctrl.item.provisionalName"]',
type: 'vn-autocomplete[ng-model="$ctrl.item.typeFk"]',
@@ -793,13 +805,13 @@ export default {
workerCalendar: {
year: 'vn-worker-calendar vn-autocomplete[ng-model="$ctrl.year"]',
totalHolidaysUsed: 'vn-worker-calendar div.totalBox > div',
- januaryThirtyFirst: 'vn-worker-calendar vn-calendar:nth-child(2) section:nth-child(33) > div',
- marchTwentyThird: 'vn-worker-calendar vn-calendar:nth-child(4) section:nth-child(29) > div',
- mayFourth: 'vn-worker-calendar vn-calendar:nth-child(6) section:nth-child(8) > div',
- mayEighth: 'vn-worker-calendar vn-calendar:nth-child(6) section:nth-child(12) > div',
- mayTwelfth: 'vn-worker-calendar vn-calendar:nth-child(6) section:nth-child(16) > div',
- mayThirteenth: 'vn-worker-calendar vn-calendar:nth-child(6) section:nth-child(17) > div',
- mayFourteenth: 'vn-worker-calendar vn-calendar:nth-child(6) section:nth-child(18) > div',
+ penultimateMondayOfJanuary: 'vn-worker-calendar vn-calendar:nth-child(2) section:nth-child(22) > div',
+ lastMondayOfMarch: 'vn-worker-calendar vn-calendar:nth-child(4) section:nth-child(29) > div',
+ fistMondayOfMay: 'vn-worker-calendar vn-calendar:nth-child(6) section:nth-child(8) > div',
+ secondFridayOfJun: 'vn-worker-calendar vn-calendar:nth-child(6) section:nth-child(12) > div',
+ secondTuesdayOfMay: 'vn-worker-calendar vn-calendar:nth-child(6) section:nth-child(16) > div',
+ secondWednesdayOfMay: 'vn-worker-calendar vn-calendar:nth-child(6) section:nth-child(17) > div',
+ secondThursdayOfMay: 'vn-worker-calendar vn-calendar:nth-child(6) section:nth-child(18) > div',
holidays: 'vn-worker-calendar > vn-side-menu div:nth-child(3) > vn-chip:nth-child(1)',
absence: 'vn-worker-calendar > vn-side-menu div:nth-child(3) > vn-chip:nth-child(2)',
halfHoliday: 'vn-worker-calendar > vn-side-menu div:nth-child(3) > vn-chip:nth-child(3)',
@@ -828,12 +840,13 @@ export default {
},
travelIndex: {
anySearchResult: 'vn-travel-index vn-tbody > a',
- firstSearchResult: 'vn-travel-index vn-tbody > a:nth-child(1)'
+ firstSearchResult: 'vn-travel-index vn-tbody > a:nth-child(1)',
+ firstTravelAddEntryButton: 'vn-travel-index a:nth-child(1) vn-icon[icon="icon-ticket"]',
},
travelExtraCommunity: {
anySearchResult: 'vn-travel-extra-community > vn-data-viewer div > vn-tbody > vn-tr',
- firstTravelReference: 'vn-travel-extra-community vn-card:nth-child(1) vn-td-editable[name="reference"]',
- firstTravelLockedKg: 'vn-travel-extra-community vn-card:nth-child(1) vn-td-editable[name="lockedKg"]',
+ firstTravelReference: 'vn-travel-extra-community vn-tbody:nth-child(2) vn-td-editable[name="reference"]',
+ firstTravelLockedKg: 'vn-travel-extra-community vn-tbody:nth-child(2) vn-td-editable[name="lockedKg"]',
removeContinentFilter: 'vn-searchbar > form > vn-textfield > div.container > div.prepend > prepend > div > span:nth-child(3) > vn-icon > i'
},
travelBasicData: {
@@ -867,6 +880,7 @@ export default {
dotMenu: 'vn-travel-descriptor vn-icon-button[icon="more_vert"]',
dotMenuClone: '#clone',
dotMenuCloneWithEntries: '#cloneWithEntries',
+ dotMenuAddEntry: '[name="addEntry"]',
acceptClonation: 'tpl-buttons > button[response="accept"]'
},
travelCreate: {
@@ -897,6 +911,10 @@ export default {
volumetric: 'vn-zone-basic-data vn-check[ng-model="$ctrl.zone.isVolumetric"]',
saveButton: 'vn-zone-basic-data vn-submit > button',
},
+ entryCreate: {
+ travel: 'vn-entry-create vn-autocomplete[ng-model="$ctrl.entry.travelFk"]',
+ company: 'vn-entry-create vn-autocomplete[ng-model="$ctrl.entry.companyFk"]'
+ },
entrySummary: {
header: 'vn-entry-summary > vn-card > h5',
reference: 'vn-entry-summary vn-label-value[label="Reference"]',
diff --git a/e2e/paths/03-worker/05_calendar.spec.js b/e2e/paths/03-worker/05_calendar.spec.js
index e82006b3c..08ef71f13 100644
--- a/e2e/paths/03-worker/05_calendar.spec.js
+++ b/e2e/paths/03-worker/05_calendar.spec.js
@@ -27,31 +27,31 @@ describe('Worker calendar path', () => {
it('should set two days as holidays on the calendar and check the total holidays increased by 1.5', async() => {
await page.waitToClick(selectors.workerCalendar.holidays);
await page.waitForTimeout(reasonableTimeBetweenClicks);
- await page.waitToClick(selectors.workerCalendar.januaryThirtyFirst);
+ await page.waitToClick(selectors.workerCalendar.penultimateMondayOfJanuary);
await page.waitForTimeout(reasonableTimeBetweenClicks);
await page.waitToClick(selectors.workerCalendar.absence);
await page.waitForTimeout(reasonableTimeBetweenClicks);
- await page.waitToClick(selectors.workerCalendar.marchTwentyThird);
+ await page.waitToClick(selectors.workerCalendar.lastMondayOfMarch);
await page.waitForTimeout(reasonableTimeBetweenClicks);
await page.waitToClick(selectors.workerCalendar.halfHoliday);
await page.waitForTimeout(reasonableTimeBetweenClicks);
- await page.waitToClick(selectors.workerCalendar.mayFourth);
+ await page.waitToClick(selectors.workerCalendar.fistMondayOfMay);
await page.waitForTimeout(reasonableTimeBetweenClicks);
await page.waitToClick(selectors.workerCalendar.furlough);
await page.waitForTimeout(reasonableTimeBetweenClicks);
- await page.waitToClick(selectors.workerCalendar.mayTwelfth);
+ await page.waitToClick(selectors.workerCalendar.secondTuesdayOfMay);
await page.waitForTimeout(reasonableTimeBetweenClicks);
- await page.waitToClick(selectors.workerCalendar.mayThirteenth);
+ await page.waitToClick(selectors.workerCalendar.secondWednesdayOfMay);
await page.waitForTimeout(reasonableTimeBetweenClicks);
- await page.waitToClick(selectors.workerCalendar.mayFourteenth);
+ await page.waitToClick(selectors.workerCalendar.secondThursdayOfMay);
await page.waitForTimeout(reasonableTimeBetweenClicks);
await page.waitToClick(selectors.workerCalendar.halfFurlough);
await page.waitForTimeout(reasonableTimeBetweenClicks);
- await page.waitToClick(selectors.workerCalendar.mayEighth);
+ await page.waitToClick(selectors.workerCalendar.secondFridayOfJun);
await page.waitForTimeout(reasonableTimeBetweenClicks);
const result = await page.waitToGetProperty(selectors.workerCalendar.totalHolidaysUsed, 'innerText');
@@ -71,31 +71,31 @@ describe('Worker calendar path', () => {
await page.waitForTimeout(reasonableTimeBetweenClicks);
await page.waitToClick(selectors.workerCalendar.holidays);
await page.waitForTimeout(reasonableTimeBetweenClicks);
- await page.waitToClick(selectors.workerCalendar.januaryThirtyFirst);
+ await page.waitToClick(selectors.workerCalendar.penultimateMondayOfJanuary);
await page.waitForTimeout(reasonableTimeBetweenClicks);
await page.waitToClick(selectors.workerCalendar.absence);
await page.waitForTimeout(reasonableTimeBetweenClicks);
- await page.waitToClick(selectors.workerCalendar.marchTwentyThird);
+ await page.waitToClick(selectors.workerCalendar.lastMondayOfMarch);
await page.waitForTimeout(reasonableTimeBetweenClicks);
await page.waitToClick(selectors.workerCalendar.halfHoliday);
await page.waitForTimeout(reasonableTimeBetweenClicks);
- await page.waitToClick(selectors.workerCalendar.mayFourth);
+ await page.waitToClick(selectors.workerCalendar.fistMondayOfMay);
await page.waitForTimeout(reasonableTimeBetweenClicks);
await page.waitToClick(selectors.workerCalendar.furlough);
await page.waitForTimeout(reasonableTimeBetweenClicks);
- await page.waitToClick(selectors.workerCalendar.mayTwelfth);
+ await page.waitToClick(selectors.workerCalendar.secondTuesdayOfMay);
await page.waitForTimeout(reasonableTimeBetweenClicks);
- await page.waitToClick(selectors.workerCalendar.mayThirteenth);
+ await page.waitToClick(selectors.workerCalendar.secondWednesdayOfMay);
await page.waitForTimeout(reasonableTimeBetweenClicks);
- await page.waitToClick(selectors.workerCalendar.mayFourteenth);
+ await page.waitToClick(selectors.workerCalendar.secondThursdayOfMay);
await page.waitForTimeout(reasonableTimeBetweenClicks);
await page.waitToClick(selectors.workerCalendar.halfFurlough);
await page.waitForTimeout(reasonableTimeBetweenClicks);
- await page.waitToClick(selectors.workerCalendar.mayEighth);
+ await page.waitToClick(selectors.workerCalendar.secondFridayOfJun);
});
it('should check the total holidays used are back to what it was', async() => {
@@ -116,7 +116,7 @@ describe('Worker calendar path', () => {
await page.waitForTimeout(reasonableTimeBetweenClicks);
await page.waitToClick(selectors.workerCalendar.holidays);
await page.waitForTimeout(reasonableTimeBetweenClicks);
- await page.waitToClick(selectors.workerCalendar.januaryThirtyFirst);
+ await page.waitToClick(selectors.workerCalendar.penultimateMondayOfJanuary);
});
it('should check the total holidays used are now the initial ones', async() => {
diff --git a/e2e/paths/04-item/14_fixedPrice.spec.js b/e2e/paths/04-item/14_fixedPrice.spec.js
new file mode 100644
index 000000000..477daa151
--- /dev/null
+++ b/e2e/paths/04-item/14_fixedPrice.spec.js
@@ -0,0 +1,61 @@
+import selectors from '../../helpers/selectors.js';
+import getBrowser from '../../helpers/puppeteer';
+
+describe('Item fixed prices path', () => {
+ let browser;
+ let page;
+ beforeAll(async() => {
+ browser = await getBrowser();
+ page = browser.page;
+ await page.loginAndModule('buyer', 'item');
+ await page.accessToSection('item.fixedPrice');
+ });
+
+ afterAll(async() => {
+ await browser.close();
+ });
+
+ it('should click on the add new foxed price button', async() => {
+ await page.waitToClick(selectors.itemFixedPrice.add);
+ await page.waitForSelector(selectors.itemFixedPrice.fourthFixedPrice);
+ });
+
+ it('should fill the fixed price data', async() => {
+ const now = new Date();
+ const searchValue = 'Chest ammo box';
+ await page.waitToClick(selectors.itemFixedPrice.fourthItemID);
+ await page.write('body > div > div > div.content > div.filter.ng-scope > vn-textfield', searchValue);
+ try {
+ await page.waitForFunction(searchValue => {
+ const element = document.querySelector('li.active');
+ if (element)
+ return element.innerText.toLowerCase().includes(searchValue.toLowerCase());
+ }, {}, searchValue);
+ } catch (error) {
+ const builtSelector = await page.selectorFormater(selectors.ticketSales.moreMenuState);
+ const inputValue = await page.evaluate(() => {
+ return document.querySelector('.vn-drop-down.shown vn-textfield input').value;
+ });
+ throw new Error(`${builtSelector} value is ${inputValue}! ${error}`);
+ }
+ await page.keyboard.press('Enter');
+ await page.autocompleteSearch(selectors.itemFixedPrice.fourthWarehouse, 'Warehouse one');
+ await page.writeOnEditableTD(selectors.itemFixedPrice.fourthPPU, '20');
+ await page.writeOnEditableTD(selectors.itemFixedPrice.fourthPPP, '10');
+ await page.writeOnEditableTD(selectors.itemFixedPrice.fourthMinPrice, '5');
+ await page.pickDate(selectors.itemFixedPrice.fourthStarted, now);
+ await page.pickDate(selectors.itemFixedPrice.fourthEnded, now);
+ await page.waitForTimeout(1000);
+ const message = await page.waitForSnackbar();
+
+ expect(message.text).toContain('Data saved!');
+ });
+
+ it('should reload the section and check the created price has the expected ID', async() => {
+ await page.accessToSection('item.index');
+ await page.accessToSection('item.fixedPrice');
+ const result = await page.getProperty('vn-fixed-price > div > vn-card > vn-table > div > vn-tbody > vn-tr:nth-child(4) > vn-td:nth-child(1) > span', 'innerText');
+
+ expect(result).toContain('13');
+ });
+});
diff --git a/e2e/paths/10-travel/03_descriptor.spec.js b/e2e/paths/10-travel/03_descriptor.spec.js
index cdca379ad..a1a035317 100644
--- a/e2e/paths/10-travel/03_descriptor.spec.js
+++ b/e2e/paths/10-travel/03_descriptor.spec.js
@@ -33,6 +33,61 @@ describe('Travel descriptor path', () => {
expect(state).toBe('travel.card.summary');
});
+ it('should be redirected to the create entry view', async() => {
+ await page.waitToClick(selectors.travelDescriptor.dotMenu);
+ await page.waitToClick(selectors.travelDescriptor.dotMenuAddEntry);
+ await page.waitForState('entry.create');
+ const state = await page.getState();
+
+ expect(state).toBe('entry.create');
+ });
+
+ it('should check some data was imported from the travel', async() => {
+ const travel = await page.waitToGetProperty(selectors.entryCreate.travel, 'value');
+ const campany = await page.waitToGetProperty(selectors.entryCreate.company, 'value');
+
+ expect(travel).toContain('Warehouse');
+ expect(campany).toContain('VNL');
+ });
+
+ it('should navigate back to the travel index', async() => {
+ await page.waitToClick('.cancel');
+ await page.waitToClick(selectors.globalItems.homeButton);
+ await page.selectModule('travel');
+ await page.waitForState('travel.index');
+ const state = await page.getState();
+
+ expect(state).toBe('travel.index');
+ });
+
+ it('should click on the add entry button of the third result to be redirected to create entry', async() => {
+ await page.keyboard.press('Enter');
+ await page.waitToClick(selectors.travelIndex.firstTravelAddEntryButton);
+ await page.waitForState('entry.create');
+ const state = await page.getState();
+
+ expect(state).toBe('entry.create');
+ });
+
+ it('should check again some data was imported from the travel', async() => {
+ const travel = await page.waitToGetProperty(selectors.entryCreate.travel, 'value');
+ const campany = await page.waitToGetProperty(selectors.entryCreate.company, 'value');
+
+ expect(travel).toContain('Warehouse');
+ expect(campany).toContain('VNL');
+ });
+
+ it('should navigate to the travel summary of a given travel', async() => {
+ await page.waitToClick('.cancel');
+ await page.waitToClick(selectors.globalItems.homeButton);
+ await page.selectModule('travel');
+ await page.accessToSearchResult('3');
+ await page.waitForState('travel.card.summary');
+ const state = await page.getState();
+
+ expect(state).toBe('travel.card.summary');
+ });
+
it('should be redirected to the create travel when using the clone option of the dot menu', async() => {
await page.waitToClick(selectors.travelDescriptor.dotMenu);
await page.waitToClick(selectors.travelDescriptor.dotMenuClone);
diff --git a/e2e/paths/10-travel/04_extra_community.spec.js b/e2e/paths/10-travel/04_extra_community.spec.js
index d902bb45e..60a3fddb3 100644
--- a/e2e/paths/10-travel/04_extra_community.spec.js
+++ b/e2e/paths/10-travel/04_extra_community.spec.js
@@ -21,6 +21,7 @@ describe('Travel extra community path', () => {
await page.waitForSpinnerLoad();
await page.writeOnEditableTD(selectors.travelExtraCommunity.firstTravelReference, 'edited reference');
await page.writeOnEditableTD(selectors.travelExtraCommunity.firstTravelLockedKg, '1500');
+ await page.waitForTimeout(1000);
});
it('should reload the index and confirm the reference and locked kg were edited', async() => {
diff --git a/loopback/locale/es.json b/loopback/locale/es.json
index b54c0cc67..aa54c6b94 100644
--- a/loopback/locale/es.json
+++ b/loopback/locale/es.json
@@ -166,5 +166,6 @@
"The selected ticket is not suitable for this route": "El ticket seleccionado no es apto para esta ruta",
"Sorts whole route": "Reordena ruta entera",
"New ticket request has been created with price": "Se ha creado una nueva petición de compra '{{description}}' para el día {{shipped}}, con una cantidad de {{quantity}} y un precio de {{price}} €",
- "New ticket request has been created": "Se ha creado una nueva petición de compra '{{description}}' para el día {{shipped}}, con una cantidad de {{quantity}}"
+ "New ticket request has been created": "Se ha creado una nueva petición de compra '{{description}}' para el día {{shipped}}, con una cantidad de {{quantity}}",
+ "That item doesn't exists": "Ese artículo no existe"
}
\ No newline at end of file
diff --git a/modules/client/back/methods/client/updateAddress.js b/modules/client/back/methods/client/updateAddress.js
index db8ed38d6..27f9faf1c 100644
--- a/modules/client/back/methods/client/updateAddress.js
+++ b/modules/client/back/methods/client/updateAddress.js
@@ -5,7 +5,7 @@ module.exports = function(Self) {
description: 'Updates a client address updating default address',
accepts: [{
arg: 'ctx',
- type: 'Object',
+ type: 'object',
http: {source: 'context'}
},
{
@@ -70,7 +70,7 @@ module.exports = function(Self) {
}],
returns: {
root: true,
- type: 'Object'
+ type: 'object'
},
http: {
verb: 'patch',
diff --git a/modules/item/back/methods/fixed-price/filter.js b/modules/item/back/methods/fixed-price/filter.js
new file mode 100644
index 000000000..22cf2bf44
--- /dev/null
+++ b/modules/item/back/methods/fixed-price/filter.js
@@ -0,0 +1,192 @@
+
+const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
+const buildFilter = require('vn-loopback/util/filter').buildFilter;
+const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
+
+module.exports = Self => {
+ Self.remoteMethodCtx('filter', {
+ description: 'Find all instances of the model matched by filter from the data source.',
+ accessType: 'READ',
+ accepts: [
+ {
+ arg: 'filter',
+ type: 'object',
+ description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
+ },
+ {
+ arg: 'search',
+ type: 'string',
+ description: `If it's and integer searchs by itemFk, otherwise it searchs by the itemType code`,
+ },
+ {
+ arg: 'itemFk',
+ type: 'integer',
+ description: 'The item id',
+ },
+ {
+ arg: 'typeFk',
+ type: 'integer',
+ description: 'The item type id',
+ },
+ {
+ arg: 'categoryFk',
+ type: 'integer',
+ description: 'The item category id',
+ },
+ {
+ arg: 'warehouseFk',
+ type: 'integer',
+ description: 'The warehouse id',
+ },
+ {
+ arg: 'buyerFk',
+ type: 'integer',
+ description: 'The buyer id',
+ },
+ {
+ arg: 'rate2',
+ type: 'integer',
+ description: 'The price per unit',
+ },
+ {
+ arg: 'rate3',
+ type: 'integer',
+ description: 'The price per package',
+ },
+ {
+ arg: 'minPrice',
+ type: 'integer',
+ description: 'The minimum price of the item',
+ },
+ {
+ arg: 'hasMinPrice',
+ type: 'boolean',
+ description: 'whether a minimum price has been defined for the item',
+ },
+ {
+ arg: 'started',
+ type: 'date',
+ description: 'Price validity start date',
+ },
+ {
+ arg: 'ended',
+ type: 'date',
+ description: 'Price validity end date',
+ },
+ {
+ arg: 'tags',
+ type: ['object'],
+ description: 'List of tags to filter with',
+ },
+ {
+ arg: 'mine',
+ type: 'Boolean',
+ description: `Search requests attended by the current user`
+ }
+ ],
+ returns: {
+ type: ['Object'],
+ root: true
+ },
+ http: {
+ path: `/filter`,
+ verb: 'GET'
+ }
+ });
+
+ Self.filter = async(ctx, filter) => {
+ const conn = Self.dataSource.connector;
+ let userId = ctx.req.accessToken.userId;
+
+ if (ctx.args.mine)
+ ctx.args.buyerFk = userId;
+
+ const where = buildFilter(ctx.args, (param, value) => {
+ switch (param) {
+ case 'search':
+ return /^\d+$/.test(value)
+ ? {'fp.itemFk': {inq: value}}
+ : {'it.code': {like: `%${value}%`}};
+ case 'categoryFk':
+ return {'it.categoryFk': value};
+ case 'buyerFk':
+ return {'it.workerFk': value};
+ case 'warehouseFk':
+ case 'rate2':
+ case 'rate3':
+ case 'started':
+ case 'ended':
+ param = `fp.${param}`;
+ return {[param]: value};
+ case 'minPrice':
+ case 'hasMinPrice':
+ case 'typeFk':
+ param = `i.${param}`;
+ return {[param]: value};
+ }
+ });
+ filter = mergeFilters(filter, {where});
+
+ const stmts = [];
+ let stmt;
+
+ stmt = new ParameterizedSQL(
+ `SELECT fp.id,
+ fp.itemFk,
+ fp.warehouseFk,
+ fp.rate2,
+ fp.rate3,
+ fp.started,
+ fp.ended,
+ i.minPrice,
+ i.hasMinPrice,
+ i.name,
+ i.subName,
+ i.tag5,
+ i.value5,
+ i.tag6,
+ i.value6,
+ i.tag7,
+ i.value7,
+ i.tag8,
+ i.value8,
+ i.tag9,
+ i.value9,
+ i.tag10,
+ i.value10
+ FROM priceFixed fp
+ JOIN item i ON i.id = fp.itemFk
+ JOIN itemType it ON it.id = i.typeFk`
+ );
+
+ if (ctx.args.tags) {
+ let i = 1;
+ for (const tag of ctx.args.tags) {
+ const tAlias = `it${i++}`;
+
+ if (tag.tagFk) {
+ stmt.merge({
+ sql: `JOIN vn.itemTag ${tAlias} ON ${tAlias}.itemFk = i.id
+ AND ${tAlias}.tagFk = ?
+ AND ${tAlias}.value LIKE ?`,
+ params: [tag.tagFk, `%${tag.value}%`],
+ });
+ } else {
+ stmt.merge({
+ sql: `JOIN vn.itemTag ${tAlias} ON ${tAlias}.itemFk = i.id
+ AND ${tAlias}.value LIKE ?`,
+ params: [`%${tag.value}%`],
+ });
+ }
+ }
+ }
+
+ stmt.merge(conn.makeWhere(filter.where));
+ stmt.merge(conn.makePagination(filter));
+
+ const fixedPriceIndex = stmts.push(stmt) - 1;
+ const sql = ParameterizedSQL.join(stmts, ';');
+ const result = await conn.executeStmt(sql);
+ return fixedPriceIndex === 0 ? result : result[fixedPriceIndex];
+ };
+};
diff --git a/modules/item/back/methods/fixed-price/specs/filter.spec.js b/modules/item/back/methods/fixed-price/specs/filter.spec.js
new file mode 100644
index 000000000..ff259357f
--- /dev/null
+++ b/modules/item/back/methods/fixed-price/specs/filter.spec.js
@@ -0,0 +1,86 @@
+const app = require('vn-loopback/server/server');
+
+describe('fixed price filter()', () => {
+ it('should return 1 result filtering by item ID', async() => {
+ const itemID = 3;
+ const ctx = {
+ req: {accessToken: {userId: 1}},
+ args: {
+ search: itemID
+ }
+ };
+ const result = await app.models.FixedPrice.filter(ctx);
+
+ expect(result.length).toEqual(1);
+ expect(result[0].id).toEqual(2);
+ expect(result[0].itemFk).toEqual(itemID);
+ });
+
+ it('should return 1 result filtering by item type code', async() => {
+ const itemCode = 'CRI';
+ const ctx = {
+ req: {accessToken: {userId: 1}},
+ args: {
+ search: itemCode
+ }
+ };
+ const itemType = await app.models.ItemType.findOne({
+ where: {code: itemCode},
+ fields: ['id']
+ });
+ const items = await app.models.Item.find({
+ where: {typeFk: itemType.id},
+ fields: ['id']
+ });
+ const IDs = items.map(item => {
+ return item.id;
+ });
+
+ const result = await app.models.FixedPrice.filter(ctx);
+ const firstResult = result[0];
+
+ expect(result.length).toEqual(1);
+ expect(firstResult.id).toEqual(2);
+ expect(IDs).toContain(firstResult.itemFk);
+ });
+
+ it('should return 2 results filtering by warehouse', async() => {
+ const warehouseID = 1;
+ const ctx = {
+ req: {accessToken: {userId: 1}},
+ args: {
+ warehouseFk: warehouseID
+ }
+ };
+ const result = await app.models.FixedPrice.filter(ctx);
+ const length = result.length;
+ const anyResult = result[Math.floor(Math.random() * Math.floor(length))];
+
+ expect(result.length).toEqual(2);
+ expect(anyResult.warehouseFk).toEqual(warehouseID);
+ });
+
+ it('should return no results filtering by hasMinPrice', async() => {
+ const ctx = {
+ req: {accessToken: {userId: 1}},
+ args: {
+ hasMinPrice: true
+ }
+ };
+ const result = await app.models.FixedPrice.filter(ctx);
+
+ expect(result.length).toEqual(0);
+ });
+
+ it('should return no results filtering by typeFk', async() => {
+ const ctx = {
+ req: {accessToken: {userId: 1}},
+ args: {
+ typeFk: 1
+ }
+ };
+ const result = await app.models.FixedPrice.filter(ctx);
+
+ expect(result.length).toEqual(1);
+ });
+});
diff --git a/modules/item/back/methods/fixed-price/specs/upsertFixedPrice.spec.js b/modules/item/back/methods/fixed-price/specs/upsertFixedPrice.spec.js
new file mode 100644
index 000000000..63c211293
--- /dev/null
+++ b/modules/item/back/methods/fixed-price/specs/upsertFixedPrice.spec.js
@@ -0,0 +1,62 @@
+const app = require('vn-loopback/server/server');
+
+describe('upsertFixedPrice()', () => {
+ const now = new Date();
+ const fixedPriceId = 1;
+ let originalFixedPrice;
+ let originalItem;
+
+ beforeAll(async() => {
+ originalFixedPrice = await app.models.FixedPrice.findById(fixedPriceId);
+ originalItem = await app.models.Item.findById(originalFixedPrice.itemFk);
+ });
+
+ afterAll(async() => {
+ await originalFixedPrice.save();
+ await originalItem.save();
+ });
+
+ it(`should toggle the hasMinPrice boolean if there's a minPrice and update the rest of the data`, async() => {
+ const ctx = {args: {
+ id: fixedPriceId,
+ itemFk: originalFixedPrice.itemFk,
+ warehouseFk: 1,
+ rate2: 100,
+ rate3: 300,
+ started: now,
+ ended: now,
+ minPrice: 100,
+ hasMinPrice: false
+ }};
+
+ const result = await app.models.FixedPrice.upsertFixedPrice(ctx, ctx.args.id);
+
+ delete ctx.args.started;
+ delete ctx.args.ended;
+ ctx.args.hasMinPrice = true;
+
+ expect(result).toEqual(jasmine.objectContaining(ctx.args));
+ });
+
+ it(`should toggle the hasMinPrice boolean if there's no minPrice and update the rest of the data`, async() => {
+ const ctx = {args: {
+ id: fixedPriceId,
+ itemFk: originalFixedPrice.itemFk,
+ warehouseFk: 1,
+ rate2: 2.5,
+ rate3: 2,
+ started: now,
+ ended: now,
+ minPrice: 0,
+ hasMinPrice: true
+ }};
+
+ const result = await app.models.FixedPrice.upsertFixedPrice(ctx, ctx.args.id);
+
+ delete ctx.args.started;
+ delete ctx.args.ended;
+ ctx.args.hasMinPrice = false;
+
+ expect(result).toEqual(jasmine.objectContaining(ctx.args));
+ });
+});
diff --git a/modules/item/back/methods/fixed-price/upsertFixedPrice.js b/modules/item/back/methods/fixed-price/upsertFixedPrice.js
new file mode 100644
index 000000000..dbdeebdab
--- /dev/null
+++ b/modules/item/back/methods/fixed-price/upsertFixedPrice.js
@@ -0,0 +1,116 @@
+module.exports = Self => {
+ Self.remoteMethod('upsertFixedPrice', {
+ description: 'Inserts or updates a fixed price for an item',
+ accessType: 'WRITE',
+ accepts: [{
+ arg: 'ctx',
+ type: 'object',
+ http: {source: 'context'}
+ },
+ {
+ arg: 'id',
+ type: 'number',
+ description: 'The fixed price id'
+ },
+ {
+ arg: 'itemFk',
+ type: 'number'
+ },
+ {
+ arg: 'warehouseFk',
+ type: 'number'
+ },
+ {
+ arg: 'started',
+ type: 'date'
+ },
+ {
+ arg: 'ended',
+ type: 'date'
+ },
+ {
+ arg: 'rate2',
+ type: 'number'
+ },
+ {
+ arg: 'rate3',
+ type: 'number'
+ },
+ {
+ arg: 'minPrice',
+ type: 'number'
+ },
+ {
+ arg: 'hasMinPrice',
+ type: 'any'
+ }],
+ returns: {
+ type: 'object',
+ root: true
+ },
+ http: {
+ path: `/upsertFixedPrice`,
+ verb: 'PATCH'
+ }
+ });
+
+ Self.upsertFixedPrice = async ctx => {
+ const models = Self.app.models;
+
+ const args = ctx.args;
+ const tx = await models.Address.beginTransaction({});
+ try {
+ const options = {transaction: tx};
+ delete args.ctx; // removed unwanted data
+
+ const fixedPrice = await models.FixedPrice.upsert(args, options);
+ const targetItem = await models.Item.findById(args.itemFk, null, options);
+ await targetItem.updateAttributes({
+ minPrice: args.minPrice,
+ hasMinPrice: args.minPrice ? true : false
+ }, options);
+
+ const itemFields = [
+ 'minPrice',
+ 'hasMinPrice',
+ 'name',
+ 'subName',
+ 'tag5',
+ 'value5',
+ 'tag6',
+ 'value6',
+ 'tag7',
+ 'value7',
+ 'tag8',
+ 'value8',
+ 'tag9',
+ 'value9',
+ 'tag10',
+ 'value10'
+ ];
+
+ const fieldsCopy = [].concat(itemFields);
+ const filter = {
+ include: {
+ relation: 'item',
+ scope: {
+ fields: fieldsCopy
+ }
+ }
+ };
+
+ const result = await models.FixedPrice.findById(fixedPrice.id, filter, options);
+ const item = result.item();
+
+ for (let key of itemFields)
+ result[key] = item[key];
+
+ await tx.commit();
+ return result;
+ } catch (e) {
+ await tx.rollback();
+ throw e;
+ }
+ };
+};
+
diff --git a/modules/item/back/methods/item/new.js b/modules/item/back/methods/item/new.js
index 1d3f6f230..e6288aede 100644
--- a/modules/item/back/methods/item/new.js
+++ b/modules/item/back/methods/item/new.js
@@ -60,7 +60,6 @@ module.exports = Self => {
query = `SET @isTriggerDisabled = FALSE`;
await Self.rawSql(query, null, options);
-
query = `CALL vn.itemRefreshTags(?)`;
await Self.rawSql(query, [item.id], options);
await tx.commit();
diff --git a/modules/item/back/model-config.json b/modules/item/back/model-config.json
index 13e30dc15..9f101f9c7 100644
--- a/modules/item/back/model-config.json
+++ b/modules/item/back/model-config.json
@@ -76,5 +76,8 @@
},
"TaxType": {
"dataSource": "vn"
+ },
+ "FixedPrice": {
+ "dataSource": "vn"
}
}
diff --git a/modules/item/back/models/fixed-price.js b/modules/item/back/models/fixed-price.js
new file mode 100644
index 000000000..9c78c586f
--- /dev/null
+++ b/modules/item/back/models/fixed-price.js
@@ -0,0 +1,4 @@
+module.exports = Self => {
+ require('../methods/fixed-price/filter')(Self);
+ require('../methods/fixed-price/upsertFixedPrice')(Self);
+};
diff --git a/modules/item/back/models/fixed-price.json b/modules/item/back/models/fixed-price.json
new file mode 100644
index 000000000..85e9194a3
--- /dev/null
+++ b/modules/item/back/models/fixed-price.json
@@ -0,0 +1,59 @@
+{
+ "name": "FixedPrice",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "priceFixed"
+ }
+ },
+ "properties": {
+ "id": {
+ "type": "number",
+ "id": true,
+ "description": "Identifier"
+ },
+ "itemFk": {
+ "type": "number",
+ "required": true
+ },
+ "warehouseFk": {
+ "type": "number"
+ },
+ "rate2": {
+ "type": "number",
+ "required": true
+ },
+ "rate3": {
+ "type": "number",
+ "required": true
+ },
+ "started": {
+ "type": "date",
+ "required": true
+ },
+ "ended": {
+ "type": "date",
+ "required": true
+ }
+ },
+ "relations": {
+ "item": {
+ "type": "belongsTo",
+ "model": "Item",
+ "foreignKey": "itemFk"
+ },
+ "warehouse": {
+ "type": "belongsTo",
+ "model": "Warehouse",
+ "foreignKey": "warehouseFk"
+ }
+ },
+ "acls": [
+ {
+ "accessType": "READ",
+ "principalType": "ROLE",
+ "principalId": "buyer",
+ "permission": "ALLOW"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/modules/item/back/models/item.json b/modules/item/back/models/item.json
index 96fec5161..d48e4e95d 100644
--- a/modules/item/back/models/item.json
+++ b/modules/item/back/models/item.json
@@ -12,119 +12,119 @@
},
"properties": {
"id": {
- "type": "Number",
+ "type": "number",
"id": true,
"description": "Id"
},
"name": {
- "type": "String",
+ "type": "string",
"description": "Name"
},
"size": {
- "type": "Number",
+ "type": "number",
"description": "Size"
},
"category": {
- "type": "String",
+ "type": "string",
"description": "Category"
},
"typeFk": {
- "type": "Number",
+ "type": "number",
"description": "Type",
"required": true
},
"stems": {
- "type": "Number",
+ "type": "number",
"description": "Stems"
},
"description": {
- "type": "String",
+ "type": "string",
"description": "Description"
},
"isOnOffer": {
- "type": "Boolean",
+ "type": "boolean",
"description": "Offer"
},
"isBargain": {
- "type": "Boolean",
+ "type": "boolean",
"description": "Bargain"
},
"isActive": {
- "type": "Boolean",
+ "type": "boolean",
"description": "Active"
},
"comment": {
- "type": "String",
+ "type": "string",
"description": "Comment"
},
"relevancy": {
- "type": "Number",
+ "type": "number",
"description": "Relevancy"
},
"density": {
- "type": "Number",
+ "type": "number",
"description": "Density"
},
"stemMultiplier": {
- "type": "Number",
+ "type": "number",
"description": "Multiplier"
},"image": {
- "type": "String",
+ "type": "string",
"description": "Image"
},
"longName": {
- "type": "String",
+ "type": "string",
"description": "Long name"
},
"subName": {
- "type": "String",
+ "type": "string",
"description": "Subname"
},
"tag5": {
- "type": "String"
+ "type": "string"
},
"value5": {
- "type": "String"
+ "type": "string"
},
"tag6": {
- "type": "String"
+ "type": "string"
},
"value6": {
- "type": "String"
+ "type": "string"
},
"tag7": {
- "type": "String"
+ "type": "string"
},
"value7": {
- "type": "String"
+ "type": "string"
},
"tag8": {
- "type": "String"
+ "type": "string"
},
"value8": {
- "type": "String"
+ "type": "string"
},
"tag9": {
- "type": "String"
+ "type": "string"
},
"value9": {
- "type": "String"
+ "type": "string"
},
"tag10": {
- "type": "String"
+ "type": "string"
},
"value10": {
- "type": "String"
+ "type": "string"
},
"compression": {
- "type": "Number"
+ "type": "number"
},
"hasKgPrice": {
- "type": "Boolean",
+ "type": "boolean",
"description": "Price per Kg"
},
"expenseFk": {
- "type": "Number",
+ "type": "number",
"mysql": {
"columnName": "expenceFk"
}
@@ -132,8 +132,11 @@
"minPrice": {
"type": "number"
},
+ "hasMinPrice": {
+ "type": "boolean"
+ },
"isFragile": {
- "type": "Boolean"
+ "type": "boolean"
}
},
"relations": {
diff --git a/modules/item/front/fixed-price-search-panel/index.html b/modules/item/front/fixed-price-search-panel/index.html
new file mode 100644
index 000000000..5a1e7781e
--- /dev/null
+++ b/modules/item/front/fixed-price-search-panel/index.html
@@ -0,0 +1,136 @@
+