diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql
index 5e85c78a1..d38bc89d8 100644
--- a/db/dump/fixtures.sql
+++ b/db/dump/fixtures.sql
@@ -2178,6 +2178,15 @@ INSERT INTO `hedera`.`imageCollectionSize`(`id`, `collectionFk`,`width`, `height
VALUES
(1, 4, 160, 160);
+INSERT INTO `vn`.`rateConfig`(`rate0`, `rate1`, `rate2`, `rate3`)
+ VALUES
+ (36, 31, 25, 21);
+
+INSERT INTO `vn`.`rate`(`dated`, `warehouseFk`, `rate0`, `rate1`, `rate2`, `rate3`)
+ VALUES
+ (DATE_ADD(CURDATE(), INTERVAL -1 YEAR), 1, 10, 15, 20, 25),
+ (CURDATE(), 1, 12, 17, 22, 27);
+
INSERT INTO `vn`.`awb` (id, code, package, weight, created, amount, transitoryFk, taxFk)
VALUES
(1, '07546501420', 67, 671, CURDATE(), 1761, 1, 1),
@@ -2247,4 +2256,4 @@ INSERT INTO `vn`.`duaInvoiceIn`(`id`, `duaFk`, `invoiceInFk`)
INSERT INTO `vn`.`ticketRecalc`(`ticketFk`)
SELECT `id` FROM `vn`.`ticket`;
-CALL `vn`.`ticket_doRecalc`();
\ No newline at end of file
+CALL `vn`.`ticket_doRecalc`();
diff --git a/e2e/assets/07_import_buys.json b/e2e/assets/07_import_buys.json
new file mode 100644
index 000000000..5f0f74342
--- /dev/null
+++ b/e2e/assets/07_import_buys.json
@@ -0,0 +1,127 @@
+{
+ "invoices": [
+ {
+ "tx_company": "TESSAROSES S.A.",
+ "id_invoice": "20062926",
+ "id_purchaseorder": "20106319",
+ "tx_customer_ref": "",
+ "id_customer": "56116",
+ "id_customer_floricode": "",
+ "nm_bill": "VERDNATURA LEVANTE SL",
+ "nm_ship": "VERDNATURA LEVANTE SL",
+ "nm_cargo": "OYAMBARILLO",
+ "dt_purchaseorder": "06/19/2020",
+ "dt_fly": "06/20/2020",
+ "dt_invoice": "06/19/2020",
+ "nm_incoterm": "FOB UIO",
+ "tx_awb": "729-6340 2846",
+ "tx_hawb": "LA0061832844",
+ "tx_oe": "05520204000335992",
+ "nu_totalstemsPO": "850",
+ "mny_flower": "272.5000",
+ "mny_freight": "0.0000",
+ "mny_total": "272.5000",
+ "nu_boxes": "4",
+ "nu_fulls": "1.75",
+ "dt_posted": "2020-06-19T13:31:41",
+ "boxes": [
+ {
+ "id_box": "200573095",
+ "nm_box": "HB",
+ "tp_box": "HB",
+ "tx_label": "",
+ "nu_length": "96",
+ "nu_width": "32",
+ "nu_height": "30.5",
+ "products": [
+ {
+ "id_floricode": "27887",
+ "id_migros_variety": "",
+ "nm_product": "FREEDOM 60CM 25ST",
+ "nm_species": "ROSES",
+ "nm_variety": "FREEDOM",
+ "nu_length": "60",
+ "nu_stems_bunch": "25",
+ "nu_bunches": "10",
+ "mny_rate_stem": "0.3500",
+ "mny_freight_unit": "0.0000",
+ "barcodes": "202727621,202725344,202725345,202725571,202725730,202725731,202725732,202725925,202726131,202726685"
+ }
+ ]
+ },
+ {
+ "id_box": "200573106",
+ "nm_box": "HB",
+ "tp_box": "HB",
+ "tx_label": "",
+ "nu_length": "96",
+ "nu_width": "32",
+ "nu_height": "30.5",
+ "products": [
+ {
+ "id_floricode": "27887",
+ "id_migros_variety": "",
+ "nm_product": "FREEDOM 70CM 25ST",
+ "nm_species": "ROSES",
+ "nm_variety": "FREEDOM",
+ "nu_length": "70",
+ "nu_stems_bunch": "25",
+ "nu_bunches": "8",
+ "mny_rate_stem": "0.4000",
+ "mny_freight_unit": "0.0000",
+ "barcodes": "202727077,202727078,202727079,202727080,202727650,202727654,202727656,202727657"
+ }
+ ]
+ },
+ {
+ "id_box": "200573117",
+ "nm_box": "HB",
+ "tp_box": "HB",
+ "tx_label": "",
+ "nu_length": "96",
+ "nu_width": "32",
+ "nu_height": "30.5",
+ "products": [
+ {
+ "id_floricode": "28409",
+ "id_migros_variety": "",
+ "nm_product": "TIBET 40CM 25ST",
+ "nm_species": "ROSES",
+ "nm_variety": "TIBET",
+ "nu_length": "40",
+ "nu_stems_bunch": "25",
+ "nu_bunches": "12",
+ "mny_rate_stem": "0.2500",
+ "mny_freight_unit": "0.0000",
+ "barcodes": "202723350,202723351,202723352,202723353,202723354,202723355,202723356,202723357,202726690,202726745,202726813,202726814"
+ }
+ ]
+ },
+ {
+ "id_box": "200573506",
+ "nm_box": "QB 2",
+ "tp_box": "QB",
+ "tx_label": "",
+ "nu_length": "80",
+ "nu_width": "30",
+ "nu_height": "17.5",
+ "products": [
+ {
+ "id_floricode": "27887",
+ "id_migros_variety": "",
+ "nm_product": "FREEDOM 50CM 25ST",
+ "nm_species": "ROSES",
+ "nm_variety": "FREEDOM",
+ "nu_length": "50",
+ "nu_stems_bunch": "25",
+ "nu_bunches": "4",
+ "mny_rate_stem": "0.3000",
+ "mny_freight_unit": "0.0000",
+ "barcodes": "202727837,202727839,202727842,202726682"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/storage/dms/ecc/3.jpeg b/e2e/assets/thermograph.jpeg
similarity index 100%
rename from storage/dms/ecc/3.jpeg
rename to e2e/assets/thermograph.jpeg
diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js
index 75ae95293..d06e9c75d 100644
--- a/e2e/helpers/selectors.js
+++ b/e2e/helpers/selectors.js
@@ -1015,6 +1015,17 @@ export default {
travelsQuicklink: 'vn-entry-descriptor vn-quick-link[icon="local_airport"] > a',
entriesQuicklink: 'vn-entry-descriptor vn-quick-link[icon="icon-entry"] > a'
},
+ entryBuys: {
+ importButton: 'vn-entry-buy-index vn-icon[icon="publish"]',
+ ref: 'vn-entry-buy-import vn-textfield[ng-model="$ctrl.import.ref"]',
+ observation: 'vn-entry-buy-import vn-textarea[ng-model="$ctrl.import.observation"]',
+ file: 'vn-entry-buy-import vn-input-file[ng-model="$ctrl.import.file"]',
+ firstImportedItem: 'vn-entry-buy-import tbody:nth-child(2) vn-autocomplete[ng-model="buy.itemFk"]',
+ secondImportedItem: 'vn-entry-buy-import tbody:nth-child(3) vn-autocomplete[ng-model="buy.itemFk"]',
+ thirdImportedItem: 'vn-entry-buy-import tbody:nth-child(4) vn-autocomplete[ng-model="buy.itemFk"]',
+ fourthImportedItem: 'vn-entry-buy-import tbody:nth-child(5) vn-autocomplete[ng-model="buy.itemFk"]',
+ importBuysButton: 'vn-entry-buy-import button[type="submit"]'
+ },
entryLatestBuys: {
firstBuy: 'vn-entry-latest-buys vn-tbody > a:nth-child(1)',
allBuysCheckBox: 'vn-entry-latest-buys vn-thead vn-check',
diff --git a/e2e/paths/10-travel/05_thermograph.spec.js b/e2e/paths/10-travel/05_thermograph.spec.js
index 44fc783f0..97077554f 100644
--- a/e2e/paths/10-travel/05_thermograph.spec.js
+++ b/e2e/paths/10-travel/05_thermograph.spec.js
@@ -38,7 +38,7 @@ describe('Travel thermograph path', () => {
it('should select the file to upload', async() => {
let currentDir = process.cwd();
- let filePath = `${currentDir}/storage/dms/ecc/3.jpeg`;
+ let filePath = `${currentDir}/e2e/assets/thermograph.jpeg`;
const [fileChooser] = await Promise.all([
page.waitForFileChooser(),
diff --git a/e2e/paths/12-entry/03_latestBuys.spec.js b/e2e/paths/12-entry/03_latestBuys.spec.js
index 8e9de8158..f2d64e3b4 100644
--- a/e2e/paths/12-entry/03_latestBuys.spec.js
+++ b/e2e/paths/12-entry/03_latestBuys.spec.js
@@ -41,6 +41,6 @@ describe('Entry lastest buys path', () => {
it('should navigate to the entry.buy section by clicking one of the buys', async() => {
await page.waitToClick(selectors.entryLatestBuys.firstBuy);
- await page.waitForState('entry.card.buy');
+ await page.waitForState('entry.card.buy.index');
});
});
diff --git a/e2e/paths/12-entry/07_import_buys.spec.js b/e2e/paths/12-entry/07_import_buys.spec.js
new file mode 100644
index 000000000..02db0ded5
--- /dev/null
+++ b/e2e/paths/12-entry/07_import_buys.spec.js
@@ -0,0 +1,62 @@
+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);
+ });
+});
diff --git a/front/core/components/field/index.js b/front/core/components/field/index.js
index d18973bbe..e10ef6383 100644
--- a/front/core/components/field/index.js
+++ b/front/core/components/field/index.js
@@ -20,7 +20,8 @@ export default class Field extends FormInput {
super.$onInit();
if (this.info) this.classList.add('has-icons');
- this.input.addEventListener('change', () => this.onChange());
+ this.input.addEventListener('change', event =>
+ this.onChange(event));
}
set field(value) {
@@ -82,6 +83,9 @@ export default class Field extends FormInput {
this._required = value;
let required = this.element.querySelector('.required');
display(required, this._required);
+
+ this.$.$applyAsync(() =>
+ this.input.setAttribute('required', value));
}
get required() {
@@ -186,10 +190,13 @@ export default class Field extends FormInput {
this.refreshHint();
}
- onChange() {
+ onChange($event) {
// Changes doesn't reflect until appling async
this.$.$applyAsync(() => {
- this.emit('change', {value: this.field});
+ this.emit('change', {
+ value: this.field,
+ $event: $event
+ });
});
}
}
diff --git a/front/core/components/input-file/index.js b/front/core/components/input-file/index.js
index 8bdb1a4fe..962f38b73 100644
--- a/front/core/components/input-file/index.js
+++ b/front/core/components/input-file/index.js
@@ -71,12 +71,23 @@ export default class InputFile extends Field {
this.input.click();
}
- onChange() {
+ onChange($event) {
this.emit('change', {
value: this.field,
- $files: this.files
+ $files: this.files,
+ $event: $event
});
}
+
+ get accept() {
+ return this._accept;
+ }
+
+ set accept(value) {
+ this._accept = value;
+ this.$.$applyAsync(() =>
+ this.input.setAttribute('accept', value));
+ }
}
ngModule.vnComponent('vnInputFile', {
diff --git a/modules/entry/back/methods/entry/importBuys.js b/modules/entry/back/methods/entry/importBuys.js
new file mode 100644
index 000000000..10871f4ad
--- /dev/null
+++ b/modules/entry/back/methods/entry/importBuys.js
@@ -0,0 +1,100 @@
+
+const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
+module.exports = Self => {
+ Self.remoteMethodCtx('importBuys', {
+ description: 'Imports the buys from a list',
+ accessType: 'WRITE',
+ accepts: [{
+ arg: 'id',
+ type: 'number',
+ required: true,
+ description: 'The entry id',
+ http: {source: 'path'}
+ },
+ {
+ arg: 'options',
+ type: 'object',
+ description: 'Callback options',
+ },
+ {
+ arg: 'ref',
+ type: 'string',
+ description: 'The buyed boxes ids',
+ },
+ {
+ arg: 'observation',
+ type: 'string',
+ description: 'The observation',
+ },
+ {
+ arg: 'buys',
+ type: ['Object'],
+ description: 'The buys',
+ }],
+ returns: {
+ type: ['Object'],
+ root: true
+ },
+ http: {
+ path: `/:id/importBuys`,
+ verb: 'POST'
+ }
+ });
+
+ Self.importBuys = async(ctx, id, options = {}) => {
+ const conn = Self.dataSource.connector;
+ const args = ctx.args;
+ const models = Self.app.models;
+
+ let tx;
+ if (!options.transaction) {
+ tx = await Self.beginTransaction({});
+ options.transaction = tx;
+ }
+
+ try {
+ const entry = await models.Entry.findById(id, null, options);
+ await entry.updateAttributes({
+ observation: args.observation,
+ ref: args.ref
+ }, options);
+
+ const buys = [];
+ for (let buy of args.buys) {
+ buys.push({
+ entryFk: entry.id,
+ itemFk: buy.itemFk,
+ stickers: 1,
+ quantity: 1,
+ packing: buy.packing,
+ grouping: buy.grouping,
+ buyingValue: buy.buyingValue,
+ packageFk: buy.packageFk
+ });
+ }
+
+ const createdBuys = await models.Buy.create(buys, options);
+ const buyIds = createdBuys.map(buy => buy.id);
+
+ 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`, [buyIds]);
+
+ stmts.push(stmt);
+ stmts.push('CALL buy_recalcPrices()');
+
+ const sql = ParameterizedSQL.join(stmts, ';');
+ await conn.executeStmt(sql, options);
+ if (tx) await tx.commit();
+ } catch (e) {
+ if (tx) await tx.rollback();
+ throw e;
+ }
+ };
+};
diff --git a/modules/entry/back/methods/entry/importBuysPreview.js b/modules/entry/back/methods/entry/importBuysPreview.js
new file mode 100644
index 000000000..9d6662327
--- /dev/null
+++ b/modules/entry/back/methods/entry/importBuysPreview.js
@@ -0,0 +1,40 @@
+module.exports = Self => {
+ Self.remoteMethod('importBuysPreview', {
+ description: 'Calculates the preview buys for an entry import',
+ accessType: 'READ',
+ accepts: [{
+ arg: 'id',
+ type: 'number',
+ required: true,
+ description: 'The entry id',
+ http: {source: 'path'}
+ },
+ {
+ arg: 'buys',
+ type: ['Object'],
+ description: 'The buys',
+ }],
+ returns: {
+ type: ['Object'],
+ root: true
+ },
+ http: {
+ path: `/:id/importBuysPreview`,
+ verb: 'GET'
+ }
+ });
+
+ Self.importBuysPreview = async(id, buys) => {
+ const models = Self.app.models;
+ for (let buy of buys) {
+ const packaging = await models.Packaging.findOne({
+ fields: ['id'],
+ where: {volume: {gte: buy.volume}},
+ order: 'volume ASC'
+ });
+ buy.packageFk = packaging.id;
+ }
+
+ return buys;
+ };
+};
diff --git a/modules/entry/back/methods/entry/specs/importBuys.spec.js b/modules/entry/back/methods/entry/specs/importBuys.spec.js
new file mode 100644
index 000000000..d0793a2f6
--- /dev/null
+++ b/modules/entry/back/methods/entry/specs/importBuys.spec.js
@@ -0,0 +1,80 @@
+const app = require('vn-loopback/server/server');
+const LoopBackContext = require('loopback-context');
+
+describe('entry import()', () => {
+ let newEntry;
+ const buyerId = 35;
+ const companyId = 442;
+ const travelId = 1;
+ const supplierId = 1;
+ const activeCtx = {
+ accessToken: {userId: buyerId},
+ };
+
+ beforeAll(async done => {
+ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
+ active: activeCtx
+ });
+
+ done();
+ });
+
+ it('should import the buy rows', async() => {
+ const expectedRef = '1, 2';
+ const expectedObservation = '123456';
+ const ctx = {
+ req: activeCtx,
+ args: {
+ observation: expectedObservation,
+ ref: expectedRef,
+ buys: [
+ {
+ itemFk: 1,
+ buyingValue: 5.77,
+ description: 'Bow',
+ grouping: 1,
+ packing: 1,
+ size: 1,
+ volume: 1200,
+ packageFk: '94'
+ },
+ {
+ itemFk: 4,
+ buyingValue: 2.16,
+ description: 'Arrow',
+ grouping: 1,
+ packing: 1,
+ size: 25,
+ volume: 1125,
+ packageFk: '94'
+ }
+ ]
+ }
+ };
+ const tx = await app.models.Entry.beginTransaction({});
+ const options = {transaction: tx};
+
+ newEntry = await app.models.Entry.create({
+ dated: new Date(),
+ supplierFk: supplierId,
+ travelFk: travelId,
+ companyFk: companyId,
+ observation: 'The entry',
+ ref: 'Entry ref'
+ }, options);
+
+ await app.models.Entry.importBuys(ctx, newEntry.id, options);
+
+ const updatedEntry = await app.models.Entry.findById(newEntry.id, null, options);
+ const entryBuys = await app.models.Buy.find({
+ where: {entryFk: newEntry.id}
+ }, options);
+
+ expect(updatedEntry.observation).toEqual(expectedObservation);
+ expect(updatedEntry.ref).toEqual(expectedRef);
+ expect(entryBuys.length).toEqual(2);
+
+ // Restores
+ await tx.rollback();
+ });
+});
diff --git a/modules/entry/back/methods/entry/specs/importBuysPreview.spec.js b/modules/entry/back/methods/entry/specs/importBuysPreview.spec.js
new file mode 100644
index 000000000..d286993ad
--- /dev/null
+++ b/modules/entry/back/methods/entry/specs/importBuysPreview.spec.js
@@ -0,0 +1,41 @@
+const app = require('vn-loopback/server/server');
+const LoopBackContext = require('loopback-context');
+
+describe('entry importBuysPreview()', () => {
+ const entryId = 1;
+ beforeAll(async done => {
+ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
+ active: activeCtx
+ });
+
+ done();
+ });
+
+ it('should return the buys with the calculated packageFk', async() => {
+ const expectedPackageFk = '3';
+ const buys = [
+ {
+ itemFk: 1,
+ buyingValue: 5.77,
+ description: 'Bow',
+ grouping: 1,
+ size: 1,
+ volume: 1200
+ },
+ {
+ itemFk: 4,
+ buyingValue: 2.16,
+ description: 'Arrow',
+ grouping: 1,
+ size: 25,
+ volume: 1125
+ }
+ ];
+
+ const result = await app.models.Entry.importBuysPreview(entryId, buys);
+ const randomIndex = Math.floor(Math.random() * result.length);
+ const buy = result[randomIndex];
+
+ expect(buy.packageFk).toEqual(expectedPackageFk);
+ });
+});
diff --git a/modules/entry/back/models/entry.js b/modules/entry/back/models/entry.js
index 94dbe787d..f1a22fddd 100644
--- a/modules/entry/back/models/entry.js
+++ b/modules/entry/back/models/entry.js
@@ -2,4 +2,6 @@ module.exports = Self => {
require('../methods/entry/filter')(Self);
require('../methods/entry/getEntry')(Self);
require('../methods/entry/getBuys')(Self);
+ require('../methods/entry/importBuys')(Self);
+ require('../methods/entry/importBuysPreview')(Self);
};
diff --git a/modules/entry/back/models/entry.json b/modules/entry/back/models/entry.json
index 40d6d29dd..78d3c5e4f 100644
--- a/modules/entry/back/models/entry.json
+++ b/modules/entry/back/models/entry.json
@@ -28,7 +28,7 @@
"type": "boolean"
},
"notes": {
- "type": "String"
+ "type": "string"
},
"isConfirmed": {
"type": "boolean"
diff --git a/modules/entry/front/buy/import/index.html b/modules/entry/front/buy/import/index.html
new file mode 100644
index 000000000..74b6c708a
--- /dev/null
+++ b/modules/entry/front/buy/import/index.html
@@ -0,0 +1,116 @@
+