Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2603-travel_create_autocomplete_date

This commit is contained in:
Jorge Padawan 2021-02-01 09:05:06 +01:00
commit 7408d22b66
142 changed files with 3800 additions and 1697 deletions

View File

@ -0,0 +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');

View File

@ -0,0 +1,2 @@
ALTER TABLE `vn`.`itemImageQueue`
ADD attempts INT default 0 NULL AFTER error;

View File

@ -3,4 +3,4 @@ host = localhost
port = 3306
user = root
password = root
default-character-set=utf8
default-character-set=utf8

File diff suppressed because one or more lines are too long

View File

@ -770,25 +770,25 @@ INSERT INTO `vn`.`intrastat`(`id`, `description`, `taxClassFk`, `taxCodeFk`)
(05080000, 'Coral y materiales similares', 2, 2),
(06021010, 'Plantas vivas: Esqueje/injerto, Vid', 1, 1);
INSERT INTO `vn`.`item`(`id`, `typeFk`, `size`, `inkFk`, `stems`, `originFk`, `description`, `producerFk`, `intrastatFk`, `isOnOffer`, `expenceFk`, `isBargain`, `comment`, `relevancy`, `image`, `taxClassFk`, `subName`, `minPrice`)
INSERT INTO `vn`.`item`(`id`, `typeFk`, `size`, `inkFk`, `stems`, `originFk`, `description`, `producerFk`, `intrastatFk`, `isOnOffer`, `expenceFk`, `isBargain`, `comment`, `relevancy`, `image`, `taxClassFk`, `subName`, `minPrice`, `stars`)
VALUES
(1, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, '1', 1, NULL, 0),
(2, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, '2', 1, NULL, 0),
(3, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 0, 4751000000, 0, NULL, 0, '3', 1, NULL, 0),
(4, 1, 60, 'YEL', 1, 1, 'Increases block', 1, 05080000, 1, 4751000000, 0, NULL, 0, '4', 2, NULL, 0),
(5, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 1, 4751000000, 0, NULL, 0, '5', 2, NULL, 0),
(6, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '6', 2, NULL, 0),
(7, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '7', 2, NULL, 0),
(8, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, '8', 1, NULL, 0),
(9, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, '9', 1, NULL, 0),
(10, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 0, 4751000000, 0, NULL, 0, '10', 1, NULL, 0),
(11, 1, 60, 'YEL', 1, 1, NULL, 1, 05080000, 1, 4751000000, 0, NULL, 0, '11', 2, NULL, 0),
(12, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 1, 4751000000, 0, NULL, 0, '12', 2, NULL, 0),
(13, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '13', 2, NULL, 0),
(14, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '', 2, NULL, 0),
(15, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '', 2, NULL, 0),
(16, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '', 2, NULL, 0),
(71, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 1, 4751000000, 0, NULL, 0, '', 2, NULL, 0);
(1, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, '1', 1, NULL, 0, 1),
(2, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, '2', 1, NULL, 0, 2),
(3, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 0, 4751000000, 0, NULL, 0, '3', 1, NULL, 0, 5),
(4, 1, 60, 'YEL', 1, 1, 'Increases block', 1, 05080000, 1, 4751000000, 0, NULL, 0, '4', 2, NULL, 0, 3),
(5, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 1, 4751000000, 0, NULL, 0, '5', 2, NULL, 0, 3),
(6, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '6', 2, NULL, 0, 4),
(7, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '7', 2, NULL, 0, 4),
(8, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, '8', 1, NULL, 0, 5),
(9, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, '9', 1, NULL, 0, 4),
(10, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 0, 4751000000, 0, NULL, 0, '10', 1, NULL, 0, 4),
(11, 1, 60, 'YEL', 1, 1, NULL, 1, 05080000, 1, 4751000000, 0, NULL, 0, '11', 2, NULL, 0, 4),
(12, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 1, 4751000000, 0, NULL, 0, '12', 2, NULL, 0, 3),
(13, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '13', 2, NULL, 0, 2),
(14, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '', 2, NULL, 0, 4),
(15, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '', 2, NULL, 0, 0),
(16, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '', 2, NULL, 0, 0),
(71, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 1, 4751000000, 0, NULL, 0, '', 2, NULL, 0, 0);
INSERT INTO `vn`.`priceFixed`(`id`, `itemFk`, `rate0`, `rate1`, `rate2`, `rate3`, `started`, `ended`, `bonus`, `warehouseFk`, `created`)
VALUES

File diff suppressed because it is too large Load Diff

View File

