Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2984-invoiceOut_global

This commit is contained in:
Joan Sanchez 2021-08-09 12:59:00 +02:00
commit 8e84f98629
35 changed files with 750 additions and 166 deletions

View File

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

View File

@ -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() => { 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 currentDir = process.cwd();
let filePath = `${currentDir}/e2e/assets/07_import_buys.json`; 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 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.firstImportedItem, 'Ranged Reinforced weapon pistol 9mm');
await page.autocompleteSearch(selectors.entryBuys.secondImportedItem, 'Melee Reinforced weapon heavy shield 1x0.5m'); 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.thirdImportedItem, 'Container medical box 1m');

View File

@ -57,7 +57,14 @@
"The postcode doesn't exist. Please enter a correct one": "The postcode doesn't exist. Please enter a correct one", "The postcode doesn't exist. Please enter a correct one": "The postcode doesn't exist. Please enter a correct one",
"Can't create stowaway for this ticket": "Can't create stowaway for this ticket", "Can't create stowaway for this ticket": "Can't create stowaway for this ticket",
"Swift / BIC can't be empty": "Swift / BIC can't be empty", "Swift / BIC can't be empty": "Swift / BIC can't be empty",
"Bought units from buy request": "Bought {{quantity}} units of {{concept}} [{{itemId}}]({{{urlItem}}}) for the ticket id [{{ticketId}}]({{{url}}})", "Deleted sales from ticket": "I have deleted the following lines from the ticket [{{ticketId}}]({{{ticketUrl}}}): {{{deletions}}}",
"Added sale to ticket": "I have added the following line to the ticket [{{ticketId}}]({{{ticketUrl}}}): {{{addition}}}",
"Changed sale discount": "I have changed the following lines discounts from the ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}",
"Created claim": "I have created the claim [{{claimId}}]({{{claimUrl}}}) for the following lines from the ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}",
"Changed sale price": "I have changed the price of [{{itemId}} {{concept}}]({{{itemUrl}}}) ({{quantity}}) from {{oldPrice}}€ ➔ *{{newPrice}}€* of the ticket [{{ticketId}}]({{{ticketUrl}}})",
"Changed sale quantity": "I have changed the quantity of [{{itemId}} {{concept}}]({{{itemUrl}}}) from {{oldQuantity}} ➔ *{{newQuantity}}* of the ticket [{{ticketId}}]({{{ticketUrl}}})",
"Changed sale reserved state": "I have changed the following lines reserved state from the ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}",
"Bought units from buy request": "Bought {{quantity}} units of [{{itemId}} {{concept}}]({{{urlItem}}}) for the ticket id [{{ticketId}}]({{{url}}})",
"MESSAGE_INSURANCE_CHANGE": "I have changed the insurence credit of client [{{clientName}} ({{clientId}})]({{{url}}}) to *{{credit}} €*", "MESSAGE_INSURANCE_CHANGE": "I have changed the insurence credit of client [{{clientName}} ({{clientId}})]({{{url}}}) to *{{credit}} €*",
"Changed client paymethod": "I have changed the pay method for client [{{clientName}} ({{clientId}})]({{{url}}})", "Changed client paymethod": "I have changed the pay method for client [{{clientName}} ({{clientId}})]({{{url}}})",
"Sent units from ticket": "I sent *{{quantity}}* units of [{{concept}} ({{itemId}})]({{{itemUrl}}}) to *\"{{nickname}}\"* coming from ticket id [{{ticketId}}]({{{ticketUrl}}})", "Sent units from ticket": "I sent *{{quantity}}* units of [{{concept}} ({{itemId}})]({{{itemUrl}}}) to *\"{{nickname}}\"* coming from ticket id [{{ticketId}}]({{{ticketUrl}}})",
@ -99,5 +106,9 @@
"None": "None", "None": "None",
"error densidad = 0": "error densidad = 0", "error densidad = 0": "error densidad = 0",
"This document already exists on this ticket": "This document already exists on this ticket", "This document already exists on this ticket": "This document already exists on this ticket",
"serial non editable": "This serial doesn't allow to set a reference" "serial non editable": "This serial doesn't allow to set a reference",
"nickname": "nickname",
"State": "State",
"regular": "regular",
"reserved": "reserved"
} }

View File

