Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2603-travel_create_autocomplete_date

This commit is contained in:
Jorge Padawan 2021-02-10 16:59:29 +01:00
commit bb02a0de6b
103 changed files with 1902 additions and 405 deletions

View File

@ -35,3 +35,4 @@ rules:
space-in-parens: ["error", "never"]
jasmine/no-focused-tests: 0
jasmine/prefer-toHaveBeenCalledWith: 0
arrow-spacing: ["error", { "before": true, "after": true }]

8
Jenkinsfile vendored
View File

@ -49,7 +49,7 @@ pipeline {
NODE_ENV = ""
}
steps {
nodejs('node-lts') {
nodejs('node-v12') {
sh 'npm install --no-audit --prefer-offline'
sh 'gulp install --ci'
}
@ -66,14 +66,14 @@ pipeline {
parallel {
stage('Frontend') {
steps {
nodejs('node-lts') {
nodejs('node-v12') {
sh 'jest --ci --reporters=default --reporters=jest-junit --maxWorkers=2'
}
}
}
// stage('Backend') {
// steps {
// nodejs('node-lts') {
// nodejs('node-v12') {
// sh 'gulp launchBackTest --ci'
// }
// }
@ -89,7 +89,7 @@ pipeline {
CREDS = credentials('docker-registry')
}
steps {
nodejs('node-lts') {
nodejs('node-v12') {
sh 'gulp build'
}

View File

@ -0,0 +1,39 @@
module.exports = Self => {
Self.remoteMethodCtx('notifyIssues', {
description: 'Notifies new urgent issues',
accessType: 'READ',
returns: {
type: 'Object',
root: true
},
http: {
path: `/notifyIssues`,
verb: 'GET'
}
});
Self.notifyIssues = async ctx => {
const models = Self.app.models;
const $t = ctx.req.__; // $translate
const [urgentIssue] = await Self.rawSql(`
SELECT * FROM managedesktop.vn_workOrderInmediata LIMIT 1
`);
if (!urgentIssue) return;
const message = $t(`There's a new urgent ticket`, {
title: urgentIssue.title,
issueId: urgentIssue.workOrderId
});
const department = await models.Department.findOne({
where: {code: 'IT'}
});
const channelName = department && department.chatName;
if (channelName)
return Self.send(ctx, `#${channelName}`, `@all ➔ ${message}`);
return;
};
};

View File

@ -0,0 +1,38 @@
const app = require('vn-loopback/server/server');
describe('Chat notifyIssue()', () => {
const ctx = {req: {accessToken: {userId: 1}}};
ctx.req.__ = value => {
return value;
};
const chatModel = app.models.Chat;
const departmentId = 31;
it(`should not call to the send() method and neither return a response`, async() => {
spyOn(chatModel, 'send').and.callThrough();
spyOn(chatModel, 'rawSql').and.returnValue([]);
const response = await chatModel.notifyIssues(ctx);
expect(chatModel.send).not.toHaveBeenCalled();
expect(response).toBeUndefined();
});
it(`should return a response calling the send() method`, async() => {
spyOn(chatModel, 'send').and.callThrough();
spyOn(chatModel, 'rawSql').and.returnValue([{title: 'Issue title'}]);
const department = await app.models.Department.findById(departmentId);
let orgChatName = department.chatName;
await department.updateAttribute('chatName', 'IT');
const response = await chatModel.notifyIssues(ctx);
expect(response.statusCode).toEqual(200);
expect(response.message).toEqual('Fake notification sent');
expect(chatModel.send).toHaveBeenCalledWith(ctx, '#IT', `@all ➔ There's a new urgent ticket`);
// restores
await department.updateAttribute('chatName', orgChatName);
});
});

View File

@ -1,6 +1,6 @@
const app = require('vn-loopback/server/server');
describe('chat send()', () => {
describe('Chat send()', () => {
it('should return a "Fake notification sent" as response', async() => {
let ctx = {req: {accessToken: {userId: 1}}};
let response = await app.models.Chat.send(ctx, '@salesPerson', 'I changed something');

View File

@ -1,6 +1,6 @@
const app = require('vn-loopback/server/server');
describe('chat sendCheckingPresence()', () => {
describe('Chat sendCheckingPresence()', () => {
const today = new Date();
today.setHours(6, 0);
const ctx = {req: {accessToken: {userId: 1}}};

View File

@ -11,25 +11,36 @@ module.exports = Self => {
type: 'Number',
description: 'The document id',
http: {source: 'path'}
}, {
},
{
arg: 'warehouseId',
type: 'Number',
description: 'The warehouse id'
}, {
},
{
arg: 'companyId',
type: 'Number',
description: 'The company id'
}, {
},
{
arg: 'dmsTypeId',
type: 'Number',
description: 'The dms type id'
}, {
},
{
arg: 'reference',
type: 'String'
}, {
},
{
arg: 'description',
type: 'String'
}, {
},
{
arg: 'hasFile',
type: 'Boolean',
description: 'True if has an attached file'
},
{
arg: 'hasFileAttached',
type: 'Boolean',
description: 'True if has an attached file'
@ -70,7 +81,8 @@ module.exports = Self => {
companyFk: args.companyId,
warehouseFk: args.warehouseId,
reference: args.reference,
description: args.description
description: args.description,
hasFile: args.hasFile
}, myOptions);
if (args.hasFileAttached)

View File

@ -57,6 +57,9 @@ module.exports = Self => {
const entity = await models[imageCollection.model].findById(id, {
fields: ['id', imageCollection.property]
});
if (!entity) return false;
const image = await models.Image.findOne({where: {
collectionFk: collection,
name: entity[imageCollection.property]}

View File

@ -35,6 +35,15 @@
"DmsContainer": {
"dataSource": "dmsStorage"
},
"Dms": {
"dataSource": "vn"
},
"DmsType": {
"dataSource": "vn"
},
"EmailUser": {
"dataSource": "vn"
},
"Image": {
"dataSource": "vn"
},
@ -53,38 +62,32 @@
"Province": {
"dataSource": "vn"
},
"TempContainer": {
"dataSource": "tempStorage"
},
"UserConfig": {
"dataSource": "vn"
},
"Warehouse": {
"dataSource": "vn"
},
"SageWithholding": {
"dataSource": "vn"
},
"UserConfigView": {
"dataSource": "vn"
},
"EmailUser": {
"dataSource": "vn"
},
"Dms": {
"dataSource": "vn"
},
"DmsType": {
"dataSource": "vn"
},
"Town": {
"Payment": {
"dataSource": "vn"
},
"Postcode": {
"dataSource": "vn"
},
"SageWithholding": {
"dataSource": "vn"
},
"TempContainer": {
"dataSource": "tempStorage"
},
"Town": {
"dataSource": "vn"
},
"UserConfig": {
"dataSource": "vn"
},
"UserConfigView": {
"dataSource": "vn"
},
"UserLog": {
"dataSource": "vn"
},
"Warehouse": {
"dataSource": "vn"
}
}

View File

@ -8,17 +8,20 @@
},
"properties": {
"id": {
"type": "Number",
"type": "number",
"id": true,
"description": "Identifier"
},
"description": {
"type": "String",
"type": "string",
"required": true
},
"receiptDescription": {
"type": "String",
"type": "string",
"required": true
},
"code": {
"type": "string"
}
},
"acls": [{

View File

@ -1,4 +1,5 @@
module.exports = Self => {
require('../methods/chat/send')(Self);
require('../methods/chat/sendCheckingPresence')(Self);
require('../methods/chat/notifyIssues')(Self);
};

View File

@ -18,6 +18,9 @@
},
"expired": {
"type": "date"
},
"isOfficial": {
"type": "boolean"
}
},

View File

@ -30,7 +30,7 @@
"type": "string"
},
"hardCopyNumber": {
"type": "Number"
"type": "number"
},
"hasFile": {
"type": "boolean"

View File

@ -26,7 +26,7 @@
"nRefs": {
"type": "Number",
"required": true,
"default": 0
"default": 1
}
},
"relations": {

64
back/models/payment.json Normal file
View File

@ -0,0 +1,64 @@
{
"name": "Payment",
"base": "VnModel",
"options": {
"mysql": {
"table": "payment"
}
},
"properties": {
"id": {
"type": "number",
"id": true,
"description": "Identifier"
},
"received": {
"type": "date"
},
"amount": {
"type": "number"
},
"divisa": {
"type": "number"
},
"concept": {
"type": "string"
},
"created": {
"type": "date"
},
"isConciliated": {
"type": "boolean"
},
"dueDated": {
"type": "date"
}
},
"relations": {
"supplier": {
"type": "belongsTo",
"model": "Supplier",
"foreignKey": "supplierFk"
},
"currency": {
"type": "belongsTo",
"model": "Currency",
"foreignKey": "currencyFk"
},
"bank": {
"type": "belongsTo",
"model": "Bank",
"foreignKey": "bankFk"
},
"payMethod": {
"type": "belongsTo",
"model": "PayMethodFk",
"foreignKey": "payMethodFk"
},
"company": {
"type": "belongsTo",
"model": "Company",
"foreignKey": "companyFk"
}
}
}

View File

@ -1,2 +1 @@
INSERT INTO salix.ACL (model, property, accessType, permission, principalType, principalId) VALUES ('Image', '*', 'WRITE', 'ALLOW', 'ROLE', 'employee');
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('PayDem', '*', 'READ', 'ALLOW', 'ROLE', 'employee');

View File

@ -1,3 +1,5 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('FixedPrice', '*', '*', 'ALLOW', 'ROLE', 'buyer');
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('PayDem', '*', 'READ', 'ALLOW', 'ROLE', 'employee');
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('Client', 'createReceipt', '*', 'ALLOW', 'ROLE', 'administrative');
INSERT INTO salix.ACL (model, property, accessType, permission, principalType, principalId)
VALUES ('PrintServerQueue', '*', 'WRITE', 'ALLOW', 'ROLE', 'employee');
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('FixedPrice', '*', '*', 'ALLOW', 'ROLE', 'buyer');

View File

@ -0,0 +1,82 @@
DROP PROCEDURE IF EXISTS vn.ledger_doCompensation;
DELIMITER $$
$$
CREATE DEFINER=`root`@`%` PROCEDURE `vn`.`ledger_doCompensation`(vDated DATE, vCompensationAccount VARCHAR(10) , vBankFk VARCHAR(10), vConcept VARCHAR(255), vAmount DECIMAL(10,2), vCompanyFk INT, vOriginalAccount VARCHAR(10))
BEGIN
/**
* Compensa un pago o un recibo insertando en contabilidad
*
* @param vDated fecha en la cual se anota
* @param vCompensationAccount cuenta contable contra la que se compensa
* @param vBankFk banco de la compensacion
* @param vConcept descripcion
* @param vAmount cantidad que se compensa
* @param vCompany empresa
* @param vOriginalAccount cuenta contable desde la cual se compensa
*
*/
DECLARE vNewBookEntry INT;
DECLARE vIsClientCompensation INT;
DECLARE vClientFk INT;
DECLARE vSupplierFk INT;
DECLARE vIsOriginalAClient BOOL;
DECLARE vPayMethodCompensation INT;
CALL ledger_next(vNewBookEntry);
SELECT COUNT(id) INTO vIsOriginalAClient FROM client WHERE accountingAccount LIKE vOriginalAccount COLLATE utf8_general_ci;
SELECT id, COUNT(id) INTO vClientFk, vIsClientCompensation
FROM client
WHERE accountingAccount LIKE vCompensationAccount COLLATE utf8_general_ci;
SET @vAmount1:= 0.0;
SET @vAmount2:= 0.0;
INSERT INTO XDiario (ASIEN, FECHA, SUBCTA, CONTRA, CONCEPTO, EURODEBE, EUROHABER, empresa_id)
VALUES ( vNewBookEntry,
vDated,
vOriginalAccount,
vCompensationAccount,
vConcept,
@vAmount1:= IF(
(vIsOriginalAClient OR NOT vIsOriginalAClient)
AND vAmount > 0,
0,
ABS(vAmount)
),
@vAmount2:= IF(@vAmount1,
0,
ABS(vAmount)
),
vCompanyFk
),
( vNewBookEntry,
vDated,
vCompensationAccount,
vOriginalAccount,
vConcept,
@vAmount2,
@vAmount1,
vCompanyFk);
IF vIsClientCompensation THEN
IF vIsOriginalAClient THEN
SET vAmount = -vAmount;
END IF;
INSERT INTO receipt(invoiceFk, amountPaid, payed, bankFk, companyFk, clientFk, isConciliate)
VALUES (vConcept, vAmount, vDated, vBankFk, vCompanyFk, vClientFk, TRUE);
ELSE
IF NOT vIsOriginalAClient THEN
SET vAmount = -vAmount;
END IF;
SELECT id INTO vSupplierFk FROM supplier WHERE `account` LIKE vCompensationAccount COLLATE utf8_general_ci;
SELECT id INTO vPayMethodCompensation FROM payMethod WHERE `code` = 'compensation';
INSERT INTO payment (received, dueDated, supplierFk, amount, bankFk, payMethodFk, concept, companyFk, isConciliated)
VALUES(vDated, vDated, vSupplierFk, vAmount, vBankFk, vPayMethodCompensation, vConcept, vCompanyFk, TRUE);
END IF;
END$$
DELIMITER ;

View File

@ -0,0 +1,17 @@
DROP TRIGGER IF EXISTS vn.receipt_beforInsert;
DELIMITER $$
$$
CREATE TRIGGER receipt_beforInsert
BEFORE INSERT
ON receipt FOR EACH ROW
BEGIN
SELECT isAutoConciliated INTO @isAutoConciliated
FROM accounting a
JOIN accountingType at2 ON at2.id = a.accountingTypeFk
WHERE a.id =NEW.bankFk;
SET NEW.isConciliate = @isAutoConciliated;
END
$$
DELIMITER ;

View File

@ -0,0 +1,4 @@
ALTER TABLE `vn`.`department`
ADD code VARCHAR(45) NULL AFTER id;
UPDATE `vn`.`department` t SET t.code = 'IT', t.chatName = 'informatica-cau' WHERE t.id = 31;

View File

@ -0,0 +1,137 @@
DROP PROCEDURE `vn`.`item_getBalance`;
DELIMITER $$
$$
CREATE DEFINER=`root`@`%` PROCEDURE `vn`.`item_getBalance`(IN vItemId INT, IN vWarehouse INT)
BEGIN
DECLARE vDateInventory DATETIME;
DECLARE vCurdate DATE DEFAULT CURDATE();
DECLARE vDayEnd DATETIME DEFAULT util.dayEnd(vCurdate);
SELECT inventoried INTO vDateInventory FROM config;
SET @a = 0;
SET @currentLineFk = 0;
SET @shipped = '';
SELECT DATE(@shipped:= shipped) shipped,
alertLevel,
stateName,
origin,
reference,
clientFk,
name,
`in`,
`out`,
@a := @a + IFNULL(`in`,0) - IFNULL(`out`,0) as balance,
@currentLineFk := IF (@shipped < CURDATE()
OR (@shipped = CURDATE() AND (isPicked OR alertLevel >= 2)),
lineFk,@currentLineFk) lastPreparedLineFk,
isTicket,
lineFk,
isPicked,
clientType
FROM
( SELECT tr.landed AS shipped,
b.quantity AS `in`,
NULL AS `out`,
al.alertLevel AS alertLevel,
st.name AS stateName,
s.name AS name,
e.ref AS reference,
e.id AS origin,
s.id AS clientFk,
IF(al.alertLevel = 3, TRUE, FALSE) isPicked,
FALSE AS isTicket,
b.id lineFk,
NULL `order`,
NULL AS clientType
FROM buy b
JOIN entry e ON e.id = b.entryFk
JOIN travel tr ON tr.id = e.travelFk
JOIN supplier s ON s.id = e.supplierFk
JOIN alertLevel al ON al.alertLevel =
CASE
WHEN tr.shipped < CURDATE() THEN 3
WHEN tr.shipped = CURDATE() AND tr.isReceived = TRUE THEN 3
ELSE 0
END
JOIN state st ON st.code = al.code
WHERE tr.landed >= vDateInventory
AND vWarehouse = tr.warehouseInFk
AND b.itemFk = vItemId
AND e.isInventory = FALSE
AND e.isRaid = FALSE
UNION ALL
SELECT tr.shipped,
NULL as `in`,
b.quantity AS `out`,
al.alertLevel AS alertLevel,
st.name AS stateName,
s.name AS name,
e.ref AS reference,
e.id AS origin,
s.id AS clientFk,
IF(al.alertLevel = 3, TRUE, FALSE) isPicked,
FALSE AS isTicket,
b.id,
NULL `order`,
NULL AS clientType
FROM buy b
JOIN entry e ON e.id = b.entryFk
JOIN travel tr ON tr.id = e.travelFk
JOIN warehouse w ON w.id = tr.warehouseOutFk
JOIN supplier s ON s.id = e.supplierFk
JOIN alertLevel al ON al.alertLevel =
CASE
WHEN tr.shipped < CURDATE() THEN 3
WHEN tr.shipped = CURDATE() AND tr.isReceived = TRUE THEN 3
ELSE 0
END
JOIN state st ON st.code = al.code
WHERE tr.shipped >= vDateInventory
AND vWarehouse =tr.warehouseOutFk
AND s.id <> 4
AND b.itemFk = vItemId
AND e.isInventory = FALSE
AND w.isFeedStock = FALSE
AND e.isRaid = FALSE
UNION ALL
SELECT DATE(t.shipped),
NULL as `in`,
s.quantity AS `out`,
al.alertLevel AS alertLevel,
st.name AS stateName,
t.nickname AS name,
t.refFk AS reference,
t.id AS origin,
t.clientFk,
stk.id AS isPicked,
TRUE AS isTicket,
s.id,
st.`order`,
ct.code AS clientType
FROM sale s
JOIN ticket t ON t.id = s.ticketFk
LEFT JOIN ticketState ts ON ts.ticket = t.id
LEFT JOIN state st ON st.code = ts.code
JOIN client c ON c.id = t.clientFk
JOIN clientType ct ON ct.id = c.clientTypeFk
JOIN alertLevel al ON al.alertLevel =
CASE
WHEN t.shipped < curdate() THEN 3
WHEN t.shipped > util.dayEnd(curdate()) THEN 0
ELSE IFNULL(ts.alertLevel, 0)
END
LEFT JOIN state stPrep ON stPrep.`code` = 'PREPARED'
LEFT JOIN saleTracking stk ON stk.saleFk = s.id AND stk.stateFk = stPrep.id
WHERE t.shipped >= vDateInventory
AND s.itemFk = vItemId
AND vWarehouse =t.warehouseFk
ORDER BY shipped, alertLevel DESC, isTicket, `order` DESC, isPicked DESC, `in` DESC, `out` DESC
) AS itemDiary;
END$$
DELIMITER ;

View File

@ -0,0 +1,2 @@
ALTER TABLE `vn`.`supplier` ADD COLUMN `workerFk` INT(11) NULL DEFAULT NULL COMMENT 'Responsible for approving invoices' AFTER `isTrucker`;
ALTER TABLE `vn`.`supplier` ADD CONSTRAINT `supplier_workerFk` FOREIGN KEY (`workerFk`) REFERENCES `vn`.`worker` (`id`) ON UPDATE CASCADE;

View File

@ -151,20 +151,19 @@ INSERT INTO `vn`.`shelving` (`code`, `parkingFk`, `isPrinted`, `priority`, `park
INSERT INTO `vn`.`accountingType`(`id`, `description`, `receiptDescription`,`code`)
VALUES
(1, 'CC y Polizas de crédito', NULL, NULL),
(2, 'Caja registradora', NULL, NULL),
(3, 'Tarjeta de credito', NULL, NULL),
(4, 'Lineas de financiacion', NULL, NULL),
(5, 'Otros productos', NULL, NULL),
(6, 'Prestamos', NULL, NULL),
(2, 'Cash', 'Cash', 'cash'),
(3, 'Credit card', 'Credit Card', 'creditCard'),
(4, 'Finalcial lines', NULL, NULL),
(5, 'Other products', NULL, NULL),
(6, 'Loans', NULL, NULL),
(7, 'Leasing', NULL, NULL),
(8, 'Compensaciones', NULL, NULL),
(9, 'Cash', 'Cash', NULL),
(10, 'Card', 'Pay on receipt', NULL);
(8, 'Compensations', 'Compensations', 'compensation');
INSERT INTO `vn`.`bank`(`id`, `bank`, `account`, `cash`, `entityFk`, `isActive`, `currencyFk`)
VALUES
(1, 'Pay on receipt', '0000000000', 10, 0, 1, 1),
(2, 'Cash', '1111111111', 9, 0, 1, 1);
(1, 'Pay on receipt', '5720000001', 3, 0, 1, 1),
(2, 'Cash', '5700000001', 2, 0, 1, 1),
(3, 'Compensation', '4000000000', 8, 0, 1, 1);
INSERT INTO `vn`.`deliveryMethod`(`id`, `code`, `description`)
VALUES
@ -212,13 +211,13 @@ UPDATE `vn`.`agencyMode` SET `web` = 1;
UPDATE `vn`.`agencyMode` SET `code` = 'refund' WHERE `id` = 23;
INSERT INTO `vn`.`payMethod`(`id`, `name`, `graceDays`, `outstandingDebt`, `ibanRequired`)
INSERT INTO `vn`.`payMethod`(`id`,`code`, `name`, `graceDays`, `outstandingDebt`, `ibanRequired`)
VALUES
(1, 'PayMethod one', 0, 001, 0),
(2, 'PayMethod two', 10, 001, 0),
(3, 'PayMethod three', 0, 001, 0),
(4, 'PayMethod with IBAN', 0, 001, 1),
(5, 'PayMethod five', 10, 001, 0);
(1, NULL, 'PayMethod one', 0, 001, 0),
(2, NULL, 'PayMethod two', 10, 001, 0),
(3, 'compensation', 'PayMethod three', 0, 001, 0),
(4, NULL, 'PayMethod with IBAN', 0, 001, 1),
(5, NULL, 'PayMethod five', 10, 001, 0);
INSERT INTO `vn`.`payDem`(`id`, `payDem`)
VALUES
@ -271,17 +270,17 @@ INSERT INTO `vn`.`contactChannel`(`id`, `name`)
INSERT INTO `vn`.`client`(`id`,`name`,`fi`,`socialName`,`contact`,`street`,`city`,`postcode`,`phone`,`mobile`,`fax`,`isRelevant`,`email`,`iban`,`dueDay`,`accountingAccount`,`isEqualizated`,`provinceFk`,`hasToInvoice`,`credit`,`countryFk`,`isActive`,`gestdocFk`,`quality`,`payMethodFk`,`created`,`isToBeMailed`,`contactChannelFk`,`hasSepaVnl`,`hasCoreVnl`,`hasCoreVnh`,`riskCalculated`,`clientTypeFk`,`mailAddress`,`cplusTerIdNifFk`,`hasToInvoiceByAddress`,`isTaxDataChecked`,`isFreezed`,`creditInsurance`,`isCreatedAsServed`,`hasInvoiceSimplified`,`salesPersonFk`,`isVies`,`eypbc`)
VALUES
(101, 'Bruce Wayne', '84612325V', 'Batman', 'Alfred', '1007 Mountain Drive, Gotham', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'BruceWayne@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 18, 0, 1),
(102, 'Petter Parker', '87945234L', 'Spider man', 'Aunt May', '20 Ingram Street', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'PetterParker@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 18, 0, 1),
(103, 'Clark Kent', '06815934E', 'Super man', 'lois lane', '344 Clinton Street', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'ClarkKent@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 0, 19, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 18, 0, 1),
(104, 'Tony Stark', '06089160W', 'Iron man', 'Pepper Potts', '10880 Malibu Point', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'TonyStark@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 18, 0, 1),
(102, 'Petter Parker', '87945234L', 'Spider man', 'Aunt May', '20 Ingram Street, Queens, USA', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'PetterParker@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 18, 0, 1),
(103, 'Clark Kent', '06815934E', 'Super man', 'lois lane', '344 Clinton Street, Apartament 3-D', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'ClarkKent@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 0, 19, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 18, 0, 1),
(104, 'Tony Stark', '06089160W', 'Iron man', 'Pepper Potts', '10880 Malibu Point, 90265', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'TonyStark@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 18, 0, 1),
(105, 'Max Eisenhardt', '251628698', 'Magneto', 'Rogue', 'Unknown Whereabouts', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'MaxEisenhardt@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 8, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 1, NULL, 0, 0, 18, 0, 1),
(106, 'DavidCharlesHaller', '53136686Q', 'Legion', 'Charles Xavier', 'Evil hideout', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'DavidCharlesHaller@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 0, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 19, 0, 1),
(107, 'Hank Pym', '09854837G', 'Ant man', 'Hawk', 'Anthill', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'HankPym@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 0, 0, NULL, 0, 0, 19, 0, 1),
(106, 'DavidCharlesHaller', '53136686Q', 'Legion', 'Charles Xavier', 'City of New York, New York, USA', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'DavidCharlesHaller@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 0, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 19, 0, 1),
(107, 'Hank Pym', '09854837G', 'Ant man', 'Hawk', 'Anthill, San Francisco, California', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'HankPym@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 0, 0, NULL, 0, 0, 19, 0, 1),
(108, 'Charles Xavier', '22641921P', 'Professor X', 'Beast', '3800 Victory Pkwy, Cincinnati, OH 45207, USA', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'CharlesXavier@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 1, NULL, 0, 0, 19, 0, 1),
(109, 'Bruce Banner', '16104829E', 'Hulk', 'Black widow', 'Somewhere in New York', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'BruceBanner@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 0, 0, NULL, 0, 0, 19, 0, 1),
(110, 'Jessica Jones', '58282869H', 'Jessica Jones', 'Luke Cage', 'NYCC 2015 Poster', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'JessicaJones@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 0, 0, NULL, 0, 0, NULL, 0, 1),
(111, 'Missing', NULL, 'Missing man', 'Anton', 'The space', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, NULL, NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 4, NULL, 1, 0, 1, 0, NULL, 1, 0, NULL, 0, 1),
(112, 'Trash', NULL, 'Garbage man', 'Unknown name', 'New York city', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, NULL, NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 4, NULL, 1, 0, 1, 0, NULL, 1, 0, NULL, 0, 1);
(111, 'Missing', NULL, 'Missing man', 'Anton', 'The space, Universe far away', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, NULL, NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 4, NULL, 1, 0, 1, 0, NULL, 1, 0, NULL, 0, 1),
(112, 'Trash', NULL, 'Garbage man', 'Unknown name', 'New York city, Underground', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, NULL, NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 4, NULL, 1, 0, 1, 0, NULL, 1, 0, NULL, 0, 1);
INSERT INTO `vn`.`client`(`id`, `name`, `fi`, `socialName`, `contact`, `street`, `city`, `postcode`, `isRelevant`, `email`, `iban`,`dueDay`,`accountingAccount`, `isEqualizated`, `provinceFk`, `hasToInvoice`, `credit`, `countryFk`, `isActive`, `gestdocFk`, `quality`, `payMethodFk`,`created`, `isTaxDataChecked`)
SELECT id, name, CONCAT(RPAD(CONCAT(id,9),8,id),'A'), CONCAT(name, 'Social'), CONCAT(name, 'Contact'), CONCAT(name, 'Street'), 'SILLA', 46460, 1, CONCAT(name,'@mydomain.com'), NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1,NULL, 10, 5, CURDATE(), 1
@ -1234,11 +1233,11 @@ INSERT INTO `vn`.`annualAverageInvoiced`(`clientFk`, `invoiced`)
(104, 500),
(105, 5000);
INSERT INTO `vn`.`supplier`(`id`, `name`, `nickname`,`account`,`countryFk`,`nif`,`isFarmer`,`commission`, `created`, `isActive`, `street`, `city`, `provinceFk`, `postCode`, `payMethodFk`, `payDemFk`, `payDay`, `taxTypeSageFk`, `withholdingSageFk`, `transactionTypeSageFk`)
INSERT INTO `vn`.`supplier`(`id`, `name`, `nickname`,`account`,`countryFk`,`nif`,`isFarmer`,`commission`, `created`, `isActive`, `street`, `city`, `provinceFk`, `postCode`, `payMethodFk`, `payDemFk`, `payDay`, `taxTypeSageFk`, `withholdingSageFk`, `transactionTypeSageFk`, `workerFk`)
VALUES
(1, 'Plants SL', 'Plants nick', 4100000001, 1, '06089160W', 0, 0, CURDATE(), 1, 'supplier address 1', 'PONTEVEDRA', 1, 15214, 1, 1, 15, 4, 1, 1),
(2, 'Farmer King', 'The farmer', 4000020002, 1, '87945234L', 1, 0, CURDATE(), 1, 'supplier address 2', 'SILLA', 2, 43022, 1, 2, 10, 93, 2, 8),
(442, 'Verdnatura Levante SL', 'Verdnatura', 5115000442, 1, '06815934E', 0, 0, CURDATE(), 1, 'supplier address 3', 'SILLA', 1, 43022, 1, 2, 15, 6, 9, 3);
(1, 'Plants SL', 'Plants nick', 4100000001, 1, '06089160W', 0, 0, CURDATE(), 1, 'supplier address 1', 'PONTEVEDRA', 1, 15214, 1, 1, 15, 4, 1, 1, 18),
(2, 'Farmer King', 'The farmer', 4000020002, 1, '87945234L', 1, 0, CURDATE(), 1, 'supplier address 2', 'SILLA', 2, 43022, 1, 2, 10, 93, 2, 8, 18),
(442, 'Verdnatura Levante SL', 'Verdnatura', 5115000442, 1, '06815934E', 0, 0, CURDATE(), 1, 'supplier address 3', 'SILLA', 1, 43022, 1, 2, 15, 6, 9, 3, 18);
INSERT INTO `vn`.`supplierContact`(`id`, `supplierFk`, `phone`, `mobile`, `email`, `observation`, `name`)
VALUES

View File

@ -186,8 +186,11 @@ export default {
clientBalance: {
company: 'vn-client-balance-index vn-autocomplete[ng-model="$ctrl.companyId"]',
newPaymentButton: `vn-float-button`,
newPaymentBank: '.vn-dialog.shown vn-autocomplete[ng-model="$ctrl.receipt.bankFk"]',
newPaymentAmount: '.vn-dialog.shown vn-input-number[ng-model="$ctrl.receipt.amountPaid"]',
newPaymentBank: '.vn-dialog.shown vn-autocomplete[ng-model="$ctrl.bankFk"]',
newPaymentAmount: '.vn-dialog.shown vn-input-number[ng-model="$ctrl.amountPaid"]',
newDescription: 'vn-textfield[ng-model="$ctrl.receipt.description"]',
deliveredAmount: '.vn-dialog vn-input-number[ng-model="$ctrl.deliveredAmount"]',
refundAmount: '.vn-dialog vn-input-number[ng-model="$ctrl.amountToReturn"]',
saveButton: '.vn-dialog.shown [response="accept"]',
firstLineBalance: 'vn-client-balance-index vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(8)',
firstLineReference: 'vn-client-balance-index vn-tbody > vn-tr:nth-child(1) > vn-td-editable',
@ -313,7 +316,7 @@ export default {
fourthRelevancy: 'vn-item-tags vn-horizontal:nth-child(4) [ng-model="itemTag.priority"]',
fourthRemoveTagButton: 'vn-item-tags vn-horizontal:nth-child(4) vn-icon-button[icon="delete"]',
fifthTag: 'vn-item-tags vn-horizontal:nth-child(5) > vn-autocomplete[ng-model="itemTag.tagFk"]',
fifthValue: 'vn-item-tags vn-horizontal:nth-child(5) vn-textfield[ng-model="itemTag.value"]',
fifthValue: 'vn-item-tags vn-horizontal:nth-child(5) vn-autocomplete[ng-model="itemTag.value"]',
fifthRelevancy: 'vn-item-tags vn-horizontal:nth-child(5) vn-input-number[ng-model="itemTag.priority"]',
sixthTag: 'vn-item-tags vn-horizontal:nth-child(6) > vn-autocomplete[ng-model="itemTag.tagFk"]',
sixthValue: 'vn-item-tags vn-horizontal:nth-child(6) vn-textfield[ng-model="itemTag.value"]',
@ -389,18 +392,22 @@ export default {
descriptorTicketId: 'vn-ticket-descriptor > vn-descriptor-content > div > div.body > div.top > div'
},
ticketsIndex: {
anySearchResult: 'vn-ticket-index vn-tbody > a',
openAdvancedSearchButton: 'vn-searchbar .append vn-icon[icon="arrow_drop_down"]',
advancedSearchInvoiceOut: 'vn-ticket-search-panel vn-textfield[ng-model="filter.refFk"]',
advancedSearchDaysOnward: 'vn-ticket-search-panel vn-input-number[ng-model="filter.scopeDays"]',
advancedSearchClient: 'vn-ticket-search-panel vn-textfield[ng-model="filter.clientFk"]',
advancedSearchButton: 'vn-ticket-search-panel button[type=submit]',
newTicketButton: 'vn-ticket-index a[ui-sref="ticket.create"]',
searchResult: 'vn-ticket-index vn-card > vn-table > div > vn-tbody > a.vn-tr',
firstTicketCheckbox: 'vn-ticket-index vn-tbody > a:nth-child(1) > vn-td:nth-child(1) > vn-check',
secondTicketCheckbox: 'vn-ticket-index vn-tbody > a:nth-child(2) > vn-td:nth-child(1) > vn-check',
thirdTicketCheckbox: 'vn-ticket-index vn-tbody > a:nth-child(3) > vn-td:nth-child(1) > vn-check',
sixthTicketCheckbox: 'vn-ticket-index vn-tbody > a:nth-child(6) > vn-td:nth-child(1) > vn-check',
payoutButton: 'vn-ticket-index vn-button[icon="icon-recovery"]',
payoutCompany: '.vn-dialog vn-autocomplete[ng-model="$ctrl.receipt.companyFk"]',
payoutBank: '.vn-dialog vn-autocomplete[ng-model="$ctrl.receipt.bankFk"]',
payoutBank: '.vn-dialog vn-autocomplete[ng-model="$ctrl.bankFk"]',
payoutDescription: 'vn-textfield[ng-model="$ctrl.receipt.description"]',
submitPayout: '.vn-dialog button[response="accept"]',
searchWeeklyResult: 'vn-ticket-weekly-index vn-table vn-tbody > vn-tr',
searchResultDate: 'vn-ticket-summary [label=Landed] span',

View File

@ -38,7 +38,7 @@ describe('Client balance path', () => {
expect(message.text).toContain('Data saved!');
});
it('should click the new payment button', async() => {
it('should reload the section', async() => {
await page.closePopup();
await page.reloadSection('client.card.balance.index');
});
@ -46,7 +46,8 @@ describe('Client balance path', () => {
it('should create a new payment that clears the debt', async() => {
await page.closePopup();
await page.waitToClick(selectors.clientBalance.newPaymentButton);
await page.autocompleteSearch(selectors.clientBalance.newPaymentBank, 'Pay on receipt');
await page.autocompleteSearch(selectors.clientBalance.newPaymentBank, 'Cash');
await page.write(selectors.clientBalance.newDescription, 'Description');
await page.waitToClick(selectors.clientBalance.saveButton);
const message = await page.waitForSnackbar();
@ -78,16 +79,24 @@ describe('Client balance path', () => {
expect(firstBalanceLine).toContain('0.00');
});
it('should create a new payment that sets the balance to positive value', async() => {
it('should create a new payment and check the cash comparison works correctly', async() => {
const amountPaid = '100';
const cashHanded = '500';
const expectedRefund = '400';
await page.waitToClick(selectors.clientBalance.newPaymentButton);
await page.overwrite(selectors.clientBalance.newPaymentAmount, '100');
await page.write(selectors.clientBalance.newPaymentAmount, amountPaid);
await page.write(selectors.clientBalance.newDescription, 'Payment');
await page.write(selectors.clientBalance.deliveredAmount, cashHanded);
const refund = await page.waitToGetProperty(selectors.clientBalance.refundAmount, 'value');
await page.waitToClick(selectors.clientBalance.saveButton);
const message = await page.waitForSnackbar();
expect(refund).toEqual(expectedRefund);
expect(message.text).toContain('Data saved!');
});
it('should check balance is now -100', async() => {
it('should check the balance value is now -100', async() => {
let result = await page
.waitToGetProperty(selectors.clientBalance.firstLineBalance, 'innerText');
@ -96,7 +105,9 @@ describe('Client balance path', () => {
it('should create a new payment that sets the balance back to the original negative value', async() => {
await page.waitToClick(selectors.clientBalance.newPaymentButton);
await page.autocompleteSearch(selectors.clientBalance.newPaymentBank, 'Pay on receipt');
await page.overwrite(selectors.clientBalance.newPaymentAmount, '-150');
await page.write(selectors.clientBalance.newDescription, 'Description');
await page.waitToClick(selectors.clientBalance.saveButton);
const message = await page.waitForSnackbar();

View File

@ -16,7 +16,7 @@ describe('Item create tags path', () => {
await browser.close();
});
it(`should create a new tag and delete a former one`, async() => {
it('should create a new tag and delete a former one', async() => {
await page.waitToClick(selectors.itemTags.fourthRemoveTagButton);
await page.waitToClick(selectors.itemTags.addItemTagButton);
await page.autocompleteSearch(selectors.itemTags.seventhTag, 'Ancho de la base');
@ -29,7 +29,7 @@ describe('Item create tags path', () => {
expect(message.text).toContain('Data saved!');
});
it(`should confirm the fourth row data is the expected one`, async() => {
it('should confirm the fourth row data is the expected one', async() => {
await page.reloadSection('item.card.tags');
await page.waitForSelector('vn-item-tags');
let result = await page.waitToGetProperty(selectors.itemTags.fourthTag, 'value');
@ -47,7 +47,7 @@ describe('Item create tags path', () => {
expect(result).toEqual('4');
});
it(`should confirm the fifth row data is the expected one`, async() => {
it('should confirm the fifth row data is the expected one', async() => {
let tag = await page
.waitToGetProperty(selectors.itemTags.fifthTag, 'value');
@ -62,7 +62,7 @@ describe('Item create tags path', () => {
expect(relevancy).toEqual('5');
});
it(`should confirm the sixth row data is the expected one`, async() => {
it('should confirm the sixth row data is the expected one', async() => {
let tag = await page
.waitToGetProperty(selectors.itemTags.sixthTag, 'value');

View File

@ -22,23 +22,31 @@ describe('Ticket index payout path', () => {
it('should check the second ticket from a client and 1 of another', async() => {
await page.waitToClick(selectors.globalItems.searchButton);
await page.waitToClick(selectors.ticketsIndex.secondTicketCheckbox);
await page.waitToClick(selectors.ticketsIndex.sixthTicketCheckbox);
await page.waitToClick(selectors.ticketsIndex.thirdTicketCheckbox);
await page.waitToClick(selectors.ticketsIndex.payoutButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('You cannot make a payment on account from multiple clients');
});
it('should uncheck the sixth ticket result and check the third which is from the same client then open the payout form', async() => {
await page.waitToClick(selectors.ticketsIndex.sixthTicketCheckbox);
await page.waitToClick(selectors.ticketsIndex.thirdTicketCheckbox);
it('should search for tickets of the same client then open the payout form', async() => {
await page.waitToClick(selectors.ticketsIndex.openAdvancedSearchButton);
await page.write(selectors.ticketsIndex.advancedSearchClient, '101');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketsIndex.anySearchResult, 6);
await page.waitToClick(selectors.ticketsIndex.firstTicketCheckbox);
await page.waitToClick(selectors.ticketsIndex.secondTicketCheckbox);
await page.waitToClick(selectors.ticketsIndex.payoutButton);
await page.waitForSelector(selectors.ticketsIndex.payoutCompany);
});
it('should fill the company and bank to perform a payout', async() => {
await page.autocompleteSearch(selectors.ticketsIndex.payoutCompany, 'VNL');
await page.autocompleteSearch(selectors.ticketsIndex.payoutBank, 'cash');
await page.write(selectors.clientBalance.newPaymentAmount, '100');
await page.write(selectors.ticketsIndex.payoutDescription, 'Payment');
await page.waitToClick(selectors.ticketsIndex.submitPayout);
const message = await page.waitForSnackbar();

View File

@ -20,7 +20,7 @@ export default class Autocomplete extends Field {
constructor($element, $, $compile, $transclude) {
super($element, $, $compile);
this.$transclude = $transclude;
this.$compile = $compile;
this._selection = null;
this.input = this.element.querySelector('input');
}
@ -149,6 +149,9 @@ export default class Autocomplete extends Field {
where: where
};
if (this.include)
filter.include = this.include;
let json = encodeURIComponent(JSON.stringify(filter));
this.$http.get(`${this.url}?filter=${json}`).then(
json => this.onSelectionRequest(json.data),
@ -182,8 +185,14 @@ export default class Autocomplete extends Field {
} else {
display = this._selection[this.showField];
if (hasTemplate) {
let template = this.$transclude(() => {}, null, 'tplItem').text();
display = this.$interpolate(template)(this._selection);
let template = this.$transclude(() => {}, null, 'tplItem');
const element = template[0];
const description = element.querySelector('.text-secondary');
if (description) description.remove();
const displayElement = angular.element(element);
const displayText = displayElement.text();
display = this.$interpolate(displayText)(this._selection);
}
}
}

View File

@ -2,7 +2,13 @@ import ngModule from '../../module';
import Section from '../section';
import './style.scss';
export default class Summary extends Section {}
export default class Summary extends Section {
listEmails(email) {
if (!email) return;
const emailList = email.split(',');
return emailList.join(', ');
}
}
ngModule.vnComponent('vnSummary', {
controller: Summary

View File

@ -0,0 +1,30 @@
import './index.js';
describe('Salix', () => {
describe('Component vnSummary', () => {
let controller;
beforeEach(ngModule('salix'));
beforeEach(inject($componentController => {
const $element = angular.element('<vn-summary></vn-summary>');
controller = $componentController('vnSummary', {$element});
}));
describe('listEmails()', () => {
it('should do nothing when receives no arguments', () => {
const emailList = controller.listEmails();
expect(emailList).toBeUndefined();
});
it('should format the receives emails to be separated with comma', () => {
const expectedResult = 'captainmarvel@marvel.com, ironman@marvel.com';
const emailList = controller.listEmails('captainmarvel@marvel.com,ironman@marvel.com');
expect(emailList).toEqual(expectedResult);
});
});
});
});

View File

@ -85,8 +85,11 @@
"You need to fill sage information before you check verified data": "You need to fill sage information before you check verified data",
"The social name cannot be empty": "The social name cannot be empty",
"The nif cannot be empty": "The nif cannot be empty",
"Amount cannot be zero": "Amount cannot be zero",
"Company has to be official": "Company has to be official",
"A travel with this data already exists": "A travel with this data already exists",
"The observation type can't be repeated": "The observation type can't be repeated",
"New ticket request has been created with price": "New ticket request has been created '{{description}}' for day <strong>{{shipped}}</strong>, with a quantity of <strong>{{quantity}}</strong> and a price of <strong>{{price}} €</strong>",
"New ticket request has been created": "New ticket request has been created '{{description}}' for day <strong>{{shipped}}</strong>, with a quantity of <strong>{{quantity}}</strong>"
"New ticket request has been created with price": "New ticket request has been created *'{{description}}'* for day *{{shipped}}*, with a quantity of *{{quantity}}* and a price of *{{price}} €*",
"New ticket request has been created": "New ticket request has been created *'{{description}}'* for day *{{shipped}}*, with a quantity of *{{quantity}}*",
"There's a new urgent ticket": "There's a new urgent ticket: [{{title}}](https://cau.verdnatura.es/WorkOrder.do?woMode=viewWO&woID={{issueId}})"
}

View File

@ -161,6 +161,8 @@
"The nif cannot be empty": "El NIF no puede quedar en blanco",
"You need to fill sage information before you check verified data": "Debes rellenar la información de sage antes de marcar datos comprobados",
"ASSIGN_ZONE_FIRST": "Asigna una zona primero",
"Amount cannot be zero": "El importe no puede ser cero",
"Company has to be official": "Empresa inválida",
"You can not select this payment method without a registered bankery account": "No se puede utilizar este método de pago si no has registrado una cuenta bancaria",
"You can't upload images on the test environment": "No puedes subir imágenes en el entorno de pruebas",
"The selected ticket is not suitable for this route": "El ticket seleccionado no es apto para esta ruta",

25
loopback/util/date.js Normal file
View File

@ -0,0 +1,25 @@
/**
* Transforms a UTC date to string without datetime.
*
* @param {date} date Date to format
* @return {String} Formatted date string
*/
function toString(date) {
date = new Date(date);
let day = date.getDate();
let month = date.getMonth() + 1;
let year = date.getFullYear();
if (day < 10)
day = `0${day}`;
if (month < 10)
month = `0${month}`;
return `${day}-${month}-${year}`;
}
module.exports = {
toString: toString
};

View File

@ -86,16 +86,15 @@ class Controller extends Section {
});
}
showDeleteConfirm(index) {
this.sale = this.salesClaimed[index];
showDeleteConfirm($index) {
this.claimedIndex = $index;
this.$.confirm.show();
}
deleteClaimedSale() {
let query = `ClaimBeginnings/${this.sale.id}`;
this.$http.delete(query).then(() => {
this.$.model.remove(this.claimedIndex);
this.$.model.save().then(() => {
this.vnApp.showSuccess(this.$t('Data saved!'));
this.$.model.remove(this.sale);
this.calculateTotals();
});
}

View File

@ -73,13 +73,16 @@ describe('claim', () => {
describe('deleteClaimedSale()', () => {
it('should make a delete and call refresh and showSuccess', () => {
controller.sale = {id: 1};
const claimedIndex = 1;
controller.claimedIndex = claimedIndex;
jest.spyOn(controller.$.model, 'remove');
jest.spyOn(controller.$.model, 'save');
jest.spyOn(controller.vnApp, 'showSuccess');
$httpBackend.expectDELETE(`ClaimBeginnings/1`).respond('ok');
controller.deleteClaimedSale();
$httpBackend.flush();
controller.deleteClaimedSale();
expect(controller.$.model.remove).toHaveBeenCalledWith(claimedIndex);
expect(controller.$.model.save).toHaveBeenCalledWith();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,145 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = function(Self) {
Self.remoteMethodCtx('createReceipt', {
description: 'Creates receipt and its compensation if necessary',
accepts: [{
arg: 'clientFk',
type: 'number',
description: 'The client id',
http: {source: 'path'}
},
{
arg: 'payed',
type: 'Date',
required: true
},
{
arg: 'companyFk',
type: 'number',
required: true
},
{
arg: 'bankFk',
type: 'number',
required: true
},
{
arg: 'amountPaid',
type: 'number',
required: true
},
{
arg: 'description',
type: 'string',
required: true
},
{
arg: 'compensationAccount',
type: 'any'
}],
returns: {
root: true,
type: 'Object'
},
http: {
verb: 'post',
path: '/:clientFk/createReceipt'
}
});
Self.createReceipt = async ctx => {
const models = Self.app.models;
const args = ctx.args;
const tx = await models.Address.beginTransaction({});
try {
const options = {transaction: tx};
delete args.ctx; // Remove unwanted properties
const newReceipt = await models.Receipt.create(args, options);
const clientOriginal = await models.Client.findById(args.clientFk);
const bank = await models.Bank.findById(args.bankFk);
const accountingType = await models.AccountingType.findById(bank.accountingTypeFk);
if (accountingType.code == 'compensation') {
if (!args.compensationAccount)
throw new UserError('Compensation account is empty');
const supplierCompensation = await models.Supplier.findOne({
where: {
account: args.compensationAccount
}
});
let clientCompensation = {};
if (!supplierCompensation) {
clientCompensation = await models.Client.findOne({
where: {
accountingAccount: args.compensationAccount
}
});
}
if (!supplierCompensation && !clientCompensation)
throw new UserError('Invalid account');
await Self.rawSql(
`CALL vn.ledger_doCompensation(?, ?, ?, ?, ?, ?, ?)`,
[
Date(),
args.compensationAccount,
args.bankFk,
accountingType.receiptDescription + args.compensationAccount,
args.amountPaid,
args.companyFk,
clientOriginal.accountingAccount
],
options);
} else {
const description = `${clientOriginal.id} : ${clientOriginal.socialName} - ${accountingType.receiptDescription}`;
const [xdiarioNew] = await Self.rawSql(
`SELECT xdiario_new(?, CURDATE(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ledger;`,
[
null,
bank.account,
clientOriginal.accountingAccount,
description,
args.amountPaid,
0,
0,
'',
'',
null,
null,
false,
args.companyFk
],
options);
await Self.rawSql(
`SELECT xdiario_new(?, CURDATE(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`,
[
xdiarioNew.ledger,
clientOriginal.accountingAccount,
bank.account,
description,
0,
args.amountPaid,
0,
'',
'',
null,
null,
false,
args.companyFk
],
options);
}
await tx.commit();
return newReceipt;
} catch (e) {
await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,171 @@
const app = require('vn-loopback/server/server');
describe('Client createReceipt', () => {
const clientFk = 108;
const payed = Date();
const companyFk = 442;
const amountPaid = 12.50;
const description = 'Receipt description';
it('should create a new receipt', async() => {
const bankFk = 1;
let ctx = {
args: {
clientFk: clientFk,
payed: payed,
companyFk: companyFk,
bankFk: bankFk,
amountPaid: amountPaid,
description: description
}
};
const receipt = await app.models.Client.createReceipt(ctx);
delete ctx.args.payed;
const till = await app.models.Till.findOne({
where: {
bankFk: bankFk,
in: amountPaid,
number: clientFk
}
});
expect(receipt).toEqual(jasmine.objectContaining(ctx.args));
// restores
await receipt.destroy();
await till.destroy();
});
it('should throw Compensation account is empty', async() => {
const bankFk = 3;
let ctx = {
args: {
clientFk: clientFk,
payed: payed,
companyFk: companyFk,
bankFk: bankFk,
amountPaid: amountPaid,
description: description
}
};
try {
await app.models.Client.createReceipt(ctx);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toEqual('Compensation account is empty');
});
it('should throw Invalid account if compensationAccount does not belongs to a client nor a supplier', async() => {
let error;
const bankFk = 3;
const ctx = {
args: {
clientFk: clientFk,
payed: payed,
companyFk: companyFk,
bankFk: bankFk,
amountPaid: amountPaid,
description: description,
compensationAccount: 'non existing account'
}
};
try {
await app.models.Client.createReceipt(ctx);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toEqual('Invalid account');
});
it('should create a new receipt with a compensation for a client', async() => {
const bankFk = 3;
const ctx = {
args: {
clientFk: clientFk,
payed: payed,
companyFk: companyFk,
bankFk: bankFk,
amountPaid: amountPaid,
description: description,
compensationAccount: '4300000001'
}
};
const receipt = await app.models.Client.createReceipt(ctx);
const receiptCompensated = await app.models.Receipt.findOne({
where: {
clientFk: 1,
bankFk: ctx.args.bankFk
}
});
const till = await app.models.Till.findOne({
where: {
bankFk: bankFk,
in: amountPaid,
number: clientFk
}
});
delete ctx.args.payed;
expect(receipt).toEqual(jasmine.objectContaining(ctx.args));
expect(receipt.amountPaid).toEqual(-receiptCompensated.amountPaid);
// restores
await receipt.destroy();
await receiptCompensated.destroy();
await till.destroy();
});
it('should create a new receipt with a compensation for a supplier', async() => {
const ctx = {
args: {
clientFk: clientFk,
payed: payed,
companyFk: companyFk,
bankFk: 3,
amountPaid: amountPaid,
description: description,
compensationAccount: '4100000001'
}
};
const receipt = await app.models.Client.createReceipt(ctx);
const paymentCompensated = await app.models.Payment.findOne({
where: {
clientFk: ctx.args.sale,
payed: ctx.args.payed,
amountPaid: ctx.args.amountPaid,
bankFk: ctx.args.bankFk
}
});
const till = await app.models.Till.findOne({
where: {
bankFk: ctx.args.bankFk,
in: amountPaid,
number: clientFk
}
});
delete ctx.args.payed;
expect(receipt).toEqual(jasmine.objectContaining(ctx.args));
expect(paymentCompensated.amountPaid).toEqual(paymentCompensated.amountPaid);
// restores
await receipt.destroy();
await paymentCompensated.destroy();
await till.destroy();
});
});

View File

@ -8,6 +8,7 @@ describe('Address updateAddress', () => {
const customAgentOneId = 1;
it('should throw the non uee member error if no incoterms is defined', async() => {
let err;
const ctx = {
args: {
provinceFk: provinceId,
@ -26,6 +27,7 @@ describe('Address updateAddress', () => {
});
it('should throw a non uee member error if no customsAgent is defined', async() => {
let err;
const ctx = {
args: {
provinceFk: provinceId,

View File

@ -104,6 +104,9 @@
"ClientDms": {
"dataSource": "vn"
},
"Till": {
"dataSource": "vn"
},
"CustomsAgent": {
"dataSource": "vn"
},

View File

@ -0,0 +1,84 @@
{
"name": "XDiario",
"base": "VnModel",
"options": {
"mysql": {
"table": "XDiario"
}
},
"properties": {
"ASIEN": {
"type": "number",
"id": true,
"description": "Identifier"
},
"FECHA": {
"type": "date"
},
"SUBCTA": {
"type": "string"
},
"CONTRA": {
"type": "string"
},
"CONCEPTO": {
"type": "string"
},
"EURODEBE": {
"type": "number"
},
"EUROHABER": {
"type": "number"
},
"BASEEURO": {
"type": "number"
},
"SERIE": {
"type": "string"
},
"CAMBIO": {
"type": "number"
},
"DEBEME": {
"type": "number"
},
"HABERME": {
"type": "number"
},
"FACTURA": {
"type": "string"
},
"IVA": {
"type": "number"
},
"RECEQUIV": {
"type": "number"
},
"METAL": {
"type": "number"
},
"METALIMP": {
"type": "number"
},
"CLIENTE": {
"type": "string"
},
"METALEJE": {
"type": "string"
},
"AUXILIAR": {
"type": "string"
},
"MONEDAUSO": {
"type": "string"
}
},
"relations": {
"company": {
"type": "belongsTo",
"model": "Company",
"foreignKey": "empresa_id"
}
}
}

View File

@ -30,6 +30,7 @@ module.exports = Self => {
require('../methods/client/createAddress')(Self);
require('../methods/client/updateAddress')(Self);
require('../methods/client/consumption')(Self);
require('../methods/client/createReceipt')(Self);
// Validations

View File

@ -1,13 +1,44 @@
module.exports = function(Self) {
require('../methods/receipt/filter')(Self);
Self.validateBinded('amountPaid', isNotZero, {
message: 'Amount cannot be zero',
allowNull: false,
allowBlank: false
});
function isNotZero(value) {
return !isNaN(value) && value != 0;
}
Self.validateAsync('companyFk', isOfficialCompany, {
message: 'Company has to be official'
});
async function isOfficialCompany(err, done) {
const hasCompany = await Self.app.models.Company.exists(this.companyFk);
if (!hasCompany) err();
done();
}
Self.observe('before save', async function(ctx) {
if (ctx.isNewInstance) {
let token = ctx.options.accessToken;
let userId = token && token.userId;
let worker = await Self.app.models.Worker.findOne({where: {userFk: userId}});
ctx.instance.workerFk = worker.id;
ctx.instance.workerFk = userId;
await Self.app.models.Till.create({
workerFk: userId,
bankFk: ctx.instance.bankFk,
in: ctx.instance.amountPaid,
concept: ctx.instance.description,
dated: ctx.instance.payed,
serie: 'A',
isAccountable: true,
number: ctx.instance.clientFk,
companyFk: ctx.instance.companyFk
});
}
});
};

View File

@ -9,17 +9,19 @@
"properties": {
"id": {
"id": true,
"type": "Number",
"type": "number",
"description": "Identifier"
},
"amountPaid": {
"type": "Number"
"type": "number",
"required": true
},
"amountUnpaid": {
"type": "Number"
"type": "number"
},
"payed": {
"type": "date"
"type": "date",
"required": true
},
"created": {
"type": "date"
@ -31,7 +33,16 @@
"type": "string",
"mysql": {
"columnName": "invoiceFk"
}
},
"required": true
},
"bankFk": {
"type": "number",
"required": true
},
"companyFk": {
"type": "number",
"required": true
}
},
"relations": {

View File

@ -0,0 +1,64 @@
{
"name": "Till",
"base": "VnModel",
"options": {
"mysql": {
"table": "till"
}
},
"properties": {
"id": {
"type": "number",
"id": true,
"description": "Identifier"
},
"dated": {
"type": "date",
"required": true
},
"isAccountable": {
"type": "boolean"
},
"serie": {
"type": "string",
"required": true
},
"number": {
"type": "number"
},
"concept": {
"type": "string",
"required": true
},
"in": {
"type": "number"
},
"out": {
"type": "number"
},
"created": {
"type": "date"
},
"isConciliate": {
"type": "boolean"
}
},
"relations": {
"bank": {
"type": "belongsTo",
"model": "Bank",
"foreignKey": "bankFk"
},
"worker": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "workerFk"
},
"company": {
"type": "belongsTo",
"model": "Company",
"foreignKey": "companyFk"
}
}
}

View File

@ -6,19 +6,23 @@
auto-load="true"
url="Companies"
data="companies"
order="code">
order="code"
required="true">
</vn-crud-model>
<vn-horizontal>
<vn-date-picker
label="Date"
ng-model="$ctrl.receipt.payed">
ng-model="$ctrl.receipt.payed"
required="true">
</vn-date-picker>
<vn-autocomplete
data="companies"
label="Company"
show-field="code"
value-field="id"
ng-model="$ctrl.receipt.companyFk">
ng-model="$ctrl.receipt.companyFk"
required="true"
rule>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
@ -29,27 +33,51 @@
value-field="id"
fields="['accountingTypeFk']"
include="{relation: 'accountingType'}"
ng-model="$ctrl.receipt.bankFk"
ng-model="$ctrl.bankFk"
search-function="{or: [{id: $search}, {bank: {like: '%'+ $search +'%'}}]}"
selection="$ctrl.bankSelection"
order="id">
order="id"
required="true">
<tpl-item>{{id}}: {{bank}}</tpl-item>
</vn-autocomplete>
<vn-input-number
vn-focus
label="Amount"
ng-model="$ctrl.receipt.amountPaid"
ng-model="$ctrl.amountPaid"
step="0.01"
rule>
required="true">
</vn-input-number>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
label="Reference"
ng-model="$ctrl.receipt.description"
rule>
rule
required="true">
</vn-textfield>
</vn-horizontal>
<vn-vertical ng-show="$ctrl.bankSelection.accountingType.code == 'cash'">
<h6 translate>Cash</h6>
<vn-horizontal>
<vn-input-number
ng-model="$ctrl.deliveredAmount"
label="Delivered amount">
</vn-input-number>
<vn-input-number
disabled="true"
ng-model="$ctrl.amountToReturn"
label="Amount to return">
</vn-input-number>
</vn-horizontal>
</vn-vertical>
<vn-vertical ng-show="$ctrl.bankSelection.accountingType.code == 'compensation'">
<h6 translate>Compensation</h6>
<vn-textfield
ng-model="$ctrl.receipt.compensationAccount"
label="Compensation Account"
on-change="$ctrl.accountShortToStandard(value)">
</vn-textfield>
</vn-vertical>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>

View File

@ -6,10 +6,7 @@ class Controller extends Dialog {
super($element, $, $transclude);
this.receipt = {
payed: new Date(),
clientFk: this.$params.id,
companyFk: this.vnConfig.companyFk,
bankFk: this.vnConfig.bankFk
payed: new Date()
};
}
@ -17,12 +14,9 @@ class Controller extends Dialog {
this.receipt.payed = value;
}
set bankFk(value) {
this.receipt.bankFk = value;
}
set amountPaid(value) {
this.receipt.amountPaid = value;
this.amountToReturn = this.deliveredAmount - value;
}
get amountPaid() {
@ -63,6 +57,30 @@ class Controller extends Dialog {
}
}
set deliveredAmount(value) {
this._deliveredAmount = value;
this.amountToReturn = value - this.receipt.amountPaid;
}
get deliveredAmount() {
return this._deliveredAmount;
}
get bankFk() {
if (!this.receipt.bankFk)
this.receipt.bankFk = this.vnConfig.bankFk;
return this.receipt.bankFk;
}
set bankFk(value) {
this.receipt.bankFk = value;
}
accountShortToStandard(value) {
this.receipt.compensationAccount = value.replace('.', '0'.repeat(11 - value.length));
}
getAmountPaid() {
const filter = {
where: {
@ -80,7 +98,7 @@ class Controller extends Dialog {
if (response !== 'accept')
return super.responseHandler(response);
return this.$http.post(`Receipts`, this.receipt)
return this.$http.post(`Clients/${this.clientFk}/createReceipt`, this.receipt)
.then(() => super.responseHandler(response))
.then(() => this.vnApp.showSuccess(this.$t('Data saved!')));
}

View File

@ -65,12 +65,30 @@ describe('Client', () => {
controller.$params = {id: 101};
$httpBackend.expect('POST', `Receipts`).respond({id: 1});
$httpBackend.expect('POST', `Clients/101/createReceipt`).respond({id: 1});
controller.responseHandler('accept');
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
describe('deliveredAmount() setter', () => {
it('should set the deliveredAmount property', () => {
controller.amountPaid = 999;
controller.deliveredAmount = 1000;
expect(controller.amountToReturn).toEqual(1);
});
});
describe('accountShortToStandard()', () => {
it('should get de account in stardard format', () => {
const shortAccount = '4.3';
controller.accountShortToStandard(shortAccount);
expect(controller.receipt.compensationAccount).toEqual('4000000003');
});
});
});
});

View File

@ -139,7 +139,8 @@
<vn-client-balance-create
vn-id="balance-create"
on-accept="$ctrl.getData()"
company-fk="$ctrl.companyId">
company-fk="$ctrl.companyId"
client-fk="$ctrl.$params.id">
</vn-client-balance-create>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">

View File

@ -0,0 +1,2 @@
Compensation: Compensación
Cash: Efectivo

View File

@ -82,7 +82,6 @@
value-field="id"
label="Previous client"
info="In case of a company succession, specify the grantor company"
vn-acl="salesAssistant"
rule>
</vn-autocomplete>
</vn-horizontal>

View File

@ -58,3 +58,6 @@ Samples: Plantillas
Send sample: Enviar plantilla
Log: Historial
Consumption: Consumo
Compensation Account: Cuenta para compensar
Amount to return: Cantidad a devolver
Delivered amount: Cantidad entregada

View File

@ -40,7 +40,7 @@
value="{{$ctrl.summary.mobile}}">
</vn-label-value>
<vn-label-value label="Email" no-ellipsize
value="{{$ctrl.summary.email}}">
value="{{$ctrl.listEmails($ctrl.summary.email)}}">
</vn-label-value>
<vn-label-value label="Sales person">
<span

View File

@ -19,42 +19,56 @@ module.exports = Self => {
type: 'String',
description: 'Searchs the invoiceOut by id',
http: {source: 'query'}
}, {
},
{
arg: 'clientFk',
type: 'Integer',
description: 'The client id',
http: {source: 'query'}
}, {
},
{
arg: 'fi',
type: 'String',
description: 'The client fiscal id',
http: {source: 'query'}
},
{
arg: 'hasPdf',
type: 'Boolean',
description: 'Whether the the invoiceOut has PDF or not',
http: {source: 'query'}
}, {
},
{
arg: 'amount',
type: 'Number',
description: 'The amount filter',
http: {source: 'query'}
}, {
},
{
arg: 'min',
type: 'Number',
description: 'The minimun amount flter',
http: {source: 'query'}
}, {
},
{
arg: 'max',
type: 'Number',
description: 'The maximun amount flter',
http: {source: 'query'}
}, {
},
{
arg: 'issued',
type: 'Date',
description: 'The issued date filter',
http: {source: 'query'}
}, {
},
{
arg: 'created',
type: 'Date',
description: 'The created date filter',
http: {source: 'query'}
}, {
},
{
arg: 'dued',
type: 'Date',
description: 'The due date filter',
@ -88,6 +102,8 @@ module.exports = Self => {
return {'i.created': value};
case 'clientFk':
return {'i.clientFk': value};
case 'fi':
return {'c.fi': value};
case 'amount':
case 'companyFk':
case 'issued':

View File

@ -1,2 +1,3 @@
InvoiceOut: Facturas
Search invoices by reference: Buscar facturas por referencia
Client fiscal id: CIF del cliente

View File

@ -15,6 +15,11 @@
label="Client id"
ng-model="filter.clientFk">
</vn-textfield>
<vn-textfield
vn-one
label="Client fiscal id"
ng-model="filter.fi">
</vn-textfield>
<vn-check
vn-one
triple-state="true"

View File

@ -48,7 +48,11 @@ module.exports = Self => {
const srcFile = image.url.split('/').pop();
const dotIndex = srcFile.lastIndexOf('.');
const fileName = srcFile.substring(0, dotIndex);
let fileName = srcFile.substring(0, dotIndex);
if (dotIndex == -1)
fileName = srcFile;
const file = `${fileName}.png`;
const filePath = path.join(tempPath, file);

View File

@ -20,8 +20,12 @@ module.exports = Self => {
});
Self.getBalance = async filter => {
let where = filter.where;
const where = filter.where;
let [diary] = await Self.rawSql(`CALL vn.item_getBalance(?, ?)`, [where.itemFk, where.warehouseFk]);
for (const entry of diary)
if (entry.clientType === 'loses') entry.highlighted = true;
return diary;
};
};

View File

@ -0,0 +1,33 @@
const app = require('vn-loopback/server/server');
const LoopBackContext = require('loopback-context');
describe('item getBalance()', () => {
it('should return the balance lines of a client type loses in which one has highlighted true', async() => {
const activeCtx = {
accessToken: {userId: 9},
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
const losesClientId = 111;
const ticket = await app.models.Ticket.findById(7);
const originalClientId = ticket.clientFk;
await ticket.updateAttribute('clientFk', losesClientId);
const filter = {
where: {
itemFk: 1,
warehouseFk: 1
}
};
const results = await app.models.Item.getBalance(filter);
const result = results.find(element => element.clientType == 'loses');
expect(result.highlighted).toBe(true);
// restores
await ticket.updateAttribute('clientFk', originalClientId);
});
});

View File

@ -18,8 +18,7 @@
"required": true
},
"error": {
"type": "string",
"required": true
"type": "string"
},
"attempts": {
"type": "number"

View File

@ -68,7 +68,8 @@
"stemMultiplier": {
"type": "number",
"description": "Multiplier"
},"image": {
},
"image": {
"type": "string",
"description": "Image"
},

View File

@ -64,6 +64,7 @@
<vn-td>{{::sale.stateName | dashIfEmpty}}</vn-td>
<vn-td>{{::sale.reference | dashIfEmpty}}</vn-td>
<vn-td class="truncate" expand>
<span ng-class="::{'warning chip': sale.highlighted}">
<span ng-if="::!sale.isTicket">
{{::sale.name | dashIfEmpty}}
</span>
@ -73,6 +74,7 @@
class="link">
{{::sale.name | dashIfEmpty}}
</span>
</span>
</vn-td>
<vn-td number class="in">{{::sale.in | dashIfEmpty}}</vn-td>
<vn-td number>{{::sale.out | dashIfEmpty}}</vn-td>

View File

@ -32,14 +32,14 @@
rule>
</vn-autocomplete>
<vn-textfield vn-three
ng-show="tag.selection.isFree || tag.selection.isFree == undefined"
ng-if="tag.selection.isFree || tag.selection.isFree == undefined"
vn-id="text"
label="Value"
ng-model="itemTag.value"
rule>
</vn-textfield>
<vn-autocomplete vn-three
ng-show="tag.selection.isFree === false"
ng-if="tag.selection.isFree === false"
url="{{'Tags/' + itemTag.tagFk + '/filterValue'}}"
search-function="{value: $search}"
label="Value"

View File

@ -9,6 +9,7 @@
vn-focus
panel="vn-route-search-panel"
info="Search routes by id"
fetch-params="$ctrl.fetchParams($params)"
filter="$ctrl.filterParams"
model="model">
</vn-searchbar>

View File

@ -3,16 +3,36 @@ import ModuleMain from 'salix/components/module-main';
export default class Route extends ModuleMain {
$postLink() {
let to = new Date();
const to = new Date();
to.setDate(to.getDate() + 1);
to.setHours(0, 0, 0, 0);
let from = new Date();
const from = new Date();
from.setDate(from.getDate());
from.setHours(0, 0, 0, 0);
this.filterParams = {from, to, warehouseFk: this.vnConfig.warehouseFk};
this.filterParams = {from, to};
this.$.model.applyFilter(null, this.filterParams);
}
fetchParams($params) {
const hasEntries = Object.entries($params).length;
if (!hasEntries)
$params.scopeDays = 1;
if (typeof $params.scopeDays === 'number') {
const from = new Date();
from.setHours(0, 0, 0, 0);
const to = new Date(from.getTime());
to.setDate(to.getDate() + $params.scopeDays);
to.setHours(23, 59, 59, 999);
Object.assign($params, {from, to});
}
return $params;
}
}
ngModule.vnComponent('vnRoute', {

View File

@ -0,0 +1,49 @@
import './index.js';
describe('Route Component vnRoute', () => {
let controller;
beforeEach(ngModule('route'));
beforeEach(inject($componentController => {
let $element = angular.element(`<div></div>`);
controller = $componentController('vnRoute', {$element});
}));
describe('fetchParams()', () => {
it('should return a range of dates with passed scope days', () => {
let params = controller.fetchParams({
scopeDays: 2
});
const from = new Date();
from.setHours(0, 0, 0, 0);
const to = new Date(from.getTime());
to.setDate(to.getDate() + params.scopeDays);
to.setHours(23, 59, 59, 999);
const expectedParams = {
from,
scopeDays: params.scopeDays,
to
};
expect(params).toEqual(expectedParams);
});
it('should return default value for scope days', () => {
let params = controller.fetchParams({
scopeDays: 1
});
expect(params.scopeDays).toEqual(1);
});
it('should return the given scope days', () => {
let params = controller.fetchParams({
scopeDays: 2
});
expect(params.scopeDays).toEqual(2);
});
});
});

View File

@ -1,6 +1,6 @@
<div class="search-panel">
<form ng-submit="$ctrl.onSearch()">
<vn-horizontal>
<form id="manifold-form" ng-submit="$ctrl.onSearch()">
<vn-horizontal class="vn-px-lg vn-pt-lg">
<vn-textfield
vn-one
label="General search"
@ -9,7 +9,7 @@
vn-focus>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-autocomplete
vn-one
ng-model="filter.workerFk"
@ -29,19 +29,37 @@
value-field="id">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<section class="vn-px-md">
<vn-horizontal class="manifold-panel vn-pa-md">
<vn-date-picker
vn-one
label="From"
ng-model="filter.from">
ng-model="filter.from"
on-change="$ctrl.from = value">
</vn-date-picker>
<vn-date-picker
vn-one
label="To"
ng-model="filter.to">
ng-model="filter.to"
on-change="$ctrl.to = value">
</vn-date-picker>
<vn-none class="or vn-px-md" translate>Or</vn-none>
<vn-input-number
vn-one
min="0"
step="1"
label="Days onward"
ng-model="filter.scopeDays"
on-change="$ctrl.scopeDays = value"
display-controls="true">
</vn-input-number>
<vn-icon color-marginal
icon="info"
vn-tooltip="Cannot choose a range of dates and days onward at the same time">
</vn-icon>
</vn-horizontal>
<vn-horizontal>
</section>
<vn-horizontal class="vn-px-lg">
<vn-autocomplete
vn-one
label="Vehicle"
@ -56,7 +74,7 @@
ng-model="filter.m3">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-autocomplete
vn-one
label="Warehouse"
@ -71,7 +89,7 @@
ng-model="filter.description">
</vn-textfield>
</vn-horizontal>
<vn-horizontal class="vn-mt-lg">
<vn-horizontal class="vn-px-lg vn-pb-lg vn-mt-lg">
<vn-submit label="Search"></vn-submit>
</vn-horizontal>
</form>

View File

@ -1,7 +1,43 @@
import ngModule from '../module';
import SearchPanel from 'core/components/searchbar/search-panel';
class Controller extends SearchPanel {
constructor($, $element) {
super($, $element);
this.filter = this.$.filter;
}
get from() {
return this._from;
}
set from(value) {
this._from = value;
this.filter.scopeDays = null;
}
get to() {
return this._to;
}
set to(value) {
this._to = value;
this.filter.scopeDays = null;
}
get scopeDays() {
return this._scopeDays;
}
set scopeDays(value) {
this._scopeDays = value;
this.filter.from = null;
this.filter.to = null;
}
}
ngModule.vnComponent('vnRouteSearchPanel', {
template: require('./index.html'),
controller: SearchPanel
controller: Controller
});

View File

@ -0,0 +1,48 @@
import './index';
describe('Route Component vnRouteSearchPanel', () => {
let controller;
beforeEach(ngModule('route'));
beforeEach(inject($componentController => {
controller = $componentController('vnRouteSearchPanel', {$element: null});
controller.$t = () => {};
controller.filter = {};
}));
describe('from() setter', () => {
it('should clear the scope days when setting the from property', () => {
controller.filter.scopeDays = 1;
controller.from = new Date();
expect(controller.filter.scopeDays).toBeNull();
expect(controller.from).toBeDefined();
});
});
describe('to() setter', () => {
it('should clear the scope days when setting the to property', () => {
controller.filter.scopeDays = 1;
controller.to = new Date();
expect(controller.filter.scopeDays).toBeNull();
expect(controller.to).toBeDefined();
});
});
describe('scopeDays() setter', () => {
it('should clear the date range when setting the scopeDays property', () => {
controller.filter.from = new Date();
controller.filter.to = new Date();
controller.scopeDays = 1;
expect(controller.filter.from).toBeNull();
expect(controller.filter.to).toBeNull();
expect(controller.scopeDays).toBeDefined();
});
});
});

View File

@ -42,6 +42,7 @@ module.exports = Self => {
'sageTaxTypeFk',
'sageTransactionTypeFk',
'sageWithholdingFk',
'workerFk'
],
include: [
{
@ -85,7 +86,19 @@ module.exports = Self => {
scope: {
fields: ['id', 'withholding']
}
},
{
relation: 'worker',
scope: {
fields: ['userFk'],
include: {
relation: 'user',
scope: {
fields: ['nickname']
}
}
}
},
]
};
let supplier = await Self.app.models.Supplier.findOne(filter);

View File

@ -11,75 +11,78 @@
},
"properties": {
"id": {
"type": "Number",
"type": "number",
"id": true,
"description": "Identifier"
},
"name": {
"type": "String"
"type": "string"
},
"account": {
"type": "String"
"type": "string"
},
"countryFk": {
"type": "Number"
"type": "number"
},
"nif": {
"type": "String"
"type": "string"
},
"isFarmer": {
"type": "Boolean"
"type": "boolean"
},
"phone": {
"type": "Number"
"type": "number"
},
"retAccount": {
"type": "Number"
"type": "number"
},
"commission": {
"type": "Boolean"
"type": "boolean"
},
"created": {
"type": "Date"
"type": "date"
},
"postcodeFk": {
"type": "Number"
"type": "number"
},
"isActive": {
"type": "Boolean"
"type": "boolean"
},
"isOfficial": {
"type": "Boolean"
"type": "boolean"
},
"isSerious": {
"type": "Boolean"
"type": "boolean"
},
"note": {
"type": "String"
"type": "string"
},
"street": {
"type": "String"
"type": "string"
},
"city": {
"type": "String"
"type": "string"
},
"provinceFk": {
"type": "Number"
"type": "number"
},
"postCode": {
"type": "String"
"type": "string"
},
"payMethodFk": {
"type": "Number"
"type": "number"
},
"payDemFk": {
"type": "Number"
"type": "number"
},
"payDay": {
"type": "Number"
"type": "number"
},
"nickname": {
"type": "String"
"type": "string"
},
"workerFk": {
"type": "number"
},
"sageTaxTypeFk": {
"type": "number",
@ -126,6 +129,11 @@
"model": "Client",
"foreignKey": "nif",
"primaryKey": "fi"
},
"worker": {
"type": "belongsTo",
"model": "Worker",
"foreignKey": "workerFk"
},
"sageTaxType": {
"type": "belongsTo",

View File

@ -15,6 +15,17 @@
rule
vn-focus>
</vn-textfield>
<vn-autocomplete
vn-one
ng-model="$ctrl.supplier.workerFk"
url="Clients/activeWorkersWithRole"
search-function="{firstName: $search}"
show-field="nickname"
value-field="id"
where="{role: 'employee'}"
label="Responsible"
info="Responsible for approving invoices">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-check

View File

@ -1,3 +1,4 @@
Notes: Notas
Active: Activo
Verified: Verificado
Responsible for approving invoices: Responsable de aprobar las facturas

View File

@ -31,6 +31,13 @@
label="Alias"
value="{{::$ctrl.summary.nickname}}">
</vn-label-value>
<vn-label-value label="Responsible">
<span
ng-click="workerDescriptor.show($event, $ctrl.summary.workerFk)"
class="link">
{{$ctrl.summary.worker.user.nickname}}
</span>
</vn-label-value>
<vn-label-value no-ellipsize
label="Notes"
value="{{::$ctrl.summary.note}}">

View File

@ -7,3 +7,4 @@ Sage tax type: Tipo de impuesto Sage
Sage transaction type: Tipo de transacción Sage
Sage withholding: Retencion Sage
Go to the supplier: Ir al proveedor
Responsible: Responsable

View File

@ -197,6 +197,7 @@ module.exports = Self => {
t.id,
t.shipped,
CAST(DATE(t.shipped) AS CHAR) AS shippedDate,
HOUR(t.shipped) AS shippedHour,
t.nickname,
t.refFk,
t.routeFk,

View File

@ -101,7 +101,7 @@ module.exports = Self => {
if (!shipped && landed) {
const shippedResult = await models.Agency.getShipped(landed,
address.id, agencyModeId, warehouseId);
shipped = shippedResult && shippedResult.shipped;
shipped = (shippedResult && shippedResult.shipped) || landed;
}
if (shipped && !landed) {

View File

@ -28,7 +28,7 @@ describe('ticket new()', () => {
params.shipped,
params.landed,
params.warehouseId,
params.companyFk,
params.companyId,
params.addressId
).catch(e => {
error = e;
@ -53,7 +53,7 @@ describe('ticket new()', () => {
params.shipped,
params.landed,
params.warehouseId,
params.companyFk,
params.companyId,
params.addressId
).catch(response => {
expect(response.message).toEqual(`This address doesn't exist`);
@ -79,7 +79,7 @@ describe('ticket new()', () => {
params.shipped,
params.landed,
params.warehouseId,
params.companyFk,
params.companyId,
params.addressId,
params.agencyModeId);
@ -87,4 +87,27 @@ describe('ticket new()', () => {
expect(ticket.id).toBeGreaterThan(newestTicketIdInFixtures);
});
it('should return the set a shipped when the agency is not especified', async() => {
let params = {
clientId: 104,
landed: today,
shipped: null,
warehouseId: 2,
companyId: 442,
addressId: 4,
agencyModeId: null
};
ticket = await app.models.Ticket.new(ctx,
params.clientId,
params.shipped,
params.landed,
params.warehouseId,
params.companyId,
params.addressId,
params.agencyModeId);
expect(ticket.shipped).toEqual(jasmine.any(Date));
});
});

View File

@ -1,4 +1,5 @@
const LoopBackContext = require('loopback-context');
const dateUtil = require('vn-loopback/util/date');
module.exports = function(Self) {
require('../methods/ticket-request/filter')(Self);
@ -26,12 +27,7 @@ module.exports = function(Self) {
if (instance.price)
messageText = 'New ticket request has been created with price';
const shipped = new Intl.DateTimeFormat('es', {
year: 'numeric',
month: 'numeric',
day: 'numeric'
}).format(ticket.shipped);
const shipped = dateUtil.toString(ticket.shipped);
const message = $t(messageText, {
description: instance.description,
shipped: shipped,

View File

@ -39,7 +39,7 @@
</vn-autocomplete>
<vn-autocomplete
disabled="!$ctrl.clientId || !$ctrl.landed || !$ctrl.warehouseId"
data="$ctrl._availableAgencies"
data="$ctrl.agencies"
label="Agency"
show-field="agencyMode"
value-field="agencyModeFk"

View File

@ -100,9 +100,12 @@ class Controller extends Component {
ticket.agencyModeFk = null;
this.$http.get(`Agencies/getAgenciesWithWarehouse`, {params}).then(res => {
this._availableAgencies = res.data;
this.agencyModeId = this.defaultAddress.agencyModeFk;
this.agencies = res.data;
const defaultAgency = this.agencies.find(agency=> {
return agency.agencyModeFk == this.defaultAddress.agencyModeFk;
});
if (defaultAgency)
this.agencyModeId = defaultAgency.agencyModeFk;
});
}
}

View File

@ -18,12 +18,12 @@
<vn-th field="salesPersonFk" class="expendable">Salesperson</vn-th>
<vn-th field="shipped">Date</vn-th>
<vn-th>Hour</vn-th>
<vn-th field="hour" shrink>Closure</vn-th>
<vn-th field="nickname">Alias</vn-th>
<vn-th field="provinceFk" class="expendable">Province</vn-th>
<vn-th field="stateFk" >State</vn-th>
<vn-th field="zoneFk">Zone</vn-th>
<vn-th field="warehouseFk">Warehouse</vn-th>
<vn-th field="hour" shrink>Closure</vn-th>
<vn-th number>Total</vn-th>
<vn-th></vn-th>
</vn-tr>
@ -86,6 +86,7 @@
</span>
</vn-td>
<vn-td>{{::ticket.shipped | date: 'HH:mm'}}</vn-td>
<vn-td shrink>{{::ticket.zoneLanding | date: 'HH:mm'}}</vn-td>
<vn-td>
<span
title="{{::ticket.nickname}}"
@ -118,7 +119,6 @@
</span>
</vn-td>
<vn-td>{{::ticket.warehouse}}</vn-td>
<vn-td shrink>{{::ticket.zoneLanding | date: 'HH:mm'}}</vn-td>
<vn-td number>
<span class="chip {{$ctrl.totalPriceColor(ticket)}}">
{{::ticket.total | currency: 'EUR': 2}}

View File

@ -2,7 +2,7 @@
vn-id="model"
url="Tickets/filter"
limit="20"
order="shipped DESC, zoneHour DESC, zoneMinute DESC, clientFk">
order="shippedDate DESC, shippedHour ASC, zoneLanding ASC">
</vn-crud-model>
<vn-portal slot="topbar">
<vn-searchbar

View File

@ -367,6 +367,7 @@ class Controller extends Section {
notAvailables
};
this.newSMS = {
ticketId: this.ticket.id,
destinationFk: this.ticket.clientFk,
destination: phone,
message: this.$t('Product not available', params)

View File

@ -35,7 +35,7 @@
ng-model="filter.to"
on-change="$ctrl.to = value">
</vn-date-picker>
<vn-none class="or vn-px-md">O</vn-none>
<vn-none class="or vn-px-md" translate>Or</vn-none>
<vn-input-number
vn-one
min="0"

View File

@ -18,3 +18,4 @@ DELIVERED: Servido
ON_PREPARATION: En preparacion
PACKED: Encajado
Cannot choose a range of dates and days onward at the same time: No se puede selecionar un rango de fechas y días en adelante a la vez
Or: O

View File

@ -54,18 +54,14 @@ class Controller extends Summary {
}
setOkState() {
let params = {};
if (this.$params.id)
params = {ticketFk: this.$params.id};
if (!this.$params.id)
params = {ticketFk: this.ticket.id};
const params = {ticketFk: 'id' in this.ticket ? this.ticket.id : this.$params.id};
params.code = 'OK';
this.$http.post(`TicketTrackings/changeState`, params)
.then(() => this.reload())
.then(() => {
if ('id' in this.$params) this.reload();
})
.then(() => {
this.vnApp.showSuccess(this.$t('Data saved!'));
});

View File

@ -12,29 +12,38 @@ describe('Travel Component vnTravel', () => {
describe('fetchParams()', () => {
it('should return a range of dates with passed scope days', () => {
/**
* Calculates the difference in days between two dates, it also
* accounts for cases where the two dates in question span a
* daylight saving time (DST) change.
*
* @param {Date} a The start date
* @param {Date} b The end date
* @return {Number} The difference in days
*/
function diffInDays(a, b) {
const utc1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
const utc2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
const msInDay = 86400 * 1000;
return Math.floor((utc2 - utc1) / msInDay);
}
let params = controller.fetchParams({
scopeDays: 2
});
const shippedFrom = new Date();
shippedFrom.setHours(0, 0, 0, 0);
const shippedTo = new Date(shippedFrom.getTime());
shippedTo.setDate(shippedTo.getDate() + params.scopeDays);
shippedTo.setHours(23, 59, 59, 999);
let params = controller.fetchParams({scopeDays: 2});
const diff = diffInDays(
params.shippedFrom,
new Date(params.shippedTo.getTime() + 1)
);
const expectedParams = {
shippedFrom,
scopeDays: params.scopeDays,
shippedTo
};
expect(diff).toEqual(3);
expect(params).toEqual(expectedParams);
});
it('should return default value for scope days', () => {
let params = controller.fetchParams({
scopeDays: 1
});
expect(params.scopeDays).toEqual(1);
});
it('should return the given scope days', () => {
let params = controller.fetchParams({
scopeDays: 2
});
expect(params.scopeDays).toEqual(2);
});
});
});

View File

@ -1,6 +1,6 @@
<div class="search-panel">
<form ng-submit="$ctrl.onSearch()">
<vn-horizontal>
<form ng-submit="$ctrl.onSearch()" id="manifold-form">
<vn-horizontal class="vn-px-lg vn-pt-lg">
<vn-textfield
vn-one
label="General search"
@ -9,7 +9,7 @@
vn-focus>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-textfield
vn-one
label="Reference"
@ -21,7 +21,7 @@
ng-model="filter.totalEntries">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-textfield
vn-one
label="Travel id"
@ -35,19 +35,37 @@
value-field="id">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<section class="vn-px-md">
<vn-horizontal class="manifold-panel vn-pa-md">
<vn-date-picker
vn-one
label="Shipped from"
ng-model="filter.shippedFrom">
ng-model="filter.shippedFrom"
on-change="$ctrl.shippedFrom = value">
</vn-date-picker>
<vn-date-picker
vn-one
label="Shipped to"
ng-model="filter.shippedTo">
ng-model="filter.shippedTo"
on-change="$ctrl.shippedTo = value">
</vn-date-picker>
<vn-none class="or vn-px-md" translate>Or</vn-none>
<vn-input-number
vn-one
min="0"
step="1"
label="Days onward"
ng-model="filter.scopeDays"
on-change="$ctrl.scopeDays = value"
display-controls="true">
</vn-input-number>
<vn-icon color-marginal
icon="info"
vn-tooltip="Cannot choose a range of dates and days onward at the same time">
</vn-icon>
</vn-horizontal>
<vn-horizontal>
</section>
<vn-horizontal class="vn-px-lg">
<vn-date-picker
vn-one
label="Landed from"
@ -59,7 +77,7 @@
ng-model="filter.landedTo">
</vn-date-picker>
</vn-horizontal>
<vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-autocomplete vn-one
label="Warehouse Out"
ng-model="filter.warehouseOutFk"
@ -75,7 +93,7 @@
value-field="id">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal class="vn-mt-lg">
<vn-horizontal class="vn-px-lg vn-pb-lg vn-mt-lg">
<vn-submit label="Search"></vn-submit>
</vn-horizontal>
</form>

View File

@ -1,7 +1,43 @@
import ngModule from '../module';
import SearchPanel from 'core/components/searchbar/search-panel';
class Controller extends SearchPanel {
constructor($, $element) {
super($, $element);
this.filter = this.$.filter;
}
get shippedFrom() {
return this._shippedFrom;
}
set shippedFrom(value) {
this._shippedFrom = value;
this.filter.scopeDays = null;
}
get shippedTo() {
return this._shippedTo;
}
set shippedTo(value) {
this._shippedTo = value;
this.filter.scopeDays = null;
}
get scopeDays() {
return this._scopeDays;
}
set scopeDays(value) {
this._scopeDays = value;
this.filter.shippedFrom = null;
this.filter.shippedTo = null;
}
}
ngModule.vnComponent('vnTravelSearchPanel', {
template: require('./index.html'),
controller: SearchPanel
controller: Controller
});

View File

@ -0,0 +1,48 @@
import './index';
describe('Travel Component vnTravelSearchPanel', () => {
let controller;
beforeEach(ngModule('travel'));
beforeEach(inject($componentController => {
controller = $componentController('vnTravelSearchPanel', {$element: null});
controller.$t = () => {};
controller.filter = {};
}));
describe('shippedFrom() setter', () => {
it('should clear the scope days when setting the from property', () => {
controller.filter.scopeDays = 1;
controller.shippedFrom = new Date();
expect(controller.filter.scopeDays).toBeNull();
expect(controller.shippedFrom).toBeDefined();
});
});
describe('shippedTo() setter', () => {
it('should clear the scope days when setting the to property', () => {
controller.filter.scopeDays = 1;
controller.shippedTo = new Date();
expect(controller.filter.scopeDays).toBeNull();
expect(controller.shippedTo).toBeDefined();
});
});
describe('scopeDays() setter', () => {
it('should clear the date range when setting the scopeDays property', () => {
controller.filter.shippedFrom = new Date();
controller.filter.shippedTo = new Date();
controller.scopeDays = 1;
expect(controller.filter.shippedFrom).toBeNull();
expect(controller.filter.shippedTo).toBeNull();
expect(controller.scopeDays).toBeDefined();
});
});
});

View File

@ -28,7 +28,7 @@ module.exports = Self => {
const account = await Self.app.models.Account.findById(userId);
const stmt = new ParameterizedSQL(
`SELECT d.id dmsFk, d.reference, d.description, d.file, d.created
`SELECT d.id dmsFk, d.reference, d.description, d.file, d.created, d.hardCopyNumber, d.hasFile
FROM workerDocument wd
JOIN dms d ON d.id = wd.document
JOIN dmsType dt ON dt.id = d.dmsTypeFk

View File

@ -26,11 +26,11 @@ module.exports = Self => {
const workerModel = Self.app.models.Worker;
const targetTimeEntry = await Self.findById(id);
const isSubordinate = await workerModel.isSubordinate(ctx, targetTimeEntry.userFk);
const isHHRR = await Self.app.models.Account.hasRole(currentUserId, 'hr');
const isTeamBoss = await Self.app.models.Account.hasRole(currentUserId, 'teamBoss');
const isHimself = currentUserId == targetTimeEntry.userFk;
const notAllowed = isSubordinate === false || (isSubordinate && currentUserId == targetTimeEntry.userFk && !isHHRR);
const notAllowed = isSubordinate === false || (isSubordinate && isHimself && !isTeamBoss);
if (notAllowed)
throw new UserError(`You don't have enough privileges`);

View File

@ -5,6 +5,8 @@ describe('workerTimeControl add/delete timeEntry()', () => {
const HHRRId = 37;
const teamBossId = 13;
const employeeId = 1;
const salesPersonId = 106;
const salesBossId = 19;
let activeCtx = {
accessToken: {userId: 50},
};
@ -85,13 +87,13 @@ describe('workerTimeControl add/delete timeEntry()', () => {
});
it('should try but fail to delete his own time entry', async() => {
activeCtx.accessToken.userId = teamBossId;
activeCtx.accessToken.userId = salesBossId;
let error;
let todayAtSeven = new Date();
todayAtSeven.setHours(19, 30, 0, 0);
let data = {
workerFk: teamBossId,
workerFk: salesPersonId,
timed: todayAtSeven
};
@ -100,6 +102,7 @@ describe('workerTimeControl add/delete timeEntry()', () => {
createdTimeEntry = await app.models.WorkerTimeControl.findById(timeEntry.id);
try {
activeCtx.accessToken.userId = salesPersonId;
await app.models.WorkerTimeControl.deleteTimeEntry(ctx, createdTimeEntry.id);
} catch (e) {
error = e;
@ -110,6 +113,30 @@ describe('workerTimeControl add/delete timeEntry()', () => {
expect(error.message).toBe(`You don't have enough privileges`);
});
it('should delete the created time entry for the team boss as himself', async() => {
activeCtx.accessToken.userId = teamBossId;
let todayAtFive = new Date();
todayAtFive.setHours(17, 30, 0, 0);
let data = {
workerFk: teamBossId,
timed: todayAtFive
};
timeEntry = await app.models.WorkerTimeControl.addTimeEntry(ctx, data);
createdTimeEntry = await app.models.WorkerTimeControl.findById(timeEntry.id);
expect(createdTimeEntry).toBeDefined();
await app.models.WorkerTimeControl.deleteTimeEntry(ctx, createdTimeEntry.id);
createdTimeEntry = await app.models.WorkerTimeControl.findById(timeEntry.id);
expect(createdTimeEntry).toBeNull();
});
it('should delete the created time entry for the team boss as HHRR', async() => {
activeCtx.accessToken.userId = HHRRId;

View File

@ -9,28 +9,31 @@
"properties": {
"id": {
"id": true,
"type": "Number"
"type": "number"
},
"code": {
"type": "string"
},
"name": {
"type": "String"
"type": "string"
},
"parentFk": {
"type": "Number"
"type": "number"
},
"lft": {
"type": "Number"
"type": "number"
},
"rgt": {
"type": "Number"
"type": "number"
},
"sons": {
"type": "Number"
"type": "number"
},
"chatName": {
"type": "String"
"type": "string"
},
"notificationEmail": {
"type": "String"
"type": "string"
}
}
}

View File

@ -57,7 +57,7 @@ class Controller extends Descriptor {
onUploadResponse() {
const timestamp = new Date().getTime();
const src = this.$rootScope.imagePath('user', '520x520', this.worker.id);
const zoomSrc = this.$rootScope.imagePath('user', '1600x900', this.worker.id);
const zoomSrc = this.$rootScope.imagePath('user', '1600x1600', this.worker.id);
const newSrc = `${src}&t=${timestamp}`;
const newZoomSrc = `${zoomSrc}&t=${timestamp}`;

View File

@ -68,7 +68,7 @@
</vn-input-file>
</vn-horizontal>
<vn-vertical>
<vn-check disabled="true"
<vn-check disabled="watcher.orgData.hasFile"
label="Generate identifier for original file"
ng-model="$ctrl.dms.hasFile">
</vn-check>

View File

@ -15,6 +15,7 @@
<vn-thead>
<vn-tr>
<vn-th field="dmsFk" shrink>Id</vn-th>
<vn-th field="hardCopyNumber" shrink number>Order</vn-th>
<vn-th field="reference" shrink>Reference</vn-th>
<vn-th expand>Description</vn-th>
<vn-th field="hasFile" shrink>Original</vn-th>
@ -28,6 +29,12 @@
<vn-tbody>
<vn-tr ng-repeat="document in $ctrl.workerDms">
<vn-td number shrink>{{::document.dmsFk}}</vn-td>
<vn-td shrink number>
<span class="chip" title="{{::document.hardCopyNumber}}"
ng-class="{'message': document.hardCopyNumber}">
{{::document.hardCopyNumber}}
</span>
</vn-td>
<vn-td expand>
<span title="{{::document.reference}}">
{{::document.reference}}
@ -40,7 +47,7 @@
</vn-td>
<vn-td shrink>
<vn-check
ng-model="document.dms.hasFile"
ng-model="document.hasFile"
disabled="true">
</vn-check>
</vn-td>

View File

@ -6,42 +6,6 @@ class Controller extends Component {
constructor($element, $, vnFile) {
super($element, $);
this.vnFile = vnFile;
this.filter = {
include: {
relation: 'dms',
scope: {
fields: [
'dmsTypeFk',
'reference',
'hardCopyNumber',
'workerFk',
'description',
'hasFile',
'file',
'created',
],
include: [
{
relation: 'dmsType',
scope: {
fields: ['name']
}
}, {
relation: 'worker',
scope: {
fields: ['userFk'],
include: {
relation: 'user',
scope: {
fields: ['nickname']
}
},
}
}
]
},
}
};
}
deleteDms(index) {

View File

@ -1,5 +1,3 @@
const app = require('vn-loopback/server/server');
module.exports = Self => {
Self.validate('range', function(err) {
if (this.type == 'range'

Some files were not shown because too many files have changed in this diff Show More