@ -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"]',
@ -420,6 +432,7 @@ export default {
moreMenuDeleteTicket: '.vn-menu [name="deleteTicket"]',
moreMenuRestoreTicket: '.vn-menu [name="restoreTicket"]',
moreMenuMakeInvoice: '.vn-menu [name="makeInvoice"]',
moreMenuRegenerateInvoice: '.vn-menu [name="regenerateInvoice"]',
moreMenuChangeShippedHour: '.vn-menu [name="changeShipped"]',
moreMenuPaymentSMS: '.vn-menu [name="sendPaymentSms"]',
moreMenuSendImportSms: '.vn-menu [name="sendImportSms"]',
@ -721,9 +734,9 @@ export default {
},
workerSummary: {
header: 'vn-worker-summary h5',
id: 'vn-worker-summary vn-one:nth-child(1) > vn-label-value:nth-child(2) > section > span',
email: 'vn-worker-summary vn-one:nth-child(1) > vn-label-value:nth-child(3) > section > span',
department: 'vn-worker-summary vn-one:nth-child(1) > vn-label-value:nth-child(4) > section > span',
id: 'vn-worker-summary vn-one:nth-child(1) > vn-label-value:nth-child(3) > section > span',
email: 'vn-worker-summary vn-one:nth-child(1) > vn-label-value:nth-child(4) > section > span',
department: 'vn-worker-summary vn-one:nth-child(1) > vn-label-value:nth-child(5) > section > span',
userId: 'vn-worker-summary vn-one:nth-child(2) > vn-label-value:nth-child(2) > section > span',
userName: 'vn-worker-summary vn-one:nth-child(2) > vn-label-value:nth-child(3) > section > span',
role: 'vn-worker-summary vn-one:nth-child(2) > vn-label-value:nth-child(4) > section > span',
@ -793,13 +806,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,11 +841,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',
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: {
@ -866,6 +881,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: {
@ -896,6 +912,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"]',

View File

@ -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() => {

View File

@ -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');
});
});

View File

@ -129,10 +129,10 @@ describe('Ticket descriptor path', () => {
});
describe('Make invoice', () => {
it('should login as adminBoss role then search for a ticket', async() => {
it('should login as administrative role then search for a ticket', async() => {
const invoiceableTicketId = '14';
await page.loginAndModule('adminBoss', 'ticket');
await page.loginAndModule('administrative', 'ticket');
await page.accessToSearchResult(invoiceableTicketId);
await page.waitForState('ticket.card.summary');
});
@ -160,6 +160,18 @@ describe('Ticket descriptor path', () => {
expect(result).toEqual('T4444445');
});
it(`should regenerate the invoice using the descriptor menu`, async() => {
const expectedMessage = 'Invoice sent for a regeneration, will be available in a few minutes';
await page.waitToClick(selectors.ticketDescriptor.moreMenu);
await page.waitForContentLoaded();
await page.waitToClick(selectors.ticketDescriptor.moreMenuRegenerateInvoice);
await page.respondToDialog('accept');
const message = await page.waitForSnackbar();
expect(message.text).toContain(expectedMessage);
});
});
describe('SMS', () => {

View File

@ -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);

View File

@ -16,19 +16,24 @@ describe('Travel extra community path', () => {
await browser.close();
});
it('should edit the travel reference', async() => {
it('should edit the travel reference and the locked kilograms', async() => {
await page.waitToClick(selectors.travelExtraCommunity.removeContinentFilter);
await page.waitForSpinnerLoad();
await page.writeOnEditableTD(selectors.travelExtraCommunity.firstTravelReference, 'edited reference');
await page.waitForSpinnerLoad();
await page.writeOnEditableTD(selectors.travelExtraCommunity.firstTravelLockedKg, '1500');
await page.waitForTimeout(1000);
});
it('should reload the index and confirm the reference was edited', async() => {
it('should reload the index and confirm the reference and locked kg were edited', async() => {
await page.accessToSection('travel.index');
await page.accessToSection('travel.extraCommunity');
await page.waitToClick(selectors.travelExtraCommunity.removeContinentFilter);
await page.waitForTextInElement(selectors.travelExtraCommunity.firstTravelReference, 'edited reference');
const reference = await page.getProperty(selectors.travelExtraCommunity.firstTravelReference, 'innerText');
const lockedKg = await page.getProperty(selectors.travelExtraCommunity.firstTravelLockedKg, 'innerText');
expect(reference).toContain('edited reference');
expect(lockedKg).toContain(1500);
});
});

View File

@ -51,3 +51,4 @@ import './treeview';
import './wday-picker';
import './datalist';
import './contextmenu';
import './rating';

View File

@ -0,0 +1,5 @@
<div>
<vn-icon ng-repeat="star in ::$ctrl.stars" ng-class="::{active: star.isActive}"
icon="star_rate">
</vn-icon>
</div>

View File

@ -0,0 +1,39 @@
import ngModule from '../../module';
import FormInput from '../form-input';
import './style.scss';
export default class Rating extends FormInput {
constructor($element, $scope) {
super($element, $scope);
this.maxStars = 5;
this.stars = [];
}
get field() {
return super.field;
}
set field(value) {
super.field = value;
this.populateStars();
}
populateStars() {
for (let i = 0; i < this.maxStars; i++) {
const star = {isActive: false};
if (i < this.field)
star.isActive = true;
this.stars.push(star);
}
}
}
ngModule.vnComponent('vnRating', {
template: require('./index.html'),
controller: Rating,
bindings: {
maxStars: '<?',
}
});

View File

@ -0,0 +1,38 @@
describe('Component vnRating', () => {
let $element;
let $ctrl;
beforeEach(ngModule('vnCore'));
beforeEach(inject(($compile, $rootScope) => {
$element = $compile(`<vn-rating ng-model="$ctrl.stars"></vn-rating>`)($rootScope);
$ctrl = $element.controller('vnRating');
}));
afterEach(() => {
$element.remove();
});
describe('field() setter', () => {
it(`should change field value and then call the populateStars() method`, () => {
jest.spyOn($ctrl, 'populateStars');
$ctrl.field = 5;
expect($ctrl.populateStars).toHaveBeenCalledWith();
expect($ctrl.stars.length).toEqual(5);
});
});
describe('populateStars()', () => {
it(`should populate the stars array and mark four of them as active`, () => {
jest.spyOn($ctrl, 'populateStars');
$ctrl.field = 4;
const activeStars = $ctrl.stars.filter(star => star.isActive);
expect(activeStars.length).toEqual(4);
});
});
});

View File

@ -0,0 +1,11 @@
@import "variables";
vn-rating {
vn-icon {
color: $color-primary-light
}
vn-icon.active {
color: $color-primary
}
}

View File

@ -85,6 +85,25 @@ vn-table {
max-width: 400px;
min-width: 0;
}
&[vn-fetched-tags] {
width: 235px;
min-width: 155px;
& > vn-one {
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.75rem;
}
& > vn-one:nth-child(2) h3 {
color: $color-font-secondary;
text-transform: uppercase;
line-height: initial;
font-size: 0.75rem
}
}
&[vn-fetched-tags][wide] {
width: 430px;
}
vn-icon.bright, i.bright {
color: #f7931e;
}

View File

@ -61,6 +61,8 @@ $color-hover-cd: rgba(0, 0, 0, .1);
$color-hover-dc: .7;
$color-disabled: .6;
$color-primary-medium: lighten($color-primary, 20%);
$color-primary-light: lighten($color-primary, 35%);
$color-font-link-medium: lighten($color-font-link, 20%);
$color-font-link-light: lighten($color-font-link, 35%);
$color-main-medium: lighten($color-main, 20%);

View File

@ -11,8 +11,8 @@ vn-descriptor-content {
& > img[ng-src] {
min-height: 16em;
display: block;
height: 100%;
width: 100%;
width: 256px;
height: 256px;
}
vn-float-button {

View File

@ -54,13 +54,15 @@
flex: 1;
}
& > .tags {
padding-bottom: 3px;
height: 48px;
& > vn-label-value {
font-size: .75rem;
}
}
vn-rating vn-icon {
font-size: 1rem
}
}
.footer {
font-size: .8rem;

View File

@ -168,5 +168,6 @@
"New ticket request has been created with price": "Se ha creado una nueva petición de compra '{{description}}' para el día <strong>{{shipped}}</strong>, con una cantidad de <strong>{{quantity}}</strong> y un precio de <strong>{{price}} €</strong>",
"New ticket request has been created": "Se ha creado una nueva petición de compra '{{description}}' para el día <strong>{{shipped}}</strong>, con una cantidad de <strong>{{quantity}}</strong>",
"Swift / BIC cannot be empty": "Swift / BIC no puede estar vacío",
"This BIC already exist.": "Este BIC ya existe."
"This BIC already exist.": "Este BIC ya existe.",
"That item doesn't exists": "Ese artículo no existe"
}

View File

@ -31,6 +31,9 @@
{"state": "account.alias.card.users", "icon": "groups"}
]
},
"keybindings": [
{"key": "u", "state": "account.index"}
],
"routes": [
{
"url": "/account",

View File

@ -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',

View File

@ -64,12 +64,15 @@
</span>
</vn-td>
<vn-td expand>{{::sale.shipped | date: 'dd/MM/yyyy'}}</vn-td>
<vn-td expand>
<vn-td vn-fetched-tags wide>
<vn-one title="{{::sale.concept}}">{{::sale.concept}}</vn-one>
<vn-one ng-if="::sale.subName">
<h3 title="{{::sale.subName}}">{{::sale.subName}}</h3>
</vn-one>
<vn-fetched-tags
max-length="6"
item="::sale"
name="::sale.concept"
sub-name="::sale.subName">
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td number>{{::sale.quantity | dashIfEmpty}}</vn-td>

View File

@ -93,7 +93,12 @@ module.exports = Self => {
let where = buildFilter(ctx.args, (param, value) => {
switch (param) {
case 'search':
return {'e.id': value};
return /^\d+$/.test(value)
? {'e.id': value}
: {or: [
{'s.name': {like: `%${value}%`}},
{'s.nickname': {like: `%${value}%`}}
]};
case 'ref':
param = `e.${param}`;
return {[param]: {like: `%${value}%`}};
@ -141,6 +146,7 @@ module.exports = Self => {
e.invoiceInFk,
t.landed,
s.name AS supplierName,
s.nickname AS supplierAlias,
co.code AS companyCode,
cu.code AS currencyCode
FROM vn.entry e

View File

@ -97,12 +97,15 @@
{{::buy.description | dashIfEmpty}}
</vn-td>
<vn-td number>{{::buy.size}}</vn-td>
<vn-td expand>
<vn-td vn-fetched-tags>
<vn-one title="{{::buy.name}}">{{::buy.name}}</vn-one>
<vn-one ng-if="::buy.subName">
<h3 title="{{::buy.subName}}">{{::buy.subName}}</h3>
</vn-one>
<vn-fetched-tags
max-length="6"
item="::buy"
name="::buy.name"
sub-name="::buy.subName">
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td shrink title="{{::buy.type}}">

View File

@ -8,11 +8,14 @@
<vn-searchbar
vn-focus
panel="vn-entry-search-panel"
info="Search entrys by id"
info="Search entry by id or a suppliers by name or alias"
model="model">
</vn-searchbar>
</vn-portal>
<vn-portal slot="menu">
<vn-left-menu></vn-left-menu>
</vn-portal>
<ui-view></ui-view>
<ui-view></ui-view>

View File

@ -16,6 +16,9 @@
{"state": "entry.card.log", "icon": "history"}
]
},
"keybindings": [
{"key": "e", "state": "entry.index"}
],
"routes": [
{
"url": "/entry",

View File

@ -5,7 +5,7 @@
vn-one
label="General search"
ng-model="filter.search"
info="Search entries by id"
info="Search entry by id or a suppliers by name or alias"
vn-focus>
</vn-textfield>
</vn-horizontal>
@ -45,8 +45,11 @@
label="Supplier"
ng-model="filter.supplierFk"
url="Suppliers"
fields="['name','nickname']"
search-function="{or: [{nickname: {like: '%'+ $search +'%'}}, {name: {like: '%'+ $search +'%'}}]}"
show-field="name"
value-field="id">
<tpl-item>{{name}}: {{nickname}}</tpl-item>
</vn-autocomplete>
<vn-date-picker
vn-one

View File

@ -4,4 +4,5 @@ Nickname: Alias
From: Desde
To: Hasta
Agency: Agencia
Warehouse: Almacén
Warehouse: Almacén
Search entry by id or a suppliers by name or alias: Buscar entrada por id o proveedores por nombre y alias

View File

@ -142,12 +142,12 @@
{{::line.item.minPrice | currency: 'EUR':2}}
</span>
</td>
<td expand colspan="6">
<td vn-fetched-tags colspan="6">
<vn-one title="{{::line.item.name}}">{{::line.item.name}}</vn-one>
<vn-fetched-tags
expand
max-length="6"
item="::line.item"
name="::line.item.name"
sub-name="::line.item.subName">
tabindex="-1">
</vn-fetched-tags>
</td>
</tr>

View File

@ -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];
};
};

View File

@ -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);
});
});

View File

@ -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));
});
});

View File

@ -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;
}
};
};

View File

