Merge branch 'dev' into 4059-revocar-permisos-client-account
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Javi Gallego 2023-06-09 07:26:29 +02:00
commit ec856c0db2
57 changed files with 538 additions and 896 deletions

View File

@ -5,14 +5,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2326.01] - 2023-06-29
### Added
### Changed
### Fixed
-
## [2324.01] - 2023-06-08
### Added
-
- (Tickets -> Abono) Al abonar permite crear el ticket abono con almacén o sin almmacén
- (General -> Desplegables) Mejorada eficiencia de carga de datos
### Changed
-
- (General -> Permisos) Mejorada seguridad
### Fixed
-

View File

@ -11,9 +11,9 @@ RUN apt-get update \
ca-certificates \
gnupg2 \
graphicsmagick \
&& curl -fsSL https://deb.nodesource.com/setup_14.x | bash - \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& npm install -g npm@8.19.2
&& npm install -g npm@9.6.6
# Puppeteer

8
Jenkinsfile vendored
View File

@ -39,7 +39,7 @@ pipeline {
NODE_ENV = ""
}
steps {
nodejs('node-v14') {
nodejs('node-v20') {
sh 'npm install --no-audit --prefer-offline'
sh 'gulp install --ci'
}
@ -57,14 +57,14 @@ pipeline {
parallel {
stage('Frontend') {
steps {
nodejs('node-v14') {
nodejs('node-v20') {
sh 'jest --ci --reporters=default --reporters=jest-junit --maxWorkers=2'
}
}
}
stage('Backend') {
steps {
nodejs('node-v14') {
nodejs('node-v20') {
sh 'npm run test:back:ci'
}
}
@ -80,7 +80,7 @@ pipeline {
CREDENTIALS = credentials('docker-registry')
}
steps {
nodejs('node-v14') {
nodejs('node-v20') {
sh 'gulp build'
}

View File

@ -0,0 +1,127 @@
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_canAdvance`(vDateFuture DATE, vDateToAdvance DATE, vWarehouseFk INT)
BEGIN
/**
* Devuelve los tickets y la cantidad de lineas de venta que se pueden adelantar.
*
* @param vDateFuture Fecha de los tickets que se quieren adelantar.
* @param vDateToAdvance Fecha a cuando se quiere adelantar.
* @param vWarehouseFk Almacén
*/
DECLARE vDateInventory DATE;
SELECT inventoried INTO vDateInventory FROM config;
DROP TEMPORARY TABLE IF EXISTS tmp.stock;
CREATE TEMPORARY TABLE tmp.stock
(itemFk INT PRIMARY KEY,
amount INT)
ENGINE = MEMORY;
INSERT INTO tmp.stock(itemFk, amount)
SELECT itemFk, SUM(quantity) amount FROM
(
SELECT itemFk, quantity
FROM itemTicketOut
WHERE shipped >= vDateInventory
AND shipped < vDateFuture
AND warehouseFk = vWarehouseFk
UNION ALL
SELECT itemFk, quantity
FROM itemEntryIn
WHERE landed >= vDateInventory
AND landed < vDateFuture
AND isVirtualStock = FALSE
AND warehouseInFk = vWarehouseFk
UNION ALL
SELECT itemFk, quantity
FROM itemEntryOut
WHERE shipped >= vDateInventory
AND shipped < vDateFuture
AND warehouseOutFk = vWarehouseFk
) t
GROUP BY itemFk HAVING amount != 0;
DROP TEMPORARY TABLE IF EXISTS tmp.filter;
CREATE TEMPORARY TABLE tmp.filter
(INDEX (id))
SELECT
origin.ticketFk futureId,
dest.ticketFk id,
dest.state,
origin.futureState,
origin.futureIpt,
dest.ipt,
origin.workerFk,
origin.futureLiters,
origin.futureLines,
dest.shipped,
origin.shipped futureShipped,
dest.totalWithVat,
origin.totalWithVat futureTotalWithVat,
dest.agency,
origin.futureAgency,
dest.lines,
dest.liters,
origin.futureLines - origin.hasStock AS notMovableLines,
(origin.futureLines = origin.hasStock) AS isFullMovable,
origin.classColor futureClassColor,
dest.classColor
FROM (
SELECT
s.ticketFk,
t.workerFk,
t.shipped,
t.totalWithVat,
st.name futureState,
t.addressFk,
am.name futureAgency,
count(s.id) futureLines,
GROUP_CONCAT(DISTINCT ipt.code ORDER BY ipt.code) futureIpt,
CAST(SUM(litros) AS DECIMAL(10,0)) futureLiters,
SUM((s.quantity <= IFNULL(st.amount,0))) hasStock,
st.classColor
FROM ticket t
JOIN sale s ON s.ticketFk = t.id
JOIN saleVolume sv ON sv.saleFk = s.id
JOIN item i ON i.id = s.itemFk
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN state st ON st.id = ts.stateFk
JOIN agencyMode am ON t.agencyModeFk = am.id
LEFT JOIN itemPackingType ipt ON ipt.code = i.itemPackingTypeFk
LEFT JOIN tmp.stock st ON st.itemFk = i.id
WHERE t.shipped BETWEEN vDateFuture AND util.dayend(vDateFuture)
AND t.warehouseFk = vWarehouseFk
GROUP BY t.id
) origin
JOIN (
SELECT
t.id ticketFk,
t.addressFk,
st.name state,
GROUP_CONCAT(DISTINCT ipt.code ORDER BY ipt.code) ipt,
t.shipped,
t.totalWithVat,
am.name agency,
CAST(SUM(litros) AS DECIMAL(10,0)) liters,
CAST(COUNT(*) AS DECIMAL(10,0)) `lines`,
st.classColor
FROM ticket t
JOIN sale s ON s.ticketFk = t.id
JOIN saleVolume sv ON sv.saleFk = s.id
JOIN item i ON i.id = s.itemFk
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN state st ON st.id = ts.stateFk
JOIN agencyMode am ON t.agencyModeFk = am.id
LEFT JOIN itemPackingType ipt ON ipt.code = i.itemPackingTypeFk
WHERE t.shipped BETWEEN vDateToAdvance AND util.dayend(vDateToAdvance)
AND t.warehouseFk = vWarehouseFk
AND st.order <= 5
GROUP BY t.id
) dest ON dest.addressFk = origin.addressFk
WHERE origin.hasStock != 0;
DROP TEMPORARY TABLE tmp.stock;
END$$
DELIMITER ;

View File

@ -0,0 +1,74 @@
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_canbePostponed`(vOriginDated DATE, vFutureDated DATE, vWarehouseFk INT)
BEGIN
/**
* Devuelve un listado de tickets susceptibles de fusionarse con otros tickets en el futuro
*
* @param vOriginDated Fecha en cuestión
* @param vFutureDated Fecha en el futuro a sondear
* @param vWarehouseFk Identificador de vn.warehouse
*/
DROP TEMPORARY TABLE IF EXISTS tmp.filter;
CREATE TEMPORARY TABLE tmp.filter
(INDEX (id))
SELECT sv.ticketFk id,
sub2.id futureId,
GROUP_CONCAT(DISTINCT i.itemPackingTypeFk ORDER BY i.itemPackingTypeFk) ipt,
CAST(sum(litros) AS DECIMAL(10,0)) liters,
CAST(count(*) AS DECIMAL(10,0)) `lines`,
st.name state,
sub2.iptd futureIpt,
sub2.state futureState,
t.clientFk,
t.warehouseFk,
ts.alertLevel,
t.shipped,
sub2.shipped futureShipped,
t.workerFk,
st.code stateCode,
sub2.code futureStateCode,
st.classColor,
sub2.classColor futureClassColor
FROM vn.saleVolume sv
JOIN vn.sale s ON s.id = sv.saleFk
JOIN vn.item i ON i.id = s.itemFk
JOIN vn.ticket t ON t.id = sv.ticketFk
JOIN vn.address a ON a.id = t.addressFk
JOIN vn.province p ON p.id = a.provinceFk
JOIN vn.country c ON c.id = p.countryFk
JOIN vn.ticketState ts ON ts.ticketFk = t.id
JOIN vn.state st ON st.id = ts.stateFk
JOIN vn.alertLevel al ON al.id = ts.alertLevel
LEFT JOIN vn.ticketParking tp ON tp.ticketFk = t.id
LEFT JOIN (
SELECT *
FROM (
SELECT
t.addressFk,
t.id,
t.shipped,
st.name state,
st.code,
st.classColor,
GROUP_CONCAT(DISTINCT i.itemPackingTypeFk ORDER BY i.itemPackingTypeFk) iptd
FROM vn.ticket t
JOIN vn.ticketState ts ON ts.ticketFk = t.id
JOIN vn.state st ON st.id = ts.stateFk
JOIN vn.sale s ON s.ticketFk = t.id
JOIN vn.item i ON i.id = s.itemFk
WHERE t.shipped BETWEEN vFutureDated
AND util.dayend(vFutureDated)
AND t.warehouseFk = vWarehouseFk
GROUP BY t.id
) sub
GROUP BY sub.addressFk
) sub2 ON sub2.addressFk = t.addressFk AND t.id != sub2.id
WHERE t.shipped BETWEEN vOriginDated AND util.dayend(vOriginDated)
AND t.warehouseFk = vWarehouseFk
AND al.code = 'FREE'
AND tp.ticketFk IS NULL
GROUP BY sv.ticketFk
HAVING futureId;
END$$
DELIMITER ;

View File

@ -0,0 +1 @@
ALTER TABLE `vn`.`ticket` MODIFY COLUMN warehouseFk smallint(6) unsigned DEFAULT NULL NULL;

View File

View File

@ -595,6 +595,8 @@ export default {
moreMenuUpdateDiscount: 'vn-item[name="discount"]',
moreMenuRecalculatePrice: 'vn-item[name="calculatePrice"]',
moreMenuRefund: 'vn-item[name="refund"]',
refundWithWarehouse: 'vn-item[name="refundWithWarehouse"]',
refundWithoutWarehouse: 'vn-item[name="refundWithoutWarehouse"]',
moreMenuUpdateDiscountInput: 'vn-input-number[ng-model="$ctrl.edit.discount"] input',
transferQuantityInput: '.vn-popover.shown vn-table > div > vn-tbody > vn-tr > vn-td-editable > span > text',
transferQuantityCell: '.vn-popover.shown vn-table > div > vn-tbody > vn-tr > vn-td-editable',

View File

@ -220,14 +220,25 @@ describe('Ticket Edit sale path', () => {
it('should log in as salesAssistant and navigate to ticket sales', async() => {
await page.loginAndModule('salesAssistant', 'ticket');
await page.accessToSearchResult('16');
await page.accessToSearchResult('17');
await page.accessToSection('ticket.card.sale');
});
it('should select the third sale and create a refund', async() => {
it('should select the first sale and create a refund with warehouse', async() => {
await page.waitToClick(selectors.ticketSales.firstSaleCheckbox);
await page.waitToClick(selectors.ticketSales.moreMenu);
await page.waitToClick(selectors.ticketSales.moreMenuRefund);
await page.waitToClick(selectors.ticketSales.refundWithWarehouse);
await page.waitForSnackbar();
await page.waitForState('ticket.card.sale');
});
it('should select the first sale and create a refund without warehouse', async() => {
await page.accessToSearchResult('18');
await page.waitToClick(selectors.ticketSales.firstSaleCheckbox);
await page.waitToClick(selectors.ticketSales.moreMenu);
await page.waitToClick(selectors.ticketSales.moreMenuRefund);
await page.waitToClick(selectors.ticketSales.refundWithoutWarehouse);
await page.waitForSnackbar();
await page.waitForState('ticket.card.sale');
});
@ -246,7 +257,6 @@ describe('Ticket Edit sale path', () => {
it('should select the third sale and create a claim of it', async() => {
await page.accessToSearchResult('16');
await page.accessToSection('ticket.card.sale');
await page.waitToClick(selectors.ticketSales.firstSaleCheckbox);
await page.waitToClick(selectors.ticketSales.thirdSaleCheckbox);
await page.waitToClick(selectors.ticketSales.moreMenu);
await page.waitToClick(selectors.ticketSales.moreMenuCreateClaim);

View File

@ -49,7 +49,11 @@ describe('Claim summary path', () => {
});
it(`should click on the first sale ID making the item descriptor visible`, async() => {
await page.waitToClick(selectors.claimSummary.firstSaleItemId);
const firstItem = selectors.claimSummary.firstSaleItemId;
await page.evaluate(selectors => {
document.querySelector(selectors).scrollIntoView();
}, firstItem);
await page.click(firstItem);
await page.waitImgLoad(selectors.claimSummary.firstSaleDescriptorImage);
const visible = await page.isVisible(selectors.claimSummary.itemDescriptorPopover);

View File

@ -53,12 +53,10 @@
</span>
</div>
</div>
<div
class="model vn-pb-sm vn-px-sm"
title="{{::log.changedModelValue}}"
<div class="model vn-pb-sm vn-px-sm"
ng-if="::log.changedModelId || log.changedModelValue">
<span class="model-id" ng-if="::log.changedModelId">#{{::log.changedModelId}}</span>
<span class="model-value">{{::log.changedModelValue}}</span>
<span class="model-value" title="{{::log.changedModelValue}}">{{::log.changedModelValue}}</span>
</div>
<div class="changes vn-pa-sm"
ng-class="{expanded: log.expand}"
@ -75,16 +73,16 @@
<span class="json-field" title="{{::prop.name}}">
{{::prop.nameI18n}}:
</span>
<vn-json-value value="::$ctrl.mainVal(prop, log.action)"></vn-json-value><span ng-if="::!$last">,</span>
<vn-json-value value="::prop.val.val"></vn-json-value><span ng-if="::!$last">,</span>
</span>
<div ng-if="log.expand" class="expanded-json">
<div ng-repeat="prop in ::log.props">
<span class="json-field" title="{{::prop.name}}">
{{::prop.nameI18n}}:
</span>
<vn-json-value value="::$ctrl.mainVal(prop, log.action)"></vn-json-value>
<vn-log-value val="::prop.val"></vn-log-value>
<span ng-if="::log.action == 'update'">
<vn-json-value value="::prop.old"></vn-json-value>
<vn-log-value val="::prop.old"></vn-log-value>
</span>
</div>
</div>
@ -163,12 +161,17 @@
data="$ctrl.models"
class="changed-model">
</vn-autocomplete>
<!-- FIXME: Cannot use LIKE with JSON columns
<vn-textfield
label="Changes"
ng-model="filter.changes">
<append>
<vn-icon
icon="info_outline"
vn-tooltip="Search by changes"
pointer>
</vn-icon>
</append>
</vn-textfield>
-->
<vn-vertical>
<vn-check
label="Creates"

View File

@ -64,29 +64,47 @@ export default class Controller extends Section {
set logs(value) {
this._logs = value;
if (!value) return;
const empty = {};
const validations = window.validations;
const castJsonValue = this.castJsonValue;
for (const log of value) {
const oldValues = log.oldInstance || empty;
const newValues = log.newInstance || empty;
const notDelete = log.action != 'delete';
const olds = (notDelete ? log.oldInstance : null) || empty;
const vals = (notDelete ? log.newInstance : log.oldInstance) || empty;
const locale = validations[log.changedModel]?.locale || empty;
log.changedModelI18n = firstUpper(locale.name) || log.changedModel;
let props = Object.keys(oldValues).concat(Object.keys(newValues));
let props = Object.keys(olds).concat(Object.keys(vals));
props = [...new Set(props)];
log.props = [];
for (const prop of props) {
if (prop.endsWith('$')) continue;
log.props.push({
name: prop,
nameI18n: firstUpper(locale.columns?.[prop]) || prop,
old: this.castJsonValue(oldValues[prop]),
new: this.castJsonValue(newValues[prop])
old: getVal(olds, prop),
val: getVal(vals, prop)
});
}
log.props.sort(
(a, b) => a.nameI18n.localeCompare(b.nameI18n));
}
function getVal(vals, prop) {
let val, id;
const showProp = `${prop}$`;
if (vals[showProp] != null) {
val = vals[showProp];
id = vals[prop];
} else
val = vals[prop];
return {val: castJsonValue(val), id};
}
}
get models() {
@ -113,10 +131,6 @@ export default class Controller extends Section {
: value;
}
mainVal(prop, action) {
return action == 'delete' ? prop.old : prop.new;
}
relativeDate(dateVal) {
if (dateVal == null) return '';
const date = new Date(dateVal);
@ -150,14 +164,16 @@ export default class Controller extends Section {
if (value == null || value == '') return null;
switch (prop) {
case 'search':
if (/^[0-9]+$/.test(value))
return {changedModelId: value};
else
return {changedModelValue: {like: `%${value}%`}};
const or = [];
if (/^[\w_-]+$/.test(value))
or.push({changedModelId: value});
if (!/^[0-9]+$/.test(value))
or.push({changedModelValue: {like: `%${value}%`}});
return or.length ? {or} : null;
case 'changes':
return {or: [
{oldInstance: {like: `%${value}%`}},
{newInstance: {like: `%${value}%`}},
{oldJson: {like: `%${value}%`}},
{newJson: {like: `%${value}%`}},
{description: {like: `%${value}%`}}
]};
case 'who':
@ -238,3 +254,12 @@ ngModule.vnComponent('vnLog', {
url: '@'
}
});
ngModule.component('vnLogValue', {
template:
'<vn-json-value value="::$ctrl.val.val"></vn-json-value>' +
'<span ng-if="::$ctrl.val.id" class="id-value"> #{{::$ctrl.val.id}}</span>',
bindings: {
val: '<?',
}
});

View File

@ -2,6 +2,9 @@ Date: Fecha
Concept: Concepto
Search: Buscar
Search by id or concept: Buscar por identificador o concepto
Search by changes: |
Buscar por cambios realizados. Los atributos deben buscarse por su nombre
interno, para obtenerlo situar el cursor sobre el nombre.
Entity: Entidad
Action: Acción
Author: Autor

View File

@ -105,6 +105,7 @@ vn-log {
& > .model-id {
color: $color-font-secondary;
font-size: .9rem;
float: right;
}
}
}
@ -144,3 +145,7 @@ vn-log {
}
}
}
vn-log-value > .id-value {
font-size: .9rem;
color: $color-font-secondary;
}

View File

@ -1,4 +1,61 @@
{
"name": "Log",
"base": "VnModel"
"name": "Log",
"base": "VnModel",
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"oldJson": {
"type": "String",
"mysql": {"columnName": "oldInstance"}
},
"newJson": {
"type": "String",
"mysql": {"columnName": "newInstance"}
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "string"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -1,4 +1,5 @@
name: subrole
columns:
id: id
role: rol
inheritsFrom: inherits

View File

@ -1,4 +1,5 @@
name: subrol
columns:
id: id
role: rol
inheritsFrom: hereda

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "account.roleLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "account.userLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "claimLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -0,0 +1,10 @@
name: SMS
columns:
id: id
senderFk: sender
sender: sender number
destination: destination
message: message
statusCode: status code
status: status
created: created

View File

@ -0,0 +1,10 @@
name: SMS
columns:
id: id
senderFk: remitente
sender: número remitente
destination: destinatario
message: mensaje
statusCode: código estado
status: estado
created: creado

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "clientLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "entryLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "string"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -5,57 +5,5 @@
"mysql": {
"table": "invoiceInLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "string"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": [
"creationDate DESC",
"id DESC"
]
}
}

View File

@ -2,11 +2,19 @@ module.exports = Self => {
Self.remoteMethod('refund', {
description: 'Create refund tickets with sales and services if provided',
accessType: 'WRITE',
accepts: [{
arg: 'ref',
type: 'string',
description: 'The invoice reference'
}],
accepts: [
{
arg: 'ref',
type: 'string',
description: 'The invoice reference',
required: true
},
{
arg: 'withWarehouse',
type: 'boolean',
required: true
}
],
returns: {
type: ['number'],
root: true
@ -17,7 +25,7 @@ module.exports = Self => {
}
});
Self.refund = async(ref, options) => {
Self.refund = async(ref, withWarehouse, options) => {
const models = Self.app.models;
const myOptions = {};
let tx;
@ -35,7 +43,7 @@ module.exports = Self => {
const tickets = await models.Ticket.find(filter, myOptions);
const ticketsIds = tickets.map(ticket => ticket.id);
const refundedTickets = await models.Ticket.refund(ticketsIds, myOptions);
const refundedTickets = await models.Ticket.refund(ticketsIds, withWarehouse, myOptions);
if (tx) await tx.commit();

View File

@ -3,6 +3,7 @@ const LoopBackContext = require('loopback-context');
describe('InvoiceOut refund()', () => {
const userId = 5;
const withWarehouse = true;
const activeCtx = {
accessToken: {userId: userId},
};
@ -15,7 +16,7 @@ describe('InvoiceOut refund()', () => {
const options = {transaction: tx};
try {
const result = await models.InvoiceOut.refund('T1111111', options);
const result = await models.InvoiceOut.refund('T1111111', withWarehouse, options);
expect(result).toBeDefined();

View File

@ -76,14 +76,27 @@
translate>
Show CITES letter
</vn-item>
<vn-item
ng-click="refundConfirmation.show()"
name="refundInvoice"
<vn-item class="dropdown"
vn-click-stop="refundMenu.show($event, 'left')"
vn-tooltip="Create a single ticket with all the content of the current invoice"
vn-acl="invoicing, claimManager, salesAssistant"
vn-acl-action="remove"
translate>
Refund
Refund...
<vn-menu vn-id="refundMenu">
<vn-list>
<vn-item
ng-click="$ctrl.refundInvoiceOut(true)"
translate>
with warehouse
</vn-item>
<vn-item
ng-click="$ctrl.refundInvoiceOut(false)"
translate>
without warehouse
</vn-item>
</vn-list>
</vn-menu>
</vn-item>
</vn-list>
</vn-menu>
@ -97,11 +110,6 @@
on-accept="$ctrl.bookInvoiceOut()"
question="Are you sure you want to book this invoice?">
</vn-confirm>
<vn-confirm
vn-id="refundConfirmation"
on-accept="$ctrl.refundInvoiceOut()"
question="Are you sure you want to refund this invoice?">
</vn-confirm>
<vn-client-descriptor-popover
vn-id="clientDescriptor">
</vn-client-descriptor-popover>

View File

@ -114,9 +114,9 @@ class Controller extends Section {
});
}
refundInvoiceOut() {
refundInvoiceOut(withWarehouse) {
const query = 'InvoiceOuts/refund';
const params = {ref: this.invoiceOut.ref};
const params = {ref: this.invoiceOut.ref, withWarehouse: withWarehouse};
this.$http.post(query, params).then(res => {
const refundTicket = res.data;
this.vnApp.showSuccess(this.$t('The following refund ticket have been created', {

View File

@ -20,3 +20,4 @@ Regenerate PDF invoice: Regenerar PDF factura
The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado
The email can't be empty: El correo no puede estar vacío
The following refund tickets have been created: "Se han creado los siguientes tickets de abono: {{ticketIds}}"
Refund...: Abono...

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "itemLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -23,7 +23,7 @@ class Controller extends SearchPanel {
addValue() {
this.filter.values.push({});
setTimeout(() => this.popover.relocate());
setTimeout(() => this.parentPopover.relocate());
}
changeTag() {
@ -36,7 +36,7 @@ ngModule.vnComponent('vnOrderCatalogSearchPanel', {
controller: Controller,
bindings: {
onSubmit: '&?',
popover: '<?',
parentPopover: '<?',
resultTags: '<?'
}
});

View File

@ -104,7 +104,7 @@
on-close="$ctrl.onPopoverClose()">
<vn-order-catalog-search-panel
on-submit="$ctrl.onPanelSubmit($filter)"
popover="popover"
parent-popover="popover"
result-tags="$ctrl.resultTags">
</vn-order-catalog-search-panel>
</vn-popover>

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "routeLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -1,58 +1,9 @@
{
"name": "ShelvingLog",
"name": "ShelvingLog",
"base": "Log",
"options": {
"mysql": {
"table": "shelvingLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
"options": {
"mysql": {
"table": "shelvingLog"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "supplierLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "string"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -11,6 +11,11 @@ module.exports = Self => {
{
arg: 'servicesIds',
type: ['number']
},
{
arg: 'withWarehouse',
type: 'boolean',
required: true
}
],
returns: {
@ -23,7 +28,7 @@ module.exports = Self => {
}
});
Self.refund = async(salesIds, servicesIds, options) => {
Self.refund = async(salesIds, servicesIds, withWarehouse, options) => {
const models = Self.app.models;
const myOptions = {};
let tx;
@ -65,7 +70,7 @@ module.exports = Self => {
const now = Date.vnNew();
const [firstTicketId] = ticketsIds;
const refundTicket = await createTicketRefund(firstTicketId, now, refundAgencyMode, refoundZoneId, myOptions);
const refundTicket = await createTicketRefund(firstTicketId, now, refundAgencyMode, refoundZoneId, withWarehouse, myOptions);
for (const sale of sales) {
const createdSale = await models.Sale.create({
@ -113,7 +118,7 @@ module.exports = Self => {
}
};
async function createTicketRefund(ticketId, now, refundAgencyMode, refoundZoneId, myOptions) {
async function createTicketRefund(ticketId, now, refundAgencyMode, refoundZoneId, withWarehouse, myOptions) {
const models = Self.app.models;
const filter = {include: {relation: 'address'}};
@ -125,7 +130,7 @@ module.exports = Self => {
addressFk: ticket.address().id,
agencyModeFk: refundAgencyMode.id,
nickname: ticket.address().nickname,
warehouseFk: ticket.warehouseFk,
warehouseFk: withWarehouse ? ticket.warehouseFk : null,
companyFk: ticket.companyFk,
landed: now,
zoneFk: refoundZoneId

View File

@ -6,8 +6,8 @@ describe('Sale refund()', () => {
const activeCtx = {
accessToken: {userId: userId},
};
const servicesIds = [3];
const withWarehouse = true;
beforeEach(() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
@ -22,7 +22,7 @@ describe('Sale refund()', () => {
try {
const options = {transaction: tx};
const refundedTicket = await models.Sale.refund(salesIds, servicesIds, options);
const refundedTicket = await models.Sale.refund(salesIds, servicesIds, withWarehouse, options);
expect(refundedTicket).toBeDefined();
@ -40,7 +40,7 @@ describe('Sale refund()', () => {
try {
const options = {transaction: tx};
const ticket = await models.Sale.refund(salesIds, servicesIds, options);
const ticket = await models.Sale.refund(salesIds, servicesIds, withWarehouse, options);
const refundedTicket = await models.Ticket.findOne({
where: {

View File

@ -7,6 +7,11 @@ module.exports = Self => {
arg: 'ticketsIds',
type: ['number'],
required: true
},
{
arg: 'withWarehouse',
type: 'boolean',
required: true
}
],
returns: {
@ -19,7 +24,7 @@ module.exports = Self => {
}
});
Self.refund = async(ticketsIds, options) => {
Self.refund = async(ticketsIds, withWarehouse, options) => {
const models = Self.app.models;
const myOptions = {};
let tx;
@ -41,7 +46,7 @@ module.exports = Self => {
const services = await models.TicketService.find(filter, myOptions);
const servicesIds = services.map(service => service.id);
const refundedTickets = await models.Sale.refund(salesIds, servicesIds, myOptions);
const refundedTickets = await models.Sale.refund(salesIds, servicesIds, withWarehouse, myOptions);
if (tx) await tx.commit();

View File

@ -1,58 +1,9 @@
{
"name": "TicketLog",
"name": "TicketLog",
"base": "Log",
"options": {
"mysql": {
"table": "ticketLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
"options": {
"mysql": {
"table": "ticketLog"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -150,7 +150,7 @@
<td>{{::ticket.futureIpt | dashIfEmpty}}</td>
<td>
<span
class="chip {{ticket.classColor}}">
class="chip {{ticket.futureClassColor}}">
{{::ticket.futureState | dashIfEmpty}}
</span>
</td>

View File

@ -102,13 +102,6 @@ export default class Controller extends Section {
return checkedLines;
}
stateColor(state) {
if (state === 'OK')
return 'success';
else if (state === 'Libre')
return 'notice';
}
dateRange(value) {
const minHour = new Date(value);
minHour.setHours(0, 0, 0, 0);

View File

@ -61,24 +61,6 @@ describe('Component vnTicketAdvance', () => {
});
});
describe('stateColor()', () => {
it('should return success to the OK tickets', () => {
const ok = controller.stateColor(controller.$.model.data[0].state);
const notOk = controller.stateColor(controller.$.model.data[1].state);
expect(ok).toEqual('success');
expect(notOk).not.toEqual('success');
});
it('should return success to the FREE tickets', () => {
const notFree = controller.stateColor(controller.$.model.data[0].state);
const free = controller.stateColor(controller.$.model.data[1].state);
expect(free).toEqual('notice');
expect(notFree).not.toEqual('notice');
});
});
describe('dateRange()', () => {
it('should return two dates with the hours at the start and end of the given date', () => {
const now = Date.vnNew();

View File

@ -141,12 +141,27 @@
translate>
Recalculate components
</vn-item>
<vn-item
ng-click="refundAllConfirmation.show()"
<vn-item class="dropdown"
vn-click-stop="refundMenu.show($event, 'left')"
vn-acl="invoicing, claimManager, salesAssistant"
vn-acl-action="remove"
vn-tooltip="Create a single ticket with all the content of the current ticket"
translate>
Refund all
Refund all...
<vn-menu vn-id="refundMenu">
<vn-list>
<vn-item
ng-click="$ctrl.refund(true)"
translate>
with warehouse
</vn-item>
<vn-item
ng-click="$ctrl.refund(false)"
translate>
without warehouse
</vn-item>
</vn-list>
</vn-menu>
</vn-item>
</vn-list>
</vn-menu>
@ -319,14 +334,6 @@
message="Recalculate components">
</vn-confirm>
<!-- Refund all confirmation dialog -->
<vn-confirm
vn-id="refundAllConfirmation"
on-accept="$ctrl.refund()"
question="Are you sure you want to refund all?"
message="Refund all">
</vn-confirm>
<!-- Client balance popup-->
<vn-client-balance-create
vn-id="balance-create"

View File

@ -297,16 +297,17 @@ class Controller extends Section {
.then(() => this.vnApp.showSuccess(this.$t('Data saved!')));
}
async refund() {
const params = {ticketsIds: [this.id]};
refund(withWarehouse) {
const params = {ticketsIds: [this.id], withWarehouse: withWarehouse};
const query = 'Tickets/refund';
return this.$http.post(query, params).then(res => {
const refundTicket = res.data;
this.vnApp.showSuccess(this.$t('The following refund ticket have been created', {
ticketId: refundTicket.id
}));
this.$state.go('ticket.card.sale', {id: refundTicket.id});
});
return this.$http.post(query, params)
.then(res => {
const refundTicket = res.data;
this.vnApp.showSuccess(this.$t('The following refund ticket have been created', {
ticketId: refundTicket.id
}));
this.$state.go('ticket.card.sale', {id: refundTicket.id});
});
}
onSmsSend(sms) {

View File

@ -10,7 +10,9 @@ Send CSV: Enviar CSV
Send CSV Delivery Note: Enviar albarán en CSV
Send PDF Delivery Note: Enviar albarán en PDF
Show Proforma: Ver proforma
Refund all: Abonar todo
Refund all...: Abonar todo...
with warehouse: con almacén
without warehouse: sin almacén
Invoice sent: Factura enviada
The following refund ticket have been created: "Se ha creado siguiente ticket de abono: {{ticketId}}"
Transfer client: Transferir cliente
@ -18,3 +20,4 @@ SMS Notify changes: SMS Notificar cambios
PDF sent!: ¡PDF enviado!
Already exist signed delivery note: Ya existe albarán de entrega firmado
Are you sure you want to replace this delivery note?: ¿Seguro que quieres reemplazar este albarán de entrega?
Create a single ticket with all the content of the current ticket: Crea un ticket único con todo el contenido del ticket actual

View File

@ -158,7 +158,7 @@
<td>{{::ticket.futureIpt | dashIfEmpty}}</td>
<td>
<span
class="chip {{ticket.classColor}}">
class="chip {{ticket.futureClassColor}}">
{{::ticket.futureState}}
</span>
</td>

View File

@ -529,11 +529,28 @@
ng-if="$ctrl.isEditable && $ctrl.hasReserves()">
Unmark as reserved
</vn-item>
<vn-item translate
name="refund"
ng-click="$ctrl.createRefund()"
vn-acl="invoicing, claimManager, salesAssistant"
vn-acl-action="remove">
Refund
</vn-item>
<vn-item class="dropdown"
name="refund"
vn-click-stop="refundMenu.show($event, 'left')"
vn-acl="invoicing, claimManager, salesAssistant"
vn-acl-action="remove"
translate>
Refund...
<vn-menu vn-id="refundMenu">
<vn-list>
<vn-item
name="refundWithWarehouse"
ng-click="$ctrl.createRefund(true)"
translate>
with warehouse
</vn-item>
<vn-item
name="refundWithoutWarehouse"
ng-click="$ctrl.createRefund(false)"
translate>
without warehouse
</vn-item>
</vn-list>
</vn-menu>
</vn-item>
</vn-menu>

View File

@ -520,13 +520,12 @@ class Controller extends Section {
});
}
createRefund() {
createRefund(withWarehouse) {
const sales = this.selectedValidSales();
if (!sales) return;
const salesIds = sales.map(sale => sale.id);
const params = {salesIds: salesIds};
const params = {salesIds: salesIds, withWarehouse: withWarehouse};
const query = 'Sales/refund';
this.$http.post(query, params).then(res => {
const refundTicket = res.data;

View File

@ -36,7 +36,7 @@ Warehouse: Almacen
Agency: Agencia
Shipped: F. envio
Packaging: Encajado
Refund: Abono
Refund...: Abono...
Promotion mana: Maná promoción
Claim mana: Maná reclamación
History: Historial

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "travelLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "string"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -1,55 +1,14 @@
{
"name": "DeviceProductionLog",
"base": "Log",
"options": {
"mysql": {
"table": "deviceProductionLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"deviceProduction": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"created": {
"type": "date"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"changedModel": {
"type": "string"
},
"changedModelId": {
"type": "number"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "userFk"
"name": "DeviceProductionLog",
"base": "Log",
"options": {
"mysql": {
"table": "deviceProductionLog"
}
},
"scope": {
"order": ["created DESC", "id DESC"]
"properties": {
"deviceProduction": {
"type": "number"
}
}
}

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "workerLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "string"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "zoneLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "string"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "salix-back",
"version": "23.22.01",
"version": "23.24.01",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "salix-back",
"version": "23.22.01",
"version": "23.24.01",
"license": "GPL-3.0",
"dependencies": {
"axios": "^1.2.2",

View File

@ -1,6 +1,6 @@
{
"name": "salix-back",
"version": "23.24.01",
"version": "23.26.01",
"author": "Verdnatura Levante SL",
"description": "Salix backend",
"license": "GPL-3.0",

View File

@ -9,7 +9,7 @@ let mode = env == 'development' ? env : 'production';
let baseConfig = {
entry: {salix: 'salix'},
mode: mode,
mode,
output: {
path: path.join(__dirname, 'dist'),
publicPath: '/'