Merge branch 'dev' into 2770-supplier_address
gitea/salix/pipeline/head This commit looks good
Details
gitea/salix/pipeline/head This commit looks good
Details
This commit is contained in:
commit
61a63ad76f
|
@ -1036,6 +1036,21 @@ export default {
|
||||||
entriesQuicklink: 'vn-entry-descriptor vn-quick-link[icon="icon-entry"] > a'
|
entriesQuicklink: 'vn-entry-descriptor vn-quick-link[icon="icon-entry"] > a'
|
||||||
},
|
},
|
||||||
entryBuys: {
|
entryBuys: {
|
||||||
|
anyBuyLine: 'vn-entry-buy-index tr.dark-row',
|
||||||
|
allBuyCheckbox: 'vn-entry-buy-index thead vn-check',
|
||||||
|
firstBuyCheckbox: 'vn-entry-buy-index tbody:nth-child(2) vn-check',
|
||||||
|
deleteBuysButton: 'vn-entry-buy-index vn-button[icon="delete"]',
|
||||||
|
addBuyButton: 'vn-entry-buy-index vn-icon[icon="add_circle"]',
|
||||||
|
secondBuyPackingPrice: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.price3"]',
|
||||||
|
secondBuyGroupingPrice: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.price2"]',
|
||||||
|
secondBuyPrice: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.buyingValue"]',
|
||||||
|
secondBuyGrouping: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.grouping"]',
|
||||||
|
secondBuyPacking: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.packing"]',
|
||||||
|
secondBuyWeight: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.weight"]',
|
||||||
|
secondBuyStickers: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.stickers"]',
|
||||||
|
secondBuyPackage: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-autocomplete[ng-model="buy.packageFk"]',
|
||||||
|
secondBuyQuantity: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.quantity"]',
|
||||||
|
secondBuyItem: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-autocomplete[ng-model="buy.itemFk"]',
|
||||||
importButton: 'vn-entry-buy-index vn-icon[icon="publish"]',
|
importButton: 'vn-entry-buy-index vn-icon[icon="publish"]',
|
||||||
ref: 'vn-entry-buy-import vn-textfield[ng-model="$ctrl.import.ref"]',
|
ref: 'vn-entry-buy-import vn-textfield[ng-model="$ctrl.import.ref"]',
|
||||||
observation: 'vn-entry-buy-import vn-textarea[ng-model="$ctrl.import.observation"]',
|
observation: 'vn-entry-buy-import vn-textarea[ng-model="$ctrl.import.observation"]',
|
||||||
|
|
|
@ -0,0 +1,187 @@
|
||||||
|
import selectors from '../../helpers/selectors.js';
|
||||||
|
import getBrowser from '../../helpers/puppeteer';
|
||||||
|
|
||||||
|
describe('Entry import, create and edit buys path', () => {
|
||||||
|
let browser;
|
||||||
|
let page;
|
||||||
|
|
||||||
|
beforeAll(async() => {
|
||||||
|
browser = await getBrowser();
|
||||||
|
page = browser.page;
|
||||||
|
await page.loginAndModule('buyer', 'entry');
|
||||||
|
await page.accessToSearchResult('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async() => {
|
||||||
|
await browser.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should count the summary buys and find there only one at this point', async() => {
|
||||||
|
const buysCount = await page.countElement(selectors.entrySummary.anyBuyLine);
|
||||||
|
|
||||||
|
expect(buysCount).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to the buy section and then click the import button opening the import form', async() => {
|
||||||
|
await page.accessToSection('entry.card.buy.index');
|
||||||
|
await page.waitToClick(selectors.entryBuys.importButton);
|
||||||
|
await page.waitForState('entry.card.buy.import');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fill the form, import the designated JSON file and select items for each import and confirm import', async() => {
|
||||||
|
await page.write(selectors.entryBuys.ref, 'a reference');
|
||||||
|
await page.write(selectors.entryBuys.observation, 'an observation');
|
||||||
|
|
||||||
|
let currentDir = process.cwd();
|
||||||
|
let filePath = `${currentDir}/e2e/assets/07_import_buys.json`;
|
||||||
|
|
||||||
|
const [fileChooser] = await Promise.all([
|
||||||
|
page.waitForFileChooser(),
|
||||||
|
page.waitToClick(selectors.entryBuys.file)
|
||||||
|
]);
|
||||||
|
await fileChooser.accept([filePath]);
|
||||||
|
|
||||||
|
await page.autocompleteSearch(selectors.entryBuys.firstImportedItem, 'Ranged Reinforced weapon pistol 9mm');
|
||||||
|
await page.autocompleteSearch(selectors.entryBuys.secondImportedItem, 'Melee Reinforced weapon heavy shield 1x0.5m');
|
||||||
|
await page.autocompleteSearch(selectors.entryBuys.thirdImportedItem, 'Container medical box 1m');
|
||||||
|
await page.autocompleteSearch(selectors.entryBuys.fourthImportedItem, 'Container ammo box 1m');
|
||||||
|
|
||||||
|
await page.waitToClick(selectors.entryBuys.importBuysButton);
|
||||||
|
|
||||||
|
const message = await page.waitForSnackbar();
|
||||||
|
const state = await page.getState();
|
||||||
|
|
||||||
|
expect(message.text).toContain('Data saved!');
|
||||||
|
expect(state).toBe('entry.card.buy.index');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should count the buys to find 4 buys have been added', async() => {
|
||||||
|
await page.waitForNumberOfElements(selectors.entryBuys.anyBuyLine, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete the four buys that were just added', async() => {
|
||||||
|
await page.waitToClick(selectors.entryBuys.allBuyCheckbox);
|
||||||
|
await page.waitToClick(selectors.entryBuys.firstBuyCheckbox);
|
||||||
|
await page.waitToClick(selectors.entryBuys.deleteBuysButton);
|
||||||
|
await page.waitToClick(selectors.globalItems.acceptButton);
|
||||||
|
await page.waitForNumberOfElements(selectors.entryBuys.anyBuyLine, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a new buy', async() => {
|
||||||
|
await page.waitToClick(selectors.entryBuys.addBuyButton);
|
||||||
|
await page.write(selectors.entryBuys.secondBuyPackingPrice, '999');
|
||||||
|
await page.write(selectors.entryBuys.secondBuyGroupingPrice, '999');
|
||||||
|
await page.write(selectors.entryBuys.secondBuyPrice, '999');
|
||||||
|
await page.write(selectors.entryBuys.secondBuyGrouping, '999');
|
||||||
|
await page.write(selectors.entryBuys.secondBuyPacking, '999');
|
||||||
|
await page.write(selectors.entryBuys.secondBuyWeight, '999');
|
||||||
|
await page.write(selectors.entryBuys.secondBuyStickers, '999');
|
||||||
|
await page.autocompleteSearch(selectors.entryBuys.secondBuyPackage, '1');
|
||||||
|
await page.write(selectors.entryBuys.secondBuyQuantity, '999');
|
||||||
|
await page.autocompleteSearch(selectors.entryBuys.secondBuyItem, '1');
|
||||||
|
const message = await page.waitForSnackbar();
|
||||||
|
|
||||||
|
expect(message.text).toContain('Data saved!');
|
||||||
|
|
||||||
|
await page.waitForNumberOfElements(selectors.entryBuys.anyBuyLine, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should edit the newest buy', async() => {
|
||||||
|
await page.clearInput(selectors.entryBuys.secondBuyPackingPrice);
|
||||||
|
await page.waitForTextInField(selectors.entryBuys.secondBuyPackingPrice, '');
|
||||||
|
await page.write(selectors.entryBuys.secondBuyPackingPrice, '100');
|
||||||
|
await page.waitForSnackbar();
|
||||||
|
|
||||||
|
await page.clearInput(selectors.entryBuys.secondBuyGroupingPrice);
|
||||||
|
await page.waitForTextInField(selectors.entryBuys.secondBuyGroupingPrice, '');
|
||||||
|
await page.write(selectors.entryBuys.secondBuyGroupingPrice, '200');
|
||||||
|
await page.waitForSnackbar();
|
||||||
|
|
||||||
|
await page.clearInput(selectors.entryBuys.secondBuyPrice);
|
||||||
|
await page.waitForTextInField(selectors.entryBuys.secondBuyPrice, '');
|
||||||
|
await page.write(selectors.entryBuys.secondBuyPrice, '300');
|
||||||
|
await page.waitForSnackbar();
|
||||||
|
|
||||||
|
await page.clearInput(selectors.entryBuys.secondBuyGrouping);
|
||||||
|
await page.waitForTextInField(selectors.entryBuys.secondBuyGrouping, '');
|
||||||
|
await page.write(selectors.entryBuys.secondBuyGrouping, '400');
|
||||||
|
await page.waitForSnackbar();
|
||||||
|
|
||||||
|
await page.clearInput(selectors.entryBuys.secondBuyPacking);
|
||||||
|
await page.waitForTextInField(selectors.entryBuys.secondBuyPacking, '');
|
||||||
|
await page.write(selectors.entryBuys.secondBuyPacking, '500');
|
||||||
|
await page.waitForSnackbar();
|
||||||
|
|
||||||
|
await page.clearInput(selectors.entryBuys.secondBuyWeight);
|
||||||
|
await page.waitForTextInField(selectors.entryBuys.secondBuyWeight, '');
|
||||||
|
await page.write(selectors.entryBuys.secondBuyWeight, '600');
|
||||||
|
await page.waitForSnackbar();
|
||||||
|
|
||||||
|
await page.clearInput(selectors.entryBuys.secondBuyStickers);
|
||||||
|
await page.waitForTextInField(selectors.entryBuys.secondBuyStickers, '');
|
||||||
|
await page.write(selectors.entryBuys.secondBuyStickers, '700');
|
||||||
|
await page.waitForSnackbar();
|
||||||
|
|
||||||
|
await page.autocompleteSearch(selectors.entryBuys.secondBuyPackage, '94');
|
||||||
|
await page.waitForSnackbar();
|
||||||
|
|
||||||
|
await page.clearInput(selectors.entryBuys.secondBuyQuantity);
|
||||||
|
await page.waitForTextInField(selectors.entryBuys.secondBuyQuantity, '');
|
||||||
|
await page.write(selectors.entryBuys.secondBuyQuantity, '800');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reload the section and check the packing price is as expected', async() => {
|
||||||
|
await page.reloadSection('entry.card.buy.index');
|
||||||
|
const result = await page.waitToGetProperty(selectors.entryBuys.secondBuyPackingPrice, 'value');
|
||||||
|
|
||||||
|
expect(result).toEqual('100');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reload the section and check the grouping price is as expected', async() => {
|
||||||
|
const result = await page.waitToGetProperty(selectors.entryBuys.secondBuyGroupingPrice, 'value');
|
||||||
|
|
||||||
|
expect(result).toEqual('200');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reload the section and check the price is as expected', async() => {
|
||||||
|
const result = await page.waitToGetProperty(selectors.entryBuys.secondBuyPrice, 'value');
|
||||||
|
|
||||||
|
expect(result).toEqual('300');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reload the section and check the grouping is as expected', async() => {
|
||||||
|
const result = await page.waitToGetProperty(selectors.entryBuys.secondBuyGrouping, 'value');
|
||||||
|
|
||||||
|
expect(result).toEqual('400');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reload the section and check the packing is as expected', async() => {
|
||||||
|
const result = await page.waitToGetProperty(selectors.entryBuys.secondBuyPacking, 'value');
|
||||||
|
|
||||||
|
expect(result).toEqual('500');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reload the section and check the weight is as expected', async() => {
|
||||||
|
const result = await page.waitToGetProperty(selectors.entryBuys.secondBuyWeight, 'value');
|
||||||
|
|
||||||
|
expect(result).toEqual('600');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reload the section and check the stickers are as expected', async() => {
|
||||||
|
const result = await page.waitToGetProperty(selectors.entryBuys.secondBuyStickers, 'value');
|
||||||
|
|
||||||
|
expect(result).toEqual('700');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reload the section and check the package is as expected', async() => {
|
||||||
|
const result = await page.waitToGetProperty(selectors.entryBuys.secondBuyPackage, 'value');
|
||||||
|
|
||||||
|
expect(result).toEqual('94');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reload the section and check the quantity is as expected', async() => {
|
||||||
|
const result = await page.waitToGetProperty(selectors.entryBuys.secondBuyQuantity, 'value');
|
||||||
|
|
||||||
|
expect(result).toEqual('800');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,62 +0,0 @@
|
||||||
import selectors from '../../helpers/selectors.js';
|
|
||||||
import getBrowser from '../../helpers/puppeteer';
|
|
||||||
|
|
||||||
describe('Entry import buys path', () => {
|
|
||||||
let browser;
|
|
||||||
let page;
|
|
||||||
|
|
||||||
beforeAll(async() => {
|
|
||||||
browser = await getBrowser();
|
|
||||||
page = browser.page;
|
|
||||||
await page.loginAndModule('buyer', 'entry');
|
|
||||||
await page.accessToSearchResult('1');
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async() => {
|
|
||||||
await browser.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should count the summary buys and find there only one at this point', async() => {
|
|
||||||
const buysCount = await page.countElement(selectors.entrySummary.anyBuyLine);
|
|
||||||
|
|
||||||
expect(buysCount).toEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should navigate to the buy section and then click the import button opening the import form', async() => {
|
|
||||||
await page.accessToSection('entry.card.buy.index');
|
|
||||||
await page.waitToClick(selectors.entryBuys.importButton);
|
|
||||||
await page.waitForState('entry.card.buy.import');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fill the form, import the designated JSON file and select items for each import and confirm import', async() => {
|
|
||||||
await page.write(selectors.entryBuys.ref, 'a reference');
|
|
||||||
await page.write(selectors.entryBuys.observation, 'an observation');
|
|
||||||
|
|
||||||
let currentDir = process.cwd();
|
|
||||||
let filePath = `${currentDir}/e2e/assets/07_import_buys.json`;
|
|
||||||
|
|
||||||
const [fileChooser] = await Promise.all([
|
|
||||||
page.waitForFileChooser(),
|
|
||||||
page.waitToClick(selectors.entryBuys.file)
|
|
||||||
]);
|
|
||||||
await fileChooser.accept([filePath]);
|
|
||||||
|
|
||||||
await page.autocompleteSearch(selectors.entryBuys.firstImportedItem, 'Ranged Reinforced weapon pistol 9mm');
|
|
||||||
await page.autocompleteSearch(selectors.entryBuys.secondImportedItem, 'Melee Reinforced weapon heavy shield 1x0.5m');
|
|
||||||
await page.autocompleteSearch(selectors.entryBuys.thirdImportedItem, 'Container medical box 1m');
|
|
||||||
await page.autocompleteSearch(selectors.entryBuys.fourthImportedItem, 'Container ammo box 1m');
|
|
||||||
|
|
||||||
await page.waitToClick(selectors.entryBuys.importBuysButton);
|
|
||||||
|
|
||||||
const message = await page.waitForSnackbar();
|
|
||||||
const state = await page.getState();
|
|
||||||
|
|
||||||
expect(message.text).toContain('Data saved!');
|
|
||||||
expect(state).toBe('entry.card.buy.index');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should navigate to the entry summary and count the buys to find 4 buys have been added', async() => {
|
|
||||||
await page.waitToClick('vn-icon[icon="preview"]');
|
|
||||||
await page.waitForNumberOfElements(selectors.entrySummary.anyBuyLine, 5);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -21,6 +21,9 @@ vn-chip {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.transparent {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
&.colored {
|
&.colored {
|
||||||
background-color: $color-main;
|
background-color: $color-main;
|
||||||
color: $color-font-bg;
|
color: $color-font-bg;
|
||||||
|
|
|
@ -0,0 +1,165 @@
|
||||||
|
|
||||||
|
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
|
||||||
|
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethodCtx('addBuy', {
|
||||||
|
description: 'Inserts a new buy for the current entry',
|
||||||
|
accessType: 'WRITE',
|
||||||
|
accepts: [{
|
||||||
|
arg: 'id',
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
description: 'The entry id',
|
||||||
|
http: {source: 'path'}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'itemFk',
|
||||||
|
type: 'number',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'quantity',
|
||||||
|
type: 'number',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'packageFk',
|
||||||
|
type: 'string',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'packing',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'grouping',
|
||||||
|
type: 'number'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'weight',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'stickers',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'price2',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'price3',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'buyingValue',
|
||||||
|
type: 'number'
|
||||||
|
}],
|
||||||
|
returns: {
|
||||||
|
type: 'object',
|
||||||
|
root: true
|
||||||
|
},
|
||||||
|
http: {
|
||||||
|
path: `/:id/addBuy`,
|
||||||
|
verb: 'POST'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.addBuy = async(ctx, options) => {
|
||||||
|
const conn = Self.dataSource.connector;
|
||||||
|
let tx;
|
||||||
|
let myOptions = {};
|
||||||
|
|
||||||
|
if (typeof options == 'object')
|
||||||
|
Object.assign(myOptions, options);
|
||||||
|
|
||||||
|
if (!myOptions.transaction) {
|
||||||
|
tx = await Self.beginTransaction({});
|
||||||
|
myOptions.transaction = tx;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const models = Self.app.models;
|
||||||
|
|
||||||
|
ctx.args.entryFk = ctx.args.id;
|
||||||
|
|
||||||
|
// remove unwanted properties
|
||||||
|
delete ctx.args.id;
|
||||||
|
delete ctx.args.ctx;
|
||||||
|
|
||||||
|
const newBuy = await models.Buy.create(ctx.args, myOptions);
|
||||||
|
|
||||||
|
let filter = {
|
||||||
|
fields: [
|
||||||
|
'id',
|
||||||
|
'itemFk',
|
||||||
|
'stickers',
|
||||||
|
'packing',
|
||||||
|
'grouping',
|
||||||
|
'quantity',
|
||||||
|
'packageFk',
|
||||||
|
'weight',
|
||||||
|
'buyingValue',
|
||||||
|
'price2',
|
||||||
|
'price3'
|
||||||
|
],
|
||||||
|
include: {
|
||||||
|
relation: 'item',
|
||||||
|
scope: {
|
||||||
|
fields: [
|
||||||
|
'id',
|
||||||
|
'typeFk',
|
||||||
|
'name',
|
||||||
|
'size',
|
||||||
|
'minPrice',
|
||||||
|
'tag5',
|
||||||
|
'value5',
|
||||||
|
'tag6',
|
||||||
|
'value6',
|
||||||
|
'tag7',
|
||||||
|
'value7',
|
||||||
|
'tag8',
|
||||||
|
'value8',
|
||||||
|
'tag9',
|
||||||
|
'value9',
|
||||||
|
'tag10',
|
||||||
|
'value10',
|
||||||
|
'groupingMode'
|
||||||
|
],
|
||||||
|
include: {
|
||||||
|
relation: 'itemType',
|
||||||
|
scope: {
|
||||||
|
fields: ['code', 'description']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let stmts = [];
|
||||||
|
let stmt;
|
||||||
|
|
||||||
|
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.buyRecalc');
|
||||||
|
stmt = new ParameterizedSQL(
|
||||||
|
`CREATE TEMPORARY TABLE tmp.buyRecalc
|
||||||
|
(INDEX (id))
|
||||||
|
ENGINE = MEMORY
|
||||||
|
SELECT ? AS id`, [newBuy.id]);
|
||||||
|
|
||||||
|
stmts.push(stmt);
|
||||||
|
stmts.push('CALL buy_recalcPrices()');
|
||||||
|
|
||||||
|
const sql = ParameterizedSQL.join(stmts, ';');
|
||||||
|
await conn.executeStmt(sql, myOptions);
|
||||||
|
|
||||||
|
const buy = await models.Buy.findById(newBuy.id, filter, myOptions);
|
||||||
|
|
||||||
|
if (tx) await tx.commit();
|
||||||
|
|
||||||
|
return buy;
|
||||||
|
} catch (e) {
|
||||||
|
if (tx) await tx.rollback();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,50 @@
|
||||||
|
module.exports = Self => {
|
||||||
|
Self.remoteMethodCtx('deleteBuys', {
|
||||||
|
description: 'Deletes the selected buys',
|
||||||
|
accessType: 'WRITE',
|
||||||
|
accepts: [{
|
||||||
|
arg: 'buys',
|
||||||
|
type: ['object'],
|
||||||
|
required: true,
|
||||||
|
description: 'The buys to delete'
|
||||||
|
}],
|
||||||
|
returns: {
|
||||||
|
type: ['object'],
|
||||||
|
root: true
|
||||||
|
},
|
||||||
|
http: {
|
||||||
|
path: `/deleteBuys`,
|
||||||
|
verb: 'POST'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self.deleteBuys = async(ctx, options) => {
|
||||||
|
const models = Self.app.models;
|
||||||
|
let tx;
|
||||||
|
let myOptions = {};
|
||||||
|
|
||||||
|
if (typeof options == 'object')
|
||||||
|
Object.assign(myOptions, options);
|
||||||
|
|
||||||
|
if (!myOptions.transaction) {
|
||||||
|
tx = await Self.beginTransaction({});
|
||||||
|
myOptions.transaction = tx;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let promises = [];
|
||||||
|
for (let buy of ctx.args.buys) {
|
||||||
|
const buysToDelete = models.Buy.destroyById(buy.id, myOptions);
|
||||||
|
promises.push(buysToDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleted = await Promise.all(promises);
|
||||||
|
|
||||||
|
if (tx) await tx.commit();
|
||||||
|
return deleted;
|
||||||
|
} catch (e) {
|
||||||
|
if (tx) await tx.rollback();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,42 @@
|
||||||
|
const app = require('vn-loopback/server/server');
|
||||||
|
const LoopBackContext = require('loopback-context');
|
||||||
|
|
||||||
|
describe('entry addBuy()', () => {
|
||||||
|
const activeCtx = {
|
||||||
|
accessToken: {userId: 18},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
req: activeCtx
|
||||||
|
};
|
||||||
|
|
||||||
|
const entryId = 2;
|
||||||
|
it('should create a new buy for the given entry', async() => {
|
||||||
|
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||||
|
active: activeCtx
|
||||||
|
});
|
||||||
|
const itemId = 4;
|
||||||
|
const quantity = 10;
|
||||||
|
|
||||||
|
ctx.args = {
|
||||||
|
id: entryId,
|
||||||
|
itemFk: itemId,
|
||||||
|
quantity: quantity,
|
||||||
|
packageFk: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
const tx = await app.models.Entry.beginTransaction({});
|
||||||
|
const options = {transaction: tx};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newBuy = await app.models.Entry.addBuy(ctx, options);
|
||||||
|
|
||||||
|
expect(newBuy.itemFk).toEqual(itemId);
|
||||||
|
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,33 @@
|
||||||
|
const app = require('vn-loopback/server/server');
|
||||||
|
const LoopBackContext = require('loopback-context');
|
||||||
|
|
||||||
|
describe('sale deleteBuys()', () => {
|
||||||
|
const activeCtx = {
|
||||||
|
accessToken: {userId: 18},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
req: activeCtx
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should delete the buy', async() => {
|
||||||
|
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||||
|
active: activeCtx
|
||||||
|
});
|
||||||
|
|
||||||
|
const tx = await app.models.Entry.beginTransaction({});
|
||||||
|
const options = {transaction: tx};
|
||||||
|
|
||||||
|
ctx.args = {buys: [{id: 1}]};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await app.models.Buy.deleteBuys(ctx, options);
|
||||||
|
|
||||||
|
expect(result).toEqual([{count: 1}]);
|
||||||
|
await tx.rollback();
|
||||||
|
} catch (e) {
|
||||||
|
await tx.rollback();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,4 +1,5 @@
|
||||||
module.exports = Self => {
|
module.exports = Self => {
|
||||||
require('../methods/entry/editLatestBuys')(Self);
|
require('../methods/entry/editLatestBuys')(Self);
|
||||||
require('../methods/entry/latestBuysFilter')(Self);
|
require('../methods/entry/latestBuysFilter')(Self);
|
||||||
|
require('../methods/entry/deleteBuys')(Self);
|
||||||
};
|
};
|
||||||
|
|
|
@ -57,14 +57,12 @@
|
||||||
"entry": {
|
"entry": {
|
||||||
"type": "belongsTo",
|
"type": "belongsTo",
|
||||||
"model": "Entry",
|
"model": "Entry",
|
||||||
"foreignKey": "entryFk",
|
"foreignKey": "entryFk"
|
||||||
"required": true
|
|
||||||
},
|
},
|
||||||
"item": {
|
"item": {
|
||||||
"type": "belongsTo",
|
"type": "belongsTo",
|
||||||
"model": "Item",
|
"model": "Item",
|
||||||
"foreignKey": "itemFk",
|
"foreignKey": "itemFk"
|
||||||
"required": true
|
|
||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"type": "belongsTo",
|
"type": "belongsTo",
|
||||||
|
|
|
@ -2,6 +2,7 @@ module.exports = Self => {
|
||||||
require('../methods/entry/filter')(Self);
|
require('../methods/entry/filter')(Self);
|
||||||
require('../methods/entry/getEntry')(Self);
|
require('../methods/entry/getEntry')(Self);
|
||||||
require('../methods/entry/getBuys')(Self);
|
require('../methods/entry/getBuys')(Self);
|
||||||
|
require('../methods/entry/addBuy')(Self);
|
||||||
require('../methods/entry/importBuys')(Self);
|
require('../methods/entry/importBuys')(Self);
|
||||||
require('../methods/entry/importBuysPreview')(Self);
|
require('../methods/entry/importBuysPreview')(Self);
|
||||||
};
|
};
|
||||||
|
|
|
@ -55,7 +55,7 @@ class Controller extends Section {
|
||||||
|
|
||||||
fetchBuys(buys) {
|
fetchBuys(buys) {
|
||||||
const params = {buys};
|
const params = {buys};
|
||||||
const query = `Entries/${this.entry.id}/importBuysPreview`;
|
const query = `Entries/${this.$params.id}/importBuysPreview`;
|
||||||
this.$http.get(query, {params}).then(res => {
|
this.$http.get(query, {params}).then(res => {
|
||||||
this.import.buys = res.data;
|
this.import.buys = res.data;
|
||||||
});
|
});
|
||||||
|
@ -71,7 +71,7 @@ class Controller extends Section {
|
||||||
if (hasAnyEmptyRow)
|
if (hasAnyEmptyRow)
|
||||||
throw new Error(`Some of the imported buys doesn't have an item`);
|
throw new Error(`Some of the imported buys doesn't have an item`);
|
||||||
|
|
||||||
const query = `Entries/${this.entry.id}/importBuys`;
|
const query = `Entries/${this.$params.id}/importBuys`;
|
||||||
return this.$http.post(query, params)
|
return this.$http.post(query, params)
|
||||||
.then(() => this.vnApp.showSuccess(this.$t('Data saved!')))
|
.then(() => this.vnApp.showSuccess(this.$t('Data saved!')))
|
||||||
.then(() => this.$state.go('entry.card.buy.index'));
|
.then(() => this.$state.go('entry.card.buy.index'));
|
||||||
|
|
|
@ -16,6 +16,7 @@ describe('Entry', () => {
|
||||||
controller.entry = {
|
controller.entry = {
|
||||||
id: 1
|
id: 1
|
||||||
};
|
};
|
||||||
|
controller.$params = {id: 1};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('fillData()', () => {
|
describe('fillData()', () => {
|
||||||
|
|
|
@ -1,9 +1,198 @@
|
||||||
|
<vn-crud-model
|
||||||
|
auto-load="true"
|
||||||
|
vn-id="model"
|
||||||
|
url="Entries/{{$ctrl.$params.id}}/getBuys"
|
||||||
|
data="$ctrl.buys">
|
||||||
|
</vn-crud-model>
|
||||||
|
<vn-watcher
|
||||||
|
vn-id="watcher"
|
||||||
|
data="$ctrl.buys">
|
||||||
|
</vn-watcher>
|
||||||
|
<div class="vn-w-xl">
|
||||||
|
<vn-card class="vn-pa-lg">
|
||||||
|
<vn-horizontal class="header">
|
||||||
|
<vn-tool-bar class="vn-mb-md">
|
||||||
|
<vn-button
|
||||||
|
disabled="$ctrl.selectedBuys() == 0"
|
||||||
|
ng-click="deleteBuys.show()"
|
||||||
|
vn-tooltip="Delete buy(s)"
|
||||||
|
icon="delete">
|
||||||
|
</vn-button>
|
||||||
|
</vn-tool-bar>
|
||||||
|
<vn-one class="taxes" ng-if="$ctrl.sales.length > 0">
|
||||||
|
<p><vn-label translate>Subtotal</vn-label> {{$ctrl.ticket.totalWithoutVat | currency: 'EUR':2}}</p>
|
||||||
|
<p><vn-label translate>VAT</vn-label> {{$ctrl.ticket.totalWithVat - $ctrl.ticket.totalWithoutVat | currency: 'EUR':2}}</p>
|
||||||
|
<p><vn-label><strong>Total</strong></vn-label> <strong>{{$ctrl.ticket.totalWithVat | currency: 'EUR':2}}</strong></p>
|
||||||
|
</vn-one>
|
||||||
|
</vn-horizontal>
|
||||||
|
<table class="vn-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th shrink>
|
||||||
|
<vn-multi-check model="model" on-change="$ctrl.resetChanges()">
|
||||||
|
</vn-multi-check>
|
||||||
|
</th>
|
||||||
|
<th translate center>Item</th>
|
||||||
|
<th translate center>Quantity</th>
|
||||||
|
<th translate center>Package</th>
|
||||||
|
<th translate>Stickers</th>
|
||||||
|
<th translate>Weight</th>
|
||||||
|
<th translate>Packing</th>
|
||||||
|
<th translate>Grouping</th>
|
||||||
|
<th translate>Buying value</th>
|
||||||
|
<th translate expand>Grouping price</th>
|
||||||
|
<th translate expand>Packing price</th>
|
||||||
|
<th translate>Import</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody ng-repeat="buy in $ctrl.buys">
|
||||||
|
<tr>
|
||||||
|
<td shrink>
|
||||||
|
<vn-check tabindex="-1" ng-model="buy.checked">
|
||||||
|
</vn-check>
|
||||||
|
</td>
|
||||||
|
<td shrink>
|
||||||
|
<span
|
||||||
|
ng-if="buy.id"
|
||||||
|
ng-click="itemDescriptor.show($event, buy.item.id)"
|
||||||
|
class="link">
|
||||||
|
{{::buy.item.id | zeroFill:6}}
|
||||||
|
</span>
|
||||||
|
<vn-autocomplete ng-if="!buy.id" class="dense"
|
||||||
|
vn-focus
|
||||||
|
url="Items"
|
||||||
|
ng-model="buy.itemFk"
|
||||||
|
show-field="name"
|
||||||
|
value-field="id"
|
||||||
|
search-function="$ctrl.itemSearchFunc($search)"
|
||||||
|
on-change="$ctrl.saveBuy(buy)"
|
||||||
|
order="id DESC"
|
||||||
|
tabindex="1">
|
||||||
|
<tpl-item>
|
||||||
|
{{::id}} - {{::name}}
|
||||||
|
</tpl-item>
|
||||||
|
</vn-autocomplete>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<vn-input-number class="dense"
|
||||||
|
title="{{::buy.quantity | dashIfEmpty}}"
|
||||||
|
ng-model="buy.quantity"
|
||||||
|
on-change="$ctrl.saveBuy(buy)">
|
||||||
|
</vn-input-number>
|
||||||
|
</td>
|
||||||
|
<td center>
|
||||||
|
<vn-autocomplete
|
||||||
|
vn-one
|
||||||
|
title="{{::buy.packageFk | dashIfEmpty}}"
|
||||||
|
url="Packagings"
|
||||||
|
show-field="id"
|
||||||
|
value-field="id"
|
||||||
|
where="{isBox: true}"
|
||||||
|
ng-model="buy.packageFk"
|
||||||
|
on-change="$ctrl.saveBuy(buy)">
|
||||||
|
</vn-autocomplete>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<vn-input-number class="dense"
|
||||||
|
title="{{::buy.stickers | dashIfEmpty}}"
|
||||||
|
ng-model="buy.stickers"
|
||||||
|
on-change="$ctrl.saveBuy(buy)">
|
||||||
|
</vn-input-number>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<vn-input-number class="dense"
|
||||||
|
title="{{::buy.weight | dashIfEmpty}}"
|
||||||
|
ng-model="buy.weight"
|
||||||
|
on-change="$ctrl.saveBuy(buy)">
|
||||||
|
</vn-input-number>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<vn-input-number class="dense"
|
||||||
|
title="{{::buy.packing | dashIfEmpty}}"
|
||||||
|
ng-model="buy.packing"
|
||||||
|
on-change="$ctrl.saveBuy(buy)">
|
||||||
|
</vn-input-number>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<vn-input-number class="dense"
|
||||||
|
title="{{::buy.grouping | dashIfEmpty}}"
|
||||||
|
ng-model="buy.grouping"
|
||||||
|
on-change="$ctrl.saveBuy(buy)">
|
||||||
|
</vn-input-number>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<vn-input-number class="dense"
|
||||||
|
title="{{::buy.buyingValue | dashIfEmpty}}"
|
||||||
|
ng-model="buy.buyingValue"
|
||||||
|
on-change="$ctrl.saveBuy(buy)">
|
||||||
|
</vn-input-number>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<vn-input-number class="dense"
|
||||||
|
title="{{::buy.price2 | dashIfEmpty}}"
|
||||||
|
ng-model="buy.price2"
|
||||||
|
on-change="$ctrl.saveBuy(buy)">
|
||||||
|
</vn-input-number>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<vn-input-number class="dense"
|
||||||
|
title="{{::buy.price3 | dashIfEmpty}}"
|
||||||
|
ng-model="buy.price3"
|
||||||
|
on-change="$ctrl.saveBuy(buy)">
|
||||||
|
</vn-input-number>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
ng-if="buy.quantity != null && buy.buyingValue != null"
|
||||||
|
title="{{buy.quantity * buy.buyingValue | currency: 'EUR':2}}">
|
||||||
|
{{buy.quantity * buy.buyingValue | currency: 'EUR':2}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="dark-row">
|
||||||
|
<td shrink>
|
||||||
|
</td>
|
||||||
|
<td shrink>
|
||||||
|
<span translate-attr="{title: 'Item type'}">
|
||||||
|
{{::buy.item.itemType.code}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td number shrink>
|
||||||
|
<span translate-attr="{title: 'Item size'}">
|
||||||
|
{{::buy.item.size}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td center>
|
||||||
|
<span translate-attr="{title: 'Minimum price'}">
|
||||||
|
{{::buy.item.minPrice | currency: 'EUR':2}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td vn-fetched-tags colspan="9">
|
||||||
|
<vn-one title="{{::buy.item.name}}">{{::buy.item.name}}</vn-one>
|
||||||
|
<vn-fetched-tags
|
||||||
|
max-length="6"
|
||||||
|
item="::buy.item"
|
||||||
|
tabindex="-1">
|
||||||
|
</vn-fetched-tags>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div>
|
||||||
|
<vn-icon-button
|
||||||
|
vn-one
|
||||||
|
vn-tooltip="Add buy"
|
||||||
|
vn-bind="+"
|
||||||
|
icon="add_circle"
|
||||||
|
ng-click="model.insert({})">
|
||||||
|
</vn-icon-button>
|
||||||
|
</div>
|
||||||
|
</vn-card>
|
||||||
|
</div>
|
||||||
<div fixed-bottom-right>
|
<div fixed-bottom-right>
|
||||||
<vn-vertical style="align-items: center;">
|
<vn-vertical style="align-items: center;">
|
||||||
<a ui-sref="entry.card.buy.import"
|
<a ui-sref="entry.card.buy.import"
|
||||||
vn-bind="+"
|
vn-bind="+">
|
||||||
vn-acl="buyer"
|
|
||||||
vn-acl-action="remove">
|
|
||||||
<vn-button class="round md vn-mb-sm"
|
<vn-button class="round md vn-mb-sm"
|
||||||
icon="publish"
|
icon="publish"
|
||||||
vn-tooltip="Import buys"
|
vn-tooltip="Import buys"
|
||||||
|
@ -12,3 +201,13 @@
|
||||||
</a>
|
</a>
|
||||||
</vn-vertical>
|
</vn-vertical>
|
||||||
</div>
|
</div>
|
||||||
|
<vn-item-descriptor-popover
|
||||||
|
vn-id="itemDescriptor">
|
||||||
|
</vn-item-descriptor-popover>
|
||||||
|
|
||||||
|
<vn-confirm
|
||||||
|
vn-id="delete-buys"
|
||||||
|
question="You are going to delete buy(s) from this entry"
|
||||||
|
message="Continue anyway?"
|
||||||
|
on-accept="$ctrl.deleteBuys()">
|
||||||
|
</vn-confirm>
|
|
@ -1,9 +1,66 @@
|
||||||
import ngModule from '../../module';
|
import ngModule from '../../module';
|
||||||
|
import './style.scss';
|
||||||
import Section from 'salix/components/section';
|
import Section from 'salix/components/section';
|
||||||
|
|
||||||
|
export default class Controller extends Section {
|
||||||
|
saveBuy(buy) {
|
||||||
|
const missingData = !buy.itemFk || !buy.quantity || !buy.packageFk;
|
||||||
|
if (missingData) return;
|
||||||
|
|
||||||
|
let options;
|
||||||
|
if (buy.id) {
|
||||||
|
options = {
|
||||||
|
query: `Buys/${buy.id}`,
|
||||||
|
method: 'patch'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
options = {
|
||||||
|
query: `Entries/${this.entry.id}/addBuy`,
|
||||||
|
method: 'post'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.$http[options.method](options.query, buy).then(res => {
|
||||||
|
if (!res.data) return;
|
||||||
|
|
||||||
|
buy = Object.assign(buy, res.data);
|
||||||
|
this.vnApp.showSuccess(this.$t('Data saved!'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns checked instances
|
||||||
|
*
|
||||||
|
* @return {Array} Checked instances
|
||||||
|
*/
|
||||||
|
selectedBuys() {
|
||||||
|
if (!this.buys) return;
|
||||||
|
|
||||||
|
return this.buys.filter(buy => {
|
||||||
|
return buy.checked;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteBuys() {
|
||||||
|
const buys = this.selectedBuys();
|
||||||
|
const actualInstances = buys.filter(buy => buy.id);
|
||||||
|
|
||||||
|
const params = {buys: actualInstances};
|
||||||
|
|
||||||
|
if (actualInstances.length) {
|
||||||
|
this.$http.post(`Buys/deleteBuys`, params).then(() => {
|
||||||
|
this.vnApp.showSuccess(this.$t('Data saved!'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
buys.forEach(buy => {
|
||||||
|
const index = this.buys.indexOf(buy);
|
||||||
|
this.buys.splice(index, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ngModule.vnComponent('vnEntryBuyIndex', {
|
ngModule.vnComponent('vnEntryBuyIndex', {
|
||||||
template: require('./index.html'),
|
template: require('./index.html'),
|
||||||
controller: Section,
|
controller: Controller,
|
||||||
bindings: {
|
bindings: {
|
||||||
entry: '<'
|
entry: '<'
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
import './index.js';
|
||||||
|
|
||||||
|
describe('Entry buy', () => {
|
||||||
|
let controller;
|
||||||
|
let $httpBackend;
|
||||||
|
|
||||||
|
beforeEach(ngModule('entry'));
|
||||||
|
|
||||||
|
beforeEach(angular.mock.inject(($componentController, $compile, $rootScope, _$httpParamSerializer_, _$httpBackend_) => {
|
||||||
|
$httpBackend = _$httpBackend_;
|
||||||
|
let $element = $compile('<vn-entry-buy-index></vn-entry-buy-index')($rootScope);
|
||||||
|
controller = $componentController('vnEntryBuyIndex', {$element});
|
||||||
|
$httpBackend.whenGET('Entries//getBuys?filter=%7B%7D').respond([{id: 1}]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('saveBuy()', () => {
|
||||||
|
it(`should call the buys patch route if the received buy has an ID`, () => {
|
||||||
|
const buy = {id: 1, itemFk: 1, quantity: 1, packageFk: 1};
|
||||||
|
|
||||||
|
const query = `Buys/${buy.id}`;
|
||||||
|
|
||||||
|
$httpBackend.expectPATCH(query).respond(200);
|
||||||
|
controller.saveBuy(buy);
|
||||||
|
$httpBackend.flush();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should call the entry addBuy post route if the received buy has no ID`, () => {
|
||||||
|
controller.entry = {id: 1};
|
||||||
|
const buy = {itemFk: 1, quantity: 1, packageFk: 1};
|
||||||
|
|
||||||
|
const query = `Entries/${controller.entry.id}/addBuy`;
|
||||||
|
|
||||||
|
$httpBackend.expectPOST(query).respond(200);
|
||||||
|
controller.saveBuy(buy);
|
||||||
|
$httpBackend.flush();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteBuys()', () => {
|
||||||
|
it(`should perform no queries if all buys to delete were not actual instances`, () => {
|
||||||
|
controller.buys = [
|
||||||
|
{checked: true},
|
||||||
|
{checked: true},
|
||||||
|
{checked: false}];
|
||||||
|
|
||||||
|
controller.deleteBuys();
|
||||||
|
|
||||||
|
expect(controller.buys.length).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should perform a query to delete as there's an actual instance at least`, () => {
|
||||||
|
controller.buys = [
|
||||||
|
{checked: true, id: 1},
|
||||||
|
{checked: true},
|
||||||
|
{checked: false}];
|
||||||
|
|
||||||
|
const query = 'Buys/deleteBuys';
|
||||||
|
|
||||||
|
$httpBackend.expectPOST(query).respond(200);
|
||||||
|
controller.deleteBuys();
|
||||||
|
$httpBackend.flush();
|
||||||
|
|
||||||
|
expect(controller.buys.length).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1 +1,2 @@
|
||||||
Buy: Lineas de entrada
|
Buys: Compras
|
||||||
|
Delete buy(s): Eliminar compra(s)
|
|
@ -0,0 +1,28 @@
|
||||||
|
@import "variables";
|
||||||
|
|
||||||
|
|
||||||
|
vn-entry-buy-index vn-card {
|
||||||
|
max-width: $width-xl;
|
||||||
|
|
||||||
|
.dark-row {
|
||||||
|
background-color: lighten($color-marginal, 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:nth-child(1) {
|
||||||
|
border-top: 1px solid $color-marginal;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody{
|
||||||
|
border-bottom: 1px solid $color-marginal;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:nth-child(3) {
|
||||||
|
height: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$color-font-link-medium: lighten($color-font-link, 20%)
|
|
@ -83,12 +83,12 @@
|
||||||
</span>
|
</span>
|
||||||
</vn-td>
|
</vn-td>
|
||||||
<vn-td number>
|
<vn-td number>
|
||||||
<vn-chip translate-attr="buy.groupingMode == 2 ? {title: 'Minimun amount'} : {title: 'Packing'}" ng-class="{'message': buy.groupingMode == 2}">
|
<vn-chip class="transparent" translate-attr="buy.groupingMode == 2 ? {title: 'Minimun amount'} : {title: 'Packing'}" ng-class="{'message': buy.groupingMode == 2}">
|
||||||
<span translate>{{::buy.packing | dashIfEmpty}}</span>
|
<span translate>{{::buy.packing | dashIfEmpty}}</span>
|
||||||
</vn-chip>
|
</vn-chip>
|
||||||
</vn-td>
|
</vn-td>
|
||||||
<vn-td number>
|
<vn-td number>
|
||||||
<vn-chip translate-attr="buy.groupingMode == 1 ? {title: 'Minimun amount'} : {title: 'Grouping'}" ng-class="{'message': buy.groupingMode == 1}">
|
<vn-chip class="transparent" translate-attr="buy.groupingMode == 1 ? {title: 'Minimun amount'} : {title: 'Grouping'}" ng-class="{'message': buy.groupingMode == 1}">
|
||||||
<span translate>{{::buy.grouping | dashIfEmpty}}</span>
|
<span translate>{{::buy.grouping | dashIfEmpty}}</span>
|
||||||
</vn-chip>
|
</vn-chip>
|
||||||
</vn-td>
|
</vn-td>
|
||||||
|
|
|
@ -87,13 +87,14 @@
|
||||||
"url": "/buy",
|
"url": "/buy",
|
||||||
"state": "entry.card.buy",
|
"state": "entry.card.buy",
|
||||||
"abstract": true,
|
"abstract": true,
|
||||||
"component": "ui-view"
|
"component": "ui-view",
|
||||||
|
"acl": ["buyer"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url" : "/index",
|
"url" : "/index",
|
||||||
"state": "entry.card.buy.index",
|
"state": "entry.card.buy.index",
|
||||||
"component": "vn-entry-buy-index",
|
"component": "vn-entry-buy-index",
|
||||||
"description": "Buy",
|
"description": "Buys",
|
||||||
"params": {
|
"params": {
|
||||||
"entry": "$ctrl.entry"
|
"entry": "$ctrl.entry"
|
||||||
},
|
},
|
||||||
|
|
|
@ -102,12 +102,12 @@
|
||||||
<td center title="{{::line.packageFk | dashIfEmpty}}">{{::line.packageFk | dashIfEmpty}}</td>
|
<td center title="{{::line.packageFk | dashIfEmpty}}">{{::line.packageFk | dashIfEmpty}}</td>
|
||||||
<td center title="{{::line.weight}}">{{::line.weight}}</td>
|
<td center title="{{::line.weight}}">{{::line.weight}}</td>
|
||||||
<td center>
|
<td center>
|
||||||
<vn-chip translate-attr="line.groupingMode == 2 ? {title: 'Minimun amount'} : {title: 'Packing'}" ng-class="{'message': line.groupingMode == 2}">
|
<vn-chip class="transparent" translate-attr="line.groupingMode == 2 ? {title: 'Minimun amount'} : {title: 'Packing'}" ng-class="{'message': line.groupingMode == 2}">
|
||||||
<span translate>{{::line.packing | dashIfEmpty}}</span>
|
<span translate>{{::line.packing | dashIfEmpty}}</span>
|
||||||
</vn-chip>
|
</vn-chip>
|
||||||
</td>
|
</td>
|
||||||
<td center>
|
<td center>
|
||||||
<vn-chip translate-attr="line.groupingMode == 1 ? {title: 'Minimun amount'} : {title: 'Grouping'}" ng-class="{'message': line.groupingMode == 1}">
|
<vn-chip class="transparent" translate-attr="line.groupingMode == 1 ? {title: 'Minimun amount'} : {title: 'Grouping'}" ng-class="{'message': line.groupingMode == 1}">
|
||||||
<span translate>{{::line.grouping | dashIfEmpty}}</span>
|
<span translate>{{::line.grouping | dashIfEmpty}}</span>
|
||||||
</vn-chip>
|
</vn-chip>
|
||||||
</vn-td>
|
</vn-td>
|
||||||
|
|
|
@ -22,13 +22,18 @@
|
||||||
"displayHeaderFooter": true,
|
"displayHeaderFooter": true,
|
||||||
"printBackground": true
|
"printBackground": true
|
||||||
},
|
},
|
||||||
"mysql": {
|
"datasources": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"options": {
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": 3306,
|
"port": 3306,
|
||||||
"database": "vn",
|
"database": "vn",
|
||||||
"user": "root",
|
"user": "root",
|
||||||
"password": "root"
|
"password": "root"
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"smtp": {
|
"smtp": {
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": 465,
|
"port": 465,
|
||||||
|
|
|
@ -8,7 +8,7 @@ header .logo {
|
||||||
}
|
}
|
||||||
|
|
||||||
header .logo img {
|
header .logo img {
|
||||||
width: 50%
|
width: 300px
|
||||||
}
|
}
|
||||||
|
|
||||||
header .topbar {
|
header .topbar {
|
||||||
|
|
|
@ -33,13 +33,13 @@ module.exports = {
|
||||||
SELECT s.name, s.street, s.postCode, s.city, s.phone
|
SELECT s.name, s.street, s.postCode, s.city, s.phone
|
||||||
FROM company c
|
FROM company c
|
||||||
JOIN supplier s ON s.id = c.id
|
JOIN supplier s ON s.id = c.id
|
||||||
WHERE c.code = :code`, {code});
|
WHERE c.code = ?`, [code]);
|
||||||
},
|
},
|
||||||
getFiscalAddress(code) {
|
getFiscalAddress(code) {
|
||||||
return db.findOne(`
|
return db.findOne(`
|
||||||
SELECT nif, register FROM company c
|
SELECT nif, register FROM company c
|
||||||
JOIN supplier s ON s.id = c.id
|
JOIN supplier s ON s.id = c.id
|
||||||
WHERE c.code = :code`, {code});
|
WHERE c.code = ?`, [code]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: ['companyCode']
|
props: ['companyCode']
|
||||||
|
|
|
@ -1,16 +1,38 @@
|
||||||
const mysql = require('mysql2/promise');
|
const mysql = require('mysql2');
|
||||||
const config = require('./config.js');
|
const config = require('./config.js');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const PromisePoolConnection = mysql.PromisePoolConnection;
|
const PoolConnection = mysql.PoolConnection;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
init() {
|
init() {
|
||||||
if (!this.pool) {
|
if (!this.pool) {
|
||||||
this.pool = mysql.createPool(config.mysql);
|
const datasources = config.datasources;
|
||||||
this.pool.on('connection', connection => {
|
const pool = mysql.createPoolCluster();
|
||||||
connection.config.namedPlaceholders = true;
|
|
||||||
});
|
for (let datasource of datasources)
|
||||||
|
pool.add(datasource.name, datasource.options);
|
||||||
|
|
||||||
|
this.pool = pool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this.pool;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retuns a pool connection from specific cluster node
|
||||||
|
* @param {String} name - The cluster name
|
||||||
|
*
|
||||||
|
* @return {Object} - Pool connection
|
||||||
|
*/
|
||||||
|
getConnection(name) {
|
||||||
|
let pool = this.pool;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
pool.getConnection(name, function(error, connection) {
|
||||||
|
if (error) return reject(error);
|
||||||
|
|
||||||
|
resolve(connection);
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,12 +45,27 @@ module.exports = {
|
||||||
*/
|
*/
|
||||||
rawSql(query, params, connection) {
|
rawSql(query, params, connection) {
|
||||||
let pool = this.pool;
|
let pool = this.pool;
|
||||||
if (params instanceof PromisePoolConnection)
|
if (params instanceof PoolConnection)
|
||||||
connection = params;
|
connection = params;
|
||||||
if (connection) pool = connection;
|
if (connection) pool = connection;
|
||||||
|
|
||||||
return pool.query(query, params).then(([rows]) => {
|
return new Promise((resolve, reject) => {
|
||||||
return rows;
|
if (!connection) {
|
||||||
|
pool.getConnection('default', function(error, conn) {
|
||||||
|
if (error) return reject(error);
|
||||||
|
|
||||||
|
conn.query(query, params, (error, rows) => {
|
||||||
|
if (error) return reject(error);
|
||||||
|
conn.release();
|
||||||
|
resolve(rows);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
connection.query(query, params, (error, rows) => {
|
||||||
|
if (error) return reject(error);
|
||||||
|
resolve(rows);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,14 @@ const db = require('../database');
|
||||||
|
|
||||||
const dbHelper = {
|
const dbHelper = {
|
||||||
methods: {
|
methods: {
|
||||||
|
/**
|
||||||
|
* Retuns a pool connection from specific cluster node
|
||||||
|
* @param {String} name - The cluster name
|
||||||
|
*
|
||||||
|
* @return {Object} - Pool connection
|
||||||
|
*/
|
||||||
|
getConnection: name => db.getConnection(name),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a query from a raw sql
|
* Makes a query from a raw sql
|
||||||
* @param {String} query - The raw SQL query
|
* @param {String} query - The raw SQL query
|
||||||
|
|
|
@ -26,13 +26,13 @@ module.exports = {
|
||||||
}).finally(async() => {
|
}).finally(async() => {
|
||||||
await db.rawSql(`
|
await db.rawSql(`
|
||||||
INSERT INTO vn.mail (sender, replyTo, sent, subject, body, status)
|
INSERT INTO vn.mail (sender, replyTo, sent, subject, body, status)
|
||||||
VALUES (:recipient, :sender, 1, :subject, :body, :status)`, {
|
VALUES (?, ?, 1, ?, ?, ?)`, [
|
||||||
sender: options.replyTo,
|
options.replyTo,
|
||||||
recipient: options.to,
|
options.to,
|
||||||
subject: options.subject,
|
options.subject,
|
||||||
body: options.text || options.html,
|
options.text || options.html,
|
||||||
status: error && error.message || 'Sent'
|
error && error.message || 'Sent'
|
||||||
});
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -23,12 +23,10 @@ module.exports = app => {
|
||||||
JOIN ticketState ts ON ts.ticketFk = t.id
|
JOIN ticketState ts ON ts.ticketFk = t.id
|
||||||
JOIN alertLevel al ON al.alertLevel = ts.alertLevel
|
JOIN alertLevel al ON al.alertLevel = ts.alertLevel
|
||||||
WHERE al.code = 'PACKED'
|
WHERE al.code = 'PACKED'
|
||||||
AND DATE(t.shipped) BETWEEN DATE_ADD(:to, INTERVAL -2 DAY)
|
AND DATE(t.shipped) BETWEEN DATE_ADD(?, INTERVAL -2 DAY)
|
||||||
AND util.dayEnd(:to)
|
AND util.dayEnd(?)
|
||||||
AND t.refFk IS NULL
|
AND t.refFk IS NULL
|
||||||
GROUP BY e.ticketFk`, {
|
GROUP BY e.ticketFk`, [reqArgs.to, reqArgs.to]);
|
||||||
to: reqArgs.to
|
|
||||||
});
|
|
||||||
const ticketIds = tickets.map(ticket => ticket.id);
|
const ticketIds = tickets.map(ticket => ticket.id);
|
||||||
|
|
||||||
await closeAll(ticketIds, req.args);
|
await closeAll(ticketIds, req.args);
|
||||||
|
@ -40,13 +38,11 @@ module.exports = app => {
|
||||||
JOIN deliveryMethod dm ON dm.id = am.deliveryMethodFk
|
JOIN deliveryMethod dm ON dm.id = am.deliveryMethodFk
|
||||||
JOIN zone z ON z.id = t.zoneFk
|
JOIN zone z ON z.id = t.zoneFk
|
||||||
SET t.routeFk = NULL
|
SET t.routeFk = NULL
|
||||||
WHERE DATE(t.shipped) BETWEEN DATE_ADD(:to, INTERVAL -2 DAY)
|
WHERE DATE(t.shipped) BETWEEN DATE_ADD(?, INTERVAL -2 DAY)
|
||||||
AND util.dayEnd(:to)
|
AND util.dayEnd(?)
|
||||||
AND al.code NOT IN('DELIVERED','PACKED')
|
AND al.code NOT IN('DELIVERED','PACKED')
|
||||||
AND t.routeFk
|
AND t.routeFk
|
||||||
AND z.name LIKE '%MADRID%'`, {
|
AND z.name LIKE '%MADRID%'`, [reqArgs.to, reqArgs.to]);
|
||||||
to: reqArgs.to
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
@ -70,11 +66,9 @@ module.exports = app => {
|
||||||
JOIN ticketState ts ON ts.ticketFk = t.id
|
JOIN ticketState ts ON ts.ticketFk = t.id
|
||||||
JOIN alertLevel al ON al.alertLevel = ts.alertLevel
|
JOIN alertLevel al ON al.alertLevel = ts.alertLevel
|
||||||
WHERE al.code = 'PACKED'
|
WHERE al.code = 'PACKED'
|
||||||
AND t.id = :ticketId
|
AND t.id = ?
|
||||||
AND t.refFk IS NULL
|
AND t.refFk IS NULL
|
||||||
GROUP BY e.ticketFk`, {
|
GROUP BY e.ticketFk`, [reqArgs.ticketId]);
|
||||||
ticketId: reqArgs.ticketId
|
|
||||||
});
|
|
||||||
const ticketIds = tickets.map(ticket => ticket.id);
|
const ticketIds = tickets.map(ticket => ticket.id);
|
||||||
|
|
||||||
await closeAll(ticketIds, reqArgs);
|
await closeAll(ticketIds, reqArgs);
|
||||||
|
@ -108,16 +102,17 @@ module.exports = app => {
|
||||||
JOIN ticketState ts ON ts.ticketFk = t.id
|
JOIN ticketState ts ON ts.ticketFk = t.id
|
||||||
JOIN alertLevel al ON al.alertLevel = ts.alertLevel
|
JOIN alertLevel al ON al.alertLevel = ts.alertLevel
|
||||||
WHERE al.code = 'PACKED'
|
WHERE al.code = 'PACKED'
|
||||||
AND t.agencyModeFk IN(:agencyModeId)
|
AND t.agencyModeFk IN(?)
|
||||||
AND t.warehouseFk = :warehouseId
|
AND t.warehouseFk = ?
|
||||||
AND DATE(t.shipped) BETWEEN DATE_ADD(:to, INTERVAL -2 DAY)
|
AND DATE(t.shipped) BETWEEN DATE_ADD(:to, INTERVAL -2 DAY)
|
||||||
AND util.dayEnd(:to)
|
AND util.dayEnd(?)
|
||||||
AND t.refFk IS NULL
|
AND t.refFk IS NULL
|
||||||
GROUP BY e.ticketFk`, {
|
GROUP BY e.ticketFk`, [
|
||||||
agencyModeId: agenciesId,
|
agenciesId,
|
||||||
warehouseId: reqArgs.warehouseId,
|
reqArgs.warehouseId,
|
||||||
to: reqArgs.to
|
reqArgs.to,
|
||||||
});
|
reqArgs.to
|
||||||
|
]);
|
||||||
const ticketIds = tickets.map(ticket => ticket.id);
|
const ticketIds = tickets.map(ticket => ticket.id);
|
||||||
|
|
||||||
await closeAll(ticketIds, reqArgs);
|
await closeAll(ticketIds, reqArgs);
|
||||||
|
@ -144,11 +139,9 @@ module.exports = app => {
|
||||||
JOIN ticketState ts ON ts.ticketFk = t.id
|
JOIN ticketState ts ON ts.ticketFk = t.id
|
||||||
JOIN alertLevel al ON al.alertLevel = ts.alertLevel
|
JOIN alertLevel al ON al.alertLevel = ts.alertLevel
|
||||||
WHERE al.code = 'PACKED'
|
WHERE al.code = 'PACKED'
|
||||||
AND t.routeFk = :routeId
|
AND t.routeFk = ?
|
||||||
AND t.refFk IS NULL
|
AND t.refFk IS NULL
|
||||||
GROUP BY e.ticketFk`, {
|
GROUP BY e.ticketFk`, [reqArgs.routeId]);
|
||||||
routeId: reqArgs.routeId
|
|
||||||
});
|
|
||||||
const ticketIds = tickets.map(ticket => ticket.id);
|
const ticketIds = tickets.map(ticket => ticket.id);
|
||||||
|
|
||||||
await closeAll(ticketIds, reqArgs);
|
await closeAll(ticketIds, reqArgs);
|
||||||
|
@ -179,9 +172,7 @@ module.exports = app => {
|
||||||
|
|
||||||
for (const ticket of tickets) {
|
for (const ticket of tickets) {
|
||||||
try {
|
try {
|
||||||
await db.rawSql(`CALL vn.ticket_close(:ticketId)`, {
|
await db.rawSql(`CALL vn.ticket_close(?)`, [ticket.id]);
|
||||||
ticketId: ticket.id
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasToInvoice = ticket.hasToInvoice && ticket.hasDailyInvoice;
|
const hasToInvoice = ticket.hasToInvoice && ticket.hasDailyInvoice;
|
||||||
if (!ticket.salesPersonFk || !ticket.isToBeMailed || hasToInvoice) continue;
|
if (!ticket.salesPersonFk || !ticket.isToBeMailed || hasToInvoice) continue;
|
||||||
|
@ -239,20 +230,19 @@ module.exports = app => {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function invalidEmail(ticket) {
|
async function invalidEmail(ticket) {
|
||||||
await db.rawSql(`UPDATE client SET email = NULL WHERE id = :clientId`, {
|
await db.rawSql(`UPDATE client SET email = NULL WHERE id = ?`, [
|
||||||
clientId: ticket.clientFk
|
ticket.clientFk
|
||||||
});
|
]);
|
||||||
|
|
||||||
const oldInstance = `{"email": "${ticket.recipient}"}`;
|
const oldInstance = `{"email": "${ticket.recipient}"}`;
|
||||||
const newInstance = `{"email": ""}`;
|
const newInstance = `{"email": ""}`;
|
||||||
await db.rawSql(`
|
await db.rawSql(`
|
||||||
INSERT INTO clientLog (originFk, userFk, action, changedModel, oldInstance, newInstance)
|
INSERT INTO clientLog (originFk, userFk, action, changedModel, oldInstance, newInstance)
|
||||||
VALUES (:clientId, :userId, 'UPDATE', 'Client', :oldInstance, :newInstance)`, {
|
VALUES (?, NULL, 'UPDATE', 'Client', ?, ?)`, [
|
||||||
clientId: ticket.clientFk,
|
ticket.clientFk,
|
||||||
userId: null,
|
oldInstance,
|
||||||
oldInstance: oldInstance,
|
newInstance
|
||||||
newInstance: newInstance
|
]);
|
||||||
});
|
|
||||||
|
|
||||||
const body = `No se ha podido enviar el albarán <strong>${ticket.id}</strong>
|
const body = `No se ha podido enviar el albarán <strong>${ticket.id}</strong>
|
||||||
al cliente <strong>${ticket.clientFk} - ${ticket.clientName}</strong>
|
al cliente <strong>${ticket.clientFk} - ${ticket.clientName}</strong>
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
const Stylesheet = require(`${appPath}/core/stylesheet`);
|
||||||
|
|
||||||
|
module.exports = new Stylesheet([
|
||||||
|
`${appPath}/common/css/spacing.css`,
|
||||||
|
`${appPath}/common/css/misc.css`,
|
||||||
|
`${appPath}/common/css/layout.css`,
|
||||||
|
`${appPath}/common/css/email.css`,
|
||||||
|
`${__dirname}/style.css`])
|
||||||
|
.mergeStyles();
|
|
@ -0,0 +1,33 @@
|
||||||
|
.grid-block table.column-oriented {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-block table.column-oriented td.message {
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 300px
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.grid-block table.column-oriented th a {
|
||||||
|
color: #333
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-block {
|
||||||
|
max-width: 98%
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-title {
|
||||||
|
background-color: #95d831;
|
||||||
|
padding: 0 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-title h2 {
|
||||||
|
margin: 10px 0
|
||||||
|
}
|
||||||
|
|
||||||
|
.external-link {
|
||||||
|
border: 2px dashed #8dba25;
|
||||||
|
border-radius: 3px;
|
||||||
|
text-align: center
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
subject: Informe de tickets semanal
|
||||||
|
title: Informe de tickets semanal
|
||||||
|
dear: Hola
|
||||||
|
description: A continuación se el resumen de incidencias resueltas desde <strong>{0 | date('%d-%m-%Y')}</strong> hasta <strong>{1}</strong>.
|
||||||
|
totalResolved: Un total de <strong>{0}</strong> tickets han sido resueltos durante la última semana.
|
||||||
|
author: Autor
|
||||||
|
dated: Fecha
|
||||||
|
opened: Abierto
|
||||||
|
closed: Cerrado
|
||||||
|
ticketSubject: Asunto
|
||||||
|
ticketDescription: Descripción
|
||||||
|
resolution: Resolución
|
||||||
|
grafanaLink: "Puedes ver la gráfica desde el siguiente enlace:"
|
|
@ -0,0 +1,93 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html v-bind:lang="$i18n.locale">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
<meta name="format-detection" content="telephone=no">
|
||||||
|
<title>{{ $t('subject') }}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<table class="grid">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<!-- Empty block -->
|
||||||
|
<div class="grid-row">
|
||||||
|
<div class="grid-block empty"></div>
|
||||||
|
</div>
|
||||||
|
<!-- Header block -->
|
||||||
|
<div class="grid-row">
|
||||||
|
<div class="grid-block">
|
||||||
|
<email-header v-bind="$props"></email-header>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Block -->
|
||||||
|
<div class="grid-row">
|
||||||
|
<div class="grid-block vn-pa-ml">
|
||||||
|
<h1>{{ $t('title') }}</h1>
|
||||||
|
<p>{{$t('dear')}},</p>
|
||||||
|
<p v-html="$t('description', [started, ended])"></p>
|
||||||
|
<p v-html="$t('totalResolved', [resolvedTickets])"></p>
|
||||||
|
<p v-html="$t('grafanaLink')"></p>
|
||||||
|
<div class="external-link vn-pa-sm vn-m-md">
|
||||||
|
<a v-bind:href="'https://grafana.verdnatura.es/d/2kaHDi9Mk/osticket?orgId=1&from=' + startedTime + '&to=' + endedTime" target="_blank">
|
||||||
|
https://grafana.verdnatura.es/d/2kaHDi9Mk/osticket?orgId=1&from={{startedTime}}&to={{endedTime}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Block -->
|
||||||
|
<div class="grid-row">
|
||||||
|
<div class="grid-block vn-pa-ml" v-for="technician in technicians">
|
||||||
|
<div class="table-title clearfix">
|
||||||
|
<h2>{{technician.name}} (<strong>{{technician.tickets.length}}</strong>)</h2>
|
||||||
|
</div>
|
||||||
|
<table class="column-oriented">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="5%">{{$t('author')}}</th>
|
||||||
|
<th width="5%">{{$t('dated')}}</th>
|
||||||
|
<th width="30%">{{$t('ticketSubject')}}</th>
|
||||||
|
<th width="30%">{{$t('ticketDescription')}}</th>
|
||||||
|
<th width="30%">{{$t('resolution')}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody v-for="ticket in technician.tickets">
|
||||||
|
<tr>
|
||||||
|
<td>{{ticket.author}}</td>
|
||||||
|
<td class="font light-gray">
|
||||||
|
<div v-bind:title="$t('opened')">
|
||||||
|
🔓 {{ticket.created | date('%d-%m-%Y %H:%M')}}
|
||||||
|
</div>
|
||||||
|
<div v-bind:title="$t('closed')">
|
||||||
|
🔒 {{ticket.closed | date('%d-%m-%Y %H:%M')}}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a v-bind:href="'https://cau.verdnatura.es/scp/tickets.php?id=' + ticket.ticket_id">
|
||||||
|
{{ticket.number}} - {{ticket.subject}}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="message" v-html="ticket.description"></td>
|
||||||
|
<td class="message" v-html="ticket.resolution"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer block -->
|
||||||
|
<div class="grid-row">
|
||||||
|
<div class="grid-block">
|
||||||
|
<email-footer v-bind="$props"></email-footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Empty block -->
|
||||||
|
<div class="grid-row">
|
||||||
|
<div class="grid-block empty"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,68 @@
|
||||||
|
const Component = require(`${appPath}/core/component`);
|
||||||
|
const emailHeader = new Component('email-header');
|
||||||
|
const emailFooter = new Component('email-footer');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: 'osticket-report',
|
||||||
|
async serverPrefetch() {
|
||||||
|
const tickets = await this.fetchTickets();
|
||||||
|
this.resolvedTickets = tickets.length;
|
||||||
|
|
||||||
|
const technicians = [];
|
||||||
|
for (let ticket of tickets) {
|
||||||
|
const technicianName = ticket.assigned;
|
||||||
|
|
||||||
|
let technician = technicians.find(technician => {
|
||||||
|
return technician.name == technicianName;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!technician) {
|
||||||
|
technician = {
|
||||||
|
name: technicianName,
|
||||||
|
tickets: []
|
||||||
|
};
|
||||||
|
technicians.push(technician);
|
||||||
|
}
|
||||||
|
|
||||||
|
technician.tickets.push(ticket);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.technicians = technicians.sort((acumulator, value) => {
|
||||||
|
return value.tickets.length - acumulator.tickets.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.technicians)
|
||||||
|
throw new Error('Something went wrong');
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
dated: function() {
|
||||||
|
const filters = this.$options.filters;
|
||||||
|
|
||||||
|
return filters.date(new Date(), '%d-%m-%Y');
|
||||||
|
},
|
||||||
|
startedTime: function() {
|
||||||
|
return new Date(this.started).getTime();
|
||||||
|
},
|
||||||
|
endedTime: function() {
|
||||||
|
return new Date(this.ended).getTime();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchDateRange() {
|
||||||
|
return this.findOneFromDef('dateRange');
|
||||||
|
},
|
||||||
|
async fetchTickets() {
|
||||||
|
const {started, ended} = await this.fetchDateRange();
|
||||||
|
this.started = started;
|
||||||
|
this.ended = ended;
|
||||||
|
|
||||||
|
const connection = await this.getConnection('osticket');
|
||||||
|
return this.rawSqlFromDef('tickets', [started, ended], connection);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
'email-header': emailHeader.build(),
|
||||||
|
'email-footer': emailFooter.build()
|
||||||
|
},
|
||||||
|
props: {}
|
||||||
|
};
|
|
@ -0,0 +1,8 @@
|
||||||
|
SELECT @lastWeekMondayTime AS started, @lastWeekFridayTime AS ended
|
||||||
|
FROM (
|
||||||
|
SELECT @lastWeek := DATE_ADD(CURDATE(), INTERVAL -1 WEEK),
|
||||||
|
@lastWeekMonday := DATE_ADD(@lastWeek, INTERVAL (-WEEKDAY(@lastWeek)) DAY),
|
||||||
|
@lastWeekFriday := DATE_ADD(@lastWeekMonday, INTERVAL (+6) DAY),
|
||||||
|
@lastWeekMondayTime := ADDTIME(DATE(@lastWeekMonday), '00:00:00'),
|
||||||
|
@lastWeekFridayTime := ADDTIME(DATE(@lastWeekFriday), '23:59:59')
|
||||||
|
) t
|
|
@ -0,0 +1,26 @@
|
||||||
|
SELECT * FROM (
|
||||||
|
SELECT DISTINCT ot.ticket_id,
|
||||||
|
ot.number,
|
||||||
|
ot.created,
|
||||||
|
ot.closed,
|
||||||
|
otu.name AS author,
|
||||||
|
otsf.username AS assigned,
|
||||||
|
otc.subject,
|
||||||
|
ote.body AS description,
|
||||||
|
oter.body AS resolution
|
||||||
|
FROM ost_ticket ot
|
||||||
|
JOIN ost_ticket__cdata otc ON ot.ticket_id = otc.ticket_id
|
||||||
|
JOIN ost_ticket_status ots ON ot.status_id = ots.id
|
||||||
|
JOIN ost_user otu ON ot.user_id = otu.id
|
||||||
|
LEFT JOIN ost_staff otsf ON ot.staff_id = otsf.staff_id
|
||||||
|
JOIN ost_thread oth ON ot.ticket_id = oth.object_id
|
||||||
|
AND oth.object_type = 'T'
|
||||||
|
LEFT JOIN ost_thread_entry ote ON oth.id = ote.thread_id
|
||||||
|
AND ote.type = 'M'
|
||||||
|
LEFT JOIN ost_thread_entry oter ON oth.id = oter.thread_id
|
||||||
|
AND oter.type = 'R'
|
||||||
|
WHERE ots.state = 'closed'
|
||||||
|
AND closed BETWEEN ? AND ?
|
||||||
|
ORDER BY oter.created DESC
|
||||||
|
) ot GROUP BY ot.ticket_id
|
||||||
|
ORDER BY ot.assigned
|
|
@ -17,7 +17,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchPayMethod(clientId) {
|
fetchPayMethod(clientId) {
|
||||||
return this.findOneFromDef('payMethod', {clientId: clientId});
|
return this.findOneFromDef('payMethod', [clientId]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -5,4 +5,4 @@ SELECT
|
||||||
pm.code
|
pm.code
|
||||||
FROM client c
|
FROM client c
|
||||||
JOIN payMethod pm ON pm.id = c.payMethodFk
|
JOIN payMethod pm ON pm.id = c.payMethodFk
|
||||||
WHERE c.id = :clientId
|
WHERE c.id = ?
|
|
@ -27,10 +27,10 @@ module.exports = {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchRoutes(routesId) {
|
fetchRoutes(routesId) {
|
||||||
return this.rawSqlFromDef('routes', {routesId});
|
return this.rawSqlFromDef('routes', [routesId]);
|
||||||
},
|
},
|
||||||
fetchTickets(routesId) {
|
fetchTickets(routesId) {
|
||||||
return this.rawSqlFromDef('tickets', {routesId});
|
return this.rawSqlFromDef('tickets', [routesId]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -13,4 +13,4 @@ FROM route r
|
||||||
LEFT JOIN worker w ON w.id = r.workerFk
|
LEFT JOIN worker w ON w.id = r.workerFk
|
||||||
LEFT JOIN account.user u ON u.id = w.userFk
|
LEFT JOIN account.user u ON u.id = w.userFk
|
||||||
LEFT JOIN agencyMode am ON am.id = r.agencyModeFk
|
LEFT JOIN agencyMode am ON am.id = r.agencyModeFk
|
||||||
WHERE r.id IN(:routesId)
|
WHERE r.id IN(?)
|
|
@ -24,11 +24,11 @@ FROM route r
|
||||||
LEFT JOIN address a ON a.id = t.addressFk
|
LEFT JOIN address a ON a.id = t.addressFk
|
||||||
LEFT JOIN client c ON c.id = t.clientFk
|
LEFT JOIN client c ON c.id = t.clientFk
|
||||||
LEFT JOIN worker w ON w.id = client_getSalesPerson(t.clientFk, CURDATE())
|
LEFT JOIN worker w ON w.id = client_getSalesPerson(t.clientFk, CURDATE())
|
||||||
LEFT JOIN account.user u ON u.id = w.userFk
|
LEFT JOIN account.user u ON u.id = w.id
|
||||||
LEFT JOIN ticketObservation tob ON tob.ticketFk = t.id AND tob.observationTypeFk = 3
|
LEFT JOIN ticketObservation tob ON tob.ticketFk = t.id AND tob.observationTypeFk = 3
|
||||||
LEFT JOIN province p ON a.provinceFk = p.id
|
LEFT JOIN province p ON a.provinceFk = p.id
|
||||||
LEFT JOIN warehouse wh ON wh.id = t.warehouseFk
|
LEFT JOIN warehouse wh ON wh.id = t.warehouseFk
|
||||||
LEFT JOIN agencyMode am ON am.id = t.agencyModeFk
|
LEFT JOIN agencyMode am ON am.id = t.agencyModeFk
|
||||||
LEFT JOIN stowaway s ON s.id = t.id
|
LEFT JOIN stowaway s ON s.id = t.id
|
||||||
WHERE r.id IN(:routesId)
|
WHERE r.id IN(?)
|
||||||
ORDER BY t.priority, t.id
|
ORDER BY t.priority, t.id
|
|
@ -73,7 +73,7 @@ module.exports = {
|
||||||
return this.rawSqlFromDef('tickets', [invoiceId]);
|
return this.rawSqlFromDef('tickets', [invoiceId]);
|
||||||
},
|
},
|
||||||
async fetchSales(invoiceId) {
|
async fetchSales(invoiceId) {
|
||||||
const connection = await db.pool.getConnection();
|
const connection = await db.getConnection('default');
|
||||||
await this.rawSql(`DROP TEMPORARY TABLE IF EXISTS tmp.invoiceTickets`, connection);
|
await this.rawSql(`DROP TEMPORARY TABLE IF EXISTS tmp.invoiceTickets`, connection);
|
||||||
await this.rawSqlFromDef('invoiceTickets', [invoiceId], connection);
|
await this.rawSqlFromDef('invoiceTickets', [invoiceId], connection);
|
||||||
|
|
||||||
|
|
|
@ -26,10 +26,10 @@ module.exports = {
|
||||||
return this.findOneFromDef('client', [clientId]);
|
return this.findOneFromDef('client', [clientId]);
|
||||||
},
|
},
|
||||||
fetchSales(clientId, companyId) {
|
fetchSales(clientId, companyId) {
|
||||||
return this.findOneFromDef('sales', {
|
return this.findOneFromDef('sales', [
|
||||||
clientId: clientId,
|
clientId,
|
||||||
companyId: companyId,
|
companyId
|
||||||
});
|
]);
|
||||||
},
|
},
|
||||||
getBalance(sale) {
|
getBalance(sale) {
|
||||||
if (sale.debtOut)
|
if (sale.debtOut)
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
CALL vn.clientGetDebtDiary(:clientId, :companyId)
|
CALL vn.clientGetDebtDiary(?, ?)
|
|
@ -20,10 +20,18 @@ const rptSepaCore = {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchClient(clientId, companyId) {
|
fetchClient(clientId, companyId) {
|
||||||
return this.findOneFromDef('client', {companyId, clientId});
|
return this.findOneFromDef('client', [
|
||||||
|
companyId,
|
||||||
|
companyId,
|
||||||
|
clientId
|
||||||
|
]);
|
||||||
},
|
},
|
||||||
fetchSupplier(clientId, companyId) {
|
fetchSupplier(clientId, companyId) {
|
||||||
return this.findOneFromDef('supplier', {companyId, clientId});
|
return this.findOneFromDef('supplier', [
|
||||||
|
companyId,
|
||||||
|
companyId,
|
||||||
|
clientId
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -13,7 +13,7 @@ SELECT
|
||||||
FROM client c
|
FROM client c
|
||||||
JOIN country ct ON ct.id = c.countryFk
|
JOIN country ct ON ct.id = c.countryFk
|
||||||
LEFT JOIN mandate m ON m.clientFk = c.id
|
LEFT JOIN mandate m ON m.clientFk = c.id
|
||||||
AND m.companyFk = :companyId AND m.finished IS NULL
|
AND m.companyFk = ? AND m.finished IS NULL
|
||||||
LEFT JOIN province p ON p.id = c.provinceFk
|
LEFT JOIN province p ON p.id = c.provinceFk
|
||||||
WHERE (m.companyFk = :companyId OR m.companyFk IS NULL) AND c.id = :clientId
|
WHERE (m.companyFk = ? OR m.companyFk IS NULL) AND c.id = ?
|
||||||
ORDER BY m.created DESC LIMIT 1
|
ORDER BY m.created DESC LIMIT 1
|
|
@ -8,10 +8,10 @@ SELECT
|
||||||
sp.name province
|
sp.name province
|
||||||
FROM client c
|
FROM client c
|
||||||
LEFT JOIN mandate m ON m.clientFk = c.id
|
LEFT JOIN mandate m ON m.clientFk = c.id
|
||||||
AND m.companyFk = :companyId AND m.finished IS NULL
|
AND m.companyFk = ? AND m.finished IS NULL
|
||||||
LEFT JOIN supplier s ON s.id = m.companyFk
|
LEFT JOIN supplier s ON s.id = m.companyFk
|
||||||
LEFT JOIN country sc ON sc.id = s.countryFk
|
LEFT JOIN country sc ON sc.id = s.countryFk
|
||||||
LEFT JOIN province sp ON sp.id = s.provinceFk
|
LEFT JOIN province sp ON sp.id = s.provinceFk
|
||||||
LEFT JOIN province p ON p.id = c.provinceFk
|
LEFT JOIN province p ON p.id = c.provinceFk
|
||||||
WHERE (m.companyFk = :companyId OR m.companyFk IS NULL) AND c.id = :clientId
|
WHERE (m.companyFk = ? OR m.companyFk IS NULL) AND c.id = ?
|
||||||
ORDER BY m.created DESC LIMIT 1
|
ORDER BY m.created DESC LIMIT 1
|
|
@ -29,5 +29,5 @@ SELECT
|
||||||
FROM buy b
|
FROM buy b
|
||||||
JOIN item i ON i.id = b.itemFk
|
JOIN item i ON i.id = b.itemFk
|
||||||
JOIN itemType it ON it.id = i.typeFk
|
JOIN itemType it ON it.id = i.typeFk
|
||||||
WHERE b.entryFk IN(:entriesId) AND b.quantity > 0
|
WHERE b.entryFk IN(?) AND b.quantity > 0
|
||||||
ORDER BY i.typeFk , i.name
|
ORDER BY i.typeFk , i.name
|
|
@ -40,7 +40,7 @@ module.exports = {
|
||||||
return this.rawSqlFromDef('entries', [supplierId, from, to]);
|
return this.rawSqlFromDef('entries', [supplierId, from, to]);
|
||||||
},
|
},
|
||||||
fetchBuys(entriesId) {
|
fetchBuys(entriesId) {
|
||||||
return this.rawSqlFromDef('buys', {entriesId});
|
return this.rawSqlFromDef('buys', [entriesId]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -6,4 +6,4 @@ SELECT
|
||||||
FROM route r
|
FROM route r
|
||||||
JOIN agencyMode am ON am.id = r.agencyModeFk
|
JOIN agencyMode am ON am.id = r.agencyModeFk
|
||||||
JOIN vehicle v ON v.id = r.vehicleFk
|
JOIN vehicle v ON v.id = r.vehicleFk
|
||||||
WHERE r.id = :routeId
|
WHERE r.id = ?
|
|
@ -8,7 +8,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchZone(routeId) {
|
fetchZone(routeId) {
|
||||||
return this.findOneFromDef('zone', {routeId});
|
return this.findOneFromDef('zone', [routeId]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|
Loading…
Reference in New Issue