@ -18,39 +18,50 @@ module.exports = Self => {
Self.downloadImages = async() => {
const models = Self.app.models;
const container = await models.TempContainer.container('salix-image');
const tempPath = path.join(container.client.root, container.name);
const maxAttempts = 3;
try {
const tempPath = path.join('/tmp/salix-image');
const images = await Self.find({
where: {attempts: {eq: maxAttempts}}
});
// Create temporary path
await fs.mkdir(tempPath, {recursive: true});
for (let image of images) {
const currentStamp = new Date().getTime();
const updatedStamp = image.updated.getTime();
const graceTime = Math.abs(currentStamp - updatedStamp);
const maxTTL = 3600 * 48 * 1000; // 48 hours in ms;
const timer = setInterval(async() => {
const image = await Self.findOne({
where: {error: null, url: {neq: null}}
});
if (graceTime >= maxTTL)
await Self.destroyById(image.itemFk);
}
// Exit loop
if (!image) return clearInterval(timer);
download();
const srcFile = image.url.split('/').pop();
const fileName = srcFile.split('.')[0];
const file = `${fileName}.png`;
const filePath = path.join(tempPath, file);
async function download() {
const image = await Self.findOne({
where: {url: {neq: null}, attempts: {lt: maxAttempts}},
order: 'attempts, updated'
});
if (!image) return;
const srcFile = image.url.split('/').pop();
const dotIndex = srcFile.lastIndexOf('.');
const fileName = srcFile.substring(0, dotIndex);
const file = `${fileName}.png`;
const filePath = path.join(tempPath, file);
https.get(image.url, async response => {
if (response.statusCode != 200) {
const error = new Error(`Could not download the image. Status code ${response.statusCode}`);
return await errorHandler(image.itemFk, error, filePath);
}
const writeStream = fs.createWriteStream(filePath);
writeStream.on('open', () => {
https.get(image.url, async response => {
if (response.statusCode != 200) {
const error = new Error(`Could not download the image. Status code ${response.statusCode}`);
return await errorHandler(image.itemFk, error, filePath);
}
response.pipe(writeStream);
}).on('error', async error => {
await errorHandler(image.itemFk, error, filePath);
});
response.pipe(writeStream);
});
writeStream.on('error', async error => {
@ -58,31 +69,44 @@ module.exports = Self => {
});
writeStream.on('finish', async function() {
writeStream.end();
});
writeStream.on('close', async function() {
try {
await models.Image.registerImage('catalog', filePath, fileName, image.itemFk);
await image.destroy();
download();
} catch (error) {
await errorHandler(image.itemFk, error, filePath);
}
});
}, 1000);
} catch (error) {
throw new Error('Try-catch error: ', error);
}).on('error', async error => {
await errorHandler(image.itemFk, error, filePath);
});
}
async function errorHandler(rowId, error, filePath) {
try {
const row = await Self.findById(rowId);
if (!row)
throw new Error(`Could not update due error ${error}`);
if (!row) return;
await row.updateAttribute('error', error);
if (row.attempts < maxAttempts) {
await row.updateAttributes({
error: error,
attempts: row.attempts + 1,
updated: new Date()
});
}
if (filePath && fs.existsSync(filePath))
await fs.unlink(filePath);
download();
} catch (err) {
throw new Error(`ErrorHandler error: ${err}`);
throw new Error(`Image download failed: ${err}`);
}
}
};

View File

@ -119,6 +119,7 @@ module.exports = Self => {
ori.code AS origin,
ic.name AS category,
i.density,
i.stemMultiplier,
b.grouping,
b.packing,
itn.code AS niche, @visibleCalc

View File

@ -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();

View File

@ -76,5 +76,8 @@
},
"TaxType": {
"dataSource": "vn"
},
"FixedPrice": {
"dataSource": "vn"
}
}

View File

@ -0,0 +1,4 @@
module.exports = Self => {
require('../methods/fixed-price/filter')(Self);
require('../methods/fixed-price/upsertFixedPrice')(Self);
};

View File

@ -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"
}
]
}

View File

