diff --git a/db/changes/10340-summer/00-ACL.sql b/db/changes/10340-summer/00-ACL.sql
index a9a5778e5..fd92b3c1c 100644
--- a/db/changes/10340-summer/00-ACL.sql
+++ b/db/changes/10340-summer/00-ACL.sql
@@ -2,3 +2,6 @@ DELETE FROM `salix`.`ACL` WHERE id = 189;
DELETE FROM `salix`.`ACL` WHERE id = 188;
UPDATE `salix`.`ACL` tdms SET tdms.accessType = '*'
WHERE tdms.id = 165;
+INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
+ VALUES ('InvoiceOut', 'createManualInvoice', 'WRITE', 'ALLOW', 'ROLE', 'invoicing');
+
diff --git a/db/changes/10340-summer/00-buy_importReference.sql b/db/changes/10340-summer/00-buy_importReference.sql
new file mode 100644
index 000000000..f6bdc059c
--- /dev/null
+++ b/db/changes/10340-summer/00-buy_importReference.sql
@@ -0,0 +1,14 @@
+create table `vn`.`itemMatchProperties`
+(
+ itemFk int not null,
+ name varchar(80) not null,
+ producer varchar(80) not null,
+ size int not null,
+ constraint itemMatchProperties_pk
+ primary key (itemFk, name, producer, size),
+ constraint itemFk___fk
+ foreign key (itemFk) references item (id)
+ on update cascade on delete cascade
+)
+comment 'Propiedades para encontrar articulos equivalentes en verdnatura';
+
diff --git a/db/changes/10340-summer/00-invoiceFromClient.sql b/db/changes/10340-summer/00-invoiceFromClient.sql
new file mode 100644
index 000000000..d198e873d
--- /dev/null
+++ b/db/changes/10340-summer/00-invoiceFromClient.sql
@@ -0,0 +1,21 @@
+drop procedure `vn`.`invoiceFromClient`;
+
+DELIMITER $$
+$$
+create
+ definer = root@`%` procedure `vn`.`invoiceFromClient`(IN vMaxTicketDate datetime, IN vClientFk INT, IN vCompanyFk INT)
+BEGIN
+ DECLARE vMinTicketDate DATE DEFAULT TIMESTAMPADD(YEAR, -3, CURDATE());
+ SET vMaxTicketDate = util.dayend(vMaxTicketDate);
+
+ DROP TEMPORARY TABLE IF EXISTS `ticketToInvoice`;
+ CREATE TEMPORARY TABLE `ticketToInvoice`
+ (PRIMARY KEY (`id`))
+ ENGINE = MEMORY
+ SELECT id FROM ticket t
+ WHERE t.clientFk = vClientFk
+ AND t.refFk IS NULL
+ AND t.companyFk = vCompanyFk
+ AND (t.shipped BETWEEN vMinTicketDate AND vMaxTicketDate);
+END;;$$
+DELIMITER ;
diff --git a/db/changes/10340-summer/00-invoiceOut_newFromClient.sql b/db/changes/10340-summer/00-invoiceOut_newFromClient.sql
new file mode 100644
index 000000000..b4d4f916b
--- /dev/null
+++ b/db/changes/10340-summer/00-invoiceOut_newFromClient.sql
@@ -0,0 +1,45 @@
+drop procedure `vn`.`invoiceOut_newFromClient`;
+
+DELIMITER $$
+$$
+create
+ definer = root@`%` procedure `vn`.`invoiceOut_newFromClient`(IN vClientFk int, IN vSerial char(2), IN vMaxShipped date,
+ IN vCompanyFk int, IN vTaxArea varchar(25),
+ IN vRef varchar(25), OUT vInvoiceId int)
+BEGIN
+/**
+ * Factura los tickets de un cliente hasta una fecha dada
+ * @param vClientFk Id del cliente a facturar
+ * @param vSerial Serie de factura
+ * @param vMaxShipped Fecha hasta la cual cogera tickets para facturar
+ * @param vCompanyFk Id de la empresa desde la que se factura
+ * @param vTaxArea Tipo de iva en relacion a la empresa y al cliente, NULL por defecto
+ * @param vRef Referencia de la factura en caso que se quiera forzar, NULL por defecto
+ * @return vInvoiceId factura
+ */
+
+ DECLARE vIsRefEditable BOOLEAN;
+
+ IF vRef IS NOT NULL THEN
+ SELECT isRefEditable INTO vIsRefEditable
+ FROM invoiceOutSerial
+ WHERE code = vSerial;
+
+ IF NOT vIsRefEditable THEN
+ CALL util.throw('serial non editable');
+ END IF;
+ END IF;
+
+ CALL invoiceFromClient(vMaxShipped, vClientFk, vCompanyFk);
+ CALL invoiceOut_new(vSerial, CURDATE(), vTaxArea, vInvoiceId);
+
+ UPDATE invoiceOut
+ SET `ref` = vRef
+ WHERE id = vInvoiceId
+ AND vRef IS NOT NULL;
+
+ IF vSerial <> 'R' AND NOT ISNULL(vInvoiceId) AND vInvoiceId <> 0 THEN
+ CALL invoiceOutBooking(vInvoiceId);
+ END IF;
+END;;$$
+DELIMITER ;
diff --git a/db/changes/10340-summer/00-invoiceOut_newFromTicket.sql b/db/changes/10340-summer/00-invoiceOut_newFromTicket.sql
new file mode 100644
index 000000000..15be3d83c
--- /dev/null
+++ b/db/changes/10340-summer/00-invoiceOut_newFromTicket.sql
@@ -0,0 +1,38 @@
+drop procedure `vn`.`invoiceOut_newFromTicket`;
+
+DELIMITER $$
+$$
+create
+ definer = root@`%` procedure `vn`.`invoiceOut_newFromTicket`(IN vTicketFk int, IN vSerial char(2), IN vTaxArea varchar(25),
+ IN vRef varchar(25), OUT vInvoiceId int)
+BEGIN
+/**
+ * Factura un ticket
+ * @param vTicketFk Id del ticket
+ * @param vSerial Serie de factura
+ * @param vTaxArea Area de la factura en caso de querer forzarlo,
+ * en la mayoria de los casos poner NULL
+ * @return vInvoiceId
+ */
+ DECLARE vIsRefEditable BOOLEAN;
+ CALL invoiceFromTicket(vTicketFk);
+ CALL invoiceOut_new(vSerial, CURDATE(), vTaxArea, vInvoiceId);
+
+ IF vRef IS NOT NULL THEN
+ SELECT isRefEditable INTO vIsRefEditable
+ FROM invoiceOutSerial
+ WHERE code = vSerial;
+ IF NOT vIsRefEditable THEN
+ CALL util.throw('serial non editable');
+ END IF;
+
+ UPDATE invoiceOut
+ SET `ref` = vRef
+ WHERE id = vInvoiceId;
+ END IF;
+
+ IF vSerial <> 'R' AND NOT ISNULL(vInvoiceId) AND vInvoiceId <> 0 THEN
+ CALL invoiceOutBooking(vInvoiceId);
+ END IF;
+END;;$$
+DELIMITER ;
diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js
index a561a08cf..40b2ae68a 100644
--- a/e2e/helpers/selectors.js
+++ b/e2e/helpers/selectors.js
@@ -915,6 +915,15 @@ export default {
invoiceOutIndex: {
topbarSearch: 'vn-searchbar',
searchResult: 'vn-invoice-out-index vn-card > vn-table > div > vn-tbody > a.vn-tr',
+ createInvoice: 'vn-invoice-out-index > div > vn-vertical > vn-button > button vn-icon[icon="add"]',
+ createManualInvoice: 'vn-item[name="manualInvoice"]',
+ manualInvoiceForm: '.vn-invoice-out-manual',
+ manualInvoiceTicket: 'vn-autocomplete[ng-model="$ctrl.invoice.ticketFk"]',
+ manualInvoiceClient: 'vn-autocomplete[ng-model="$ctrl.invoice.clientFk"]',
+ manualInvoiceSerial: 'vn-autocomplete[ng-model="$ctrl.invoice.serial"]',
+ manualInvoiceTaxArea: 'vn-autocomplete[ng-model="$ctrl.invoice.taxArea"]',
+ saveManualInvoice: 'button[response="accept"]'
+
},
invoiceOutDescriptor: {
moreMenu: 'vn-invoice-out-descriptor vn-icon-button[icon=more_vert]',
diff --git a/e2e/paths/09-invoice-out/03_manualInvoice.spec.js b/e2e/paths/09-invoice-out/03_manualInvoice.spec.js
new file mode 100644
index 000000000..aa8cc7454
--- /dev/null
+++ b/e2e/paths/09-invoice-out/03_manualInvoice.spec.js
@@ -0,0 +1,65 @@
+import selectors from '../../helpers/selectors.js';
+import getBrowser from '../../helpers/puppeteer';
+
+describe('InvoiceOut manual invoice path', () => {
+ let browser;
+ let page;
+
+ beforeAll(async() => {
+ browser = await getBrowser();
+ page = browser.page;
+ await page.loginAndModule('administrative', 'invoiceOut');
+ });
+
+ afterAll(async() => {
+ await browser.close();
+ });
+
+ it('should open the manual invoice form', async() => {
+ await page.waitToClick(selectors.invoiceOutIndex.createInvoice);
+ await page.waitToClick(selectors.invoiceOutIndex.createManualInvoice);
+ await page.waitForSelector(selectors.invoiceOutIndex.manualInvoiceForm);
+ });
+
+ it('should create an invoice from a ticket', async() => {
+ await page.autocompleteSearch(selectors.invoiceOutIndex.manualInvoiceTicket, '7');
+ await page.autocompleteSearch(selectors.invoiceOutIndex.manualInvoiceSerial, 'Global nacional');
+ await page.autocompleteSearch(selectors.invoiceOutIndex.manualInvoiceTaxArea, 'national');
+ await page.waitToClick(selectors.invoiceOutIndex.saveManualInvoice);
+ const message = await page.waitForSnackbar();
+
+ expect(message.text).toContain('Data saved!');
+ });
+
+ it(`should have been redirected to the created invoice summary`, async() => {
+ await page.waitForState('invoiceOut.card.summary');
+ });
+
+ it(`should navigate back to the invoiceOut index`, async() => {
+ await page.waitToClick(selectors.globalItems.applicationsMenuButton);
+ await page.waitForSelector(selectors.globalItems.applicationsMenuVisible);
+ await page.waitToClick(selectors.globalItems.invoiceOutButton);
+ await page.waitForSelector(selectors.invoiceOutIndex.topbarSearch);
+ await page.waitForState('invoiceOut.index');
+ });
+
+ it('should now open the manual invoice form', async() => {
+ await page.waitToClick(selectors.invoiceOutIndex.createInvoice);
+ await page.waitToClick(selectors.invoiceOutIndex.createManualInvoice);
+ await page.waitForSelector(selectors.invoiceOutIndex.manualInvoiceForm);
+ });
+
+ it('should create an invoice from a client', async() => {
+ await page.autocompleteSearch(selectors.invoiceOutIndex.manualInvoiceClient, 'Charles Xavier');
+ await page.autocompleteSearch(selectors.invoiceOutIndex.manualInvoiceSerial, 'Global nacional');
+ await page.autocompleteSearch(selectors.invoiceOutIndex.manualInvoiceTaxArea, 'national');
+ await page.waitToClick(selectors.invoiceOutIndex.saveManualInvoice);
+ const message = await page.waitForSnackbar();
+
+ expect(message.text).toContain('Data saved!');
+ });
+
+ it(`should have been redirected to the created invoice summary`, async() => {
+ await page.waitForState('invoiceOut.card.summary');
+ });
+});
diff --git a/e2e/paths/12-entry/07_buys.spec.js b/e2e/paths/12-entry/07_buys.spec.js
index e5617b8bd..4042c99b6 100644
--- a/e2e/paths/12-entry/07_buys.spec.js
+++ b/e2e/paths/12-entry/07_buys.spec.js
@@ -29,9 +29,6 @@ describe('Entry import, create and edit buys path', () => {
});
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`;
@@ -41,6 +38,9 @@ describe('Entry import, create and edit buys path', () => {
]);
await fileChooser.accept([filePath]);
+ await page.waitForTextInField(selectors.entryBuys.ref, '200573095, 200573106, 200573117, 200573506');
+ await page.waitForTextInField(selectors.entryBuys.observation, '729-6340 2846');
+
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');
diff --git a/loopback/locale/en.json b/loopback/locale/en.json
index d77b0c26d..b71603f35 100644
--- a/loopback/locale/en.json
+++ b/loopback/locale/en.json
@@ -109,5 +109,7 @@
"This document already exists on this ticket": "This document already exists on this ticket",
"State": "State",
"regular": "regular",
- "reserved": "reserved"
+ "reserved": "reserved",
+ "A ticket with a negative base can't be invoiced": "A ticket with a negative base can't be invoiced",
+ "This client is not invoiceable": "This client is not invoiceable"
}
\ No newline at end of file
diff --git a/loopback/locale/es.json b/loopback/locale/es.json
index f301df8cc..53213adae 100644
--- a/loopback/locale/es.json
+++ b/loopback/locale/es.json
@@ -194,5 +194,13 @@
"The contract was not active during the selected date": "El contrato no estaba activo durante la fecha seleccionada",
"This document already exists on this ticket": "Este documento ya existe en el ticket",
"Some of the selected tickets are not billable": "Algunos de los tickets seleccionados no son facturables",
- "You can't invoice tickets from multiple clients": "No puedes facturar tickets de multiples clientes"
+ "You can't invoice tickets from multiple clients": "No puedes facturar tickets de multiples clientes",
+ "This client is not invoiceable": "Este cliente no es facturable",
+ "serial non editable": "Esta serie no permite asignar la referencia",
+ "Max shipped required": "La fecha límite es requerida",
+ "Can't invoice to future": "No se puede facturar a futuro",
+ "Can't invoice to past": "No se puede facturar a pasado",
+ "This ticket is already invoiced": "Este ticket ya está facturado",
+ "A ticket with an amount of zero can't be invoiced": "No se puede facturar un ticket con importe cero",
+ "A ticket with a negative base can't be invoiced": "No se puede facturar un ticket con una base negativa"
}
\ No newline at end of file
diff --git a/modules/entry/back/methods/entry/importBuys.js b/modules/entry/back/methods/entry/importBuys.js
index 325fe4d22..3ed8ac1c7 100644
--- a/modules/entry/back/methods/entry/importBuys.js
+++ b/modules/entry/back/methods/entry/importBuys.js
@@ -11,11 +11,6 @@ module.exports = Self => {
description: 'The entry id',
http: {source: 'path'}
},
- {
- arg: 'options',
- type: 'object',
- description: 'Callback options',
- },
{
arg: 'ref',
type: 'string',
@@ -28,11 +23,11 @@ module.exports = Self => {
},
{
arg: 'buys',
- type: ['Object'],
+ type: ['object'],
description: 'The buys',
}],
returns: {
- type: ['Object'],
+ type: ['object'],
root: true
},
http: {
@@ -41,23 +36,27 @@ module.exports = Self => {
}
});
- Self.importBuys = async(ctx, id, options = {}) => {
+ Self.importBuys = async(ctx, id, options) => {
const conn = Self.dataSource.connector;
const args = ctx.args;
const models = Self.app.models;
let tx;
+ const myOptions = {};
- if (!options.transaction) {
+ if (typeof options == 'object')
+ Object.assign(myOptions, options);
+
+ if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
- options.transaction = tx;
+ myOptions.transaction = tx;
}
try {
- const entry = await models.Entry.findById(id, null, options);
+ const entry = await models.Entry.findById(id, null, myOptions);
await entry.updateAttributes({
observation: args.observation,
ref: args.ref
- }, options);
+ }, myOptions);
const buys = [];
for (let buy of args.buys) {
@@ -71,9 +70,16 @@ module.exports = Self => {
buyingValue: buy.buyingValue,
packageFk: buy.packageFk
});
+
+ await models.ItemMatchProperties.upsert({
+ itemFk: buy.itemFk,
+ name: buy.description,
+ producer: buy.companyName,
+ size: buy.size
+ }, myOptions);
}
- const createdBuys = await models.Buy.create(buys, options);
+ const createdBuys = await models.Buy.create(buys, myOptions);
const buyIds = createdBuys.map(buy => buy.id);
const stmts = [];
@@ -90,7 +96,7 @@ module.exports = Self => {
stmts.push('CALL buy_recalcPrices()');
const sql = ParameterizedSQL.join(stmts, ';');
- await conn.executeStmt(sql, options);
+ await conn.executeStmt(sql, myOptions);
if (tx) await tx.commit();
} catch (e) {
if (tx) await tx.rollback();
diff --git a/modules/entry/back/methods/entry/importBuysPreview.js b/modules/entry/back/methods/entry/importBuysPreview.js
index 9ba2b58ed..790d33364 100644
--- a/modules/entry/back/methods/entry/importBuysPreview.js
+++ b/modules/entry/back/methods/entry/importBuysPreview.js
@@ -37,7 +37,21 @@ module.exports = Self => {
where: {volume: {gte: buy.volume}},
order: 'volume ASC'
}, myOptions);
- buy.packageFk = packaging.id;
+
+ if (packaging)
+ buy.packageFk = packaging.id;
+
+ const reference = await models.ItemMatchProperties.findOne({
+ fields: ['itemFk'],
+ where: {
+ name: buy.description,
+ producer: buy.companyName,
+ size: buy.size
+ }
+ }, myOptions);
+
+ if (reference)
+ buy.itemFk = reference.itemFk;
}
return buys;
diff --git a/modules/entry/back/model-config.json b/modules/entry/back/model-config.json
index eddef9c41..ad5a9063e 100644
--- a/modules/entry/back/model-config.json
+++ b/modules/entry/back/model-config.json
@@ -5,6 +5,9 @@
"Buy": {
"dataSource": "vn"
},
+ "ItemMatchProperties": {
+ "dataSource": "vn"
+ },
"EntryLog": {
"dataSource": "vn"
},
diff --git a/modules/entry/back/models/buy-import-reference.json b/modules/entry/back/models/buy-import-reference.json
new file mode 100644
index 000000000..ab64dad73
--- /dev/null
+++ b/modules/entry/back/models/buy-import-reference.json
@@ -0,0 +1,32 @@
+{
+ "name": "ItemMatchProperties",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "itemMatchProperties"
+ }
+ },
+ "properties": {
+ "itemFk": {
+ "type": "number",
+ "id": true,
+ "description": "Identifier"
+ },
+ "name": {
+ "type": "string"
+ },
+ "producer": {
+ "type": "string"
+ },
+ "size": {
+ "type": "string"
+ }
+ },
+ "relations": {
+ "item": {
+ "type": "belongsTo",
+ "model": "Item",
+ "foreignKey": "itemFk"
+ }
+ }
+}
\ No newline at end of file
diff --git a/modules/entry/front/buy/import/index.html b/modules/entry/front/buy/import/index.html
index 74b6c708a..179657dae 100644
--- a/modules/entry/front/buy/import/index.html
+++ b/modules/entry/front/buy/import/index.html
@@ -9,20 +9,6 @@
class="vn-ma-md">
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
@@ -51,7 +51,6 @@
Grouping |
Buying value |
Box |
- Volume |
@@ -70,20 +69,19 @@
{{::id}} - {{::name}}
+
+
+
+
{{::buy.description | dashIfEmpty}} |
{{::buy.size | dashIfEmpty}} |
-
-
- {{::buy.packing | dashIfEmpty}}
-
- |
-
-
- {{::buy.grouping | dashIfEmpty}}
-
-
+ | {{::buy.packing | dashIfEmpty}} |
+ {{::buy.grouping | dashIfEmpty}} |
{{::buy.buyingValue | currency: 'EUR':2}} |
|
- {{::buy.volume | number}} |
@@ -110,7 +107,95 @@
label="Cancel"
ui-sref="entry.card.buy.index">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ID
+ Item
+ Size
+ Producer
+ Color
+
+
+
+
+
+
+ {{::item.id}}
+
+
+ {{::item.name}}
+ {{::item.size}}
+ {{::item.producer.name}}
+ {{::item.ink.name}}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/entry/front/buy/import/index.js b/modules/entry/front/buy/import/index.js
index b5ff92a89..2f13b2746 100644
--- a/modules/entry/front/buy/import/index.js
+++ b/modules/entry/front/buy/import/index.js
@@ -29,6 +29,7 @@ class Controller extends Section {
this.$.$applyAsync(() => {
this.import.observation = invoice.tx_awb;
+ const companyName = invoice.tx_company;
const boxes = invoice.boxes;
const buys = [];
for (let box of boxes) {
@@ -37,11 +38,12 @@ class Controller extends Section {
const packing = product.nu_stems_bunch * product.nu_bunches;
buys.push({
description: product.nm_product,
+ companyName: companyName,
size: product.nu_length,
packing: packing,
grouping: product.nu_stems_bunch,
buyingValue: parseFloat(product.mny_rate_stem),
- volume: boxVolume
+ volume: boxVolume,
});
}
}
@@ -86,6 +88,59 @@ class Controller extends Section {
? {id: $search}
: {name: {like: '%' + $search + '%'}};
}
+
+ showFilterDialog(buy) {
+ this.activeBuy = buy;
+ this.itemFilterParams = {};
+ this.itemFilter = {
+ include: [
+ {
+ relation: 'producer',
+ scope: {
+ fields: ['name']
+ }
+ },
+ {
+ relation: 'ink',
+ scope: {
+ fields: ['name']
+ }
+ }
+ ]
+ };
+
+ this.$.filterDialog.show();
+ }
+
+ selectItem(id) {
+ this.activeBuy['itemFk'] = id;
+ this.$.filterDialog.hide();
+ }
+
+ filter() {
+ const filter = this.itemFilter;
+ const params = this.itemFilterParams;
+ const where = {};
+
+ for (let key in params) {
+ const value = params[key];
+ if (!value) continue;
+
+ switch (key) {
+ case 'name':
+ where[key] = {like: `%${value}%`};
+ break;
+ case 'producerFk':
+ case 'typeFk':
+ case 'size':
+ case 'ink':
+ where[key] = value;
+ }
+ }
+
+ filter.where = where;
+ this.$.itemsModel.applyFilter(filter);
+ }
}
Controller.$inject = ['$element', '$scope'];
diff --git a/modules/entry/front/buy/import/style.scss b/modules/entry/front/buy/import/style.scss
index dba069616..8426d4169 100644
--- a/modules/entry/front/buy/import/style.scss
+++ b/modules/entry/front/buy/import/style.scss
@@ -2,4 +2,10 @@ vn-entry-buy-import {
.vn-table > tbody td:nth-child(1) {
width: 250px
}
+}
+
+.itemFilter {
+ vn-table.scrollable {
+ height: 500px
+ }
}
\ No newline at end of file
diff --git a/modules/entry/front/buy/locale/es.yml b/modules/entry/front/buy/locale/es.yml
index c77587758..55828a3c6 100644
--- a/modules/entry/front/buy/locale/es.yml
+++ b/modules/entry/front/buy/locale/es.yml
@@ -3,4 +3,6 @@ Observation: Observación
Box: Embalaje
Import buys: Importar compras
Some of the imported buys doesn't have an item: Algunas de las compras importadas no tienen un artículo
-JSON files only: Solo ficheros JSON
\ No newline at end of file
+JSON files only: Solo ficheros JSON
+Filter item: Filtrar artículo
+Filter...: Filtrar...
\ No newline at end of file
diff --git a/modules/invoiceOut/back/methods/invoiceOut/createManualInvoice.js b/modules/invoiceOut/back/methods/invoiceOut/createManualInvoice.js
new file mode 100644
index 000000000..854c1e2f9
--- /dev/null
+++ b/modules/invoiceOut/back/methods/invoiceOut/createManualInvoice.js
@@ -0,0 +1,182 @@
+const UserError = require('vn-loopback/util/user-error');
+
+module.exports = Self => {
+ Self.remoteMethodCtx('createManualInvoice', {
+ description: 'Make a manual invoice',
+ accessType: 'WRITE',
+ accepts: [
+ {
+ arg: 'clientFk',
+ type: 'any',
+ description: 'The invoiceable client id'
+ },
+ {
+ arg: 'ticketFk',
+ type: 'any',
+ description: 'The invoiceable ticket id'
+ },
+ {
+ arg: 'maxShipped',
+ type: 'date',
+ description: 'The maximum shipped date'
+ },
+ {
+ arg: 'serial',
+ type: 'string',
+ description: 'The invoice serial'
+ },
+ {
+ arg: 'taxArea',
+ type: 'string',
+ description: 'The invoice tax area'
+ },
+ {
+ arg: 'reference',
+ type: 'string',
+ description: 'The invoice reference'
+ }
+ ],
+ returns: {
+ type: 'object',
+ root: true
+ },
+ http: {
+ path: '/createManualInvoice',
+ verb: 'POST'
+ }
+ });
+
+ Self.createManualInvoice = async(ctx, options) => {
+ const models = Self.app.models;
+ const args = ctx.args;
+
+ let tx;
+ const myOptions = {};
+
+ if (typeof options == 'object')
+ Object.assign(myOptions, options);
+
+ if (!myOptions.transaction) {
+ tx = await Self.beginTransaction({});
+ myOptions.transaction = tx;
+ }
+
+ const ticketId = args.ticketFk;
+ let clientId = args.clientFk;
+ let maxShipped = args.maxShipped;
+ let companyId;
+ let query;
+ try {
+ if (ticketId) {
+ const ticket = await models.Ticket.findById(ticketId, null, myOptions);
+ const company = await models.Company.findById(ticket.companyFk, null, myOptions);
+
+ clientId = ticket.clientFk;
+ maxShipped = ticket.shipped;
+ companyId = ticket.companyFk;
+
+ // Validates invoiced ticket
+ if (ticket.refFk)
+ throw new UserError('This ticket is already invoiced');
+
+ // Validates ticket amount
+ if (ticket.totalWithVat == 0)
+ throw new UserError(`A ticket with an amount of zero can't be invoiced`);
+
+ // Validates ticket nagative base
+ const hasNegativeBase = await getNegativeBase(ticketId, myOptions);
+ if (hasNegativeBase && company.code == 'VNL')
+ throw new UserError(`A ticket with a negative base can't be invoiced`);
+ } else {
+ if (!maxShipped)
+ throw new UserError(`Max shipped required`);
+
+ const company = await models.Ticket.findOne({
+ fields: ['companyFk'],
+ where: {
+ clientFk: clientId,
+ shipped: {lte: maxShipped}
+ }
+ }, myOptions);
+ companyId = company.companyFk;
+ }
+
+ // Validate invoiceable client
+ const isClientInvoiceable = await isInvoiceable(clientId, myOptions);
+ if (!isClientInvoiceable)
+ throw new UserError(`This client is not invoiceable`);
+
+ // Can't invoice tickets into future
+ const tomorrow = new Date();
+ tomorrow.setDate(tomorrow.getDate() + 1);
+
+ if (maxShipped >= tomorrow)
+ throw new UserError(`Can't invoice to future`);
+
+ const maxInvoiceDate = await getMaxIssued(args.serial, companyId, myOptions);
+ if (new Date() < maxInvoiceDate)
+ throw new UserError(`Can't invoice to past`);
+
+ if (ticketId) {
+ query = `CALL invoiceOut_newFromTicket(?, ?, ?, ?, @newInvoiceId)`;
+ await Self.rawSql(query, [
+ ticketId,
+ args.serial,
+ args.taxArea,
+ args.reference
+ ], myOptions);
+ } else {
+ query = `CALL invoiceOut_newFromClient(?, ?, ?, ?, ?, ?, @newInvoiceId)`;
+ await Self.rawSql(query, [
+ clientId,
+ args.serial,
+ maxShipped,
+ companyId,
+ args.taxArea,
+ args.reference
+ ], myOptions);
+ }
+
+ const [newInvoice] = await Self.rawSql(`SELECT @newInvoiceId id`, null, myOptions);
+ if (newInvoice.id)
+ await Self.createPdf(ctx, newInvoice.id, myOptions);
+
+ if (tx) await tx.commit();
+
+ return newInvoice;
+ } catch (e) {
+ if (tx) await tx.rollback();
+ throw e;
+ }
+ };
+
+ async function isInvoiceable(clientId, options) {
+ const models = Self.app.models;
+ const query = `SELECT (hasToInvoice AND isTaxDataChecked) AS invoiceable
+ FROM client
+ WHERE id = ?`;
+ const [result] = await models.InvoiceOut.rawSql(query, [clientId], options);
+
+ return result.invoiceable;
+ }
+
+ async function getNegativeBase(ticketId, options) {
+ const models = Self.app.models;
+ const query = 'SELECT vn.hasSomeNegativeBase(?) AS base';
+ const [result] = await models.InvoiceOut.rawSql(query, [ticketId], options);
+
+ return result.base;
+ }
+
+ async function getMaxIssued(serial, companyId, options) {
+ const models = Self.app.models;
+ const query = `SELECT MAX(issued) AS issued
+ FROM invoiceOut
+ WHERE serial = ? AND companyFk = ?`;
+ const [maxIssued] = await models.InvoiceOut.rawSql(query,
+ [serial, companyId], options);
+ const maxInvoiceDate = maxIssued && maxIssued.issued || new Date();
+
+ return maxInvoiceDate;
+ }
+};
diff --git a/modules/invoiceOut/back/methods/invoiceOut/createPdf.js b/modules/invoiceOut/back/methods/invoiceOut/createPdf.js
index 6e596db62..5f43e4a32 100644
--- a/modules/invoiceOut/back/methods/invoiceOut/createPdf.js
+++ b/modules/invoiceOut/back/methods/invoiceOut/createPdf.js
@@ -58,11 +58,18 @@ module.exports = Self => {
}
});
- const invoiceYear = invoiceOut.created.getFullYear().toString();
- const container = await models.InvoiceContainer.container(invoiceYear);
+ const created = invoiceOut.created;
+ const year = created.getFullYear().toString();
+ const month = created.getMonth().toString();
+ const day = created.getDate().toString();
+
+ const container = await models.InvoiceContainer.container(year);
const rootPath = container.client.root;
const fileName = `${invoiceOut.ref}.pdf`;
- fileSrc = path.join(rootPath, invoiceYear, fileName);
+ const src = path.join(rootPath, year, month, day);
+ fileSrc = path.join(src, fileName);
+
+ await fs.mkdir(src, {recursive: true});
const writeStream = fs.createWriteStream(fileSrc);
writeStream.on('open', () => {
diff --git a/modules/invoiceOut/back/methods/invoiceOut/delete.js b/modules/invoiceOut/back/methods/invoiceOut/delete.js
index 96b5e652e..d8b9d309b 100644
--- a/modules/invoiceOut/back/methods/invoiceOut/delete.js
+++ b/modules/invoiceOut/back/methods/invoiceOut/delete.js
@@ -34,13 +34,14 @@ module.exports = Self => {
try {
const invoiceOut = await Self.findById(id, {}, myOptions);
- const tickets = await Self.app.models.Ticket.find({where: {refFk: invoiceOut.ref}}, myOptions);
+ const tickets = await Self.app.models.Ticket.find({
+ where: {refFk: invoiceOut.ref}
+ }, myOptions);
const promises = [];
- tickets.forEach(ticket => {
+ for (let ticket of tickets)
promises.push(ticket.updateAttribute('refFk', null, myOptions));
- });
await Promise.all(promises);
diff --git a/modules/invoiceOut/back/methods/invoiceOut/download.js b/modules/invoiceOut/back/methods/invoiceOut/download.js
index 3c9924fc3..983647982 100644
--- a/modules/invoiceOut/back/methods/invoiceOut/download.js
+++ b/modules/invoiceOut/back/methods/invoiceOut/download.js
@@ -1,4 +1,5 @@
const fs = require('fs-extra');
+const path = require('path');
module.exports = Self => {
Self.remoteMethod('download', {
@@ -33,24 +34,31 @@ module.exports = Self => {
}
});
- Self.download = async function(id) {
- let file;
- let env = process.env.NODE_ENV;
- let [invoice] = await Self.rawSql(`SELECT invoiceOut_getPath(?) path`, [id]);
+ Self.download = async function(id, options) {
+ const models = Self.app.models;
+ const myOptions = {};
- if (env && env != 'development') {
- file = {
- path: `/var/lib/salix/pdfs/${invoice.path}`,
- contentType: 'application/pdf',
- name: `${id}.pdf`
- };
- } else {
- file = {
- path: `${process.cwd()}/README.md`,
- contentType: 'text/plain',
- name: `README.md`
- };
- }
+ if (typeof options == 'object')
+ Object.assign(myOptions, options);
+
+ const invoiceOut = await models.InvoiceOut.findById(id, null, myOptions);
+
+ const created = invoiceOut.created;
+ const year = created.getFullYear().toString();
+ const month = created.getMonth().toString();
+ const day = created.getDate().toString();
+
+ const container = await models.InvoiceContainer.container(year);
+ const rootPath = container.client.root;
+ const src = path.join(rootPath, year, month, day);
+ const fileName = `${invoiceOut.ref}.pdf`;
+ const fileSrc = path.join(src, fileName);
+
+ const file = {
+ path: fileSrc,
+ contentType: 'application/pdf',
+ name: `${id}.pdf`
+ };
await fs.access(file.path);
let stream = fs.createReadStream(file.path);
diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/createManualInvoice.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/createManualInvoice.spec.js
new file mode 100644
index 000000000..e5c3c0512
--- /dev/null
+++ b/modules/invoiceOut/back/methods/invoiceOut/specs/createManualInvoice.spec.js
@@ -0,0 +1,145 @@
+const models = require('vn-loopback/server/server').models;
+const LoopBackContext = require('loopback-context');
+
+describe('InvoiceOut createManualInvoice()', () => {
+ const userId = 1;
+ const ticketId = 16;
+ const clientId = 1106;
+ const activeCtx = {
+ accessToken: {userId: userId},
+ };
+ const ctx = {req: activeCtx};
+
+ it('should throw an error trying to invoice again', async() => {
+ spyOn(models.InvoiceOut, 'createPdf').and.returnValue(true);
+
+ const tx = await models.InvoiceOut.beginTransaction({});
+ const options = {transaction: tx};
+
+ let error;
+ try {
+ ctx.args = {
+ ticketFk: ticketId,
+ serial: 'T',
+ taxArea: 'CEE'
+ };
+ await models.InvoiceOut.createManualInvoice(ctx, options);
+ await models.InvoiceOut.createManualInvoice(ctx, options);
+
+ await tx.rollback();
+ } catch (e) {
+ error = e;
+ await tx.rollback();
+ }
+
+ expect(error.message).toContain('This ticket is already invoiced');
+ });
+
+ it('should throw an error for a ticket with an amount of zero', async() => {
+ spyOn(models.InvoiceOut, 'createPdf').and.returnValue(true);
+ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
+ active: activeCtx
+ });
+
+ const tx = await models.InvoiceOut.beginTransaction({});
+ const options = {transaction: tx};
+
+ let error;
+ try {
+ const ticket = await models.Ticket.findById(ticketId, null, options);
+ await ticket.updateAttributes({
+ totalWithVat: 0
+ }, options);
+
+ ctx.args = {
+ ticketFk: ticketId,
+ serial: 'T',
+ taxArea: 'CEE'
+ };
+ await models.InvoiceOut.createManualInvoice(ctx, options);
+
+ await tx.rollback();
+ } catch (e) {
+ error = e;
+ await tx.rollback();
+ }
+
+ expect(error.message).toContain(`A ticket with an amount of zero can't be invoiced`);
+ });
+
+ it('should throw an error when the clientFk property is set without the max shipped date', async() => {
+ spyOn(models.InvoiceOut, 'createPdf').and.returnValue(true);
+
+ const tx = await models.InvoiceOut.beginTransaction({});
+ const options = {transaction: tx};
+
+ let error;
+ try {
+ ctx.args = {
+ clientFk: clientId,
+ serial: 'T',
+ taxArea: 'CEE'
+ };
+ await models.InvoiceOut.createManualInvoice(ctx, options);
+
+ await tx.rollback();
+ } catch (e) {
+ error = e;
+ await tx.rollback();
+ }
+
+ expect(error.message).toContain(`Max shipped required`);
+ });
+
+ it('should throw an error for a non-invoiceable client', async() => {
+ spyOn(models.InvoiceOut, 'createPdf').and.returnValue(true);
+
+ const tx = await models.InvoiceOut.beginTransaction({});
+ const options = {transaction: tx};
+
+ let error;
+ try {
+ const client = await models.Client.findById(clientId, null, options);
+ await client.updateAttributes({
+ isTaxDataChecked: false
+ }, options);
+
+ ctx.args = {
+ ticketFk: ticketId,
+ serial: 'T',
+ taxArea: 'CEE'
+ };
+ await models.InvoiceOut.createManualInvoice(ctx, options);
+
+ await tx.rollback();
+ } catch (e) {
+ error = e;
+ await tx.rollback();
+ }
+
+ expect(error.message).toContain(`This client is not invoiceable`);
+ });
+
+ it('should create a manual invoice', async() => {
+ spyOn(models.InvoiceOut, 'createPdf').and.returnValue(true);
+
+ const tx = await models.InvoiceOut.beginTransaction({});
+ const options = {transaction: tx};
+
+ try {
+ ctx.args = {
+ ticketFk: ticketId,
+ serial: 'T',
+ taxArea: 'CEE'
+ };
+ const result = await models.InvoiceOut.createManualInvoice(ctx, options);
+
+ expect(result.id).toEqual(jasmine.any(Number));
+
+ await tx.rollback();
+ } catch (e) {
+ await tx.rollback();
+ throw e;
+ }
+ });
+});
diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js
index 60dd5576d..0ed0b35eb 100644
--- a/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js
+++ b/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js
@@ -19,8 +19,18 @@ describe('InvoiceOut createPdf()', () => {
};
spyOn(got, 'stream').and.returnValue(response);
- const result = await models.InvoiceOut.createPdf(ctx, invoiceId);
+ const tx = await models.InvoiceOut.beginTransaction({});
+ const options = {transaction: tx};
- expect(result.hasPdf).toBe(true);
+ try {
+ const result = await models.InvoiceOut.createPdf(ctx, invoiceId, options);
+
+ expect(result.hasPdf).toBe(true);
+
+ await tx.rollback();
+ } catch (e) {
+ await tx.rollback();
+ throw e;
+ }
});
});
diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/download.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/download.spec.js
index 2d9056708..3ad4c2f11 100644
--- a/modules/invoiceOut/back/methods/invoiceOut/specs/download.spec.js
+++ b/modules/invoiceOut/back/methods/invoiceOut/specs/download.spec.js
@@ -4,7 +4,7 @@ describe('InvoiceOut download()', () => {
it('should return the downloaded fine name', async() => {
const result = await models.InvoiceOut.download(1);
- expect(result[1]).toEqual('text/plain');
- expect(result[2]).toEqual('filename="README.md"');
+ expect(result[1]).toEqual('application/pdf');
+ expect(result[2]).toEqual('filename="1.pdf"');
});
});
diff --git a/modules/invoiceOut/back/model-config.json b/modules/invoiceOut/back/model-config.json
index e144ce80e..d52f79477 100644
--- a/modules/invoiceOut/back/model-config.json
+++ b/modules/invoiceOut/back/model-config.json
@@ -2,7 +2,25 @@
"InvoiceOut": {
"dataSource": "vn"
},
+ "InvoiceOutSerial": {
+ "dataSource": "vn"
+ },
"InvoiceContainer": {
"dataSource": "invoiceStorage"
+ },
+ "TaxArea": {
+ "dataSource": "vn"
+ },
+ "TaxClass": {
+ "dataSource": "vn"
+ },
+ "TaxClassCode": {
+ "dataSource": "vn"
+ },
+ "TaxCode": {
+ "dataSource": "vn"
+ },
+ "TaxType": {
+ "dataSource": "vn"
}
}
diff --git a/modules/invoiceOut/back/models/invoice-out-serial.json b/modules/invoiceOut/back/models/invoice-out-serial.json
new file mode 100644
index 000000000..912269fd7
--- /dev/null
+++ b/modules/invoiceOut/back/models/invoice-out-serial.json
@@ -0,0 +1,38 @@
+{
+ "name": "InvoiceOutSerial",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "invoiceOutSerial"
+ }
+ },
+ "properties": {
+ "code": {
+ "type": "string",
+ "id": true,
+ "description": "Identifier"
+ },
+ "description": {
+ "type": "string"
+ },
+ "isTaxed": {
+ "type": "boolean"
+ },
+ "isCEE": {
+ "type": "boolean"
+ }
+ },
+ "relations": {
+ "taxArea": {
+ "type": "belongsTo",
+ "model": "TaxArea",
+ "foreignKey": "taxAreaFk"
+ }
+ },
+ "acls": [{
+ "accessType": "READ",
+ "principalType": "ROLE",
+ "principalId": "$everyone",
+ "permission": "ALLOW"
+ }]
+}
\ No newline at end of file
diff --git a/modules/invoiceOut/back/models/invoiceOut.js b/modules/invoiceOut/back/models/invoice-out.js
similarity index 86%
rename from modules/invoiceOut/back/models/invoiceOut.js
rename to modules/invoiceOut/back/models/invoice-out.js
index 7c6503d8e..8a1dda41f 100644
--- a/modules/invoiceOut/back/models/invoiceOut.js
+++ b/modules/invoiceOut/back/models/invoice-out.js
@@ -6,4 +6,5 @@ module.exports = Self => {
require('../methods/invoiceOut/delete')(Self);
require('../methods/invoiceOut/book')(Self);
require('../methods/invoiceOut/createPdf')(Self);
+ require('../methods/invoiceOut/createManualInvoice')(Self);
};
diff --git a/modules/invoiceOut/back/models/invoiceOut.json b/modules/invoiceOut/back/models/invoice-out.json
similarity index 100%
rename from modules/invoiceOut/back/models/invoiceOut.json
rename to modules/invoiceOut/back/models/invoice-out.json
diff --git a/modules/invoiceOut/back/models/tax-area.json b/modules/invoiceOut/back/models/tax-area.json
new file mode 100644
index 000000000..0aa00d194
--- /dev/null
+++ b/modules/invoiceOut/back/models/tax-area.json
@@ -0,0 +1,22 @@
+{
+ "name": "TaxArea",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "taxArea"
+ }
+ },
+ "properties": {
+ "code": {
+ "type": "string",
+ "id": true,
+ "description": "Identifier"
+ }
+ },
+ "acls": [{
+ "accessType": "READ",
+ "principalType": "ROLE",
+ "principalId": "$everyone",
+ "permission": "ALLOW"
+ }]
+}
\ No newline at end of file
diff --git a/modules/item/back/models/tax-class-code.json b/modules/invoiceOut/back/models/tax-class-code.json
similarity index 100%
rename from modules/item/back/models/tax-class-code.json
rename to modules/invoiceOut/back/models/tax-class-code.json
diff --git a/modules/item/back/models/tax-class.json b/modules/invoiceOut/back/models/tax-class.json
similarity index 100%
rename from modules/item/back/models/tax-class.json
rename to modules/invoiceOut/back/models/tax-class.json
diff --git a/modules/item/back/models/tax-code.json b/modules/invoiceOut/back/models/tax-code.json
similarity index 100%
rename from modules/item/back/models/tax-code.json
rename to modules/invoiceOut/back/models/tax-code.json
diff --git a/modules/item/back/models/tax-type.json b/modules/invoiceOut/back/models/tax-type.json
similarity index 100%
rename from modules/item/back/models/tax-type.json
rename to modules/invoiceOut/back/models/tax-type.json
diff --git a/modules/invoiceOut/front/index.js b/modules/invoiceOut/front/index.js
index 9843e188b..bdb87f9a9 100644
--- a/modules/invoiceOut/front/index.js
+++ b/modules/invoiceOut/front/index.js
@@ -7,3 +7,4 @@ import './summary';
import './card';
import './descriptor';
import './descriptor-popover';
+import './index/manual';
diff --git a/modules/invoiceOut/front/index/index.html b/modules/invoiceOut/front/index/index.html
index 8685990a4..2ebd7b421 100644
--- a/modules/invoiceOut/front/index/index.html
+++ b/modules/invoiceOut/front/index/index.html
@@ -57,6 +57,26 @@
+
+
+
+
+
+
+
+ Manual invoicing
+
+
+
+
@@ -65,3 +85,6 @@
+
+
\ No newline at end of file
diff --git a/modules/invoiceOut/front/index/locale/es.yml b/modules/invoiceOut/front/index/locale/es.yml
index eb22e1779..b166cca8f 100644
--- a/modules/invoiceOut/front/index/locale/es.yml
+++ b/modules/invoiceOut/front/index/locale/es.yml
@@ -3,4 +3,5 @@ Issued: Fecha factura
Due date: Fecha vencimiento
Has PDF: PDF disponible
Minimum: Minimo
-Maximum: Máximo
\ No newline at end of file
+Maximum: Máximo
+Manual invoicing: Facturación manual
\ No newline at end of file
diff --git a/modules/invoiceOut/front/index/manual/index.html b/modules/invoiceOut/front/index/manual/index.html
new file mode 100644
index 000000000..f2fd10d6f
--- /dev/null
+++ b/modules/invoiceOut/front/index/manual/index.html
@@ -0,0 +1,75 @@
+
+ Create manual invoice
+
+
+
+
+
+
+
+
+
+ {{::id}} - {{::nickname}}
+
+
+ Or
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/invoiceOut/front/index/manual/index.js b/modules/invoiceOut/front/index/manual/index.js
new file mode 100644
index 000000000..77dd93de7
--- /dev/null
+++ b/modules/invoiceOut/front/index/manual/index.js
@@ -0,0 +1,47 @@
+import ngModule from '../../module';
+import Dialog from 'core/components/dialog';
+import './style.scss';
+
+class Controller extends Dialog {
+ constructor($element, $, $transclude) {
+ super($element, $, $transclude);
+
+ this.invoice = {
+ maxShipped: new Date()
+ };
+ }
+
+ responseHandler(response) {
+ try {
+ if (response !== 'accept')
+ return super.responseHandler(response);
+
+ if (this.invoice.clientFk && !this.invoice.maxShipped)
+ throw new Error('Client and the max shipped should be filled');
+
+ if (!this.invoice.serial || !this.invoice.taxArea)
+ throw new Error('Some fields are required');
+
+ return this.$http.post(`InvoiceOuts/createManualInvoice`, this.invoice)
+ .then(res => {
+ this.$state.go('invoiceOut.card.summary', {id: res.data.id});
+ super.responseHandler(response);
+ })
+ .then(() => this.vnApp.showSuccess(this.$t('Data saved!')));
+ } catch (e) {
+ this.vnApp.showError(this.$t(e.message));
+ return false;
+ }
+ }
+}
+
+Controller.$inject = ['$element', '$scope', '$transclude'];
+
+ngModule.vnComponent('vnInvoiceOutManual', {
+ slotTemplate: require('./index.html'),
+ controller: Controller,
+ bindings: {
+ ticketFk: '',
+ clientFk: ''
+ }
+});
diff --git a/modules/invoiceOut/front/index/manual/index.spec.js b/modules/invoiceOut/front/index/manual/index.spec.js
new file mode 100644
index 000000000..f19030129
--- /dev/null
+++ b/modules/invoiceOut/front/index/manual/index.spec.js
@@ -0,0 +1,66 @@
+import './index';
+
+describe('InvoiceOut', () => {
+ describe('Component vnInvoiceOutManual', () => {
+ let controller;
+ let $httpBackend;
+
+ beforeEach(ngModule('invoiceOut'));
+
+ beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => {
+ $httpBackend = _$httpBackend_;
+ let $scope = $rootScope.$new();
+ const $element = angular.element('');
+ const $transclude = {
+ $$boundTransclude: {
+ $$slots: []
+ }
+ };
+ controller = $componentController('vnInvoiceOutManual', {$element, $scope, $transclude});
+ }));
+
+ describe('responseHandler()', () => {
+ it('should throw an error when clientFk property is set and the maxShipped is not filled', () => {
+ jest.spyOn(controller.vnApp, 'showError');
+
+ controller.invoice = {
+ clientFk: 1101,
+ serial: 'T',
+ taxArea: 'B'
+ };
+
+ controller.responseHandler('accept');
+
+ expect(controller.vnApp.showError).toHaveBeenCalledWith(`Client and the max shipped should be filled`);
+ });
+
+ it('should throw an error when some required fields are not filled in', () => {
+ jest.spyOn(controller.vnApp, 'showError');
+
+ controller.invoice = {
+ ticketFk: 1101
+ };
+
+ controller.responseHandler('accept');
+
+ expect(controller.vnApp.showError).toHaveBeenCalledWith(`Some fields are required`);
+ });
+
+ it('should make an http POST query and then call to the parent showSuccess() method', () => {
+ jest.spyOn(controller.vnApp, 'showSuccess');
+
+ controller.invoice = {
+ ticketFk: 1101,
+ serial: 'T',
+ taxArea: 'B'
+ };
+
+ $httpBackend.expect('POST', `InvoiceOuts/createManualInvoice`).respond({id: 1});
+ controller.responseHandler('accept');
+ $httpBackend.flush();
+
+ expect(controller.vnApp.showSuccess).toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/modules/invoiceOut/front/index/manual/locale/es.yml b/modules/invoiceOut/front/index/manual/locale/es.yml
new file mode 100644
index 000000000..826057c8d
--- /dev/null
+++ b/modules/invoiceOut/front/index/manual/locale/es.yml
@@ -0,0 +1,5 @@
+Create manual invoice: Crear factura manual
+Some fields are required: Algunos campos son obligatorios
+Client and max shipped fields should be filled: Los campos de cliente y fecha límite deben rellenarse
+Max date: Fecha límite
+Serial: Serie
\ No newline at end of file
diff --git a/modules/invoiceOut/front/index/manual/style.scss b/modules/invoiceOut/front/index/manual/style.scss
new file mode 100644
index 000000000..998472157
--- /dev/null
+++ b/modules/invoiceOut/front/index/manual/style.scss
@@ -0,0 +1,5 @@
+.vn-invoice-out-manual {
+ tpl-body {
+ width: 500px
+ }
+}
\ No newline at end of file
diff --git a/modules/item/back/model-config.json b/modules/item/back/model-config.json
index 9f101f9c7..ab680b696 100644
--- a/modules/item/back/model-config.json
+++ b/modules/item/back/model-config.json
@@ -65,18 +65,6 @@
"Tag": {
"dataSource": "vn"
},
- "TaxClass": {
- "dataSource": "vn"
- },
- "TaxClassCode": {
- "dataSource": "vn"
- },
- "TaxCode": {
- "dataSource": "vn"
- },
- "TaxType": {
- "dataSource": "vn"
- },
"FixedPrice": {
"dataSource": "vn"
}
diff --git a/modules/item/front/search-panel/locale/es.yml b/modules/item/front/search-panel/locale/es.yml
index 197da0695..67a5200d7 100644
--- a/modules/item/front/search-panel/locale/es.yml
+++ b/modules/item/front/search-panel/locale/es.yml
@@ -1,6 +1,6 @@
Ink: Tinta
Origin: Origen
-Producer: Productor.
+Producer: Productor
With visible: Con visible
Field: Campo
More fields: Más campos
diff --git a/modules/order/back/methods/order/getItemTypeAvailable.js b/modules/order/back/methods/order/getItemTypeAvailable.js
index 56f6a8c0e..906095f41 100644
--- a/modules/order/back/methods/order/getItemTypeAvailable.js
+++ b/modules/order/back/methods/order/getItemTypeAvailable.js
@@ -28,30 +28,57 @@ module.exports = Self => {
});
Self.getItemTypeAvailable = async(orderId, itemCategoryId) => {
- let stmts = [];
+ const stmts = [];
let stmt;
- let order = await app.models.Order.findById(orderId);
+ const order = await app.models.Order.findById(orderId);
stmt = new ParameterizedSQL('call vn.available_calc(?, ?, ?)', [
order.landed,
order.addressFk,
order.agencyModeFk
]);
stmts.push(stmt);
- stmt = new ParameterizedSQL(`
- SELECT it.id, it.name, ic.name categoryName
+
+ stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.item');
+
+ stmt = new ParameterizedSQL(
+ `CREATE TEMPORARY TABLE tmp.item
+ (PRIMARY KEY (itemFk)) ENGINE = MEMORY
+ SELECT DISTINCT
+ i.id AS itemFk,
+ it.id AS typeFk,
+ it.name,
+ ic.name AS categoryName
FROM tmp.availableCalc ac
JOIN cache.available a ON a.calc_id = ac.calcFk
- JOIN item i ON i.id = a.item_id
- JOIN itemType it ON it.id = i.typeFk
- JOIN itemCategory ic ON ic.id = it.categoryFk
- WHERE it.categoryFk = ?
- GROUP BY it.id`, [itemCategoryId]
+ JOIN vn.item i ON i.id = a.item_id
+ JOIN vn.itemType it ON it.id = i.typeFk
+ JOIN vn.itemCategory ic ON ic.id = it.categoryFk
+ WHERE it.categoryFk = ?`, [itemCategoryId]
);
- let categoriesIndex = stmts.push(stmt) - 1;
+ stmts.push(stmt);
- let sql = ParameterizedSQL.join(stmts, ';');
- let result = await Self.rawStmt(sql);
+ stmt = new ParameterizedSQL(
+ 'CALL vn.catalog_calculate(?, ?, ?)', [
+ order.landed,
+ order.addressFk,
+ order.agencyModeFk,
+ ]
+ );
+ stmts.push(stmt);
+
+ stmt = new ParameterizedSQL(`
+ SELECT i.typeFk AS id, i.name, i.categoryName
+ FROM tmp.item i
+ JOIN tmp.ticketCalculateItem tci ON tci.itemFk = i.itemFk
+ GROUP BY i.typeFk`
+ );
+ const categoriesIndex = stmts.push(stmt) - 1;
+
+ stmts.push('DROP TEMPORARY TABLE tmp.item');
+
+ const sql = ParameterizedSQL.join(stmts, ';');
+ const result = await Self.rawStmt(sql);
return result[categoriesIndex];
};