@ -54,7 +54,7 @@
"This address doesn't exist": "Este consignatario no existe", "This address doesn't exist": "Este consignatario no existe",
"You can't create an order for a inactive client": "You can't create an order for a inactive client", "You can't create an order for a inactive client": "You can't create an order for a inactive client",
"You can't create an order for a client that doesn't has tax data verified": "You can't create an order for a client that doesn't has tax data verified", "You can't create an order for a client that doesn't has tax data verified": "You can't create an order for a client that doesn't has tax data verified",
"You must delete the claim id %d first": "Antes debes borrar la reclamacion %d", "You must delete the claim id %d first": "Antes debes borrar la reclamación %d",
"You don't have enough privileges": "No tienes suficientes permisos", "You don't have enough privileges": "No tienes suficientes permisos",
"Cannot check Equalization Tax in this NIF/CIF": "No se puede marcar RE en este NIF/CIF", "Cannot check Equalization Tax in this NIF/CIF": "No se puede marcar RE en este NIF/CIF",
"You can't make changes on the basic data of an confirmed order or with rows": "No puedes cambiar los datos basicos de una orden con artículos", "You can't make changes on the basic data of an confirmed order or with rows": "No puedes cambiar los datos basicos de una orden con artículos",
@ -122,7 +122,17 @@
"Swift / BIC can't be empty": "Swift / BIC no puede estar vacío", "Swift / BIC can't be empty": "Swift / BIC no puede estar vacío",
"Customs agent is required for a non UEE member": "El agente de aduanas es requerido para los clientes extracomunitarios", "Customs agent is required for a non UEE member": "El agente de aduanas es requerido para los clientes extracomunitarios",
"Incoterms is required for a non UEE member": "El incoterms es requerido para los clientes extracomunitarios", "Incoterms is required for a non UEE member": "El incoterms es requerido para los clientes extracomunitarios",
"Bought units from buy request": "Se ha comprado {{quantity}} unidades de {{concept}} [{{itemId}}]({{{urlItem}}}) para el ticket id [{{ticketId}}]({{{url}}})", "Deleted sales from ticket": "He eliminado las siguientes lineas del ticket [{{ticketId}}]({{{ticketUrl}}}): {{{deletions}}}",
"Added sale to ticket": "He añadido la siguiente linea al ticket [{{ticketId}}]({{{ticketUrl}}}): {{{addition}}}",
"Changed sale discount": "He cambiado el descuento de las siguientes lineas al ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}",
"Created claim": "He creado la reclamación [{{claimId}}]({{{claimUrl}}}) de las siguientes lineas del ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}",
"Changed sale price": "He cambiado el precio de [{{itemId}} {{concept}}]({{{itemUrl}}}) ({{quantity}}) de {{oldPrice}}€ ➔ *{{newPrice}}€* del ticket [{{ticketId}}]({{{ticketUrl}}})",
"Changed sale quantity": "He cambiado la cantidad de [{{itemId}} {{concept}}]({{{itemUrl}}}) de {{oldQuantity}} ➔ *{{newQuantity}}* del ticket [{{ticketId}}]({{{ticketUrl}}})",
"State": "Estado",
"regular": "normal",
"reserved": "reservado",
"Changed sale reserved state": "He cambiado el estado reservado de las siguientes lineas al ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}",
"Bought units from buy request": "Se ha comprado {{quantity}} unidades de [{{itemId}} {{concept}}]({{{urlItem}}}) para el ticket id [{{ticketId}}]({{{url}}})",
"MESSAGE_INSURANCE_CHANGE": "He cambiado el crédito asegurado del cliente [{{clientName}} ({{clientId}})]({{{url}}}) a *{{credit}} €*", "MESSAGE_INSURANCE_CHANGE": "He cambiado el crédito asegurado del cliente [{{clientName}} ({{clientId}})]({{{url}}}) a *{{credit}} €*",
"Changed client paymethod": "He cambiado la forma de pago del cliente [{{clientName}} ({{clientId}})]({{{url}}})", "Changed client paymethod": "He cambiado la forma de pago del cliente [{{clientName}} ({{clientId}})]({{{url}}})",
"Sent units from ticket": "Envio *{{quantity}}* unidades de [{{concept}} ({{itemId}})]({{{itemUrl}}}) a *\"{{nickname}}\"* provenientes del ticket id [{{ticketId}}]({{{ticketUrl}}})", "Sent units from ticket": "Envio *{{quantity}}* unidades de [{{concept}} ({{itemId}})]({{{itemUrl}}}) a *\"{{nickname}}\"* provenientes del ticket id [{{ticketId}}]({{{ticketUrl}}})",

View File

@ -3,17 +3,20 @@ const UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('createFromSales', { Self.remoteMethodCtx('createFromSales', {
description: 'Create a claim', description: 'Create a claim',
accepts: [{ accepts: [
arg: 'ticketId', {
type: 'Number', arg: 'ticketId',
required: true, type: 'number',
description: 'The origin ticket id' required: true,
}, { description: 'The origin ticket id'
arg: 'sales', },
type: ['Object'], {
required: true, arg: 'sales',
description: 'The claimed sales' type: ['object'],
}], required: true,
description: 'The claimed sales'
}
],
returns: { returns: {
type: 'object', type: 'object',
root: true root: true
@ -25,6 +28,7 @@ module.exports = Self => {
}); });
Self.createFromSales = async(ctx, ticketId, sales, options) => { Self.createFromSales = async(ctx, ticketId, sales, options) => {
const $t = ctx.req.__; // $translate
const models = Self.app.models; const models = Self.app.models;
let tx; let tx;
const myOptions = {}; const myOptions = {};
@ -39,7 +43,20 @@ module.exports = Self => {
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
try { try {
const ticket = await models.Ticket.findById(ticketId, null, myOptions); const ticket = await models.Ticket.findById(ticketId, {
include: {
relation: 'client',
scope: {
include: {
relation: 'salesPersonUser',
scope: {
fields: ['id', 'name']
}
}
}
}
}, myOptions);
if (ticket.isDeleted) if (ticket.isDeleted)
throw new UserError(`You can't create a claim for a removed ticket`); throw new UserError(`You can't create a claim for a removed ticket`);
@ -49,6 +66,8 @@ module.exports = Self => {
ticketCreated: ticket.shipped, ticketCreated: ticket.shipped,
workerFk: userId workerFk: userId
}, myOptions); }, myOptions);
let changesMade = '';
const promises = []; const promises = [];
for (const sale of sales) { for (const sale of sales) {
@ -59,10 +78,25 @@ module.exports = Self => {
}, myOptions); }, myOptions);
promises.push(newClaimBeginning); promises.push(newClaimBeginning);
changesMade += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity})`;
} }
await Promise.all(promises); await Promise.all(promises);
const salesPerson = ticket.client().salesPersonUser();
if (salesPerson) {
const origin = ctx.req.headers.origin;
const message = $t('Created claim', {
claimId: newClaim.id,
ticketId: ticketId,
ticketUrl: `${origin}/#!/ticket/${ticketId}/sale`,
claimUrl: `${origin}/#!/claim/${newClaim.id}/summary`,
changes: changesMade
});
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);
}
if (tx) await tx.commit(); if (tx) await tx.commit();
return newClaim; return newClaim;

View File