@ -9,17 +9,26 @@
},
"properties": {
"itemFk": {
"type": "Number",
"type": "number",
"id": true,
"description": "Identifier"
},
"url": {
"type": "String",
"type": "string",
"required": true
},
"error": {
"type": "String",
"type": "string",
"required": true
},
"attempts": {
"type": "number"
},
"created": {
"type": "date"
},
"updated": {
"type": "date"
}
},
"relations": {

View File

@ -12,116 +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"
},
"image": {
"type": "String",
"stemMultiplier": {
"type": "number",
"description": "Multiplier"
},"image": {
"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"
}
@ -129,8 +132,11 @@
"minPrice": {
"type": "number"
},
"hasMinPrice": {
"type": "boolean"
},
"isFragile": {
"type": "Boolean"
"type": "boolean"
}
},
"relations": {

View File

@ -1,8 +1,4 @@
<vn-horizontal>
<vn-one title="{{$ctrl.name}}">{{$ctrl.name}}</vn-one>
<vn-one ng-if="$ctrl.subName">
<h3 title="{{$ctrl.subName}}">{{$ctrl.subName}}</h3>
</vn-one>
<vn-auto>
<section
class="inline-tag ellipsize"

View File

@ -8,7 +8,5 @@ ngModule.vnComponent('vnFetchedTags', {
bindings: {
maxLength: '<',
item: '<',
name: '<?',
subName: '<?'
}
});

View File

@ -1,42 +1,30 @@
@import "variables";
vn-fetched-tags {
&.noTitle vn-one {
display: none !important;
}
& > vn-horizontal {
align-items: center;
& > vn-one {
overflow: hidden;
text-overflow: ellipsis;
min-width: 80px;
}
& > vn-auto {
flex-wrap: wrap;
& > vn-one:nth-child(2) h3 {
color: $color-font-secondary;
text-transform: uppercase;
line-height: initial;
text-align: center;
font-size: 1rem
& > .inline-tag {
margin: 1px;
}
}
& > vn-auto {
display: flex;
padding-left: 6px;
min-width: 192px;
& > .inline-tag {
display: inline-block;
color: $color-font-secondary;
margin-left: 6px;
text-align: center;
font-size: .75rem;
height: 20px;
height: 12px;
padding: 1px;
border-radius: 1px;
width: 64px;
min-width: 64px;
max-width: 64px;
flex: 1;
border: 1px solid $color-spacer;
&.empty {
@ -44,22 +32,5 @@ vn-fetched-tags {
}
}
}
@media screen and (max-width: 1600px) {
flex-direction: column;
& > vn-one {
padding-bottom: 3px
}
& > vn-auto {
white-space: initial;
padding-left: 0;
flex-wrap: wrap;
justify-content: center;
& > .inline-tag {
margin: 1px;
}
}
}
}
}

View File

@ -0,0 +1,136 @@
<vn-crud-model
url="Tags"
fields="['id','name','isFree', 'sourceTable']"
data="tags"
auto-load="true">
</vn-crud-model>
<div class="search-panel">
<form class="vn-pa-lg" ng-submit="$ctrl.onSearch()">
<vn-horizontal>
<vn-textfield
vn-one
label="General search"
ng-model="filter.search"
vn-focus>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
vn-one
url="ItemCategories"
label="Category"
show-field="name"
value-field="id"
ng-model="filter.categoryFk">
</vn-autocomplete>
<vn-autocomplete vn-one
url="ItemTypes"
label="Type"
where="{categoryFk: filter.categoryFk}"
show-field="name"
value-field="id"
ng-model="filter.typeFk"
fields="['categoryFk']"
include="'category'">
<tpl-item>
<div>{{name}}</div>
<div class="text-caption text-secondary">
{{category.name}}
</div>
</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
vn-one
ng-model="filter.buyerFk"
url="Clients/activeWorkersWithRole"
show-field="nickname"
search-function="{firstName: $search}"
value-field="id"
where="{role: 'buyer'}"
label="Buyer">
</vn-autocomplete>
<vn-autocomplete
vn-one
label="Warehouse"
ng-model="filter.warehouseFk"
url="Warehouses">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-date-picker
vn-one
label="Started"
ng-model="filter.started">
</vn-date-picker>
<vn-date-picker
vn-one
label="Ended"
ng-model="filter.ended">
</vn-date-picker>
</vn-horizontal>
<vn-horizontal>
<vn-check vn-one
triple-state="true"
label="For me"
ng-model="filter.mine">
</vn-check>
<vn-check vn-one
triple-state="true"
label="Minimum price"
ng-model="filter.hasMinPrice">
</vn-check>
</vn-horizontal>
<vn-horizontal class="vn-pt-sm">
<vn-one class="text-subtitle1" translate>
Tags
</vn-one>
<vn-icon-button
vn-none
vn-bind="+"
vn-tooltip="Add tag"
icon="add_circle"
ng-click="filter.tags.push({})">
</vn-icon-button>
</vn-horizontal>
<vn-horizontal ng-repeat="itemTag in filter.tags">
<vn-autocomplete vn-two vn-id="tag"
label="Tag"
initial-data="itemTag.tag"
ng-model="itemTag.tagFk"
data="tags"
show-field="name"
on-change="itemTag.value = null"
rule>
</vn-autocomplete>
<vn-textfield vn-three
ng-show="tag.selection.isFree || tag.selection.isFree == undefined"
vn-id="text"
label="Value"
ng-model="itemTag.value"
rule>
</vn-textfield>
<vn-autocomplete vn-three
ng-show="tag.selection.isFree === false"
url="{{'Tags/' + itemTag.tagFk + '/filterValue'}}"
search-function="{value: $search}"
label="Value"
ng-model="itemTag.value"
show-field="value"
value-field="value"
rule>
</vn-autocomplete>
<vn-icon-button
vn-none
vn-tooltip="Remove tag"
icon="delete"
ng-click="filter.tags.splice($index, 1)"
tabindex="-1">
</vn-icon-button>
</vn-horizontal>
<vn-horizontal class="vn-mt-lg">
<vn-submit label="Search"></vn-submit>
</vn-horizontal>
</form>
</div>

View File

@ -0,0 +1,19 @@
import ngModule from '../module';
import SearchPanel from 'core/components/searchbar/search-panel';
class Controller extends SearchPanel {
get filter() {
return this.$.filter;
}
set filter(value = {}) {
if (!value.tags) value.tags = [{}];
this.$.filter = value;
}
}
ngModule.vnComponent('vnFixedPriceSearchPanel', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,4 @@
Started: Inicio
Ended: Fin
Minimum price: Precio mínimo
Item ID: ID Artículo

View File

@ -0,0 +1,159 @@
<vn-crud-model
vn-id="model"
url="FixedPrices/filter"
limit="20"
data="prices"
auto-load="true"
order="itemFk">
</vn-crud-model>
<vn-crud-model
auto-load="true"
url="Warehouses"
data="warehouses"
order="name">
</vn-crud-model>
<vn-portal slot="topbar">
<vn-searchbar
auto-state="false"
panel="vn-fixed-price-search-panel"
info="Search prices by item ID or code"
placeholder="Search fixed prices"
filter="{}"
model="model">
</vn-searchbar>
</vn-portal>
<div class="vn-w-lg">
<vn-card>
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th field="itemFk" shrink>Item ID</vn-th>
<vn-th field="itemFk">Description</vn-th>
<vn-th field="warehouseFk">Warehouse</vn-th>
<vn-th field="rate2">P.P.U.</vn-th>
<vn-th field="rate3">P.P.P.</vn-th>
<vn-th field="minPrice">Min price</vn-th>
<vn-th field="started" style="width: 90px">Started</vn-th>
<vn-th field="ended" style="width: 90px">Ended</vn-th>
<vn-th shrink></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="price in prices">
<vn-td shrink>
<span
ng-if="price.itemFk"
ng-click="itemDescriptor.show($event, price.itemFk)"
class="link">
{{price.itemFk}}
</span>
<vn-autocomplete
class="dense"
ng-if="!price.itemFk"
vn-focus
url="Items"
ng-model="price.itemFk"
show-field="name"
value-field="id"
search-function="$ctrl.itemSearchFunc($search)"
on-change="$ctrl.upsertPrice(price)"
order="id DESC"
tabindex="1">
<tpl-item>
{{::id}} - {{::name}}
</tpl-item>
</vn-autocomplete>
</vn-td>
<vn-td vn-fetched-tags>
<vn-one title="{{price.name}}">{{price.name}}</vn-one>
<vn-one ng-if="price.subName">
<h3 title="{{price.subName}}">{{price.subName}}</h3>
</vn-one>
<vn-fetched-tags
max-length="6"
item="price"
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td>
<vn-autocomplete
vn-one
label="Warehouse"
ng-model="price.warehouseFk"
url="Warehouses"
on-change="$ctrl.upsertPrice(price)"
tabindex="2">
</vn-autocomplete>
</vn-td>
<vn-td-editable number>
<text>{{price.rate2 | currency: 'EUR':2}}</text>
<field>
<vn-input-number
class="dense"
vn-focus
ng-model="price.rate2"
on-change="$ctrl.upsertPrice(price)">
</vn-input-number>
</field>
</vn-td-editable>
<vn-td-editable number>
<text>{{price.rate3 | currency: 'EUR':2}}</text>
<field>
<vn-input-number
class="dense"
vn-focus
ng-model="price.rate3"
on-change="$ctrl.upsertPrice(price)">
</vn-input-number>
</field>
</vn-td-editable>
<vn-td-editable number>
<text>{{(price.hasMinPrice ? (price.minPrice | currency: 'EUR':2) : "-")}}</text>
<field>
<vn-input-number
class="dense"
vn-focus
ng-model="price.minPrice"
on-change="$ctrl.upsertPrice(price)">
</vn-input-number>
</field>
</vn-td-editable>
<vn-td>
<vn-date-picker
vn-one
label="Started"
ng-model="price.started"
on-change="$ctrl.upsertPrice(price)">
</vn-date-picker>
</vn-td>
<vn-td>
<vn-date-picker
vn-one
label="Ended"
ng-model="price.ended"
on-change="$ctrl.upsertPrice(price)">
</vn-date-picker>
</vn-td>
<vn-td shrink>
<vn-icon-button
icon="delete"
vn-tooltip="Delete"
ng-click="$ctrl.removePrice($index)">
</vn-icon-button>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
<div class="vn-pa-md">
<vn-icon-button
vn-tooltip="Add fixed price"
icon="add_circle"
vn-bind="+"
ng-click="model.insert()">
</vn-icon-button>
</div>
</vn-card>
</div>
<vn-item-descriptor-popover
vn-id="itemDescriptor">
</vn-item-descriptor-popover>

View File

@ -0,0 +1,54 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
export default class Controller extends Section {
constructor($element, $) {
super($element, $);
}
/**
* Inserts a new instance
*/
add() {
this.$.model.insert({});
}
upsertPrice(price) {
price.hasMinPrice = price.minPrice ? true : false;
let requiredFields = ['itemFk', 'started', 'ended', 'rate2', 'rate3'];
for (let field of requiredFields)
if (price[field] == undefined) return;
const query = 'FixedPrices/upsertFixedPrice';
this.$http.patch(query, price)
.then(res => {
this.vnApp.showSuccess(this.$t('Data saved!'));
Object.assign(price, res.data);
});
}
removePrice($index) {
const price = this.$.model.data[$index];
if (price.id) {
this.$http.delete(`FixedPrices/${price.id}`)
.then(() => {
this.$.model.remove($index);
this.vnApp.showSuccess(this.$t('Data saved!'));
});
} else
this.$.model.remove($index);
}
itemSearchFunc($search) {
return /^\d+$/.test($search)
? {id: $search}
: {name: {like: '%' + $search + '%'}};
}
}
ngModule.vnComponent('vnFixedPrice', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,87 @@
import './index';
describe('fixed price', () => {
describe('Component vnFixedPrice', () => {
let controller;
let $httpBackend;
beforeEach(ngModule('item'));
beforeEach(inject(($componentController, _$httpBackend_, $rootScope) => {
$httpBackend = _$httpBackend_;
const $scope = $rootScope.$new();
const $element = angular.element('<vn-fixed-price></vn-fixed-price>');
controller = $componentController('vnFixedPrice', {$element, $scope});
}));
describe('upsertPrice()', () => {
it('should do nothing if one or more required arguments are missing', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
controller.upsertPrice({});
expect(controller.vnApp.showSuccess).not.toHaveBeenCalled();
});
it('should perform an http request to update the price', () => {
const now = new Date();
jest.spyOn(controller.vnApp, 'showSuccess');
$httpBackend.expectPATCH('FixedPrices/upsertFixedPrice').respond();
controller.upsertPrice({
itemFk: 1,
started: now,
ended: now,
rate2: 1,
rate3: 2
});
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
describe('removePrice()', () => {
it(`should only remove the created instance by the model as it doesn't have an ID yet`, () => {
const $index = 0;
controller.$ = {
model: {
remove: () => {},
data: [{
foo: 'bar'
}]
}
};
jest.spyOn(controller.vnApp, 'showSuccess');
jest.spyOn(controller.$.model, 'remove');
controller.removePrice($index);
expect(controller.vnApp.showSuccess).not.toHaveBeenCalled();
expect(controller.$.model.remove).toHaveBeenCalled();
});
it('should remove the instance performing an delete http request', () => {
const $index = 0;
controller.$ = {
model: {
remove: () => {},
data: [{
id: '1'
}]
}
};
jest.spyOn(controller.vnApp, 'showSuccess');
jest.spyOn(controller.$.model, 'remove');
const query = `FixedPrices/${controller.$.model.data[0].id}`;
$httpBackend.expectDELETE(query).respond();
controller.removePrice($index);
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
expect(controller.$.model.remove).toHaveBeenCalled();
});
});
});
});

View File

@ -0,0 +1,4 @@
Fixed prices: Precios fijados
Search prices by item ID or code: Buscar por ID de artículo o código
Search fixed prices: Buscar precios fijados
Add fixed price: Añadir precio fijado

View File

@ -0,0 +1,5 @@
@import "variables";
vn-table vn-date-picker {
max-width: 90px;
}

View File

@ -21,4 +21,6 @@ import './botanical';
import './barcode';
import './summary';
import './waste';
import './fixed-price';
import './fixed-price-search-panel';

View File

@ -15,7 +15,7 @@
<vn-th field="id" shrink>Id</vn-th>
<vn-th field="grouping" shrink>Grouping</vn-th>
<vn-th field="packing" shrink>Packing</vn-th>
<vn-th field="description" style="text-align: center">Description</vn-th>
<vn-th field="description">Description</vn-th>
<vn-th field="stems" shrink>Stems</vn-th>
<vn-th field="size" shrink>Size</vn-th>
<vn-th field="niche" shrink>Niche</vn-th>
@ -25,6 +25,7 @@
<vn-th field="origin" shrink>Origin</vn-th>
<vn-th field="salesperson" shrink>Buyer</vn-th>
<vn-th field="density" shrink>Density</vn-th>
<vn-th field="stemMultiplier" shrink>Multiplier</vn-th>
<vn-th field="active" shrink>Active</vn-th>
<vn-th></vn-th>
</vn-tr>
@ -49,12 +50,15 @@
</vn-td>
<vn-td shrink>{{::item.grouping | dashIfEmpty}}</vn-td>
<vn-td shrink>{{::item.packing | dashIfEmpty}}</vn-td>
<vn-td expand>
<vn-td vn-fetched-tags>
<vn-one title="{{::item.name}}">{{::item.name}}</vn-one>
<vn-one ng-if="::item.subName">
<h3 title="{{::item.subName}}">{{::item.subName}}</h3>
</vn-one>
<vn-fetched-tags
max-length="6"
item="::item"
name="::item.name"
sub-name="::item.subName">
item="item"
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td shrink>{{::item.stems}}</vn-td>
@ -78,6 +82,7 @@
</span>
</vn-td>
<vn-td shrink>{{::item.density}}</vn-td>
<vn-td shrink >{{::item.stemMultiplier}}</vn-td>
<vn-td shrink>
<vn-check
disabled="true"

View File

@ -21,9 +21,6 @@ vn-item-product {
vn-label-value:first-of-type section{
margin-top: 9px;
}
vn-fetched-tags vn-horizontal{
margin-top: 14px;
}
}
vn-table {

View File

@ -8,7 +8,8 @@
"main": [
{"state": "item.index", "icon": "icon-item"},
{"state": "item.request", "icon": "pan_tool"},
{"state": "item.waste", "icon": "icon-claims"}
{"state": "item.waste", "icon": "icon-claims"},
{"state": "item.fixedPrice", "icon": "contact_support"}
],
"card": [
{"state": "item.card.basicData", "icon": "settings"},
@ -32,22 +33,26 @@
"abstract": true,
"description": "Items",
"component": "vn-items"
}, {
},
{
"url": "/index?q",
"state": "item.index",
"component": "vn-item-index",
"description": "Items"
}, {
},
{
"url": "/create",
"state": "item.create",
"component": "vn-item-create",
"description": "New item"
}, {
},
{
"url": "/:id",
"state": "item.card",
"abstract": true,
"component": "vn-item-card"
}, {
},
{
"url" : "/basic-data",
"state": "item.card.basicData",
"component": "vn-item-basic-data",
@ -56,7 +61,8 @@
"item": "$ctrl.item"
},
"acl": ["buyer"]
}, {
},
{
"url" : "/tags",
"state": "item.card.tags",
"component": "vn-item-tags",
@ -65,13 +71,15 @@
"item-tags": "$ctrl.itemTags"
},
"acl": ["buyer", "replenisher"]
}, {
},
{
"url" : "/tax",
"state": "item.card.tax",
"component": "vn-item-tax",
"description": "Tax",
"acl": ["administrative","buyer"]
}, {
},
{
"url" : "/niche",
"state": "item.card.niche",
"component": "vn-item-niche",
@ -80,7 +88,8 @@
"item": "$ctrl.item"
},
"acl": ["buyer","replenisher"]
}, {
},
{
"url" : "/botanical",
"state": "item.card.botanical",
"component": "vn-item-botanical",
@ -89,7 +98,8 @@
"item": "$ctrl.item"
},
"acl": ["buyer"]
}, {
},
{
"url" : "/barcode",
"state": "item.card.itemBarcode",
"component": "vn-item-barcode",
@ -98,7 +108,8 @@
"item": "$ctrl.item"
},
"acl": ["buyer","replenisher"]
}, {
},
{
"url" : "/summary",
"state": "item.card.summary",
"component": "vn-item-summary",
@ -106,7 +117,8 @@
"params": {
"item": "$ctrl.item"
}
}, {
},
{
"url" : "/diary?warehouseFk&lineFk",
"state": "item.card.diary",
"component": "vn-item-diary",
@ -115,7 +127,8 @@
"item": "$ctrl.item"
},
"acl": ["employee"]
}, {
},
{
"url" : "/last-entries",
"state": "item.card.last-entries",
"component": "vn-item-last-entries",
@ -124,12 +137,14 @@
"item": "$ctrl.item"
},
"acl": ["employee"]
}, {
},
{
"url" : "/log",
"state": "item.card.log",
"component": "vn-item-log",
"description": "Log"
}, {
},
{
"url" : "/request?q",
"state": "item.request",
"component": "vn-item-request",
@ -138,12 +153,20 @@
"item": "$ctrl.item"
},
"acl": ["employee"]
}, {
},
{
"url" : "/waste",
"state": "item.waste",
"component": "vn-item-waste",
"description": "Waste breakdown",
"acl": ["buyer"]
},
{
"url" : "/fixed-price",
"state": "item.fixedPrice",
"component": "vn-fixed-price",
"description": "Fixed prices",
"acl": ["buyer"]
}
]
}

