2167-entry_buy #613

Merged
joan merged 9 commits from 2167-entry_buy into dev 2021-04-29 07:11:33 +00:00
22 changed files with 956 additions and 170 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@ describe('Entry', () => {
controller.entry = { controller.entry = {
id: 1 id: 1
}; };
controller.$params = {id: 1};
})); }));
describe('fillData()', () => { describe('fillData()', () => {

View File

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

View File

@ -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: '<'
} }

View File

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

View File

@ -1 +1,2 @@
Buy: Lineas de entrada Buys: Compras
Delete buy(s): Eliminar compra(s)

View File

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

View File

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

View File

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

View File

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

View File

@ -91,81 +91,81 @@
vn-tooltip="{{::$ctrl.$t('Reserved')}}"> vn-tooltip="{{::$ctrl.$t('Reserved')}}">
</vn-icon> </vn-icon>
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>
<img <img
ng-src="{{::$root.imagePath('catalog', '50x50', sale.itemFk)}}" ng-src="{{::$root.imagePath('catalog', '50x50', sale.itemFk)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', sale.itemFk)}}" zoom-image="{{::$root.imagePath('catalog', '1600x900', sale.itemFk)}}"
on-error-src/> on-error-src/>
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>
<span class="link" ng-if="sale.id" <span class="link" ng-if="sale.id"
ng-click="descriptor.show($event, sale.itemFk, sale.id)"> ng-click="descriptor.show($event, sale.itemFk, sale.id)">
{{sale.itemFk}} {{sale.itemFk}}
</span> </span>
<vn-autocomplete ng-if="!sale.id" class="dense" <vn-autocomplete ng-if="!sale.id" class="dense"
vn-focus
url="Items"
ng-model="sale.itemFk"
show-field="name"
value-field="id"
search-function="$ctrl.itemSearchFunc($search)"
on-change="$ctrl.changeQuantity(sale)"
order="id DESC"
tabindex="1">
<tpl-item>
{{::id}} - {{::name}}
</tpl-item>
</vn-autocomplete>
</vn-td>
<vn-td-editable disabled="!$ctrl.isEditable" shrink>
<text>{{sale.quantity}}</text>
<field>
<vn-input-number class="dense"
vn-focus vn-focus
ng-model="sale.quantity" url="Items"
on-change="$ctrl.changeQuantity(sale)"> ng-model="sale.itemFk"
</vn-input-number> show-field="name"
</field> value-field="id"
</vn-td-editable> search-function="$ctrl.itemSearchFunc($search)"
<vn-td-editable vn-fetched-tags wide disabled="!sale.id || !$ctrl.isEditable"> on-change="$ctrl.changeQuantity(sale)"
<text> order="id DESC"
<vn-one title="{{sale.concept}}">{{sale.concept}}</vn-one> tabindex="1">
<vn-one ng-if="::sale.subName"> <tpl-item>
<h3 title="{{::sale.subName}}">{{::sale.subName}}</h3> {{::id}} - {{::name}}
</vn-one> </tpl-item>
<vn-fetched-tags </vn-autocomplete>
max-length="6" </vn-td>
item="::sale.item" <vn-td-editable disabled="!$ctrl.isEditable" shrink>
tabindex="-1"> <text>{{sale.quantity}}</text>
</vn-fetched-tags> <field>
</text> <vn-input-number class="dense"
<field> vn-focus
<vn-textfield class="dense" vn-focus ng-model="sale.quantity"
vn-id="concept" on-change="$ctrl.changeQuantity(sale)">
ng-model="sale.concept" </vn-input-number>
on-change="$ctrl.updateConcept(sale)"> </field>
</vn-textfield> </vn-td-editable>
</field> <vn-td-editable vn-fetched-tags wide disabled="!sale.id || !$ctrl.isEditable">
</vn-td-editable> <text>
<vn-td number> <vn-one title="{{sale.concept}}">{{sale.concept}}</vn-one>
<span ng-class="{'link': $ctrl.isEditable}" <vn-one ng-if="::sale.subName">
translate-attr="{title: $ctrl.isEditable ? 'Edit price' : ''}" <h3 title="{{::sale.subName}}">{{::sale.subName}}</h3>
ng-click="$ctrl.showEditPricePopover($event, sale)"> </vn-one>
{{sale.price | currency: 'EUR':2}} <vn-fetched-tags
</span> max-length="6"
</vn-td> item="::sale.item"
<vn-td number> tabindex="-1">
<span ng-class="{'link': !$ctrl.isLocked}" </vn-fetched-tags>
translate-attr="{title: !$ctrl.isLocked ? 'Edit discount' : ''}" </text>
ng-click="$ctrl.showEditDiscountPopover($event, sale)" <field>
ng-if="sale.id"> <vn-textfield class="dense" vn-focus
{{(sale.discount / 100) | percentage}} vn-id="concept"
</span> ng-model="sale.concept"
</vn-td> on-change="$ctrl.updateConcept(sale)">
<vn-td number> </vn-textfield>
{{$ctrl.getSaleTotal(sale) | currency: 'EUR':2}} </field>
</vn-td> </vn-td-editable>
</vn-tr> <vn-td number>
<span ng-class="{'link': $ctrl.isEditable}"
translate-attr="{title: $ctrl.isEditable ? 'Edit price' : ''}"
ng-click="$ctrl.showEditPricePopover($event, sale)">
{{sale.price | currency: 'EUR':2}}
</span>
</vn-td>
<vn-td number>
<span ng-class="{'link': !$ctrl.isLocked}"
translate-attr="{title: !$ctrl.isLocked ? 'Edit discount' : ''}"
ng-click="$ctrl.showEditDiscountPopover($event, sale)"
ng-if="sale.id">
{{(sale.discount / 100) | percentage}}
</span>
</vn-td>
<vn-td number>
{{$ctrl.getSaleTotal(sale) | currency: 'EUR':2}}
</vn-td>
</vn-tr>
</vn-tbody> </vn-tbody>
</vn-table> </vn-table>
<div> <div>