@ -7,7 +7,13 @@ describe('Claim createFromSales()', () => {
instance: 0, instance: 0,
quantity: 10 quantity: 10
}]; }];
const ctx = {req: {accessToken: {userId: 1}}}; const ctx = {
req: {
accessToken: {userId: 1},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
it('should create a new claim', async() => { it('should create a new claim', async() => {
const tx = await app.models.Claim.beginTransaction({}); const tx = await app.models.Claim.beginTransaction({});

View File

@ -11,11 +11,6 @@ module.exports = Self => {
description: 'The entry id', description: 'The entry id',
http: {source: 'path'} http: {source: 'path'}
}, },
{
arg: 'options',
type: 'object',
description: 'Callback options',
},
{ {
arg: 'ref', arg: 'ref',
type: 'string', type: 'string',
@ -28,11 +23,11 @@ module.exports = Self => {
}, },
{ {
arg: 'buys', arg: 'buys',
type: ['Object'], type: ['object'],
description: 'The buys', description: 'The buys',
}], }],
returns: { returns: {
type: ['Object'], type: ['object'],
root: true root: true
}, },
http: { 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 conn = Self.dataSource.connector;
const args = ctx.args; const args = ctx.args;
const models = Self.app.models; const models = Self.app.models;
let tx; let tx;
const myOptions = {};
if (!options.transaction) { if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({}); tx = await Self.beginTransaction({});
options.transaction = tx; myOptions.transaction = tx;
} }
try { try {
const entry = await models.Entry.findById(id, null, options); const entry = await models.Entry.findById(id, null, myOptions);
await entry.updateAttributes({ await entry.updateAttributes({
observation: args.observation, observation: args.observation,
ref: args.ref ref: args.ref
}, options); }, myOptions);
const buys = []; const buys = [];
for (let buy of args.buys) { for (let buy of args.buys) {
@ -71,9 +70,16 @@ module.exports = Self => {
buyingValue: buy.buyingValue, buyingValue: buy.buyingValue,
packageFk: buy.packageFk 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 buyIds = createdBuys.map(buy => buy.id);
const stmts = []; const stmts = [];
@ -90,7 +96,7 @@ module.exports = Self => {
stmts.push('CALL buy_recalcPrices()'); stmts.push('CALL buy_recalcPrices()');
const sql = ParameterizedSQL.join(stmts, ';'); const sql = ParameterizedSQL.join(stmts, ';');
await conn.executeStmt(sql, options); await conn.executeStmt(sql, myOptions);
if (tx) await tx.commit(); if (tx) await tx.commit();
} catch (e) { } catch (e) {
if (tx) await tx.rollback(); if (tx) await tx.rollback();

View File

@ -37,7 +37,21 @@ module.exports = Self => {
where: {volume: {gte: buy.volume}}, where: {volume: {gte: buy.volume}},
order: 'volume ASC' order: 'volume ASC'
}, myOptions); }, 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; return buys;

View File

@ -5,6 +5,9 @@
"Buy": { "Buy": {
"dataSource": "vn" "dataSource": "vn"
}, },
"ItemMatchProperties": {
"dataSource": "vn"
},
"EntryLog": { "EntryLog": {
"dataSource": "vn" "dataSource": "vn"
}, },

View File

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

View File

@ -9,20 +9,6 @@
class="vn-ma-md"> class="vn-ma-md">
<div class="vn-w-lg"> <div class="vn-w-lg">
<vn-card class="vn-pa-lg"> <vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-textfield vn-focus
vn-one
label="Reference"
ng-model="$ctrl.import.ref">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textarea
vn-one
label="Observation"
ng-model="$ctrl.import.observation">
</vn-textarea>
</vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-input-file <vn-input-file
vn-one vn-one
@ -40,6 +26,20 @@
</append> </append>
</vn-input-file> </vn-input-file>
</vn-horizontal> </vn-horizontal>
<vn-horizontal ng-show="$ctrl.import.ref">
<vn-textfield vn-focus
vn-one
label="Reference"
ng-model="$ctrl.import.ref">
</vn-textfield>
</vn-horizontal>
<vn-horizontal ng-show="$ctrl.import.observation">
<vn-textarea
vn-one
label="Observation"
ng-model="$ctrl.import.observation">
</vn-textarea>
</vn-horizontal>
<vn-horizontal ng-show="$ctrl.import.buys.length > 0"> <vn-horizontal ng-show="$ctrl.import.buys.length > 0">
<table class="vn-table"> <table class="vn-table">
<thead> <thead>
@ -51,7 +51,6 @@
<th translate center>Grouping</th> <th translate center>Grouping</th>
<th translate center>Buying value</th> <th translate center>Buying value</th>
<th translate center>Box</th> <th translate center>Box</th>
<th translate center>Volume</th>
</tr> </tr>
</thead> </thead>
<tbody ng-repeat="buy in $ctrl.import.buys"> <tbody ng-repeat="buy in $ctrl.import.buys">
@ -70,20 +69,19 @@
<tpl-item> <tpl-item>
{{::id}} - {{::name}} {{::id}} - {{::name}}
</tpl-item> </tpl-item>
<append>
<vn-icon-button
icon="filter_alt"
vn-click-stop="$ctrl.showFilterDialog(buy)"
vn-tooltip="Filter...">
</vn-icon-button>
</append>
</vn-autocomplete> </vn-autocomplete>
</td> </td>
<td title="{{::buy.description}}" expand>{{::buy.description | dashIfEmpty}}</td> <td title="{{::buy.description}}" expand>{{::buy.description | dashIfEmpty}}</td>
<td center title="{{::buy.size}}">{{::buy.size | dashIfEmpty}}</td> <td center title="{{::buy.size}}">{{::buy.size | dashIfEmpty}}</td>
<td center> <td center>{{::buy.packing | dashIfEmpty}}</td>
<vn-chip> <td center>{{::buy.grouping | dashIfEmpty}}</td>
<span>{{::buy.packing | dashIfEmpty}}</span>
</vn-chip>
</td>
<td center>
<vn-chip>
<span>{{::buy.grouping | dashIfEmpty}}</span>
</vn-chip>
</vn-td>
<td>{{::buy.buyingValue | currency: 'EUR':2}}</td> <td>{{::buy.buyingValue | currency: 'EUR':2}}</td>
<td center title="{{::buy.packageFk | dashIfEmpty}}"> <td center title="{{::buy.packageFk | dashIfEmpty}}">
<vn-autocomplete <vn-autocomplete
@ -95,7 +93,6 @@
ng-model="buy.packageFk"> ng-model="buy.packageFk">
</vn-autocomplete> </vn-autocomplete>
</td> </td>
<td center title="{{::buy.volume}}">{{::buy.volume | number}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -110,7 +107,95 @@
label="Cancel" label="Cancel"
ui-sref="entry.card.buy.index"> ui-sref="entry.card.buy.index">
</vn-button> </vn-button>
</vn-button>
</vn-button-bar> </vn-button-bar>
</div> </div>
</form> </form>
<vn-dialog
vn-id="filterDialog"
on-accept="$ctrl.addTime()"
message="Filter item">
<tpl-body class="itemFilter">
<vn-horizontal>
<vn-textfield
label="Name"
ng-model="$ctrl.itemFilterParams.name"
vn-focus>
</vn-textfield>
<vn-textfield
label="Size"
ng-model="$ctrl.itemFilterParams.size">
</vn-textfield>
<vn-autocomplete
label="Producer"
ng-model="$ctrl.itemFilterParams.producerFk"
url="Producers"
show-field="name"
value-field="id">
</vn-autocomplete>
<vn-autocomplete
label="Type"
ng-model="$ctrl.itemFilterParams.typeFk"
url="ItemTypes"
show-field="name"
value-field="id">
</vn-autocomplete>
<vn-autocomplete
label="Color"
ng-model="$ctrl.itemFilterParams.inkFk"
url="Inks"
show-field="name"
value-field="id">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal class="vn-mb-md">
<vn-button vn-none
label="Search"
ng-click="$ctrl.filter()">
</vn-button>
</vn-horizontal>
<vn-crud-model
vn-id="itemsModel"
url="Items"
filter="$ctrl.itemFilter"
data="items"
limit="10">
</vn-crud-model>
<vn-data-viewer
model="itemsModel"
class="vn-w-lg">
<vn-table class="scrollable">
<vn-thead>
<vn-tr>
<vn-th shrink>ID</vn-th>
<vn-th expand>Item</vn-th>
<vn-th number>Size</vn-th>
<vn-th expand>Producer</vn-th>
<vn-th>Color</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<a ng-repeat="item in items"
class="clickable vn-tr search-result"
ng-click="$ctrl.selectItem(item.id)">
<vn-td shrink>
<span
ng-click="itemDescriptor.show($event, item.id)"
class="link">
{{::item.id}}
</span>
</vn-td>
<vn-td expand>{{::item.name}}</vn-td>
<vn-td number>{{::item.size}}</vn-td>
<vn-td expand>{{::item.producer.name}}</vn-td>
<vn-td>{{::item.ink.name}}</vn-td>
</a>
</vn-tbody>
</vn-table>
</vn-data-viewer>
<vn-item-descriptor-popover
vn-id="item-descriptor"
warehouse-fk="$ctrl.vnConfig.warehouseFk">
</vn-item-descriptor-popover>
</tpl-body>
</vn-dialog>

View File

@ -29,6 +29,7 @@ class Controller extends Section {
this.$.$applyAsync(() => { this.$.$applyAsync(() => {
this.import.observation = invoice.tx_awb; this.import.observation = invoice.tx_awb;
const companyName = invoice.tx_company;
const boxes = invoice.boxes; const boxes = invoice.boxes;
const buys = []; const buys = [];
for (let box of boxes) { for (let box of boxes) {
@ -37,11 +38,12 @@ class Controller extends Section {
const packing = product.nu_stems_bunch * product.nu_bunches; const packing = product.nu_stems_bunch * product.nu_bunches;
buys.push({ buys.push({
description: product.nm_product, description: product.nm_product,
companyName: companyName,
size: product.nu_length, size: product.nu_length,
packing: packing, packing: packing,
grouping: product.nu_stems_bunch, grouping: product.nu_stems_bunch,
buyingValue: parseFloat(product.mny_rate_stem), buyingValue: parseFloat(product.mny_rate_stem),
volume: boxVolume volume: boxVolume,
}); });
} }
} }
@ -86,6 +88,59 @@ class Controller extends Section {
? {id: $search} ? {id: $search}
: {name: {like: '%' + $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']; Controller.$inject = ['$element', '$scope'];

View File

@ -2,4 +2,10 @@ vn-entry-buy-import {
.vn-table > tbody td:nth-child(1) { .vn-table > tbody td:nth-child(1) {
width: 250px width: 250px
} }
}
.itemFilter {
vn-table.scrollable {
height: 500px
}
} }

View File

@ -3,4 +3,6 @@ Observation: Observación
Box: Embalaje Box: Embalaje
Import buys: Importar compras Import buys: Importar compras
Some of the imported buys doesn't have an item: Algunas de las compras importadas no tienen un artículo 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 JSON files only: Solo ficheros JSON
Filter item: Filtrar artículo
Filter...: Filtrar...

View File

@ -1,6 +1,7 @@
<vn-crud-model <vn-crud-model
vn-id="ticketsModel" vn-id="ticketsModel"
limit="10" limit="10"
auto-load="true"
data="tickets"> data="tickets">
</vn-crud-model> </vn-crud-model>
<vn-card class="summary"> <vn-card class="summary">

View File

@ -7,7 +7,7 @@ class Controller extends Summary {
this._invoiceOut = value; this._invoiceOut = value;
if (value && value.id) { if (value && value.id) {
this.getSummary(); this.getSummary();
this.getTickets(); this.$.ticketsModel.url = `InvoiceOuts/${this.invoiceOut.id}/getTickets`;
} }
} }
@ -19,14 +19,6 @@ class Controller extends Summary {
return this.$http.get(`InvoiceOuts/${this.invoiceOut.id}/summary`) return this.$http.get(`InvoiceOuts/${this.invoiceOut.id}/summary`)
.then(res => this.summary = res.data); .then(res => this.summary = res.data);
} }
getTickets() {
this.$.$applyAsync(() => {
const query = `InvoiceOuts/${this.invoiceOut.id}/getTickets`;
this.$.ticketsModel.url = query;
this.$.ticketsModel.refresh();
});
}
} }
ngModule.vnComponent('vnInvoiceOutSummary', { ngModule.vnComponent('vnInvoiceOutSummary', {

View File

@ -27,17 +27,5 @@ describe('InvoiceOut', () => {
expect(controller.summary).toEqual('the data you are looking for'); expect(controller.summary).toEqual('the data you are looking for');
}); });
}); });
describe('getTickets()', () => {
it('should perform a and then call to the ticketModel refresh() method', () => {
jest.spyOn(controller.$.ticketsModel, 'refresh');
controller.getTickets();
$scope.$apply();
expect(controller.$.ticketsModel.url).toEqual('InvoiceOuts/1/getTickets');
expect(controller.$.ticketsModel.refresh).toHaveBeenCalledWith();
});
});
}); });
}); });

View File

@ -1,6 +1,6 @@
Ink: Tinta Ink: Tinta
Origin: Origen Origin: Origen
Producer: Productor. Producer: Productor
With visible: Con visible With visible: Con visible
Field: Campo Field: Campo
More fields: Más campos More fields: Más campos

View File

@ -28,30 +28,57 @@ module.exports = Self => {
}); });
Self.getItemTypeAvailable = async(orderId, itemCategoryId) => { Self.getItemTypeAvailable = async(orderId, itemCategoryId) => {
let stmts = []; const stmts = [];
let stmt; 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(?, ?, ?)', [ stmt = new ParameterizedSQL('call vn.available_calc(?, ?, ?)', [
order.landed, order.landed,
order.addressFk, order.addressFk,
order.agencyModeFk order.agencyModeFk
]); ]);
stmts.push(stmt); 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 FROM tmp.availableCalc ac
JOIN cache.available a ON a.calc_id = ac.calcFk JOIN cache.available a ON a.calc_id = ac.calcFk
JOIN item i ON i.id = a.item_id JOIN vn.item i ON i.id = a.item_id
JOIN itemType it ON it.id = i.typeFk JOIN vn.itemType it ON it.id = i.typeFk
JOIN itemCategory ic ON ic.id = it.categoryFk JOIN vn.itemCategory ic ON ic.id = it.categoryFk
WHERE it.categoryFk = ? WHERE it.categoryFk = ?`, [itemCategoryId]
GROUP BY it.id`, [itemCategoryId]
); );
let categoriesIndex = stmts.push(stmt) - 1; stmts.push(stmt);
let sql = ParameterizedSQL.join(stmts, ';'); stmt = new ParameterizedSQL(
let result = await Self.rawStmt(sql); '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]; return result[categoriesIndex];
}; };