View File

@ -27,8 +27,8 @@
initial-data="itemTag.tag"
ng-model="itemTag.tagFk"
data="tags"
on-change="$ctrl.getSourceTable(tag)"
show-field="name"
on-change="itemTag.value = null"
rule>
</vn-autocomplete>
<vn-textfield vn-three

View File

@ -108,6 +108,7 @@ module.exports = Self => {
i.value7,
i.tag8,
i.value8,
i.stars,
tci.price,
tci.available,
w.lastName AS lastName,

View File

@ -155,7 +155,8 @@ module.exports = Self => {
co.code companyCode,
zed.zoneFk,
zed.hourTheoretical,
zed.hourEffective
zed.hourEffective,
am.name AS agencyName
FROM hedera.order o
LEFT JOIN address a ON a.id = o.address_id
LEFT JOIN agencyMode am ON am.id = o.agency_id

View File

@ -36,6 +36,9 @@
value="{{::item.value7}}">
</vn-label-value>
</div>
<vn-rating ng-if="::item.stars"
ng-model="::item.stars">
</vn-rating>
<div class="footer">
<div class="price">
<vn-one>

View File

@ -2,11 +2,9 @@ import ngModule from '../module';
import Component from 'core/lib/component';
import './style.scss';
class Controller extends Component {}
ngModule.vnComponent('vnOrderCatalogView', {
template: require('./index.html'),
controller: Controller,
controller: Component,
bindings: {
order: '<',
model: '<'

View File

@ -25,8 +25,15 @@ class Controller extends Section {
}
$onChanges() {
if (this.order && this.order.isConfirmed)
this.$state.go('order.card.line');
this.getData().then(() => {
if (this.order && this.order.isConfirmed)
this.$state.go('order.card.line');
});
}
getData() {
return this.$http.get(`Orders/${this.$params.id}`)
.then(res => this.order = res.data);
}
/**
@ -366,8 +373,5 @@ class Controller extends Section {
ngModule.vnComponent('vnOrderCatalog', {
template: require('./index.html'),
controller: Controller,
bindings: {
order: '<'
}
controller: Controller
});

View File

@ -28,6 +28,22 @@ describe('Order', () => {
};
}));
describe('getData()', () => {
it(`should make a query an fetch the order data`, () => {
controller._order = null;
$httpBackend.expect('GET', `Orders/4`).respond(200, {id: 4, isConfirmed: true});
$httpBackend.expect('GET', `Orders/4/getItemTypeAvailable?itemCategoryId=1`).respond();
controller.getData();
$httpBackend.flush();
const order = controller.order;
expect(order.id).toEqual(4);
expect(order.isConfirmed).toBeTruthy();
});
});
describe('order() setter', () => {
it(`should call scope $applyAsync() method and apply filters from state params`, () => {
$httpBackend.expect('GET', `Orders/4/getItemTypeAvailable?itemCategoryId=1`).respond();

View File

@ -15,8 +15,8 @@
<vn-th field="isConfirmed" center>Confirmed</vn-th>
<vn-th field="created" center expand>Created</vn-th>
<vn-th field="landed" default-order="DESC" center expand>Landed</vn-th>
<vn-th field="created" center translate-attr="{title: 'Theoretical hour'}">T. Hour</vn-th>
<vn-th field="created" center>Real hour</vn-th>
<vn-th field="created" center>Hour</vn-th>
<vn-th field="agencyName" center>Agency</vn-th>
<vn-th center>Total</vn-th>
</vn-tr>
</vn-thead>
@ -52,8 +52,11 @@
{{::order.landed | date:'dd/MM/yyyy'}}
</span>
</vn-td>
<vn-td shrink>{{::order.hourTheoretical | date: 'HH:mm' | dashIfEmpty}}</vn-td>
<vn-td shrink>{{::ticket.hourEffective | date: 'HH:mm' | dashIfEmpty}}</vn-td>
<vn-td shrink>{{::(order.hourTheoretical
? order.hourTheoretical
: order.hourEffective) | dashIfEmpty
}}</vn-td>
<vn-td expand>{{::order.agencyName}}</vn-td>
<vn-td number>{{::order.total | currency: 'EUR': 2 | dashIfEmpty}}</vn-td>
<vn-td shrink>
<vn-icon-button

View File

@ -21,7 +21,7 @@
<vn-th number>Id</vn-th>
<vn-th>Description</vn-th>
<vn-th>Warehouse</vn-th>
<vn-th expand>Shipped</vn-th>
<vn-th>Shipped</vn-th>
<vn-th number>Quantity</vn-th>
<vn-th number>Price</vn-th>
<vn-th number>Amount</vn-th>
@ -42,16 +42,19 @@
{{::row.itemFk | zeroFill:6}}
</span>
</vn-td>
<vn-td expand>
<vn-td vn-fetched-tags>
<vn-one title="{{::row.item.name}}">{{::row.item.name}}</vn-one>
<vn-one ng-if="::row.item.subName">
<h3 title="{{::row.item.subName}}">{{::row.item.subName}}</h3>
</vn-one>
<vn-fetched-tags
max-length="6"
item="::row.item"
name="::row.item.name"
sub-name="::row.item.subName">
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td shrink>{{::row.warehouse.name}}</vn-td>
<vn-td expand>{{::row.shipped | date: 'dd/MM/yyyy'}}</vn-td>
<vn-td>{{::row.warehouse.name}}</vn-td>
<vn-td>{{::row.shipped | date: 'dd/MM/yyyy'}}</vn-td>
<vn-td number>{{::row.quantity}}</vn-td>
<vn-td number>
{{::row.price | currency: 'EUR':2}}

View File

@ -98,12 +98,15 @@
{{::row.itemFk | zeroFill:6}}
</span>
</vn-td>
<vn-td expand>
<vn-td expand vn-fetched-tags>
<vn-one title="{{::row.item.name}}">{{::row.item.name}}</vn-one>
<vn-one ng-if="::row.item.subName">
<h3 title="{{::row.item.subName}}">{{::row.item.subName}}</h3>
</vn-one>
<vn-fetched-tags
max-length="6"
item="::row.item"
name="::row.item.name"
sub-name="::row.item.subName">
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td number>{{::row.quantity}}</vn-td>

View File

@ -24,31 +24,34 @@
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th field="itemFk" default-order="ASC" number>Item</vn-th>
<vn-th shrink field="itemFk" default-order="ASC" number>Item</vn-th>
<vn-th>Description</vn-th>
<vn-th field="quantity" number>Quantity</vn-th>
<vn-th number>m³ per quantity</vn-th>
<vn-th shrink field="quantity" number>Quantity</vn-th>
<vn-th shrink number>m³ per quantity</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="row in rows">
<vn-td number>
<vn-td shrink number>
<span
ng-click="descriptor.show($event, row.itemFk)"
class="link">
{{::row.itemFk}}
</span>
</vn-td>
<vn-td expand>
<vn-td wide vn-fetched-tags>
<vn-one title="{{::row.item.name}}">{{::row.item.name}}</vn-one>
<vn-one ng-if="::row.item.subName">
<h3 title="{{::row.item.subName}}">{{::row.item.subName}}</h3>
</vn-one>
<vn-fetched-tags
max-length="6"
item="::row.item"
name="::row.item.name"
sub-name="::row.item.subName">
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td number>{{::row.quantity}}</vn-td>
<vn-td number>{{::row.volume | number:3}}</vn-td>
<vn-td shrink number>{{::row.quantity}}</vn-td>
<vn-td shrink number>{{::row.volume | number:3}}</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>

View File

@ -18,7 +18,6 @@ describe('route updateVolume()', () => {
expect(route.m3).toEqual(1.8);
const ticket = await app.models.Ticket.findById(14);
await ticket.updateAttributes({routeFk: routeId});
await app.models.Route.updateVolume(ctx, routeId);
@ -29,7 +28,8 @@ describe('route updateVolume()', () => {
const logs = await app.models.RouteLog.find({fields: ['id', 'newInstance']});
const m3Log = logs.filter(log => {
return log.newInstance.m3 === updatedRoute.m3;
if (log.newInstance)
return log.newInstance.m3 === updatedRoute.m3;
});
const logIdToDestroy = m3Log[0].id;

View File

@ -37,7 +37,7 @@
<vn-thead>
<vn-tr>
<vn-th field="entryFk" expand>Entry </vn-th>
<vn-td expand>{{::entry.id}}</vn-td>
<vn-td>{{::entry.id}}</vn-td>
<vn-th field="data">Date</vn-th>
<vn-td>{{::entry.shipped | date: 'dd/MM/yyyy'}}</vn-td>
<vn-th field="ref">Reference</vn-th>
@ -51,10 +51,15 @@
{{::buy.itemName}}
</span>
</vn-td>
<vn-td expand>
<vn-td vn-fetched-tags wide>
<vn-one></vn-one>
<vn-one ng-if="::buy.subName">
<h3 title="{{::buy.subName}}">{{::buy.subName}}</h3>
</vn-one>
<vn-fetched-tags
max-length="6"
item="::buy">
item="::buy"
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td number>{{::buy.quantity | dashIfEmpty}}</vn-td>

View File

@ -18,6 +18,9 @@
{"state": "supplier.card.consumption", "icon": "show_chart"}
]
},
"keybindings": [
{"key": "p", "state": "supplier.index"}
],
"routes": [
{
"url": "/supplier",

View File

@ -60,7 +60,8 @@ describe('sale updatePrice()', () => {
await originalSalesPersonMana.updateAttributes(originalSalesPersonMana);
});
it('should set price as a decimal number and check the sale has the mana component changing the salesPersonMana', async() => {
// #2736 sale updatePrice() returns inconsistent values
xit('should set price as a decimal number and check the sale has the mana component changing the salesPersonMana', async() => {
let ctx = {req: {accessToken: {userId: 18}}};
let price = 5.4;

View File

@ -30,11 +30,11 @@ module.exports = Self => {
}, {
arg: 'attenderFk',
type: 'Number',
description: `Search requests atended by the given worker`
description: `Search requests attended by a given worker id`
}, {
arg: 'mine',
type: 'Boolean',
description: `Search requests attended by the connected worker`
description: `Search requests attended by the current user`
}, {
arg: 'from',
type: 'Date',
@ -62,10 +62,9 @@ module.exports = Self => {
Self.filter = async(ctx, filter) => {
let conn = Self.dataSource.connector;
let userId = ctx.req.accessToken.userId;
let worker = await Self.app.models.Worker.findOne({where: {userFk: userId}});
if (ctx.args.mine)
ctx.args.attenderFk = worker.id;
ctx.args.attenderFk = userId;
let where = buildFilter(ctx.args, (param, value) => {
switch (param) {

View File

@ -10,75 +10,75 @@ module.exports = Self => {
accepts: [
{
arg: 'ctx',
type: 'Object',
type: 'object',
http: {source: 'context'}
}, {
arg: 'filter',
type: 'Object',
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 id, otherwise it searchs by nickname`
type: 'string',
description: `If it's and number searchs by id, otherwise it searchs by nickname`
}, {
arg: 'from',
type: 'Date',
type: 'date',
description: `The from date filter`
}, {
arg: 'to',
type: 'Date',
type: 'date',
description: `The to date filter`
}, {
arg: 'nickname',
type: 'String',
type: 'string',
description: `The nickname filter`
}, {
arg: 'id',
type: 'Integer',
type: 'number',
description: `The ticket id filter`
}, {
arg: 'clientFk',
type: 'Integer',
type: 'number',
description: `The client id filter`
}, {
arg: 'agencyModeFk',
type: 'Integer',
type: 'number',
description: `The agency mode id filter`
}, {
arg: 'warehouseFk',
type: 'Integer',
type: 'number',
description: `The warehouse id filter`
}, {
arg: 'salesPersonFk',
type: 'Integer',
type: 'number',
description: `The salesperson id filter`
}, {
arg: 'provinceFk',
type: 'Integer',
type: 'number',
description: `The province id filter`
}, {
arg: 'stateFk',
type: 'Number',
type: 'number',
description: `The state id filter`
}, {
arg: 'myTeam',
type: 'Boolean',
type: 'boolean',
description: `Whether to show only tickets for the current logged user team (For now it shows only the current user tickets)`
}, {
arg: 'problems',
type: 'Boolean',
type: 'boolean',
description: `Whether to show only tickets with problems`
}, {
arg: 'pending',
type: 'Boolean',
type: 'boolean',
description: `Whether to show only tickets with state 'Pending'`
}, {
arg: 'mine',
type: 'Boolean',
type: 'boolean',
description: `Whether to show only tickets for the current logged user`
}, {
arg: 'orderFk',
type: 'Number',
type: 'number',
description: `The order id filter`
}, {
arg: 'refFk',
@ -86,12 +86,12 @@ module.exports = Self => {
description: `The invoice reference filter`
}, {
arg: 'alertLevel',
type: 'Number',
type: 'number',
description: `The alert level of the tickets`
}
],
returns: {
type: ['Object'],
type: ['object'],
root: true
},
http: {

View File

@ -1,5 +1,4 @@
const UserError = require('vn-loopback/util/user-error');
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
module.exports = function(Self) {
Self.remoteMethodCtx('makeInvoice', {
@ -26,52 +25,54 @@ module.exports = function(Self) {
});
Self.makeInvoice = async(ctx, id) => {
const conn = Self.dataSource.connector;
let userId = ctx.req.accessToken.userId;
let models = Self.app.models;
let tx = await Self.beginTransaction({});
const userId = ctx.req.accessToken.userId;
const models = Self.app.models;
const tx = await Self.beginTransaction({});
try {
let options = {transaction: tx};
const options = {transaction: tx};
let filter = {fields: ['id', 'clientFk', 'companyFk']};
let ticket = await models.Ticket.findById(id, filter, options);
const filter = {fields: ['id', 'clientFk', 'companyFk']};
const ticket = await models.Ticket.findById(id, filter, options);
let clientCanBeInvoiced = await models.Client.canBeInvoiced(ticket.clientFk);
const clientCanBeInvoiced = await models.Client.canBeInvoiced(ticket.clientFk);
if (!clientCanBeInvoiced)
throw new UserError(`This client can't be invoiced`);
let ticketCanBeInvoiced = await models.Ticket.canBeInvoiced(ticket.id);
const ticketCanBeInvoiced = await models.Ticket.canBeInvoiced(ticket.id);
if (!ticketCanBeInvoiced)
throw new UserError(`This ticket can't be invoiced`);
const query = `SELECT vn.invoiceSerial(?, ?, ?) AS serial`;
const [result] = await Self.rawSql(query, [ticket.clientFk, ticket.companyFk, 'R'], options);
const serial = result.serial;
let query = `SELECT vn.invoiceSerial(?, ?, ?) AS serial`;
let [result] = await Self.rawSql(query, [ticket.clientFk, ticket.companyFk, 'R'], options);
let serial = result.serial;
let stmts = [];
await Self.rawSql('CALL invoiceFromTicket(?)', [id], options);
await Self.rawSql('CALL invoiceOut_new(?, CURDATE(), null, @invoiceId)', [serial], options);
stmt = new ParameterizedSQL('CALL vn.invoiceOut_newFromTicket(?, ?, ?, @invoiceId)', [
ticket.id,
serial,
null
]);
stmts.push(stmt);
const [resultInvoice] = await Self.rawSql('SELECT @invoiceId id', [], options);
let invoiceIndex = stmts.push(`SELECT @invoiceId AS invoiceId`) - 1;
const invoiceId = resultInvoice.id;
const ticketInvoice = await models.Ticket.findById(id, {fields: ['refFk']}, options);
await models.TicketLog.create({
originFk: ticket.id,
userFk: userId,
action: 'insert',
changedModel: 'Ticket',
changedModelId: ticket.id,
newInstance: ticketInvoice
}, options);
let sql = ParameterizedSQL.join(stmts, ';');
result = await conn.executeStmt(sql);
let invoiceId = result[invoiceIndex][0].invoiceId;
if (serial != 'R' && invoiceId) {
query = `CALL vn.invoiceOutBooking(?)`;
await Self.rawSql(query, [invoiceId], options);
await Self.rawSql('CALL invoiceOutBooking(?)', [invoiceId], options);
await models.PrintServerQueue.create({
reportFk: 3, // Tarea #2734 (Nueva): crear informe facturas
param1: invoiceId,
workerFk: userId
}, options);
}
let user = await models.Worker.findOne({where: {userFk: userId}}, options);
query = `INSERT INTO printServerQueue(reportFk, param1, workerFk) VALUES (?, ?, ?)`;
await Self.rawSql(query, [3, invoiceId, user.id], options);
await tx.commit();
return {invoiceFk: invoiceId, serial};

View File

@ -17,6 +17,9 @@
"Packaging": {
"dataSource": "vn"
},
"PrintServerQueue": {
"dataSource": "vn"
},
"Sale": {
"dataSource": "vn"
},

View File

@ -0,0 +1,31 @@
{
"name": "PrintServerQueue",
"description": "Print server queue",
"base": "VnModel",
"options": {
"mysql": {
"table": "printServerQueue"
}
},
"properties": {
"id": {
"type": "number",
"id": true,
"description": "Identifier"
},
"priorityFk": {
"type": "number"
},
"reportFk": {
"type": "number",
"required": true
},
"param1": {
"type": "number"
},
"workerFk": {
"type": "number",
"required": true
}
}
}

View File

@ -18,11 +18,15 @@
<vn-tbody>
<vn-tr ng-repeat="sale in $ctrl.ticket.sale.items track by sale.id">
<vn-td number>{{("000000"+sale.itemFk).slice(-6)}}</vn-td>
<vn-td expand>
<vn-td vn-fetched-tags wide>
<vn-one title="{{::sale.item.name}}">{{::sale.item.name}}</vn-one>
<vn-one ng-if="::sale.item.subName">
<h3 title="{{::sale.item.subName}}">{{::sale.item.subName}}</h3>
</vn-one>
<vn-fetched-tags
max-length="6"
item="::sale.item"
name="::sale.concept">
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td number>{{::sale.quantity}}</vn-td>

View File

@ -29,12 +29,15 @@
{{sale.itemFk | zeroFill:6}}
</span>
</td>
<td rowspan="{{::sale.components.length + 1}}" expand>
<td rowspan="{{::sale.components.length + 1}}" vn-fetched-tags>
<vn-one title="{{::sale.item.name}}">{{::sale.item.name}}</vn-one>
<vn-one ng-if="::sale.item.subName">
<h3 title="{{::sale.item.subName}}">{{::sale.item.subName}}</h3>
</vn-one>
<vn-fetched-tags
max-length="6"
item="::sale.item"
name="::sale.concept"
sub-name="::sale.item.subName">
tabindex="-1">
</vn-fetched-tags>
</td>
<td rowspan="{{::sale.components.length + 1}}" number>

View File

@ -84,6 +84,7 @@
ng-show="$ctrl.isInvoiced"
vn-acl="invoicing"
vn-acl-action="remove"
name="regenerateInvoice"
translate>
Regenerate invoice
</vn-item>

View File

@ -60,7 +60,8 @@ class Controller extends Section {
}
},
{relation: 'ship'},
{relation: 'stowaway'}]
{relation: 'stowaway'},
{relation: 'invoiceOut'}]
};
return this.$http.get(`Tickets/${this.ticketId}`, {filter})

View File

@ -21,7 +21,7 @@ Discount: Descuento
Employee : Empleado
Import: Importe
Is checked: Comprobado
Item: Articulo
Item: Artículo
Landing: Llegada
Landed: F. entrega
More: Más

View File

@ -34,12 +34,15 @@
{{::sale.itemFk | zeroFill:6}}
</span>
</vn-td>
<vn-td expand>
<vn-td vn-fetched-tags>
<vn-one title="{{::sale.item.name}}">{{::sale.item.name}}</vn-one>
<vn-one ng-if="::sale.item.subName">
<h3 title="{{::sale.item.subName}}">{{::sale.item.subName}}</h3>
</vn-one>
<vn-fetched-tags
max-length="6"
item="::sale.item"
name="::sale.concept"
sub-name="::sale.item.subName">
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td number>{{::sale.quantity}}</vn-td>

View File

@ -8,7 +8,7 @@
auto-load="true">
</vn-crud-model>
<vn-data-viewer model="model">
<vn-card class="vn-w-xl">
<vn-card class="vn-w-lg">
<vn-table model="model">
<vn-thead>
<vn-tr>
@ -39,12 +39,15 @@
{{sale.itemFk | zeroFill:6}}
</span>
</vn-td>
<vn-td expand>
<vn-td vn-fetched-tags wide>
<vn-one title="{{::sale.item.name}}">{{::sale.item.name}}</vn-one>
<vn-one ng-if="::sale.item.subName">
<h3 title="{{::sale.item.subName}}">{{::sale.item.subName}}</h3>
</vn-one>
<vn-fetched-tags
max-length="6"
item="::sale.item"
name="::sale.concept"
sub-name="::sale.item.subName">
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td number>{{::sale.quantity}}</vn-td>

View File

@ -58,8 +58,8 @@
</vn-th>
<vn-th shrink></vn-th>
<vn-th shrink></vn-th>
<vn-th number id="ticketId">Id</vn-th>
<vn-th>Quantity</vn-th>
<vn-th shrink id="ticketId">Id</vn-th>
<vn-th shrink>Quantity</vn-th>
<vn-th>Item</vn-th>
<vn-th number>Price</vn-th>
<vn-th number>Disc</vn-th>
@ -97,7 +97,7 @@
zoom-image="{{::$root.imagePath('catalog', '1600x900', sale.itemFk)}}"
on-error-src/>
</vn-td>
<vn-td number>
<vn-td shrink>
<span class="link" ng-if="sale.id"
ng-click="descriptor.show($event, sale.itemFk, sale.id)">
{{sale.itemFk}}
@ -117,7 +117,7 @@
</tpl-item>
</vn-autocomplete>
</vn-td>
<vn-td-editable disabled="!$ctrl.isEditable" number>
<vn-td-editable disabled="!$ctrl.isEditable" shrink>
<text>{{sale.quantity}}</text>
<field>
<vn-input-number class="dense"
@ -127,13 +127,16 @@
</vn-input-number>
</field>
</vn-td-editable>
<vn-td-editable disabled="!sale.id || !$ctrl.isEditable" expand>
<vn-td-editable vn-fetched-tags wide disabled="!sale.id || !$ctrl.isEditable">
<text>
<vn-one title="{{sale.concept}}">{{sale.concept}}</vn-one>
<vn-one ng-if="::sale.subName">
<h3 title="{{::sale.subName}}">{{::sale.subName}}</h3>
</vn-one>
<vn-fetched-tags
max-length="6"
item="::sale.item"
name="sale.concept"
sub-name="::sale.subName">
tabindex="-1">
</vn-fetched-tags>
</text>
<field>

View File

@ -23,6 +23,25 @@ vn-ticket-sale {
margin: 3px;
}
}
vn-td-editable[vn-fetched-tags] {
& text {
max-width: 430px;
min-width: 150px;
& vn-one {
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.75rem;
}
& vn-one:nth-child(2) h3 {
color: $color-font-secondary;
text-transform: uppercase;
line-height: initial;
font-size: 0.75rem
}
}
}
vn-dialog.edit {
@extend .edit-popover;

View File

@ -6,6 +6,7 @@
auto-load="true">
</vn-crud-model>
<vn-crud-model
vn-id="typesModel"
auto-load="true"
url="TicketServiceTypes"
data="ticketServiceTypes"

View File

@ -35,6 +35,10 @@ class Controller extends Section {
throw new UserError(`Name can't be empty`);
return this.$http.post(`TicketServiceTypes`, this.$.newServiceType)
.then(res => {
this.$.typesModel.refresh();
return res;
})
.then(res => service.ticketServiceTypeFk = res.data.id);
}

View File

@ -1,4 +1,5 @@
import './index.js';
import crudModel from 'core/mocks/crud-model';
describe('Ticket component vnTicketService', () => {
let controller;
@ -11,7 +12,7 @@ describe('Ticket component vnTicketService', () => {
beforeEach(inject(($componentController, _$httpBackend_, $rootScope) => {
$httpBackend = _$httpBackend_;
$scope = $rootScope.$new();
$scope.typesModel = crudModel;
$element = angular.element(`<div></div>`);
controller = $componentController('vnTicketService', {$scope, $element});
}));

View File

@ -149,12 +149,15 @@
</span>
</vn-td>
<vn-td number shrink>{{::sale.quantity}}</vn-td>
<vn-td expand>
<vn-fetched-tags
max-length="6"
<vn-td vn-fetched-tags wide>
<vn-one title="{{::sale.item.name}}">{{::sale.item.name}}</vn-one>
<vn-one ng-if="::sale.item.subName">
<h3 title="{{::sale.item.subName}}">{{::sale.item.subName}}</h3>
</vn-one>
<vn-fetched-tags
max-length="6"
item="::sale.item"
name="::sale.concept"
sub-name="::sale.item.subName">
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td number>{{::sale.price | currency: 'EUR':2}}</vn-td>

View File

@ -42,12 +42,16 @@
{{sale.itemFk | zeroFill:6}}
</span>
</vn-td>
<vn-td expand>
<vn-fetched-tags
max-length="6"
<vn-td vn-fetched-tags>
<vn-one title="{{::sale.item.name}}">{{::sale.item.name}}</vn-one>
<vn-one ng-if="::sale.item.subName">
<h3 title="{{::sale.item.subName}}">{{::sale.item.subName}}</h3>
</vn-one>
<vn-fetched-tags
max-length="6"
item="::sale.item"
name="::sale.concept"
sub-name="::sale.item.subName"/>
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td number>{{::sale.quantity}}</vn-td>
<vn-td number>{{::sale.saleVolume.volume | number:3}}</vn-td>

View File

@ -26,7 +26,7 @@
<vn-tr>
<vn-th field="ticketFk" number>Ticket ID</vn-th>
<vn-th field="clientName">Client</vn-th>
<vn-th>Weekday</vn-th>
<vn-th>Shipment</vn-th>
<vn-th>Agency</vn-th>
<vn-th>Warehouse</vn-th>
<vn-th>Salesperson</vn-th>

View File

@ -4,4 +4,4 @@ You are going to delete this weekly ticket: Vas a eliminar este ticket programad
This ticket will be removed from weekly tickets! Continue anyway?: Este ticket se eliminará de tickets programados! ¿Continuar de todas formas?
Search weekly ticket by id or client id: Busca tickets programados por el identificador o el identificador del cliente
Search by weekly ticket: Buscar por tickets programados
Weekday: Llegada
Shipment: Salida

View File

@ -10,68 +10,74 @@ module.exports = Self => {
accepts: [
{
arg: 'filter',
type: 'Object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
http: {source: 'query'}
}, {
type: 'object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string'
},
{
arg: 'search',
type: 'String',
description: 'Searchs the travel by id',
http: {source: 'query'}
}, {
type: 'string',
description: 'Searchs the travel by id'
},
{
arg: 'id',
type: 'Integer',
description: 'The travel id',
http: {source: 'query'}
}, {
type: 'number',
description: 'The travel id'
},
{
arg: 'shippedFrom',
type: 'Date',
description: 'The shipped from date filter',
http: {source: 'query'}
}, {
type: 'date',
description: 'The shipped from date filter'
},
{
arg: 'shippedTo',
type: 'Date',
description: 'The shipped to date filter',
http: {source: 'query'}
}, {
type: 'date',
description: 'The shipped to date filter'
},
{
arg: 'landedFrom',
type: 'Date',
description: 'The landed from date filter',
http: {source: 'query'}
}, {
type: 'date',
description: 'The landed from date filter'
},
{
arg: 'landedTo',
type: 'Date',
description: 'The landed to date filter',
http: {source: 'query'}
}, {
type: 'date',
description: 'The landed to date filter'
},
{
arg: 'agencyFk',
type: 'Number',
description: 'The agencyModeFk id',
http: {source: 'query'}
}, {
type: 'number',
description: 'The agencyModeFk id'
},
{
arg: 'warehouseOutFk',
type: 'Number',
description: 'The warehouseOutFk filter',
http: {source: 'query'}
}, {
type: 'number',
description: 'The warehouseOutFk filter'
},
{
arg: 'warehouseInFk',
type: 'Number',
description: 'The warehouseInFk filter',
http: {source: 'query'}
}, {
type: 'number',
description: 'The warehouseInFk filter'
},
{
arg: 'totalEntries',
type: 'Number',
description: 'The totalEntries filter',
http: {source: 'query'}
}, {
type: 'number',
description: 'The totalEntries filter'
},
{
arg: 'ref',
type: 'string',
description: 'The reference'
}, {
},
{
arg: 'continent',
type: 'string',
description: 'The continent code'
}
},
{
arg: 'cargoSupplierFk',
type: 'number',
description: 'The freighter supplier id'
},
],
returns: {
type: ['Object'],
@ -108,6 +114,7 @@ module.exports = Self => {
case 'warehouseOutFk':
case 'warehouseInFk':
case 'totalEntries':
case 'cargoSupplierFk':
param = `t.${param}`;
return {[param]: value};
}
@ -172,8 +179,8 @@ module.exports = Self => {
e.notes,
CAST(SUM(i.density * b.stickers * IF(pkg.volume, pkg.volume, pkg.width * pkg.depth * pkg.height) / 1000000 ) as DECIMAL(10,0)) as loadedkg,
CAST(SUM(167.5 * b.stickers * IF(pkg.volume, pkg.volume, pkg.width * pkg.depth * pkg.height) / 1000000 ) as DECIMAL(10,0)) as volumeKg
FROM entry e
JOIN tmp.travel tr ON tr.id = e.travelFk
FROM tmp.travel tr
JOIN entry e ON e.travelFk = tr.id
JOIN buy b ON b.entryFk = e.id
JOIN packaging pkg ON pkg.id = b.packageFk
JOIN item i ON i.id = b.itemFk

View File

@ -37,6 +37,9 @@
"m3": {
"type": "Number"
},
"kg": {
"type": "Number"
},
"cargoSupplierFk": {
"type": "Number"
},

View File

@ -18,6 +18,12 @@
translate>
Clone travel and his entries
</vn-item>
<a class="vn-item"
ui-sref="entry.create({travelFk: $ctrl.travel.id})"
name="addEntry"
translate>
Add entry
</a>
</vn-list>
</vn-menu>

Some files were not shown because too many files have changed in this diff Show More