View File

@ -6,18 +6,18 @@ module.exports = Self => {
accessType: 'WRITE', accessType: 'WRITE',
accepts: [{ accepts: [{
arg: 'sales', arg: 'sales',
type: ['Object'], type: ['object'],
required: true, required: true,
description: 'The sales to remove' description: 'The sales to remove'
}, },
{ {
arg: 'ticketId', arg: 'ticketId',
type: 'Number', type: 'number',
required: true, required: true,
description: 'The ticket id' description: 'The ticket id'
}], }],
returns: { returns: {
type: ['Object'], type: ['object'],
root: true root: true
}, },
http: { http: {
@ -27,10 +27,25 @@ module.exports = Self => {
}); });
Self.deleteSales = async(ctx, sales, ticketId) => { Self.deleteSales = async(ctx, sales, ticketId) => {
const $t = ctx.req.__; // $translate
const models = Self.app.models; const models = Self.app.models;
const canEditSales = await models.Sale.canEdit(ctx, sales); const canEditSales = await models.Sale.canEdit(ctx, sales);
const ticket = await models.Ticket.findById(ticketId, {
include: {
relation: 'client',
scope: {
include: {
relation: 'salesPersonUser',
scope: {
fields: ['id', 'name']
}
}
}
}
});
const isTicketEditable = await models.Ticket.isEditable(ctx, ticketId); const isTicketEditable = await models.Ticket.isEditable(ctx, ticketId);
if (!isTicketEditable) if (!isTicketEditable)
throw new UserError(`The sales of this ticket can't be modified`); throw new UserError(`The sales of this ticket can't be modified`);
@ -39,11 +54,26 @@ module.exports = Self => {
throw new UserError(`Sale(s) blocked, please contact production`); throw new UserError(`Sale(s) blocked, please contact production`);
const promises = []; const promises = [];
let deletions = '';
for (let sale of sales) { for (let sale of sales) {
const deletedSale = models.Sale.destroyById(sale.id); const deletedSale = models.Sale.destroyById(sale.id);
deletions += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity})`;
promises.push(deletedSale); promises.push(deletedSale);
} }
const salesPerson = ticket.client().salesPersonUser();
if (salesPerson) {
const origin = ctx.req.headers.origin;
const message = $t('Deleted sales from ticket', {
ticketId: ticketId,
ticketUrl: `${origin}/#!/ticket/${ticketId}/sale`,
deletions: deletions
});
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);
}
return Promise.all(promises); return Promise.all(promises);
}; };
}; };

View File

@ -5,24 +5,27 @@ module.exports = Self => {
Self.remoteMethodCtx('reserve', { Self.remoteMethodCtx('reserve', {
description: 'Change the state of a ticket', description: 'Change the state of a ticket',
accessType: 'WRITE', accessType: 'WRITE',
accepts: [{ accepts: [
arg: 'ticketId', {
type: 'Number', arg: 'ticketId',
required: true, type: 'number',
description: 'The ticket id' required: true,
}, { description: 'The ticket id'
arg: 'sales', },
type: ['Object'], {
required: true, arg: 'sales',
description: 'The sale to reserve' type: ['object'],
}, required: true,
{ description: 'The sale to reserve'
arg: 'reserved', },
type: 'Boolean', {
required: true arg: 'reserved',
}], type: 'boolean',
required: true
}
],
returns: { returns: {
type: ['Object'], type: ['object'],
root: true root: true
}, },
http: { http: {
@ -32,7 +35,9 @@ module.exports = Self => {
}); });
Self.reserve = async(ctx, ticketId, sales, reserved) => { Self.reserve = async(ctx, ticketId, sales, reserved) => {
const $t = ctx.req.__; // $translate
const models = Self.app.models; const models = Self.app.models;
const isTicketEditable = await models.Ticket.isEditable(ctx, ticketId); const isTicketEditable = await models.Ticket.isEditable(ctx, ticketId);
if (!isTicketEditable) if (!isTicketEditable)
throw new UserError(`The sales of this ticket can't be modified`); throw new UserError(`The sales of this ticket can't be modified`);
@ -42,12 +47,50 @@ module.exports = Self => {
if (!canEditSale) if (!canEditSale)
throw new UserError(`Sale(s) blocked, please contact production`); throw new UserError(`Sale(s) blocked, please contact production`);
let changesMade = '';
const promises = []; const promises = [];
for (let sale of sales) { for (let sale of sales) {
const reservedSale = models.Sale.update({id: sale.id}, {reserved: reserved}); if (sale.reserved != reserved) {
promises.push(reservedSale); const oldState = sale.reserved ? 'reserved' : 'regular';
const newState = reserved ? 'reserved' : 'regular';
const reservedSale = models.Sale.update({id: sale.id}, {reserved: reserved});
promises.push(reservedSale);
changesMade += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity}) ${$t('State')}: ${$t(oldState)} ➔ *${$t(newState)}*`;
}
} }
return Promise.all(promises); const result = await Promise.all(promises);
const ticket = await models.Ticket.findById(ticketId, {
include: {
relation: 'client',
scope: {
include: {
relation: 'salesPersonUser',
scope: {
fields: ['id', 'name']
}
}
}
}
});
const salesPerson = ticket.client().salesPersonUser();
if (salesPerson) {
const origin = ctx.req.headers.origin;
const message = $t('Changed sale reserved state', {
ticketId: ticketId,
ticketUrl: `${origin}/#!/ticket/${ticketId}/sale`,
changes: changesMade
});
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);
}
return result;
}; };
}; };

View File

@ -17,7 +17,14 @@ describe('sale deleteSales()', () => {
}); });
it('should throw an error if the ticket of the given sales is not editable', async() => { it('should throw an error if the ticket of the given sales is not editable', async() => {
let ctx = {req: {accessToken: {userId: 9}}}; let ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
let error; let error;
const sales = [{id: 1, instance: 0}, {id: 2, instance: 1}]; const sales = [{id: 1, instance: 0}, {id: 2, instance: 1}];
@ -33,7 +40,13 @@ describe('sale deleteSales()', () => {
}); });
it('should delete the sale', async() => { it('should delete the sale', async() => {
let ctx = {req: {accessToken: {userId: 9}}}; let ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
const sales = [{id: newSale.id, instance: 0}]; const sales = [{id: newSale.id, instance: 0}];
const ticketId = 16; const ticketId = 16;

View File

@ -1,7 +1,14 @@
const app = require('vn-loopback/server/server'); const app = require('vn-loopback/server/server');
describe('sale reserve()', () => { describe('sale reserve()', () => {
const ctx = {req: {accessToken: {userId: 9}}}; const ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
afterAll(async done => { afterAll(async done => {
let ctx = {req: {accessToken: {userId: 9}}}; let ctx = {req: {accessToken: {userId: 9}}};
let params = { let params = {

View File

@ -17,7 +17,13 @@ describe('sale updatePrice()', () => {
}); });
it('should throw an error if the ticket is not editable', async() => { it('should throw an error if the ticket is not editable', async() => {
let ctx = {req: {accessToken: {userId: 18}}}; const ctx = {
req: {
accessToken: {userId: 18},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
let immutableSaleId = 1; let immutableSaleId = 1;
let price = 5; let price = 5;
@ -31,7 +37,14 @@ describe('sale updatePrice()', () => {
}); });
it('should return 0 if the price is an empty string', async() => { it('should return 0 if the price is an empty string', async() => {
let ctx = {req: {accessToken: {userId: 18}}}; const ctx = {
req: {
accessToken: {userId: 18},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
let price = ''; let price = '';
await app.models.Sale.updatePrice(ctx, saleId, price); await app.models.Sale.updatePrice(ctx, saleId, price);
@ -46,7 +59,14 @@ describe('sale updatePrice()', () => {
}); });
it('should now set price as a number in a string', async() => { it('should now set price as a number in a string', async() => {
let ctx = {req: {accessToken: {userId: 18}}}; const ctx = {
req: {
accessToken: {userId: 18},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
let price = '8'; let price = '8';
await app.models.Sale.updatePrice(ctx, saleId, price); await app.models.Sale.updatePrice(ctx, saleId, price);

View File

@ -1,7 +1,13 @@
const app = require('vn-loopback/server/server'); const app = require('vn-loopback/server/server');
describe('sale updateQuantity()', () => { describe('sale updateQuantity()', () => {
const ctx = {req: {accessToken: {userId: 9}}}; const ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
it('should throw an error if the quantity is not a number', async() => { it('should throw an error if the quantity is not a number', async() => {
let error; let error;

View File

@ -19,7 +19,7 @@ module.exports = Self => {
} }
], ],
returns: { returns: {
type: 'Number', type: 'number',
root: true root: true
}, },
http: { http: {
@ -29,29 +29,37 @@ module.exports = Self => {
}); });
Self.updatePrice = async(ctx, id, newPrice) => { Self.updatePrice = async(ctx, id, newPrice) => {
let models = Self.app.models; const $t = ctx.req.__; // $translate
let tx = await Self.beginTransaction({}); const models = Self.app.models;
const tx = await Self.beginTransaction({});
try { try {
let options = {transaction: tx}; const options = {transaction: tx};
let filter = { const filter = {
include: { include: {
relation: 'ticket', relation: 'ticket',
scope: { scope: {
include: { include: {
relation: 'client', relation: 'client',
scope: { scope: {
fields: ['salesPersonFk'] fields: ['salesPersonFk'],
include: {
relation: 'salesPersonUser',
scope: {
fields: ['id', 'name']
}
}
} }
}, },
fields: ['id', 'clientFk'] fields: ['id', 'clientFk']
} }
} }
}; };
let sale = await models.Sale.findById(id, filter, options);
let isEditable = await models.Ticket.isEditable(ctx, sale.ticketFk); const sale = await models.Sale.findById(id, filter, options);
const isEditable = await models.Ticket.isEditable(ctx, sale.ticketFk);
if (!isEditable) if (!isEditable)
throw new UserError(`The sales of this ticket can't be modified`); throw new UserError(`The sales of this ticket can't be modified`);
@ -60,21 +68,19 @@ module.exports = Self => {
if (!canEditSale) if (!canEditSale)
throw new UserError(`Sale(s) blocked, please contact production`); throw new UserError(`Sale(s) blocked, please contact production`);
const oldPrice = sale.price;
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
const usesMana = await models.WorkerMana.findOne({where: {workerFk: userId}, fields: 'amount'}, options);
const componentCode = usesMana ? 'mana' : 'buyerDiscount';
const discount = await models.Component.findOne({where: {code: componentCode}}, options);
const componentId = discount.id;
const componentValue = newPrice - sale.price;
let usesMana = await models.WorkerMana.findOne({where: {workerFk: userId}, fields: 'amount'}, options); const where = {
let componentCode = usesMana ? 'mana' : 'buyerDiscount';
let discount = await models.Component.findOne({where: {code: componentCode}}, options);
let componentId = discount.id;
let componentValue = newPrice - sale.price;
let where = {
componentFk: componentId, componentFk: componentId,
saleFk: id saleFk: id
}; };
let saleComponent = await models.SaleComponent.findOne({where}, options); const saleComponent = await models.SaleComponent.findOne({where}, options);
if (saleComponent) { if (saleComponent) {
await models.SaleComponent.updateAll(where, { await models.SaleComponent.updateAll(where, {
@ -92,6 +98,22 @@ module.exports = Self => {
query = `CALL vn.manaSpellersRequery(?)`; query = `CALL vn.manaSpellersRequery(?)`;
await Self.rawSql(query, [userId], options); await Self.rawSql(query, [userId], options);
const salesPerson = sale.ticket().client().salesPersonUser();
if (salesPerson) {
const origin = ctx.req.headers.origin;
const message = $t('Changed sale price', {
ticketId: sale.ticket().id,
itemId: sale.itemFk,
concept: sale.concept,
quantity: sale.quantity,
oldPrice: oldPrice,
newPrice: newPrice,
ticketUrl: `${origin}/#!/ticket/${sale.ticket().id}/sale`,
itemUrl: `${origin}/#!/item/${sale.itemFk}/summary`
});
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);
}
await tx.commit(); await tx.commit();
return sale; return sale;

View File

@ -26,7 +26,8 @@ module.exports = Self => {
} }
}); });
Self.updateQuantity = async(ctx, id, quantity) => { Self.updateQuantity = async(ctx, id, newQuantity) => {
const $t = ctx.req.__; // $translate
const models = Self.app.models; const models = Self.app.models;
const canEditSale = await models.Sale.canEdit(ctx, [id]); const canEditSale = await models.Sale.canEdit(ctx, [id]);
@ -34,13 +35,51 @@ module.exports = Self => {
if (!canEditSale) if (!canEditSale)
throw new UserError(`Sale(s) blocked, please contact production`); throw new UserError(`Sale(s) blocked, please contact production`);
if (isNaN(quantity)) if (isNaN(newQuantity))
throw new UserError(`The value should be a number`); throw new UserError(`The value should be a number`);
let currentLine = await models.Sale.findOne({where: {id: id}}); const filter = {
if (quantity > currentLine.quantity) include: {
relation: 'ticket',
scope: {
include: {
relation: 'client',
scope: {
include: {
relation: 'salesPersonUser',
scope: {
fields: ['id', 'name']
}
}
}
}
}
}
};
const sale = await models.Sale.findById(id, filter);
if (newQuantity > sale.quantity)
throw new UserError('The new quantity should be smaller than the old one'); throw new UserError('The new quantity should be smaller than the old one');
return await currentLine.updateAttributes({quantity: quantity}); const oldQuantity = sale.quantity;
const result = await sale.updateAttributes({quantity: newQuantity});
const salesPerson = sale.ticket().client().salesPersonUser();
if (salesPerson) {
const origin = ctx.req.headers.origin;
const message = $t('Changed sale quantity', {
ticketId: sale.ticket().id,
itemId: sale.itemFk,
concept: sale.concept,
oldQuantity: oldQuantity,
newQuantity: newQuantity,
ticketUrl: `${origin}/#!/ticket/${sale.ticket().id}/sale`,
itemUrl: `${origin}/#!/item/${sale.itemFk}/summary`
});
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);
}
return result;
}; };
}; };

View File

@ -33,6 +33,7 @@ module.exports = Self => {
}); });
Self.addSale = async(ctx, id, itemId, quantity) => { Self.addSale = async(ctx, id, itemId, quantity) => {
const $t = ctx.req.__; // $translate
const models = Self.app.models; const models = Self.app.models;
const isEditable = await models.Ticket.isEditable(ctx, id); const isEditable = await models.Ticket.isEditable(ctx, id);
@ -40,7 +41,19 @@ module.exports = Self => {
throw new UserError(`The sales of this ticket can't be modified`); throw new UserError(`The sales of this ticket can't be modified`);
const item = await models.Item.findById(itemId); const item = await models.Item.findById(itemId);
const ticket = await models.Ticket.findById(id); const ticket = await models.Ticket.findById(id, {
include: {
relation: 'client',
scope: {
include: {
relation: 'salesPersonUser',
scope: {
fields: ['id', 'name']
}
}
}
}
});
const res = await models.Item.getVisibleAvailable(itemId, ticket.warehouseFk, ticket.shipped); const res = await models.Item.getVisibleAvailable(itemId, ticket.warehouseFk, ticket.shipped);
@ -63,6 +76,20 @@ module.exports = Self => {
} }
}); });
const addition = `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity})`;
const salesPerson = ticket.client().salesPersonUser();
if (salesPerson) {
const origin = ctx.req.headers.origin;
const message = $t('Added sale to ticket', {
ticketId: id,
ticketUrl: `${origin}/#!/ticket/${id}/sale`,
addition: addition
});
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);
}
return sale; return sale;
}; };
}; };

View File

@ -230,7 +230,7 @@ module.exports = Self => {
ticketUrl: `${origin}/#!/ticket/${args.id}/sale`, ticketUrl: `${origin}/#!/ticket/${args.id}/sale`,
changes: changesMade changes: changesMade
}); });
await models.Chat.sendCheckingPresence(ctx, salesPersonId, message, myOptions); await models.Chat.sendCheckingPresence(ctx, salesPersonId, message);
} }
if (tx) await tx.commit(); if (tx) await tx.commit();

View File

@ -12,7 +12,13 @@ describe('ticket addSale()', () => {
}); });
it('should create a new sale for the ticket with id 13', async() => { it('should create a new sale for the ticket with id 13', async() => {
let ctx = {req: {accessToken: {userId: 9}}}; const ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
const itemId = 4; const itemId = 4;
const quantity = 10; const quantity = 10;
newSale = await app.models.Ticket.addSale(ctx, ticketId, itemId, quantity); newSale = await app.models.Ticket.addSale(ctx, ticketId, itemId, quantity);
@ -21,7 +27,13 @@ describe('ticket addSale()', () => {
}); });
it('should not be able to add a sale if the item quantity is not available', async() => { it('should not be able to add a sale if the item quantity is not available', async() => {
let ctx = {req: {accessToken: {userId: 9}}}; const ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
const itemId = 11; const itemId = 11;
const quantity = 10; const quantity = 10;
@ -36,7 +48,13 @@ describe('ticket addSale()', () => {
}); });
it('should not be able to add a sale if the ticket is not editable', async() => { it('should not be able to add a sale if the ticket is not editable', async() => {
let ctx = {req: {accessToken: {userId: 9}}}; const ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
const notEditableTicketId = 1; const notEditableTicketId = 1;
const itemId = 4; const itemId = 4;
const quantity = 10; const quantity = 10;

View File

@ -35,7 +35,13 @@ describe('sale updateDiscount()', () => {
}); });
it('should throw an error if no sales were selected', async() => { it('should throw an error if no sales were selected', async() => {
let ctx = {req: {accessToken: {userId: 9}}}; const ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
let error; let error;
const ticketId = 11; const ticketId = 11;
const sales = []; const sales = [];
@ -51,7 +57,13 @@ describe('sale updateDiscount()', () => {
}); });
it('should throw an error if no sales belong to different tickets', async() => { it('should throw an error if no sales belong to different tickets', async() => {
let ctx = {req: {accessToken: {userId: 9}}}; const ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
let error; let error;
const ticketId = 11; const ticketId = 11;
const sales = [1, 14]; const sales = [1, 14];
@ -67,7 +79,13 @@ describe('sale updateDiscount()', () => {
}); });
it('should throw an error if the ticket is invoiced already', async() => { it('should throw an error if the ticket is invoiced already', async() => {
let ctx = {req: {accessToken: {userId: 9}}}; const ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
let error; let error;
const ticketId = 1; const ticketId = 1;
const sales = [1]; const sales = [1];
@ -83,7 +101,13 @@ describe('sale updateDiscount()', () => {
}); });
it('should update the discount if the salesPerson has mana', async() => { it('should update the discount if the salesPerson has mana', async() => {
let ctx = {req: {accessToken: {userId: 18}}}; const ctx = {
req: {
accessToken: {userId: 18},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
const ticketId = 11; const ticketId = 11;
const sales = [originalSaleId]; const sales = [originalSaleId];
const newDiscount = 100; const newDiscount = 100;
@ -105,7 +129,13 @@ describe('sale updateDiscount()', () => {
}); });
it('should update the discount and add company discount component if the worker does not have mana', async() => { it('should update the discount and add company discount component if the worker does not have mana', async() => {
let ctx = {req: {accessToken: {userId: 9}}}; const ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
const ticketId = 11; const ticketId = 11;
const sales = [originalSaleId]; const sales = [originalSaleId];
const newDiscount = 100; const newDiscount = 100;

View File

@ -17,7 +17,8 @@ module.exports = Self => {
description: 'The sales id', description: 'The sales id',
type: ['number'], type: ['number'],
required: true, required: true,
}, { },
{
arg: 'newDiscount', arg: 'newDiscount',
description: 'The new discount', description: 'The new discount',
type: 'number', type: 'number',
@ -35,6 +36,7 @@ module.exports = Self => {
}); });
Self.updateDiscount = async(ctx, id, salesIds, newDiscount) => { Self.updateDiscount = async(ctx, id, salesIds, newDiscount) => {
const $t = ctx.req.__; // $translate
const models = Self.app.models; const models = Self.app.models;
const tx = await Self.beginTransaction({}); const tx = await Self.beginTransaction({});
@ -59,7 +61,7 @@ module.exports = Self => {
} }
}; };
let sales = await models.Sale.find(filter, options); const sales = await models.Sale.find(filter, options);
if (sales.length === 0) if (sales.length === 0)
throw new UserError('Please select at least one sale'); throw new UserError('Please select at least one sale');
@ -94,8 +96,11 @@ module.exports = Self => {
where: {code: componentCode}}, options); where: {code: componentCode}}, options);
const componentId = discountComponent.id; const componentId = discountComponent.id;
let promises = []; const promises = [];
let changesMade = '';
for (let sale of sales) { for (let sale of sales) {
const oldDiscount = sale.discount;
const value = ((-sale.price * newDiscount) / 100); const value = ((-sale.price * newDiscount) / 100);
const newComponent = models.SaleComponent.upsert({ const newComponent = models.SaleComponent.upsert({
saleFk: sale.id, saleFk: sale.id,
@ -105,6 +110,7 @@ module.exports = Self => {
const updatedSale = sale.updateAttribute('discount', newDiscount, options); const updatedSale = sale.updateAttribute('discount', newDiscount, options);
promises.push(newComponent, updatedSale); promises.push(newComponent, updatedSale);
changesMade += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity}) ${oldDiscount}% ➔ *${newDiscount}%*`;
} }
await Promise.all(promises); await Promise.all(promises);
@ -112,6 +118,32 @@ module.exports = Self => {
const query = `call vn.manaSpellersRequery(?)`; const query = `call vn.manaSpellersRequery(?)`;
await Self.rawSql(query, [userId], options); await Self.rawSql(query, [userId], options);
const ticket = await models.Ticket.findById(id, {
include: {
relation: 'client',
scope: {
include: {
relation: 'salesPersonUser',
scope: {
fields: ['id', 'name']
}
}
}
}
}, options);
const salesPerson = ticket.client().salesPersonUser();
if (salesPerson) {
const origin = ctx.req.headers.origin;
const message = $t('Changed sale discount', {
ticketId: id,
ticketUrl: `${origin}/#!/ticket/${id}/sale`,
changes: changesMade
});
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);
}
await tx.commit(); await tx.commit();
} catch (error) { } catch (error) {
await tx.rollback(); await tx.rollback();

View File

@ -82,7 +82,10 @@ class Controller extends Section {
if (!this.ticket) return; if (!this.ticket) return;
this.$http.get(`Tickets/${this.ticket.id}/getVolume`) this.$http.get(`Tickets/${this.ticket.id}/getVolume`)
.then(res => this.ticketVolume = res.data[0].volume); .then(res => {
if (res.data.length)
this.ticketVolume = res.data[0].volume;
});
} }
} }

View File

@ -47,7 +47,13 @@ class Controller extends Section {
getMana() { getMana() {
this.$http.get(`Tickets/${this.$params.id}/getSalesPersonMana`) this.$http.get(`Tickets/${this.$params.id}/getSalesPersonMana`)
.then(res => this.edit.mana = res.data); .then(res => {
this.edit.mana = res.data;
this.$.$applyAsync(() => {
this.$.editDiscount.relocate();
this.$.editPricePopover.relocate();
});
});
} }
/** /**

View File

@ -42,6 +42,8 @@ describe('Ticket', () => {
$scope.sms = {open: () => {}}; $scope.sms = {open: () => {}};
$scope.ticket = ticket; $scope.ticket = ticket;
$scope.model = crudModel; $scope.model = crudModel;
$scope.editDiscount = {relocate: () => {}};
$scope.editPricePopover = {relocate: () => {}};
$httpBackend = _$httpBackend_; $httpBackend = _$httpBackend_;
Object.defineProperties($state.params, { Object.defineProperties($state.params, {
id: { id: {