Merge branch 'dev' into 7323-fineTunningWorker
gitea/salix/pipeline/pr-dev This commit looks good Details

This commit is contained in:
Jorge Penadés 2024-10-04 07:47:38 +00:00
commit c4faac7446
85 changed files with 682 additions and 3778 deletions

View File

@ -185,6 +185,7 @@ INSERT INTO `vn`.`warehouse`(`id`, `name`, `code`, `isComparative`, `isInventory
(3, 'Warehouse Three', NULL, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0),
(4, 'Warehouse Four', NULL, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1),
(5, 'Warehouse Five', NULL, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0),
(6, 'Warehouse six', 'vnh', 1, 1, 1, 1, 0, 0, 1, 1, 0, 0),
(13, 'Inventory', 'inv', 1, 1, 1, 0, 0, 0, 1, 0, 0, 0),
(60, 'Algemesi', NULL, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0);
@ -3941,6 +3942,11 @@ INSERT INTO vn.medicalReview
(id, workerFk, centerFk, `date`, `time`, isFit, amount, invoice, remark)
VALUES(3, 9, 2, '2000-01-01', '8:00', 1, 150.0, NULL, NULL);
INSERT INTO vn.stockBought (workerFk, bought, reserve, dated)
VALUES(35, 1.00, 1.00, '2001-01-01');
INSERT INTO vn.auctionConfig (id,conversionCoefficient,warehouseFk)
VALUES (1,0.6,6);
INSERT INTO vn.payrollComponent
(id, name, isSalaryAgreed, isVariable, isException)
VALUES

View File

@ -21,9 +21,8 @@ BEGIN
SELECT tob.ticketFk, tob.description
FROM vn.ticketObservation tob
JOIN vn.ticketCollection tc ON tc.ticketFk = tob.ticketFk
LEFT JOIN vn.observationType ot ON ot.id = tob.observationTypeFk
WHERE ot.`code` = 'itemPicker'
AND tc.collectionFk = vParamFk OR tc.ticketFk = vParamFk
JOIN vn.observationType ot ON ot.id = tob.observationTypeFk AND ot.`code` = 'itemPicker'
WHERE tc.collectionFk = vParamFk OR tc.ticketFk = vParamFk
)
SELECT t.id ticketFk,
IF(!(vItemPackingTypeFk <=> 'V'), cc.code, CONCAT(SUBSTRING('ABCDEFGH', tc.wagon, 1), '-', tc.`level`)) `level`,

View File

@ -5,22 +5,26 @@ CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`expeditionPallet_buil
vWorkerFk INT,
OUT vPalletFk INT
)
BEGIN
/** Construye un pallet de expediciones.
proc: BEGIN
/**
* Builds an expedition pallet.
*
* Primero comprueba si esas expediciones ya pertenecen a otro pallet,
* en cuyo caso actualiza ese pallet.
* First, it checks if these expeditions already belong to another pallet,
* in which case it returns an error.
*
* @param vExpeditions JSON_ARRAY con esta estructura [exp1, exp2, exp3, ...]
* @param vArcId INT Identificador de arcRead
* @param vWorkerFk INT Identificador de worker
* @param out vPalletFk Identificador de expeditionPallet
* @param vExpeditions JSON_ARRAY with this structure [exp1, exp2, exp3, ...]
* @param vArcId INT Identifier of arcRead
* @param vWorkerFk INT Identifier of worker
* @param out vPalletFk Identifier of expeditionPallet
*/
DECLARE vCounter INT;
DECLARE vExpeditionFk INT;
DECLARE vTruckFk INT;
DECLARE vPrinterFk INT;
DECLARE vExpeditionStateTypeFk INT;
DECLARE vFreeExpeditionCount INT;
DECLARE vExpeditionWithPallet INT;
CREATE OR REPLACE TEMPORARY TABLE tExpedition (
expeditionFk INT,
@ -44,48 +48,63 @@ BEGIN
WHERE e.id = vExpeditionFk;
END WHILE;
SELECT palletFk INTO vPalletFk
FROM (
SELECT palletFk, count(*) n
FROM tExpedition
WHERE palletFk > 0
GROUP BY palletFk
ORDER BY n DESC
LIMIT 100
) sub
LIMIT 1;
SELECT COUNT(expeditionFk) INTO vFreeExpeditionCount
FROM tExpedition
WHERE palletFk IS NULL;
IF vPalletFk IS NULL THEN
SELECT roadmapStopFk INTO vTruckFk
FROM (
SELECT rm.roadmapStopFk, count(*) n
FROM routesMonitor rm
JOIN tExpedition e ON e.routeFk = rm.routeFk
GROUP BY roadmapStopFk
ORDER BY n DESC
LIMIT 1
) sub;
SELECT COUNT(expeditionFk) INTO vExpeditionWithPallet
FROM tExpedition
WHERE palletFk;
IF vTruckFk IS NULL THEN
CALL util.throw ('TRUCK_NOT_AVAILABLE');
END IF;
INSERT INTO expeditionPallet SET truckFk = vTruckFk;
SET vPalletFk = LAST_INSERT_ID();
IF vExpeditionWithPallet THEN
UPDATE arcRead
SET error = (
SELECT GROUP_CONCAT(expeditionFk SEPARATOR ', ')
FROM tExpedition
WHERE palletFk
)
WHERE id = vArcId;
LEAVE proc;
END IF;
IF NOT vFreeExpeditionCount THEN
CALL util.throw ('NO_FREE_EXPEDITIONS');
END IF;
SELECT roadmapStopFk INTO vTruckFk
FROM (
SELECT rm.roadmapStopFk, count(*) n
FROM routesMonitor rm
JOIN tExpedition e ON e.routeFk = rm.routeFk
WHERE e.palletFk IS NULL
GROUP BY roadmapStopFk
ORDER BY n DESC
LIMIT 1
) sub;
IF vTruckFk IS NULL THEN
CALL util.throw ('TRUCK_NOT_AVAILABLE');
END IF;
INSERT INTO expeditionPallet SET truckFk = vTruckFk;
SET vPalletFk = LAST_INSERT_ID();
INSERT INTO expeditionScan(expeditionFk, palletFk, workerFk)
SELECT expeditionFk, vPalletFk, vWorkerFk
FROM tExpedition
ON DUPLICATE KEY UPDATE palletFk = vPalletFk, workerFk = vWorkerFk;
WHERE palletFk IS NULL;
SELECT id INTO vExpeditionStateTypeFk
FROM expeditionStateType
WHERE code = 'PALLETIZED';
INSERT INTO expeditionState(expeditionFk, typeFk)
SELECT expeditionFk, vExpeditionStateTypeFk FROM tExpedition;
SELECT expeditionFk, vExpeditionStateTypeFk
FROM tExpedition
WHERE palletFk IS NULL;
UPDATE arcRead SET error = NULL WHERE id = vArcId;
SELECT printerFk INTO vPrinterFk FROM arcRead WHERE id = vArcId;

View File

@ -8,19 +8,22 @@ CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`item_getSimilar`(
)
BEGIN
/**
* Propone articulos ordenados, con la cantidad
* de veces usado y segun sus caracteristicas.
*
* @param vSelf Id de artículo
* @param vWarehouseFk Id de almacen
* @param vDated Fecha
* @param vShowType Mostrar tipos
* @param vDaysInForward Días de alcance para las ventas
*/
* Propone articulos ordenados, con la cantidad
* de veces usado y segun sus caracteristicas.
*
* @param vSelf Id de artículo
* @param vWarehouseFk Id de almacen
* @param vDated Fecha
* @param vShowType Mostrar tipos
* @param vDaysInForward Días de alcance para las ventas (https://redmine.verdnatura.es/issues/7956#note-4)
*/
DECLARE vAvailableCalcFk INT;
DECLARE vVisibleCalcFk INT;
DECLARE vTypeFk INT;
DECLARE vPriority INT DEFAULT 1;
CALL cache.available_refresh(vAvailableCalcFk, FALSE, vWarehouseFk, vDated);
CALL cache.visible_refresh(vVisibleCalcFk, FALSE, vWarehouseFk);
WITH itemTags AS (
SELECT i.id,
@ -40,25 +43,9 @@ BEGIN
AND it.priority = vPriority
LEFT JOIN vn.tag t ON t.id = it.tagFk
WHERE i.id = vSelf
),
stock AS (
SELECT itemFk, SUM(visible) stock
FROM vn.itemShelvingStock
WHERE warehouseFk = vWarehouseFk
GROUP BY itemFk
),
sold AS (
SELECT SUM(s.quantity) quantity, s.itemFk
FROM vn.sale s
JOIN vn.ticket t ON t.id = s.ticketFk
LEFT JOIN vn.itemShelvingSale iss ON iss.saleFk = s.id
WHERE t.shipped >= CURDATE() + INTERVAL vDaysInForward DAY
AND iss.saleFk IS NULL
AND t.warehouseFk = vWarehouseFk
GROUP BY s.itemFk
)
SELECT i.id itemFk,
LEAST(CAST(sd.quantity AS INT), sk.stock) advanceable,
NULL advanceable, -- https://redmine.verdnatura.es/issues/7956#note-4
i.longName,
i.subName,
i.tag5,
@ -80,13 +67,13 @@ BEGIN
WHEN b.groupingMode = 'packing' THEN b.packing
ELSE 1
END minQuantity,
sk.stock located,
v.visible located,
b.price2
FROM vn.item i
LEFT JOIN sold sd ON sd.itemFk = i.id
JOIN cache.available a ON a.item_id = i.id
AND a.calc_id = vAvailableCalcFk
LEFT JOIN stock sk ON sk.itemFk = i.id
LEFT JOIN cache.visible v ON v.item_id = i.id
AND v.calc_id = vVisibleCalcFk
LEFT JOIN cache.last_buy lb ON lb.item_id = i.id
AND lb.warehouse_id = vWarehouseFk
LEFT JOIN vn.itemProposal ip ON ip.mateFk = i.id
@ -96,21 +83,20 @@ BEGIN
LEFT JOIN vn.tag t ON t.id = it.tagFk
LEFT JOIN vn.buy b ON b.id = lb.buy_id
JOIN itemTags its
WHERE (a.available > 0 OR sd.quantity < sk.stock)
WHERE a.available > 0
AND (i.typeFk = its.typeFk OR NOT vShowType)
AND i.id <> vSelf
ORDER BY (a.available > 0) DESC,
`counter` DESC,
(t.name = its.name) DESC,
(it.value = its.value) DESC,
(i.tag5 = its.tag5) DESC,
match5 DESC,
(i.tag6 = its.tag6) DESC,
match6 DESC,
(i.tag7 = its.tag7) DESC,
match7 DESC,
(i.tag8 = its.tag8) DESC,
match8 DESC
ORDER BY `counter` DESC,
(t.name = its.name) DESC,
(it.value = its.value) DESC,
(i.tag5 = its.tag5) DESC,
match5 DESC,
(i.tag6 = its.tag6) DESC,
match6 DESC,
(i.tag7 = its.tag7) DESC,
match7 DESC,
(i.tag8 = its.tag8) DESC,
match8 DESC
LIMIT 100;
END$$
DELIMITER ;

View File

@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS `vn`.`itemStateTag` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name_UNIQUE` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT
CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
INSERT IGNORE INTO `vn`.`itemStateTag` (`name`) VALUES ('Artificial');
INSERT IGNORE INTO `vn`.`itemStateTag` (`name`) VALUES ('Inactivo');
INSERT IGNORE INTO `vn`.`itemStateTag` (`name`) VALUES ('Preservado');
INSERT IGNORE INTO `vn`.`itemStateTag` (`name`) VALUES ('Seco');
INSERT IGNORE INTO `vn`.`itemStateTag` (`name`) VALUES ('Seco y preservado');
INSERT IGNORE INTO `vn`.`itemStateTag` (`name`) VALUES ('Estabilizada');
INSERT IGNORE INTO `vn`.`itemStateTag` (`name`) VALUES ('Natural y seco');
INSERT IGNORE INTO `vn`.`itemStateTag` (`name`) VALUES ('Preservado y artificial');
INSERT IGNORE INTO `vn`.`itemStateTag` (`name`) VALUES ('Usado');
UPDATE vn.tag
SET isFree=0,
sourceTable='itemStateTag'
WHERE name= 'Estado';

View File

@ -1,46 +0,0 @@
import getBrowser from '../../helpers/puppeteer';
const $ = {
id: 'vn-order-summary vn-one:nth-child(1) > vn-label-value:nth-child(1) span',
alias: 'vn-order-summary vn-one:nth-child(1) > vn-label-value:nth-child(2) span',
consignee: 'vn-order-summary vn-one:nth-child(2) > vn-label-value:nth-child(6) span',
subtotal: 'vn-order-summary vn-one.taxes > p:nth-child(1)',
vat: 'vn-order-summary vn-one.taxes > p:nth-child(2)',
total: 'vn-order-summary vn-one.taxes > p:nth-child(3)',
sale: 'vn-order-summary vn-tbody > vn-tr',
};
describe('Order summary path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'order');
await page.accessToSearchResult('16');
});
afterAll(async() => {
await browser.close();
});
it('should reach the order summary section and check data', async() => {
await page.waitForState('order.card.summary');
const id = await page.innerText($.id);
const alias = await page.innerText($.alias);
const consignee = await page.innerText($.consignee);
const subtotal = await page.innerText($.subtotal);
const vat = await page.innerText($.vat);
const total = await page.innerText($.total);
const sale = await page.countElement($.sale);
expect(id).toEqual('16');
expect(alias).toEqual('Many places');
expect(consignee).toEqual('address 26 - Gotham (Province one)');
expect(subtotal.length).toBeGreaterThan(1);
expect(vat.length).toBeGreaterThan(1);
expect(total.length).toBeGreaterThan(1);
expect(sale).toBeGreaterThan(0);
});
});

View File

@ -1,69 +0,0 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
const $ = {
form: 'vn-order-basic-data form',
observation: 'vn-order-basic-data form [vn-name="note"]',
saveButton: `vn-order-basic-data form button[type=submit]`,
acceptButton: '.vn-confirm.shown button[response="accept"]'
};
describe('Order edit basic data path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'order');
await page.accessToSearchResult('1');
await page.accessToSection('order.card.basicData');
});
afterAll(async() => {
await browser.close();
});
describe('when confirmed order', () => {
it('should not be able to change the client', async() => {
const message = await page.sendForm($.form, {
client: 'Tony Stark',
address: 'Tony Stark',
});
expect(message.text).toContain(`You can't make changes on the basic data`);
});
});
describe('when new order', () => {
it('should create an order and edit its basic data', async() => {
await page.waitToClick(selectors.globalItems.returnToModuleIndexButton);
await page.waitToClick($.acceptButton);
await page.waitForContentLoaded();
await page.waitToClick(selectors.ordersIndex.createOrderButton);
await page.waitForState('order.create');
await page.autocompleteSearch(selectors.createOrderView.client, 'Jessica Jones');
await page.pickDate(selectors.createOrderView.landedDatePicker);
await page.autocompleteSearch(selectors.createOrderView.agency, 'Other agency');
await page.waitToClick(selectors.createOrderView.createButton);
await page.waitForState('order.card.catalog');
await page.accessToSection('order.card.basicData');
const values = {
client: 'Tony Stark',
address: 'Tony Stark',
agencyMode: 'Other agency'
};
const message = await page.sendForm($.form, values);
await page.reloadSection('order.card.basicData');
const formValues = await page.fetchForm($.form, Object.keys(values));
expect(message.isSuccess).toBeTrue();
expect(formValues).toEqual(values);
});
});
});

View File

@ -1,48 +0,0 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Order lines', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'order');
await page.accessToSearchResult('8');
await page.accessToSection('order.card.line');
});
afterAll(async() => {
await browser.close();
});
it('should check the order subtotal', async() => {
const result = await page
.waitToGetProperty(selectors.orderLine.orderSubtotal, 'innerText');
expect(result).toContain('112.30');
});
it('should delete the first line in the order', async() => {
await page.waitToClick(selectors.orderLine.firstLineDeleteButton);
await page.waitToClick(selectors.orderLine.confirmButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
});
it('should confirm the order subtotal has changed', async() => {
await page.waitForTextInElement(selectors.orderLine.orderSubtotal, '92.80');
const result = await page
.waitToGetProperty(selectors.orderLine.orderSubtotal, 'innerText');
expect(result).toContain('92.80');
});
it('should confirm the whole order and redirect to ticket index filtered by clientFk', async() => {
await page.waitToClick(selectors.orderLine.confirmOrder);
await page.expectURL('ticket/index');
await page.expectURL('clientFk');
});
});

View File

@ -1,97 +0,0 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Order catalog', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'order');
});
afterAll(async() => {
await browser.close();
});
it('should open the create new order form', async() => {
await page.waitToClick(selectors.ordersIndex.createOrderButton);
await page.waitForState('order.create');
});
it('should create a new order', async() => {
await page.autocompleteSearch(selectors.createOrderView.client, 'Tony Stark');
await page.pickDate(selectors.createOrderView.landedDatePicker);
await page.autocompleteSearch(selectors.createOrderView.agency, 'Other agency');
await page.waitToClick(selectors.createOrderView.createButton);
await page.waitForState('order.card.catalog');
});
it('should add the realm and type filters and obtain results', async() => {
await page.waitToClick(selectors.orderCatalog.plantRealmButton);
await page.autocompleteSearch(selectors.orderCatalog.type, 'Anthurium');
await page.waitForNumberOfElements('section.product', 4);
const result = await page.countElement('section.product');
expect(result).toEqual(4);
});
it('should perfom an "OR" search for the item tag colors silver and brown', async() => {
await page.waitToClick(selectors.orderCatalog.openTagSearch);
await page.autocompleteSearch(selectors.orderCatalog.tag, 'Color');
await page.autocompleteSearch(selectors.orderCatalog.firstTagAutocomplete, 'silver');
await page.waitToClick(selectors.orderCatalog.addTagButton);
await page.autocompleteSearch(selectors.orderCatalog.secondTagAutocomplete, 'brown');
await page.waitToClick(selectors.orderCatalog.searchTagButton);
await page.waitForNumberOfElements('section.product', 4);
});
it('should perfom an "OR" search for the item tag tallos 2 and 9', async() => {
await page.waitToClick(selectors.orderCatalog.openTagSearch);
await page.autocompleteSearch(selectors.orderCatalog.tag, 'Tallos');
await page.write(selectors.orderCatalog.firstTagValue, '2');
await page.waitToClick(selectors.orderCatalog.addTagButton);
await page.write(selectors.orderCatalog.secondTagValue, '9');
await page.waitToClick(selectors.orderCatalog.searchTagButton);
await page.waitForNumberOfElements('section.product', 2);
});
it('should perform a general search for category', async() => {
await page.write(selectors.orderCatalog.itemTagValue, 'concussion');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements('section.product', 2);
});
it('should perfom an "AND" search for the item tag tallos 2', async() => {
await page.waitToClick(selectors.orderCatalog.openTagSearch);
await page.autocompleteSearch(selectors.orderCatalog.tag, 'Tallos');
await page.write(selectors.orderCatalog.firstTagValue, '2');
await page.waitToClick(selectors.orderCatalog.searchTagButton);
await page.waitForNumberOfElements('section.product', 1);
});
it('should remove the tag filters and have 4 results', async() => {
await page.waitForContentLoaded();
await page.waitToClick(selectors.orderCatalog.sixthFilterRemoveButton);
await page.waitForContentLoaded();
await page.waitToClick(selectors.orderCatalog.fifthFilterRemoveButton);
await page.waitForContentLoaded();
await page.waitToClick(selectors.orderCatalog.fourthFilterRemoveButton);
await page.waitForContentLoaded();
await page.waitToClick(selectors.orderCatalog.thirdFilterRemoveButton);
await page.waitForNumberOfElements('.product', 4);
const result = await page.countElement('section.product');
expect(result).toEqual(4);
});
it('should search for an item by id', async() => {
await page.accessToSearchResult('2');
await page.waitForNumberOfElements('section.product', 1);
const result = await page.countElement('section.product');
expect(result).toEqual(1);
});
});

View File

@ -1,34 +0,0 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Order Index', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'order');
});
afterAll(async() => {
await browser.close();
});
it(`should check the second search result doesn't contain a total of 0€`, async() => {
await page.waitToClick(selectors.globalItems.searchButton);
const result = await page.waitToGetProperty(selectors.ordersIndex.secondSearchResultTotal, 'innerText');
expect(result).not.toContain('0.00');
});
it('should search including empty orders', async() => {
await page.waitToClick(selectors.ordersIndex.openAdvancedSearch);
await page.waitToClick(selectors.ordersIndex.advancedSearchShowEmptyCheckbox);
await page.waitToClick(selectors.ordersIndex.advancedSearchButton);
await page.waitForTextInElement(selectors.ordersIndex.secondSearchResultTotal, '0.00');
const result = await page.waitToGetProperty(selectors.ordersIndex.secondSearchResultTotal, 'innerText');
expect(result).toContain('0.00');
});
});

View File

@ -66,10 +66,16 @@ export default class App {
]}
};
if (this.logger.$params.q)
newRoute = newRoute.concat(`?table=${this.logger.$params.q}`);
if (this.logger.$params.q) {
let tableValue = this.logger.$params.q;
const q = JSON.parse(tableValue);
if (typeof q === 'number')
tableValue = JSON.stringify({id: tableValue});
newRoute = newRoute.concat(`?table=${tableValue}`);
}
if (this.logger.$params.id && newRoute.indexOf(this.logger.$params.id) < 0)
newRoute = newRoute.concat(`${this.logger.$params.id}`);
return this.logger.$http.get('Urls/findOne', {filter})
.then(res => {

View File

@ -54,7 +54,7 @@
<th field="clientFk">
<span translate>Client</span>
</th>
<th>
<th field="isWorker">
<span translate>Es trabajador</span>
</th>
<th field="salesPersonFk">

View File

@ -57,6 +57,11 @@ export default class Controller extends Section {
field: 'observation',
searchable: false
},
{
field: 'isWorker',
checkbox: true,
},
{
field: 'created',
datepicker: true
@ -73,9 +78,6 @@ export default class Controller extends Section {
set defaulters(value) {
if (!value || !value.length) return;
for (let defaulter of value)
defaulter.isWorker = defaulter.businessTypeFk === 'worker';
this._defaulters = value;
}
@ -164,6 +166,8 @@ export default class Controller extends Section {
exprBuilder(param, value) {
switch (param) {
case 'isWorker':
return {isWorker: value};
case 'creditInsurance':
case 'amount':
case 'clientFk':

View File

@ -39,7 +39,7 @@ describe('Entry filter()', () => {
const result = await models.Entry.filter(ctx, options);
expect(result.length).toEqual(11);
expect(result.length).toEqual(12);
await tx.rollback();
} catch (e) {
@ -152,7 +152,7 @@ describe('Entry filter()', () => {
const result = await models.Entry.filter(ctx, options);
expect(result.length).toEqual(10);
expect(result.length).toEqual(11);
await tx.rollback();
} catch (e) {

View File

@ -45,8 +45,8 @@ module.exports = Self => {
i.id itemFk,
i.name itemName,
ti.quantity,
(ac.conversionCoefficient * (ti.quantity / b.packing) * buy_getVolume(b.id))
/ (vc.trolleyM3 * 1000000) volume,
ROUND((ac.conversionCoefficient * (ti.quantity / b.packing) * buy_getVolume(b.id))
/ (vc.trolleyM3 * 1000000),1) volume,
b.packagingFk packagingFk,
b.packing
FROM tmp.item ti

View File

@ -0,0 +1,3 @@
<slot-descriptor>
<vn-entry-descriptor></vn-entry-descriptor>
</slot-descriptor>

View File

@ -0,0 +1,9 @@
import ngModule from '../module';
import DescriptorPopover from 'salix/components/descriptor-popover';
class Controller extends DescriptorPopover {}
ngModule.vnComponent('vnEntryDescriptorPopover', {
slotTemplate: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,65 @@
<vn-descriptor-content
module="entry"
description="$ctrl.entry.supplier.nickname"
summary="$ctrl.$.summary">
<slot-menu>
<vn-item
ng-click="$ctrl.showEntryReport()"
translate>
Show entry report
</vn-item>
</slot-menu>
<slot-body>
<div class="attributes">
<vn-label-value label="Agency "
value="{{$ctrl.entry.travel.agency.name}}">
</vn-label-value>
<vn-label-value label="Landed"
value="{{$ctrl.entry.travel.landed | date: 'dd/MM/yyyy'}}">
</vn-label-value>
<vn-label-value label="Warehouse Out"
value="{{$ctrl.entry.travel.warehouseOut.name}}">
</vn-label-value>
</div>
<div class="icons">
<vn-icon
vn-tooltip="Is inventory entry"
icon="icon-inventory"
ng-if="$ctrl.entry.isExcludedFromAvailable">
</vn-icon>
<vn-icon
vn-tooltip="Is virtual entry"
icon="icon-net"
ng-if="$ctrl.entry.isRaid">
</vn-icon>
</div>
<div class="quicklinks">
<div ng-transclude="btnOne">
<vn-quick-link
tooltip="Supplier card"
state="['supplier.index', {q: $ctrl.entry.supplier.id }]"
icon="icon-supplier">
</vn-quick-link>
</div>
<div ng-transclude="btnTwo">
<vn-quick-link
tooltip="All travels with current agency"
state="['travel.index', {q: $ctrl.travelFilter}]"
icon="local_airport">
</vn-quick-link>
</div>
<div ng-transclude="btnThree">
<vn-quick-link
tooltip="All entries with current supplier"
state="['entry.index', {q: $ctrl.entryFilter}]"
icon="icon-entry">
</vn-quick-link>
</div>
<div ng-transclude="btnThree">
</div>
</div>
</slot-body>
</vn-descriptor-content>
<vn-popup vn-id="summary">
<vn-entry-summary entry="$ctrl.entry"></vn-entry-summary>
</vn-popup>

View File

@ -0,0 +1,99 @@
import ngModule from '../module';
import Descriptor from 'salix/components/descriptor';
class Controller extends Descriptor {
get entry() {
return this.entity;
}
set entry(value) {
this.entity = value;
}
get travelFilter() {
let travelFilter;
const entryTravel = this.entry && this.entry.travel;
if (entryTravel && entryTravel.agencyModeFk) {
travelFilter = this.entry && JSON.stringify({
agencyModeFk: entryTravel.agencyModeFk
});
}
return travelFilter;
}
get entryFilter() {
let entryTravel = this.entry && this.entry.travel;
if (!entryTravel || !entryTravel.landed) return null;
const date = new Date(entryTravel.landed);
date.setHours(0, 0, 0, 0);
const from = new Date(date.getTime());
from.setDate(from.getDate() - 10);
const to = new Date(date.getTime());
to.setDate(to.getDate() + 10);
return JSON.stringify({
supplierFk: this.entry.supplierFk,
from,
to
});
}
loadData() {
const filter = {
include: [
{
relation: 'travel',
scope: {
fields: ['id', 'landed', 'agencyModeFk', 'warehouseOutFk'],
include: [
{
relation: 'agency',
scope: {
fields: ['name']
}
},
{
relation: 'warehouseOut',
scope: {
fields: ['name']
}
},
{
relation: 'warehouseIn',
scope: {
fields: ['name']
}
}
]
}
},
{
relation: 'supplier',
scope: {
fields: ['id', 'nickname']
}
}
]
};
return this.getData(`Entries/${this.id}`, {filter})
.then(res => this.entity = res.data);
}
showEntryReport() {
this.vnReport.show(`Entries/${this.id}/entry-order-pdf`);
}
}
ngModule.vnComponent('vnEntryDescriptor', {
template: require('./index.html'),
controller: Controller,
bindings: {
entry: '<'
}
});

View File

@ -1,3 +1,6 @@
export * from './module';
import './main';
import './descriptor';
import './descriptor-popover';
import './summary';

View File

@ -8,6 +8,12 @@
"main": [
{"state": "entry.index", "icon": "icon-entry"},
{"state": "entry.latestBuys", "icon": "contact_support"}
],
"card": [
{"state": "entry.card.basicData", "icon": "settings"},
{"state": "entry.card.buy.index", "icon": "icon-lines"},
{"state": "entry.card.observation", "icon": "insert_drive_file"},
{"state": "entry.card.log", "icon": "history"}
]
},
"keybindings": [
@ -27,6 +33,90 @@
"component": "vn-entry-index",
"description": "Entries",
"acl": ["buyer", "administrative"]
},
{
"url": "/latest-buys?q",
"state": "entry.latestBuys",
"component": "vn-entry-latest-buys",
"description": "Latest buys",
"acl": ["buyer", "administrative"]
},
{
"url": "/create?supplierFk&travelFk&companyFk",
"state": "entry.create",
"component": "vn-entry-create",
"description": "New entry",
"acl": ["buyer", "administrative"]
},
{
"url": "/:id",
"state": "entry.card",
"abstract": true,
"component": "vn-entry-card"
},
{
"url": "/summary",
"state": "entry.card.summary",
"component": "vn-entry-summary",
"description": "Summary",
"params": {
"entry": "$ctrl.entry"
},
"acl": ["buyer", "administrative"]
},
{
"url": "/basic-data",
"state": "entry.card.basicData",
"component": "vn-entry-basic-data",
"description": "Basic data",
"params": {
"entry": "$ctrl.entry"
},
"acl": ["buyer", "administrative"]
},
{
"url": "/observation",
"state": "entry.card.observation",
"component": "vn-entry-observation",
"description": "Notes",
"params": {
"entry": "$ctrl.entry"
},
"acl": ["buyer", "administrative"]
},
{
"url" : "/log",
"state": "entry.card.log",
"component": "vn-entry-log",
"description": "Log",
"acl": ["buyer", "administrative"]
},
{
"url": "/buy",
"state": "entry.card.buy",
"abstract": true,
"component": "ui-view",
"acl": ["buyer"]
},
{
"url" : "/index",
"state": "entry.card.buy.index",
"component": "vn-entry-buy-index",
"description": "Buys",
"params": {
"entry": "$ctrl.entry"
},
"acl": ["buyer", "administrative"]
},
{
"url" : "/import",
"state": "entry.card.buy.import",
"component": "vn-entry-buy-import",
"description": "Import buys",
"params": {
"entry": "$ctrl.entry"
},
"acl": ["buyer"]
}
]
}

View File

@ -0,0 +1,195 @@
<vn-crud-model
vn-id="buysModel"
url="Entries/{{$ctrl.entry.id}}/getBuys"
limit="5"
data="buys"
auto-load="true">
</vn-crud-model>
<vn-card class="summary">
<h5>
<a ng-if="::$ctrl.entryData.id"
vn-tooltip="Go to the entry"
ui-sref="entry.card.summary({id: {{::$ctrl.entryData.id}}})"
name="goToSummary">
<vn-icon-button icon="launch"></vn-icon-button>
</a>
<span> #{{$ctrl.entryData.id}} - {{$ctrl.entryData.supplier.nickname}}</span>
</h5>
<vn-horizontal>
<vn-one>
<vn-label-value label="Commission"
value="{{$ctrl.entryData.commission}}">
</vn-label-value>
<vn-label-value label="Currency"
value="{{$ctrl.entryData.currency.name}}">
</vn-label-value>
<vn-label-value label="Company"
value="{{$ctrl.entryData.company.code}}">
</vn-label-value>
<vn-label-value label="Reference"
value="{{$ctrl.entryData.reference}}">
</vn-label-value>
<vn-label-value label="Invoice number"
value="{{$ctrl.entryData.invoiceNumber}}">
</vn-label-value>
</vn-one>
<vn-one>
<vn-label-value label="Reference">
<span
ng-click="travelDescriptor.show($event, $ctrl.entryData.travel.id)"
class="link">
{{$ctrl.entryData.travel.ref}}
</span>
</vn-label-value>
<vn-label-value label="Agency"
value="{{$ctrl.entryData.travel.agency.name}}">
</vn-label-value>
<vn-label-value label="Shipped"
value="{{$ctrl.entryData.travel.shipped | date: 'dd/MM/yyyy'}}">
</vn-label-value>
<vn-label-value label="Warehouse Out"
value="{{$ctrl.entryData.travel.warehouseOut.name}}">
</vn-label-value>
<vn-check
label="Delivered"
ng-model="$ctrl.entryData.travel.isDelivered"
disabled="true">
</vn-check>
<vn-label-value label="Landed"
value="{{$ctrl.entryData.travel.landed | date: 'dd/MM/yyyy'}}">
</vn-label-value>
<vn-label-value label="Warehouse In"
value="{{$ctrl.entryData.travel.warehouseIn.name}}">
</vn-label-value>
<vn-check
label="Received"
ng-model="$ctrl.entryData.travel.isReceived"
disabled="true">
</vn-check>
</vn-one>
<vn-one>
<vn-vertical>
<vn-check
label="Ordered"
ng-model="$ctrl.entryData.isOrdered"
disabled="true">
</vn-check>
<vn-check
label="Confirmed"
ng-model="$ctrl.entryData.isConfirmed"
disabled="true">
</vn-check>
<vn-check
label="Booked"
ng-model="$ctrl.entryData.isBooked"
disabled="true">
</vn-check>
<vn-check
label="Raid"
ng-model="$ctrl.entryData.isRaid"
disabled="true">
</vn-check>
<vn-check
label="Inventory"
ng-model="$ctrl.entryData.isExcludedFromAvailable"
disabled="true">
</vn-check>
</vn-vertical>
</vn-one>
</vn-horizontal>
<vn-horizontal>
<vn-auto name="buys">
<h4 translate>Buys</h4>
<table class="vn-table">
<thead>
<tr>
<th translate center field="quantity">Quantity</th>
<th translate center field="sticker">Stickers</th>
<th translate center field="packagingFk">Package</th>
<th translate center field="weight">Weight</th>
<th translate center field="packing">Packing</th>
<th translate center field="grouping">Grouping</th>
<th translate center field="buyingValue">Buying value</th>
<th translate center field="price3">Import</th>
<th translate center expand field="price">PVP</th>
</tr>
</thead>
<tbody ng-repeat="line in buys">
<tr>
<td center title="{{::line.quantity}}">{{::line.quantity}}</td>
<td center title="{{::line.stickers | dashIfEmpty}}">{{::line.stickers | dashIfEmpty}}</td>
<td center title="{{::line.packagingFk | dashIfEmpty}}">{{::line.packagingFk | dashIfEmpty}}</td>
<td center title="{{::line.weight}}">{{::line.weight}}</td>
<td center>
<vn-chip class="transparent" translate-attr="line.groupingMode == 'packing' ? {title: 'Minimun amount'} : {title: 'Packing'}" ng-class="{'message': line.groupingMode == 'packing'}">
<span>{{::line.packing | dashIfEmpty}}</span>
</vn-chip>
</td>
<td center>
<vn-chip class="transparent" translate-attr="line.groupingMode == 'grouping' ? {title: 'Minimun amount'} : {title: 'Grouping'}" ng-class="{'message': line.groupingMode == 'grouping'}">
<span>{{::line.grouping | dashIfEmpty}}</span>
</vn-chip>
</vn-td>
<td center title="{{::line.buyingValue | currency: 'EUR':2}}">{{::line.buyingValue | currency: 'EUR':2}}</td>
<td center title="{{::line.quantity * line.buyingValue | currency: 'EUR':2}}">{{::line.quantity * line.buyingValue | currency: 'EUR':2}}</td>
<td center title="Grouping / Packing">{{::line.price2 | currency: 'EUR':2 | dashIfEmpty}} / {{::line.price3 | currency: 'EUR':2 | dashIfEmpty}}</td>
</tr>
<tr class="dark-row">
<td shrink>
<span
translate-attr="{title: 'Item type'}">
{{::line.item.itemType.code}}
</span>
</td>
<td shrink>
<span
ng-click="itemDescriptor.show($event, line.item.id)"
class="link">
{{::line.item.id}}
</span>
</td>
<td number shrink>
<span
translate-attr="{title: 'Item size'}">
{{::line.item.size}}
</span>
</td>
<td center>
<span
translate-attr="{title: 'Minimum price'}">
{{::line.item.minPrice | currency: 'EUR':2}}
</span>
</td>
<td vn-fetched-tags colspan="6">
<div>
<vn-one title="{{::line.item.concept}}">{{::line.item.name}}</vn-one>
<vn-one ng-if="::line.item.subName">
<h3 title="{{::line.item.subName}}">{{::line.item.subName}}</h3>
</vn-one>
</div>
<vn-fetched-tags
max-length="6"
item="::line.item"
tabindex="-1">
</vn-fetched-tags>
</td>
</tr>
<tr class="empty-row">
<td colspan="10"></td>
</tr>
</tbody>
</table>
<vn-pagination
model="buysModel"
class="vn-pt-xs">
</vn-pagination>
</vn-auto>
</vn-horizontal>
</vn-card>
<vn-item-descriptor-popover
vn-id="item-descriptor"
warehouse-fk="$ctrl.vnConfig.warehouseFk">
</vn-item-descriptor-popover>
<vn-travel-descriptor-popover
vn-id="travelDescriptor">
</vn-travel-descriptor-popover>

View File

@ -0,0 +1,33 @@
import ngModule from '../module';
import './style.scss';
import Summary from 'salix/components/summary';
class Controller extends Summary {
get entry() {
if (!this._entry)
return this.$params;
return this._entry;
}
set entry(value) {
this._entry = value;
if (value && value.id)
this.getEntryData();
}
getEntryData() {
return this.$http.get(`Entries/${this.entry.id}/getEntry`).then(response => {
this.entryData = response.data;
});
}
}
ngModule.vnComponent('vnEntrySummary', {
template: require('./index.html'),
controller: Controller,
bindings: {
entry: '<'
}
});

View File

@ -0,0 +1,30 @@
@import "variables";
vn-entry-summary .summary {
max-width: $width-lg;
.dark-row {
background-color: lighten($color-marginal, 10%);
}
tbody tr:nth-child(1) {
border-top: $border-thin;
}
tbody tr:nth-child(1),
tbody tr:nth-child(2) {
border-left: $border-thin;
border-right: $border-thin
}
tbody tr:nth-child(3) {
height: inherit
}
tr {
margin-bottom: 10px;
}
}
$color-font-link-medium: lighten($color-font-link, 20%)

View File

@ -1,6 +1,6 @@
const {models} = require('vn-loopback/server/server');
describe('item lastEntriesFilter()', () => {
it('should return one entry for the given item', async() => {
it('should return two entry for the given item', async() => {
const minDate = Date.vnNew();
minDate.setHours(0, 0, 0, 0);
const maxDate = Date.vnNew();
@ -13,7 +13,7 @@ describe('item lastEntriesFilter()', () => {
const filter = {where: {itemFk: 1, landed: {between: [minDate, maxDate]}}};
const result = await models.Item.lastEntriesFilter(filter, options);
expect(result.length).toEqual(1);
expect(result.length).toEqual(2);
await tx.rollback();
} catch (e) {
@ -22,7 +22,7 @@ describe('item lastEntriesFilter()', () => {
}
});
it('should return five entries for the given item', async() => {
it('should return six entries for the given item', async() => {
const minDate = Date.vnNew();
minDate.setHours(0, 0, 0, 0);
minDate.setMonth(minDate.getMonth() - 2, 1);
@ -37,7 +37,7 @@ describe('item lastEntriesFilter()', () => {
const filter = {where: {itemFk: 1, landed: {between: [minDate, maxDate]}}};
const result = await models.Item.lastEntriesFilter(filter, options);
expect(result.length).toEqual(5);
expect(result.length).toEqual(6);
await tx.rollback();
} catch (e) {

View File

@ -3,7 +3,7 @@
"name": "Items",
"icon": "icon-item",
"validations" : true,
"dependencies": ["worker", "client", "ticket"],
"dependencies": ["worker", "client", "ticket", "entry"],
"menus": {
"main": [
{"state": "item.index", "icon": "icon-item"},

View File

@ -1,88 +0,0 @@
<mg-ajax path="Orders/{{patch.params.id}}/updateBasicData" options="vnPatch"></mg-ajax>
<vn-crud-model
autoload="true"
url="Addresses"
data="address"
order="nickname"
vn-id="address-model">
</vn-crud-model>
<vn-watcher
vn-id="watcher"
data="$ctrl.order"
form="form"
save="patch">
</vn-watcher>
<form name="form" ng-submit="watcher.submit()" class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-autocomplete
vn-one
url="Clients"
label="Client"
search-function="{or: [{id: $search}, {name: {like: '%'+ $search +'%'}}]}"
show-field="name"
value-field="id"
ng-model="$ctrl.order.clientFk"
vn-name="client"
selection="$ctrl.selection"
fields="['defaultAddressFk']">
<tpl-item>
<div>{{::name}}</div>
<div class="text-secondary text-caption">#{{::id}}</div>
</tpl-item>
</vn-autocomplete>
<vn-autocomplete
vn-one
fields="['id', 'nickname']"
data="address"
label="Address"
search-function="$search"
show-field="nickname"
value-field="id"
ng-model="$ctrl.order.addressFk"
vn-name="address"
on-change="$ctrl.getAvailableAgencies()">
<tpl-item>{{::nickname}}</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-date-picker
vn-one
label="Landed"
ng-model="$ctrl.order.landed"
vn-name="landed"
on-change="$ctrl.getAvailableAgencies()">
</vn-date-picker>
<vn-autocomplete
disabled="!$ctrl.order.addressFk || !$ctrl.order.landed"
data="$ctrl._availableAgencies"
label="Agency"
show-field="agencyMode"
value-field="agencyModeFk"
ng-model="$ctrl.order.agencyModeFk"
vn-name="agencyMode">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-textarea
vn-one
label="Notes"
ng-model="$ctrl.order.note"
vn-name="note"
rule>
</vn-textarea>
</vn-horizontal>
</vn-card>
<vn-button-bar>
<vn-submit
disabled="!watcher.dataChanged()"
label="Save">
</vn-submit>
<vn-button
class="cancel"
label="Undo changes"
disabled="!watcher.dataChanged()"
ng-click="watcher.loadOriginalData()">
</vn-button>
</vn-button-bar>
</form>

View File

@ -1,57 +0,0 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
class Controller extends Section {
constructor($element, $) {
super($element, $);
let isDirty = false;
this.$.$watch('$ctrl.selection', newValue => {
if (newValue) {
this.$.addressModel.where = {clientFk: newValue.id};
this.$.addressModel.refresh();
if (isDirty)
this.order.addressFk = newValue.defaultAddressFk;
isDirty = true;
} else {
this.$.addressModel.clear();
if (isDirty)
this.order.addressFk = null;
}
});
}
set order(value = {}) {
this._order = value;
const agencyModeFk = value.agencyModeFk;
this.getAvailableAgencies();
this._order.agencyModeFk = agencyModeFk;
}
get order() {
return this._order;
}
getAvailableAgencies() {
const order = this.order;
order.agencyModeFk = null;
const params = {
addressFk: order.addressFk,
landed: order.landed
};
if (params.landed && params.addressFk) {
this.$http.get(`Agencies/landsThatDay`, {params})
.then(res => this._availableAgencies = res.data);
}
}
}
ngModule.vnComponent('vnOrderBasicData', {
controller: Controller,
template: require('./index.html'),
bindings: {
order: '<'
}
});

View File

@ -1,67 +0,0 @@
import './index.js';
describe('Order', () => {
describe('Component vnOrderBasicData', () => {
let $httpBackend;
let $httpParamSerializer;
let controller;
let $scope;
beforeEach(ngModule('order'));
beforeEach(inject(($compile, _$httpBackend_, $rootScope, _$httpParamSerializer_) => {
$httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
$scope = $rootScope.$new();
$httpBackend.whenRoute('GET', 'Addresses')
.respond([{id: 2, nickname: 'address 2'}]);
$httpBackend.whenRoute('GET', 'Clients')
.respond([{id: 1, defaultAddressFk: 1}]);
$scope.order = {clientFk: 1, addressFk: 1};
let $element = $compile('<vn-order-basic-data order="order"></vn-order-basic-data>')($scope);
$httpBackend.flush();
controller = $element.controller('vnOrderBasicData');
}));
afterAll(() => {
$scope.$destroy();
$element.remove();
});
describe('constructor()', () => {
it('should update the address after the client changes', async() => {
const addressId = 999;
const id = 444;
controller.selection = {id: id, defaultAddressFk: addressId};
$scope.$digest();
expect(controller.order.addressFk).toEqual(addressId);
});
});
describe('getAvailableAgencies()', () => {
it('should set agencyModeFk to null and get the available agencies if the order has landed and client', async() => {
controller.order.agencyModeFk = 999;
controller.order.addressFk = 999;
controller.order.landed = Date.vnNew();
const expectedAgencies = [{id: 1}, {id: 2}];
const paramsObj = {
addressFk: controller.order.addressFk,
landed: controller.order.landed
};
const serializedParams = $httpParamSerializer(paramsObj);
$httpBackend.expect('GET', `Agencies/landsThatDay?${serializedParams}`).respond(expectedAgencies);
controller.getAvailableAgencies();
$httpBackend.flush();
expect(controller.order.agencyModeFk).toBeDefined();
expect(controller._availableAgencies).toEqual(expectedAgencies);
});
});
});
});

View File

@ -1 +0,0 @@
This form has been disabled because there are lines in this order or it's confirmed: Este formulario ha sido deshabilitado por que esta orden contiene líneas o está confirmada

View File

@ -1,9 +0,0 @@
vn-order-basic-data {
.disabledForm {
text-align: center;
color: red;
span {
margin: 0 auto;
}
}
}

View File

@ -1,5 +0,0 @@
<vn-portal slot="menu">
<vn-order-descriptor order="$ctrl.order"></vn-order-descriptor>
<vn-left-menu source="card"></vn-left-menu>
</vn-portal>
<ui-view></ui-view>

View File

@ -1,58 +0,0 @@
import ngModule from '../module';
import ModuleCard from 'salix/components/module-card';
class Controller extends ModuleCard {
reload() {
let filter = {
include: [
{
relation: 'agencyMode',
scope: {
fields: ['name']
}
}, {
relation: 'address',
scope: {
fields: ['nickname']
}
}, {
relation: 'rows',
scope: {
fields: ['id']
}
}, {
relation: 'client',
scope: {
fields: [
'salesPersonFk',
'name',
'isActive',
'isFreezed',
'isTaxDataChecked'
],
include: {
relation: 'salesPersonUser',
scope: {
fields: ['id', 'name']
}
}
}
}
]
};
return this.$q.all([
this.$http.get(`Orders/${this.$params.id}`, {filter})
.then(res => this.order = res.data),
this.$http.get(`Orders/${this.$params.id}/getTotal`)
.then(res => ({total: res.data}))
]).then(res => {
this.order = Object.assign.apply(null, res);
});
}
}
ngModule.vnComponent('vnOrderCard', {
template: require('./index.html'),
controller: Controller
});

View File

@ -1,31 +0,0 @@
import './index.js';
describe('Order', () => {
describe('Component vnOrderCard', () => {
let controller;
let $httpBackend;
let data = {id: 1, name: 'fooName'};
let total = 10.5;
beforeEach(ngModule('order'));
beforeEach(inject(($componentController, _$httpBackend_, $stateParams) => {
$httpBackend = _$httpBackend_;
let $element = angular.element('<div></div>');
controller = $componentController('vnOrderCard', {$element});
$stateParams.id = data.id;
$httpBackend.whenRoute('GET', 'Orders/:id').respond(data);
$httpBackend.whenRoute('GET', 'Orders/:id/getTotal').respond(200, total);
}));
it('should request data and total, merge them, and set it on the controller', () => {
controller.reload();
$httpBackend.flush();
expect(controller.order).toEqual(Object.assign({}, data, {total}));
});
});
});

View File

@ -1,54 +0,0 @@
<div class="vn-pa-lg" style="min-width: 18em">
<form name="form" ng-submit="$ctrl.onSearch()">
<vn-horizontal>
<vn-autocomplete
vn-id="tag"
vn-one
selection="filter.tagSelection"
ng-model="filter.tagFk"
data="$ctrl.resultTags"
show-field="name"
label="Tag"
on-change="itemTag.value = null">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal ng-repeat="tagValue in filter.values">
<vn-textfield
vn-one
ng-show="tag.selection.isFree != false"
vn-id="text"
label="Value"
ng-model="tagValue.value">
</vn-textfield>
<vn-autocomplete
vn-one
ng-show="tag.selection.isFree == false"
url="{{'Tags/' + tag.selection.id + '/filterValue'}}"
search-function="{value: $search}"
label="Value"
ng-model="tagValue.value"
show-field="value"
value-field="value">
</vn-autocomplete>
<vn-icon-button
vn-none
vn-tooltip="Remove tag"
icon="delete"
ng-click="filter.values.splice($index, 1)"
tabindex="-1">
</vn-icon-button>
</vn-horizontal>
<vn-horizontal>
<vn-icon-button
vn-none
vn-bind="+"
vn-tooltip="Add value"
icon="add_circle"
ng-click="$ctrl.addValue()">
</vn-icon-button>
</vn-horizontal>
<vn-horizontal class="vn-mt-lg">
<vn-submit label="Search"></vn-submit>
</vn-horizontal>
</form>
</div>

View File

@ -1,38 +0,0 @@
import ngModule from '../module';
import SearchPanel from 'core/components/searchbar/search-panel';
class Controller extends SearchPanel {
constructor($element, $) {
super($element, $);
this.filter = {};
}
get filter() {
return this.$.filter;
}
set filter(value) {
if (!value)
value = {};
if (!value.values)
value.values = [{}];
this.$.filter = value;
}
addValue() {
this.filter.values.push({});
setTimeout(() => this.parentPopover.relocate());
}
}
ngModule.vnComponent('vnOrderCatalogSearchPanel', {
template: require('./index.html'),
controller: Controller,
bindings: {
onSubmit: '&?',
parentPopover: '<?',
resultTags: '<?'
}
});

View File

@ -1,81 +0,0 @@
<vn-data-viewer
model="$ctrl.model">
<vn-horizontal class="catalog-list">
<section ng-repeat="item in $ctrl.model.data" class="product">
<vn-card>
<div class="image">
<div ng-if="::item.hex != null" class="item-color-background">
<div class="item-color" ng-style="{'background-color': '#' + item.hex}"></div>
</div>
<img
ng-src="{{::$root.imagePath('catalog', '200x200', item.id)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', item.id)}}"
on-error-src/>
</div>
<div class="description">
<h3 class="link"
ng-click="itemDescriptor.show($event, item.id)">
{{::item.name}}
</h3>
<h4 class="ellipsize">
<span translate-attr="::{title: item.subName}">{{::item.subName}}</span>
</h4>
<div class="tags">
<vn-label-value
ng-if="::item.value5"
label="{{::item.tag5}}"
value="{{::item.value5}}">
</vn-label-value>
<vn-label-value
ng-if="::item.value6"
label="{{::item.tag6}}"
value="{{::item.value6}}">
</vn-label-value>
<vn-label-value
ng-if="::item.value7"
label="{{::item.tag7}}"
value="{{::item.value7}}">
</vn-label-value>
</div>
<vn-horizontal
class="text-right text-caption alert vn-mr-xs"
ng-if="::item.minQuantity">
<vn-one>
<vn-icon
icon="production_quantity_limits"
translate-attr="{title: 'Minimal quantity'}"
class="text-subtitle1">
</vn-icon>
</vn-one>
{{::item.minQuantity}}
</vn-horizontal>
<div class="footer">
<div class="price">
<vn-one>
<span>{{::item.available}}</span>
<span translate>to</span>
<span>{{::item.price | currency:'EUR':2}}</span>
</vn-one>
<vn-icon-button vn-none
icon="add_circle"
ng-click="pricesPopover.show($event, item)"
vn-tooltip="Add">
</vn-icon-button>
</div>
<div class="priceKg" ng-show="::item.priceKg">
<span>Precio por kilo {{::item.priceKg | currency: 'EUR'}}</span>
</div>
</div>
</div>
</vn-card>
</section>
</vn-horizontal>
</vn-data-viewer>
<vn-order-prices-popover
vn-id="prices-popover"
order="$ctrl.order">
</vn-order-prices-popover>
<vn-item-descriptor-popover
vn-id="item-descriptor"
warehouse-fk="$ctrl.vnConfig.warehouseFk">
</vn-item-descriptor-popover>

View File

@ -1,12 +0,0 @@
import ngModule from '../module';
import Component from 'core/lib/component';
import './style.scss';
ngModule.vnComponent('vnOrderCatalogView', {
template: require('./index.html'),
controller: Component,
bindings: {
order: '<',
model: '<'
}
});

View File

@ -1 +0,0 @@
Order created: Orden creada

View File

@ -1,50 +0,0 @@
@import "variables";
vn-order-catalog {
.catalog-header {
border-bottom: $border-thin;
padding: $spacing-md;
align-items: center;
& > vn-one {
display: flex;
flex: 1;
span {
color: $color-font-secondary
}
}
& > vn-auto {
width: 448px;
display: flex;
overflow: hidden;
& > * {
padding-left: $spacing-md;
}
}
}
.catalog-list {
padding-top: $spacing-sm;
}
.item-color-background {
background: linear-gradient($color-bg-panel, $color-main);
border-radius: 50%;
margin-left: 140px;
margin-top: 140px;
width: 40px;
height: 40px;
position: absolute;
}
.item-color {
margin: auto;
margin-top: 5px;
border-radius: 50%;
width: 30px;
height: 30px;
position: relative;
}
.alert {
color: $color-alert;
}
}

View File

@ -1,166 +0,0 @@
<vn-crud-model
url="ItemCategories"
data="categories"
auto-load="true">
</vn-crud-model>
<vn-crud-model
vn-id="model"
url="Orders/CatalogFilter"
params="{orderFk: $ctrl.$params.id}"
limit="50"
data="$ctrl.items">
</vn-crud-model>
<vn-portal slot="topbar">
<vn-searchbar vn-id="searchbar"
auto-state="false"
info="Search by item id or name"
on-search="$ctrl.onSearch($params)">
</vn-searchbar>
</vn-portal>
<vn-order-catalog-view
model="model"
order="$ctrl.order">
</vn-order-catalog-view>
<vn-side-menu side="right">
<vn-horizontal class="item-category">
<vn-autocomplete vn-id="category"
data="categories"
ng-model="$ctrl.categoryId"
show-field="name"
value-field="id"
label="Category">
</vn-autocomplete>
<vn-one ng-repeat="category in categories">
<vn-icon
ng-class="{'active': $ctrl.categoryId == category.id}"
icon="{{::category.icon}}"
vn-tooltip="{{::category.name}}"
ng-click="$ctrl.changeCategory(category.id)">
</vn-icon>
</vn-one>
</vn-horizontal>
<vn-vertical class="input">
<vn-autocomplete vn-id="type"
data="$ctrl.itemTypes"
ng-model="$ctrl.typeId"
show-field="name"
value-field="id"
label="Type"
fields="['categoryFk']"
include="'category'">
<tpl-item>
<div>{{name}}</div>
<div class="text-caption text-secondary">
{{categoryName}}
</div>
</tpl-item>
</vn-autocomplete>
</vn-vertical>
<vn-vertical class="input vn-pt-md">
<vn-autocomplete
vn-id="field"
data="$ctrl.orderFields"
ng-model="$ctrl.orderField"
selection="$ctrl.orderSelection"
translate-fields="['name']"
order="priority DESC"
show-field="name"
value-field="field"
label="Order by"
disabled="!model.data">
</vn-autocomplete>
<vn-autocomplete
data="$ctrl.orderWays"
ng-model="$ctrl.orderWay"
translate-fields="['name']"
show-field="name"
value-field="way"
label="Order"
disabled="!model.data">
</vn-autocomplete>
<div ng-if="false && model.moreRows">
<span translate>More than</span> {{model.limit}} <span translate>results</span>
</div>
</vn-vertical>
<vn-vertical class="input vn-pt-md">
<vn-textfield vn-one
vn-id="search"
ng-keyUp="$ctrl.onSearchByTag($event)"
label="Search tag">
<prepend>
<vn-icon icon="search"></vn-icon>
</prepend>
<append>
<vn-icon
icon="keyboard_arrow_down"
ng-click="$ctrl.openPanel($event)"
style="cursor: pointer;">
</vn-icon>
</append>
</vn-textfield>
</vn-vertical>
<vn-popover
vn-id="popover"
on-close="$ctrl.onPopoverClose()">
<vn-order-catalog-search-panel
on-submit="$ctrl.onPanelSubmit($filter)"
parent-popover="popover"
result-tags="$ctrl.resultTags">
</vn-order-catalog-search-panel>
</vn-popover>
<div class="chips">
<vn-chip
ng-if="$ctrl.itemId"
removable="true"
vn-tooltip="Item id"
on-remove="$ctrl.removeItemId()"
class="colored">
<span>Id: {{$ctrl.itemId}}</span>
</vn-chip>
<vn-chip
ng-if="$ctrl.itemName"
removable="true"
vn-tooltip="Item"
on-remove="$ctrl.removeItemName()"
class="colored">
<div>
<span>
<span translate>Name</span>:
</span>
<span>{{$ctrl.itemName}}</span>
</div>
</vn-chip>
<vn-chip
ng-if="category.selection"
removable="true"
vn-tooltip="Category"
on-remove="$ctrl.categoryId = null"
class="colored">
<span translate>{{category.selection.name}}</span>
</vn-chip>
<vn-chip
ng-if="type.selection"
removable="true"
vn-tooltip="Type"
on-remove="$ctrl.typeId = null"
class="colored">
<span translate>{{type.selection.name}}</span>
</vn-chip>
<vn-chip
ng-repeat="tagGroup in $ctrl.tagGroups"
removable="true"
on-remove="$ctrl.remove($index)"
vn-tooltip="{{::$ctrl.formatTooltip(tagGroup)}}"
class="colored">
<div>
<span ng-if="::tagGroup.tagFk">
<span translate>{{::tagGroup.tagSelection.name}}</span>:
</span>
<span ng-repeat="tagValue in tagGroup.values">
<span ng-if="$index > 0">,</span>
<span>"{{::tagValue.value}}"</span>
</span>
</div>
</vn-chip>
</div>
</vn-side-menu>

View File

@ -1,377 +0,0 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
class Controller extends Section {
constructor($element, $) {
super($element, $);
this.itemTypes = [];
this._tagGroups = [];
// Static autocomplete data
this.orderWays = [
{way: 'ASC', name: 'Ascendant'},
{way: 'DESC', name: 'Descendant'},
];
this.defaultOrderFields = [
{field: 'relevancy DESC, name', name: 'Relevancy', priority: 999},
{field: 'showOrder, price', name: 'Color and price', priority: 999},
{field: 'name', name: 'Name', priority: 999},
{field: 'price', name: 'Price', priority: 999}
];
this.orderFields = [].concat(this.defaultOrderFields);
this._orderWay = this.orderWays[0].way;
this.orderField = this.orderFields[0].field;
}
$onChanges() {
this.getData().then(() => {
if (this.order && this.order.isConfirmed)
this.$state.go('order.card.line');
});
}
getData() {
return this.$http.get(`Orders/${this.$params.id}`)
.then(res => this.order = res.data);
}
/**
* Fills order autocomplete with tags
* obtained from last filtered
*/
get order() {
return this._order;
}
/**
* Sets filter values from state params
*
* @param {Object} value - Order data
*/
set order(value) {
this._order = value;
if (!value) return;
this.$.$applyAsync(() => {
if (this.$params.categoryId)
this.categoryId = parseInt(this.$params.categoryId);
if (this.$params.typeId)
this.typeId = parseInt(this.$params.typeId);
if (this.$params.tagGroups)
this.tagGroups = JSON.parse(this.$params.tagGroups);
});
}
get items() {
return this._items;
}
set items(value) {
this._items = value;
if (!value) return;
this.fetchResultTags(value);
this.buildOrderFilter();
}
get categoryId() {
return this._categoryId;
}
set categoryId(value = null) {
this._categoryId = value;
this.itemTypes = [];
this.typeId = null;
this.updateStateParams();
if (this.tagGroups.length > 0)
this.applyFilters();
if (value)
this.updateItemTypes();
}
changeCategory(id) {
if (this._categoryId == id) id = null;
this.categoryId = id;
}
get typeId() {
return this._typeId;
}
set typeId(value) {
this._typeId = value;
this.updateStateParams();
if (value || this.tagGroups.length > 0)
this.applyFilters();
}
get tagGroups() {
return this._tagGroups;
}
set tagGroups(value) {
this._tagGroups = value;
this.updateStateParams();
if (value.length)
this.applyFilters();
}
/**
* Get order way ASC/DESC
*/
get orderWay() {
return this._orderWay;
}
set orderWay(value) {
this._orderWay = value;
if (value) this.applyOrder();
}
/**
* Returns the order way selection
*/
get orderSelection() {
return this._orderSelection;
}
set orderSelection(value) {
this._orderSelection = value;
if (value) this.applyOrder();
}
/**
* Apply order to model
*/
applyOrder() {
if (this.typeId || this.tagGroups.length > 0 || this.itemName)
this.$.model.addFilter(null, {orderBy: this.getOrderBy()});
}
/**
* Returns order param
*
* @return {Object} - Order param
*/
getOrderBy() {
const isTag = !!(this.orderSelection && this.orderSelection.isTag);
return {
field: this.orderField,
way: this.orderWay,
isTag: isTag
};
}
/**
* Refreshes item type dropdown data
*/
updateItemTypes() {
let params = {
itemCategoryId: this.categoryId
};
const query = `Orders/${this.order.id}/getItemTypeAvailable`;
this.$http.get(query, {params}).then(res =>
this.itemTypes = res.data);
}
/**
* Search by tag value
* @param {object} event
*/
onSearchByTag(event) {
const value = this.$.search.value;
if (event.key !== 'Enter' || !value) return;
this.tagGroups.push({values: [{value: value}]});
this.$.search.value = null;
this.updateStateParams();
this.applyFilters();
}
remove(index) {
this.tagGroups.splice(index, 1);
this.updateStateParams();
if (this.tagGroups.length >= 0 || this.itemId || this.typeId)
this.applyFilters();
}
removeItemId() {
this.itemId = null;
this.$.searchbar.doSearch({}, 'bar');
}
removeItemName() {
this.itemName = null;
this.$.searchbar.doSearch({}, 'bar');
}
applyFilters(filter = {}) {
let newParams = {};
let newFilter = Object.assign({}, filter);
const model = this.$.model;
if (this.categoryId)
newFilter.categoryFk = this.categoryId;
if (this.typeId)
newFilter.typeFk = this.typeId;
newParams = {
orderFk: this.$params.id,
orderBy: this.getOrderBy(),
tagGroups: this.tagGroups,
};
return model.applyFilter({where: newFilter}, newParams);
}
openPanel(event) {
if (event.defaultPrevented) return;
event.preventDefault();
this.panelFilter = {};
this.$.popover.show(this.$.search.element);
}
onPanelSubmit(filter) {
this.$.popover.hide();
const values = filter.values;
const nonEmptyValues = values.filter(tagValue => {
return tagValue.value;
});
filter.values = nonEmptyValues;
if (filter.tagFk && nonEmptyValues.length) {
this.tagGroups.push(filter);
this.updateStateParams();
this.applyFilters();
}
}
/**
* Updates url state params from filter values
*/
updateStateParams() {
const params = {};
params.categoryId = undefined;
if (this.categoryId)
params.categoryId = this.categoryId;
params.typeId = undefined;
if (this.typeId)
params.typeId = this.typeId;
params.tagGroups = undefined;
if (this.tagGroups && this.tagGroups.length)
params.tagGroups = JSON.stringify(this.sanitizedTagGroupParam());
this.$state.go(this.$state.current.name, params);
}
sanitizedTagGroupParam() {
const tagGroups = [];
for (let tagGroup of this.tagGroups) {
const tagParam = {values: []};
for (let tagValue of tagGroup.values)
tagParam.values.push({value: tagValue.value});
if (tagGroup.tagFk)
tagParam.tagFk = tagGroup.tagFk;
if (tagGroup.tagSelection) {
tagParam.tagSelection = {
name: tagGroup.tagSelection.name
};
}
tagGroups.push(tagParam);
}
return tagGroups;
}
fetchResultTags(items) {
const resultTags = [];
for (let item of items) {
for (let itemTag of item.tags) {
const alreadyAdded = resultTags.findIndex(tag => {
return tag.tagFk == itemTag.tagFk;
});
if (alreadyAdded == -1)
resultTags.push({...itemTag, priority: 1});
else
resultTags[alreadyAdded].priority += 1;
}
}
this.resultTags = resultTags;
}
buildOrderFilter() {
const filter = [].concat(this.defaultOrderFields);
for (let tag of this.resultTags)
filter.push({...tag, field: tag.id, isTag: true});
this.orderFields = filter;
}
onSearch(params) {
if (!params) return;
this.itemId = null;
this.itemName = null;
if (params.search) {
if (/^\d+$/.test(params.search)) {
this.itemId = params.search;
return this.applyFilters({
'i.id': params.search
});
} else {
this.itemName = params.search;
return this.applyFilters({
'i.name': {like: `%${params.search}%`}
});
}
} else return this.applyFilters();
}
formatTooltip(tagGroup) {
const tagValues = tagGroup.values;
let title = '';
if (tagGroup.tagFk) {
const tagName = tagGroup.tagSelection.name;
title += `${tagName}: `;
}
for (let [i, tagValue] of tagValues.entries()) {
if (i > 0) title += ', ';
title += `"${tagValue.value}"`;
}
return `${title}`;
}
}
ngModule.vnComponent('vnOrderCatalog', {
template: require('./index.html'),
controller: Controller
});

View File

@ -1,386 +0,0 @@
import './index.js';
import crudModel from 'core/mocks/crud-model';
describe('Order', () => {
describe('Component vnOrderCatalog', () => {
let $scope;
let $state;
let controller;
let $httpBackend;
beforeEach(ngModule('order'));
beforeEach(inject(($componentController, _$state_, _$httpBackend_, $rootScope) => {
$httpBackend = _$httpBackend_;
$scope = $rootScope.$new();
$scope.model = crudModel;
$scope.search = {};
$scope.itemId = {};
$state = _$state_;
$state.current.name = 'my.current.state';
const $element = angular.element('<vn-order-catalog></vn-order-catalog>');
controller = $componentController('vnOrderCatalog', {$element, $scope});
controller._order = {id: 4};
controller.$params = {
categoryId: 1,
typeId: 2,
id: 4
};
}));
describe('getData()', () => {
it(`should make a query an fetch the order data`, () => {
controller._order = null;
$httpBackend.expect('GET', `Orders/4`).respond(200, {id: 4, isConfirmed: true});
$httpBackend.expect('GET', `Orders/4/getItemTypeAvailable?itemCategoryId=1`).respond();
controller.getData();
$httpBackend.flush();
const order = controller.order;
expect(order.id).toEqual(4);
expect(order.isConfirmed).toBeTruthy();
});
});
describe('order() setter', () => {
it(`should call scope $applyAsync() method and apply filters from state params`, () => {
$httpBackend.expect('GET', `Orders/4/getItemTypeAvailable?itemCategoryId=1`).respond();
controller.order = {id: 4};
$scope.$apply();
expect(controller.categoryId).toEqual(1);
expect(controller.typeId).toEqual(2);
});
});
describe('items() setter', () => {
it(`should return an object with order params`, () => {
jest.spyOn(controller, 'fetchResultTags');
jest.spyOn(controller, 'buildOrderFilter');
const expectedResult = [{field: 'showOrder, price', name: 'Color and price', priority: 999}];
const items = [{id: 1, name: 'My Item', tags: [
{tagFk: 4, name: 'Length'},
{tagFk: 5, name: 'Color'}
]}];
controller.items = items;
expect(controller.orderFields.length).toEqual(6);
expect(controller.orderFields).toEqual(jasmine.arrayContaining(expectedResult));
expect(controller.fetchResultTags).toHaveBeenCalledWith(items);
expect(controller.buildOrderFilter).toHaveBeenCalledWith();
});
});
describe('categoryId() setter', () => {
it(`should set category property to null, call updateStateParams() method and not call applyFilters()`, () => {
jest.spyOn(controller, 'updateStateParams');
controller.categoryId = null;
expect(controller.updateStateParams).toHaveBeenCalledWith();
});
it(`should set category property and then call updateStateParams() and applyFilters() methods`, () => {
jest.spyOn(controller, 'updateStateParams');
controller.categoryId = 2;
expect(controller.updateStateParams).toHaveBeenCalledWith();
});
});
describe('changeCategory()', () => {
it(`should set categoryId property to null if the new value equals to the old one`, () => {
controller.categoryId = 2;
controller.changeCategory(2);
expect(controller.categoryId).toBeNull();
});
it(`should set categoryId property`, () => {
controller.categoryId = 2;
controller.changeCategory(1);
expect(controller.categoryId).toEqual(1);
});
});
describe('typeId() setter', () => {
it(`should set type property to null, call updateStateParams() method and not call applyFilters()`, () => {
jest.spyOn(controller, 'updateStateParams');
jest.spyOn(controller, 'applyFilters');
controller.typeId = null;
expect(controller.updateStateParams).toHaveBeenCalledWith();
expect(controller.applyFilters).not.toHaveBeenCalledWith();
});
it(`should set category property and then call updateStateParams() and applyFilters() methods`, () => {
jest.spyOn(controller, 'updateStateParams');
jest.spyOn(controller, 'applyFilters');
controller.typeId = 2;
expect(controller.updateStateParams).toHaveBeenCalledWith();
expect(controller.applyFilters).toHaveBeenCalledWith();
});
});
describe('tagGroups() setter', () => {
it(`should set tagGroups property and then call updateStateParams() and applyFilters() methods`, () => {
jest.spyOn(controller, 'updateStateParams');
jest.spyOn(controller, 'applyFilters');
controller.tagGroups = [{tagFk: 11, values: [{value: 'Brown'}]}];
expect(controller.updateStateParams).toHaveBeenCalledWith();
expect(controller.applyFilters).toHaveBeenCalledWith();
});
});
describe('onSearchByTag()', () => {
it(`should not add a new tag if the event key code doesn't equals to 'Enter'`, () => {
jest.spyOn(controller, 'applyFilters');
controller.order = {id: 4};
controller.$.search.value = 'Brown';
controller.onSearchByTag({key: 'Tab'});
expect(controller.applyFilters).not.toHaveBeenCalledWith();
});
it(`should add a new tag if the event key code equals to 'Enter' an then call applyFilters()`, () => {
jest.spyOn(controller, 'applyFilters');
controller.order = {id: 4};
controller.$.search.value = 'Brown';
controller.onSearchByTag({key: 'Enter'});
expect(controller.applyFilters).toHaveBeenCalledWith();
});
});
describe('onSearch()', () => {
it(`should apply a filter by item id an then call the applyFilters method`, () => {
jest.spyOn(controller, 'applyFilters');
const itemId = 1;
controller.onSearch({search: itemId});
expect(controller.applyFilters).toHaveBeenCalledWith({
'i.id': itemId
});
});
it(`should apply a filter by item name an then call the applyFilters method`, () => {
jest.spyOn(controller, 'applyFilters');
const itemName = 'Bow';
controller.onSearch({search: itemName});
expect(controller.applyFilters).toHaveBeenCalledWith({
'i.name': {like: `%${itemName}%`}
});
});
});
describe('applyFilters()', () => {
it(`should call model applyFilter() method with a new filter`, () => {
jest.spyOn(controller.$.model, 'applyFilter');
controller._categoryId = 2;
controller._typeId = 4;
controller.applyFilters();
expect(controller.$.model.applyFilter).toHaveBeenCalledWith(
{where: {categoryFk: 2, typeFk: 4}},
{orderFk: 4, orderBy: controller.getOrderBy(), tagGroups: []});
});
});
describe('remove()', () => {
it(`should remove a tag from tags property`, () => {
jest.spyOn(controller, 'applyFilters');
controller.tagGroups = [
{tagFk: 1, values: [{value: 'Brown'}]},
{tagFk: 67, values: [{value: 'Concussion'}]}
];
controller.remove(0);
const firstTag = controller.tagGroups[0];
expect(controller.tagGroups.length).toEqual(1);
expect(firstTag.tagFk).toEqual(67);
expect(controller.applyFilters).toHaveBeenCalledWith();
});
it(`should remove a tag from tags property and call applyFilters() if there's no more tags`, () => {
jest.spyOn(controller, 'applyFilters');
controller._categoryId = 1;
controller._typeId = 1;
controller.tagGroups = [{tagFk: 1, values: [{value: 'Blue'}]}];
controller.remove(0);
expect(controller.tagGroups.length).toEqual(0);
expect(controller.applyFilters).toHaveBeenCalledWith();
});
});
describe('updateStateParams()', () => {
it(`should call state go() method passing category and type state params`, () => {
jest.spyOn(controller.$state, 'go');
controller._categoryId = 2;
controller._typeId = 4;
controller._tagGroups = [
{tagFk: 67, values: [{value: 'Concussion'}], tagSelection: {name: 'Category'}}
];
const tagGroups = JSON.stringify([
{values: [{value: 'Concussion'}], tagFk: 67, tagSelection: {name: 'Category'}}
]);
const expectedResult = {categoryId: 2, typeId: 4, tagGroups: tagGroups};
controller.updateStateParams();
expect(controller.$state.go).toHaveBeenCalledWith('my.current.state', expectedResult);
});
});
describe('getOrderBy()', () => {
it(`should return an object with order params`, () => {
controller.orderField = 'relevancy DESC, name';
controller.orderWay = 'DESC';
let expectedResult = {
field: 'relevancy DESC, name',
way: 'DESC',
isTag: false
};
let result = controller.getOrderBy();
expect(result).toEqual(expectedResult);
});
});
describe('applyOrder()', () => {
it(`should apply order param to model calling getOrderBy()`, () => {
jest.spyOn(controller, 'getOrderBy');
jest.spyOn(controller.$.model, 'addFilter');
controller.field = 'relevancy DESC, name';
controller.way = 'ASC';
controller._categoryId = 1;
controller._typeId = 1;
let expectedOrder = {orderBy: controller.getOrderBy()};
controller.applyOrder();
expect(controller.getOrderBy).toHaveBeenCalledWith();
expect(controller.$.model.addFilter).toHaveBeenCalledWith(null, expectedOrder);
});
});
describe('fetchResultTags()', () => {
it(`should create an array of non repeated tags then set the resultTags property`, () => {
const items = [
{
id: 1, name: 'My Item 1', tags: [
{tagFk: 4, name: 'Length', value: 1},
{tagFk: 5, name: 'Color', value: 'red'}
]
},
{
id: 2, name: 'My Item 2', tags: [
{tagFk: 4, name: 'Length', value: 1},
{tagFk: 5, name: 'Color', value: 'blue'}
]
}];
controller.fetchResultTags(items);
expect(controller.resultTags.length).toEqual(2);
});
});
describe('buildOrderFilter()', () => {
it(`should create an array of non repeated tags plus default filters and then set the orderFields property`, () => {
const items = [
{
id: 1, name: 'My Item 1', tags: [
{tagFk: 4, name: 'Length'},
{tagFk: 5, name: 'Color'}
]
},
{
id: 2, name: 'My Item 2', tags: [
{tagFk: 5, name: 'Color'},
{tagFk: 6, name: 'Relevancy'}
]
}];
controller.fetchResultTags(items);
controller.buildOrderFilter();
expect(controller.orderFields.length).toEqual(7);
});
});
describe('formatTooltip()', () => {
it(`should return a formatted text with the tag name and values`, () => {
const tagGroup = {
values: [{value: 'Silver'}, {value: 'Brown'}],
tagFk: 1,
tagSelection: {
name: 'Color'
}
};
const result = controller.formatTooltip(tagGroup);
expect(result).toEqual(`Color: "Silver", "Brown"`);
});
it(`should return a formatted text with the tag value`, () => {
const tagGroup = {
values: [{value: 'Silver'}]
};
const result = controller.formatTooltip(tagGroup);
expect(result).toEqual(`"Silver"`);
});
});
describe('sanitizedTagGroupParam()', () => {
it(`should return an array of tags`, () => {
const dirtyTagGroups = [{
values: [{value: 'Silver'}, {value: 'Brown'}],
tagFk: 1,
tagSelection: {
name: 'Color',
$orgRow: {name: 'Color'}
},
$orgIndex: 1
}];
controller.tagGroups = dirtyTagGroups;
const expectedResult = [{
values: [{value: 'Silver'}, {value: 'Brown'}],
tagFk: 1,
tagSelection: {
name: 'Color'
}
}];
const result = controller.sanitizedTagGroupParam();
expect(result).toEqual(expect.objectContaining(expectedResult));
});
});
});
});

View File

@ -1,3 +0,0 @@
Name: Nombre
Search by item id or name: Buscar por id de artículo o nombre
OR: O

View File

@ -1,54 +0,0 @@
@import "variables";
vn-order-catalog vn-side-menu div {
& > .input {
padding-left: $spacing-md;
padding-right: $spacing-md;
border-color: $color-spacer;
border-bottom: $border-thin;
}
.item-category {
padding: $spacing-sm;
justify-content: flex-start;
align-items: flex-start;
flex-wrap: wrap;
vn-autocomplete[vn-id="category"] {
display: none
}
& > vn-one {
padding: $spacing-sm;
min-width: 33.33%;
text-align: center;
box-sizing: border-box;
& > vn-icon {
padding: $spacing-sm;
background-color: $color-font-secondary;
border-radius: 50%;
cursor: pointer;
&.active {
background-color: $color-main;
color: #FFF
}
& > i:before {
font-size: 2.6rem;
width: 16px;
height: 16px;
}
}
}
}
.chips {
display: flex;
flex-wrap: wrap;
padding: $spacing-md;
overflow: hidden;
max-width: 100%;
}
vn-autocomplete[vn-id="type"] .list {
max-height: 320px
}
}

View File

@ -1,38 +0,0 @@
<vn-autocomplete
vn-focus
vn-id="client"
url="Clients"
label="Client"
search-function="{or: [{id: $search}, {name: {like: '%'+ $search +'%'}}]}"
show-field="name"
value-field="id"
ng-model="$ctrl.clientFk"
vn-name="client"
order="id">
<tpl-item>{{id}}: {{name}}</tpl-item>
</vn-autocomplete>
<vn-autocomplete
disabled="!$ctrl.clientFk"
url="{{ $ctrl.clientFk ? 'Clients/'+ $ctrl.clientFk +'/addresses' : null }}"
fields="['nickname', 'street', 'city']"
ng-model="$ctrl.addressFk"
vn-name="address"
show-field="nickname"
value-field="id"
label="Address">
<tpl-item>{{nickname}}: {{street}}, {{city}}</tpl-item>
</vn-autocomplete>
<vn-date-picker
label="Landed"
ng-model="$ctrl.landed"
vn-name="landed">
</vn-date-picker>
<vn-autocomplete
disabled="!$ctrl.clientFk || !$ctrl.landed"
data="$ctrl._availableAgencies"
label="Agency"
show-field="agencyMode"
value-field="agencyModeFk"
ng-model="$ctrl.order.agencyModeFk"
vn-name="agencyMode">
</vn-autocomplete>

View File

@ -1,114 +0,0 @@
import ngModule from '../module';
import Component from 'core/lib/component';
class Controller extends Component {
constructor($element, $) {
super($element, $);
this.order = {};
this.clientFk = this.$params.clientFk;
}
$onInit() {
if (this.$params && this.$params.clientFk)
this.clientFk = this.$params.clientFk;
}
set order(value) {
if (value)
this._order = value;
}
get order() {
return this._order;
}
set clientFk(value) {
this.order.clientFk = value;
if (value) {
let filter = {
include: {
relation: 'defaultAddress',
scope: {
fields: 'id'
}
},
where: {id: value}
};
filter = encodeURIComponent(JSON.stringify(filter));
let query = `Clients?filter=${filter}`;
this.$http.get(query).then(res => {
if (res.data) {
let client = res.data[0];
let defaultAddress = client.defaultAddress;
this.addressFk = defaultAddress.id;
}
});
} else
this.addressFk = null;
}
get clientFk() {
return this.order.clientFk;
}
set addressFk(value) {
this.order.addressFk = value;
this.getAvailableAgencies();
}
get addressFk() {
return this.order.addressFk;
}
set landed(value) {
this.order.landed = value;
this.getAvailableAgencies();
}
get landed() {
return this.order.landed;
}
get warehouseFk() {
return this.order.warehouseFk;
}
getAvailableAgencies() {
let order = this.order;
order.agencyModeFk = null;
let params = {
addressFk: order.addressFk,
landed: order.landed
};
if (params.landed && params.addressFk) {
this.$http.get(`Agencies/landsThatDay`, {params})
.then(res => this._availableAgencies = res.data);
}
}
onSubmit() {
this.createOrder();
}
createOrder() {
let params = {
landed: this.order.landed,
addressId: this.order.addressFk,
agencyModeId: this.order.agencyModeFk
};
this.$http.post(`Orders/new`, params).then(res => {
this.vnApp.showSuccess(this.$t('Data saved!'));
this.$state.go('order.card.catalog', {id: res.data});
});
}
}
ngModule.vnComponent('vnOrderCreateCard', {
template: require('./card.html'),
controller: Controller,
bindings: {
order: '<?'
}
});

View File

@ -1,104 +0,0 @@
import './card.js';
describe('Order', () => {
describe('Component vnOrderCreateCard', () => {
let controller;
let $httpBackend;
let $scope;
beforeEach(ngModule('order'));
beforeEach(inject(($componentController, _$httpBackend_, _vnApp_, $rootScope) => {
$httpBackend = _$httpBackend_;
$scope = $rootScope.$new();
const $element = angular.element('<vn-order-create-card></vn-order-create-card>');
controller = $componentController('vnOrderCreateCard', {$element, $scope});
controller.item = {id: 3};
}));
describe('set order', () => {
it(`should set order if the value given is not null`, () => {
controller.order = 1;
expect(controller.order).toEqual(1);
});
});
describe('set clientFk', () => {
it(`should set addressFk to null and clientFk to a value and set addressFk to a value given`, () => {
let filter = {
include: {
relation: 'defaultAddress',
scope: {
fields: 'id'
}
},
where: {id: 2}
};
filter = encodeURIComponent(JSON.stringify(filter));
let response = [
{
defaultAddress: {id: 1}
}
];
$httpBackend.whenGET(`Clients?filter=${filter}`).respond(response);
$httpBackend.expectGET(`Clients?filter=${filter}`);
controller.clientFk = 2;
$httpBackend.flush();
expect(controller.clientFk).toEqual(2);
expect(controller.order.addressFk).toBe(1);
});
});
describe('set addressFk', () => {
it(`should set agencyModeFk property to null and addressFk to a value`, () => {
controller.addressFk = 1101;
expect(controller.addressFk).toEqual(1101);
expect(controller.order.agencyModeFk).toBe(null);
});
});
describe('getAvailableAgencies()', () => {
it(`should make a query if landed and addressFk exists`, () => {
controller.order.addressFk = 1101;
controller.order.landed = 1101;
$httpBackend.whenRoute('GET', 'Agencies/landsThatDay')
.respond({data: 1});
controller.getAvailableAgencies();
$httpBackend.flush();
});
});
describe('onSubmit()', () => {
it(`should call createOrder()`, () => {
jest.spyOn(controller, 'createOrder');
controller.onSubmit();
expect(controller.createOrder).toHaveBeenCalledWith();
});
});
describe('createOrder()', () => {
it(`should make a query, call vnApp.showSuccess and $state.go if the response is defined`, () => {
controller.order.landed = 1101;
controller.order.addressFk = 1101;
controller.order.agencyModeFk = 1101;
jest.spyOn(controller.vnApp, 'showSuccess');
jest.spyOn(controller.$state, 'go');
$httpBackend.expect('POST', 'Orders/new', {landed: 1101, addressId: 1101, agencyModeId: 1101}).respond(200, 1);
controller.createOrder();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
expect(controller.$state.go).toHaveBeenCalledWith('order.card.catalog', {id: 1});
});
});
});
});

View File

@ -1,16 +0,0 @@
<div class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-order-create-card vn-id="card" on-save=""></vn-order-create-card>
</vn-card>
<vn-button-bar>
<vn-submit
ng-click="$ctrl.onSubmit()"
label="Create">
</vn-submit>
<vn-button
class="cancel"
label="Cancel"
ui-sref="order.index">
</vn-button>
</vn-button-bar>
</div>

View File

@ -1,14 +0,0 @@
import ngModule from '../module';
import Section from 'salix/components/section';
class Controller extends Section {
async onSubmit() {
let newOrderID = await this.$.card.createOrder();
this.$state.go('order.card.summary', {id: newOrderID});
}
}
ngModule.vnComponent('vnOrderCreate', {
template: require('./index.html'),
controller: Controller
});

View File

@ -1,34 +0,0 @@
import './index.js';
describe('Order', () => {
describe('Component vnOrderCreate', () => {
let $scope;
let controller;
beforeEach(ngModule('order'));
beforeEach(inject(($componentController, $rootScope) => {
$scope = $rootScope.$new();
$scope.card = {createOrder: () => {}};
const $element = angular.element('<vn-order-create></vn-order-create>');
controller = $componentController('vnOrderCreate', {$element, $scope});
}));
describe('onSubmit()', () => {
it(`should call createOrder()`, () => {
jest.spyOn(controller.$.card, 'createOrder');
controller.onSubmit();
expect(controller.$.card.createOrder).toHaveBeenCalledWith();
});
it(`should call go()`, async() => {
jest.spyOn(controller.$state, 'go');
await controller.onSubmit();
expect(controller.$state.go).toHaveBeenCalledWith('order.card.summary', {id: undefined});
});
});
});
});

View File

@ -1,6 +0,0 @@
You can't create an order for a frozen client: No puedes crear una orden a un cliente congelado
You can't create an order for an inactive client: No puedes crear una orden a un cliente inactivo
You can't create an order for a client that doesn't has tax data verified:
No puedes crear una orden a un cliente cuyos datos fiscales no han sido verificados
You can't create an order for a client that has a debt: No puedes crear una orden a un cliente que tiene deuda
New order: Nueva orden

View File

@ -1,78 +0,0 @@
<vn-descriptor-content
module="order"
description="$ctrl.order.client.name"
summary="$ctrl.$.summary">
<slot-menu>
<vn-item
ng-click="deleteOrderConfirmation.show()"
translate>
Delete order
</vn-item>
</slot-menu>
<slot-body>
<div class="attributes">
<vn-label-value
label="State"
value="{{$ctrl.$t($ctrl.order.isConfirmed ? 'Confirmed' : 'Not confirmed')}}">
</vn-label-value>
<vn-label-value
label="Sales person">
<span
ng-click="workerDescriptor.show($event, $ctrl.order.client.salesPersonFk)"
class="link">
{{$ctrl.order.client.salesPersonUser.name}}
</span>
</vn-label-value>
<vn-label-value
label="Landed"
value="{{$ctrl.order.landed | date: 'dd/MM/yyyy' }}">
</vn-label-value>
<vn-label-value
label="Agency"
value="{{$ctrl.order.agencyMode.name}}">
</vn-label-value>
<vn-label-value
label="Alias"
value="{{$ctrl.order.address.nickname}}">
</vn-label-value>
<vn-label-value
label="Items"
value="{{$ctrl.order.rows.length || 0}}">
</vn-label-value>
<vn-label-value
label="Total"
value="{{$ctrl.order.total | currency: 'EUR': 2}}">
</vn-label-value>
</div>
<div class="quicklinks">
<div ng-transclude="btnOne">
<vn-quick-link
tooltip="Order ticket list"
state="['ticket.index', {q: $ctrl.ticketFilter}]"
icon="icon-ticket">
</vn-quick-link>
</div>
<div ng-transclude="btnTwo">
<vn-quick-link
tooltip="Client card"
state="['client.card.summary', {id: $ctrl.order.clientFk}]"
icon="person">
</vn-quick-link>
</div>
<div ng-transclude="btnThree">
</div>
</div>
</slot-body>
</vn-descriptor-content>
<vn-confirm
vn-id="deleteOrderConfirmation"
on-accept="$ctrl.deleteOrder()"
message="You are going to delete this order"
question="continue anyway?">
</vn-confirm>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
<vn-popup vn-id="summary">
<vn-order-summary order="$ctrl.order"></vn-order-summary>
</vn-popup>

View File

@ -1,32 +0,0 @@
import ngModule from '../module';
import Descriptor from 'salix/components/descriptor';
class Controller extends Descriptor {
get order() {
return this.entity;
}
set order(value) {
this.entity = value;
}
get ticketFilter() {
return JSON.stringify({orderFk: this.id});
}
deleteOrder() {
return this.$http.delete(`Orders/${this.id}`)
.then(() => {
this.$state.go('order.index');
this.vnApp.showSuccess(this.$t('Order deleted'));
});
}
}
ngModule.vnComponent('vnOrderDescriptor', {
template: require('./index.html'),
controller: Controller,
bindings: {
order: '<'
}
});

View File

@ -1,29 +0,0 @@
import './index.js';
describe('Order Component vnOrderDescriptor', () => {
let $httpBackend;
let controller;
const order = {id: 1};
beforeEach(ngModule('order'));
beforeEach(inject(($componentController, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
controller = $componentController('vnOrderDescriptor', {$element: null}, {order});
}));
describe('deleteOrder()', () => {
it(`should perform a DELETE query`, () => {
jest.spyOn(controller.vnApp, 'showSuccess');
jest.spyOn(controller.$state, 'go');
$httpBackend.expectDELETE(`Orders/${order.id}`).respond();
controller.deleteOrder();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
expect(controller.$state.go).toHaveBeenCalledWith('order.index');
});
});
});

View File

@ -1,12 +0,0 @@
Client: Cliente
Confirmed: Confirmado
Not confirmed: Sin confirmar
State: Estado
Landed: F. entrega
Items: Articulos
Agency: Agencia
Sales person: Comercial
Order ticket list: Ticket del pedido
Delete order: Eliminar pedido
You are going to delete this order: El pedido se eliminará
continue anyway?: ¿Continuar de todos modos?

View File

@ -1,17 +1,3 @@
export * from './module';
import './main';
import './index/';
import './card';
import './descriptor';
import './search-panel';
import './catalog-search-panel';
import './catalog-view';
import './catalog';
import './summary';
import './line';
import './prices-popover';
import './volume';
import './create';
import './create/card';
import './basic-data';

View File

@ -1,90 +0,0 @@
<vn-auto-search
model="model">
</vn-auto-search>
<vn-data-viewer
model="model"
class="vn-mb-xl">
<vn-card>
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th field="id" number>Id</vn-th>
<vn-th field="salesPersonFk">Sales person</vn-th>
<vn-th field="clientFk">Client</vn-th>
<vn-th field="isConfirmed" center>Confirmed</vn-th>
<vn-th field="created" center expand>Created</vn-th>
<vn-th field="landed" shrink-date>Landed</vn-th>
<vn-th field="created" center>Hour</vn-th>
<vn-th field="agencyName" center>Agency</vn-th>
<vn-th field="total" center>Total</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<a
ng-repeat="order in model.data"
class="clickable search-result vn-tr"
ui-sref="order.card.summary({id: {{::order.id}}})">
<vn-td number>{{::order.id}}</vn-td>
<vn-td expand>
<span
vn-click-stop="workerDescriptor.show($event, order.salesPersonFk)"
class="link" >
{{::order.name | dashIfEmpty}}
</span>
</vn-td>
<vn-td>
<span
vn-click-stop="clientDescriptor.show($event, order.clientFk)"
class="link">
{{::order.clientName}}
</span>
</vn-td>
<vn-td center>
<vn-check
ng-model="order.isConfirmed"
disabled="true">
</vn-check>
</vn-td>
<vn-td shrink-datetime>{{::order.created | date: 'dd/MM/yyyy HH:mm'}}</vn-td>
<vn-td shrink-date>
<span class="chip {{$ctrl.compareDate(order.landed)}}">
{{::order.landed | date:'dd/MM/yyyy'}}
</span>
</vn-td>
<vn-td shrink>{{::(order.hourTheoretical
? order.hourTheoretical
: order.hourEffective) | dashIfEmpty
}}</vn-td>
<vn-td expand>{{::order.agencyName}}</vn-td>
<vn-td number>{{::order.total | currency: 'EUR': 2 | dashIfEmpty}}</vn-td>
<vn-td shrink>
<vn-icon-button
vn-click-stop="$ctrl.preview(order)"
icon="preview"
vn-tooltip="Preview">
</vn-icon-button>
</vn-td>
</a>
</vn-tbody>
</vn-table>
</vn-card>
</vn-data-viewer>
<a
ui-sref="order.create"
vn-bind="+"
vn-tooltip="New order"
fixed-bottom-right>
<vn-float-button icon="add"></vn-float-button>
</a>
<vn-client-descriptor-popover
vn-id="clientDescriptor">
</vn-client-descriptor-popover>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
<vn-popup vn-id="summary">
<vn-order-summary
order="$ctrl.selectedOrder">
</vn-order-summary>
</vn-popup>

View File

@ -1,26 +0,0 @@
import ngModule from '../module';
import Section from 'salix/components/section';
export default class Controller extends Section {
preview(order) {
this.selectedOrder = order;
this.$.summary.show();
}
compareDate(date) {
let today = Date.vnNew();
today.setHours(0, 0, 0, 0);
date = new Date(date);
date.setHours(0, 0, 0, 0);
const timeDifference = today - date;
if (timeDifference == 0) return 'warning';
if (timeDifference < 0) return 'success';
}
}
ngModule.vnComponent('vnOrderIndex', {
template: require('./index.html'),
controller: Controller
});

View File

@ -1,67 +0,0 @@
import './index.js';
describe('Component vnOrderIndex', () => {
let controller;
let $window;
let orders = [{
id: 1,
clientFk: 1,
isConfirmed: false
}, {
id: 2,
clientFk: 1,
isConfirmed: false
}, {
id: 3,
clientFk: 1,
isConfirmed: true
}];
beforeEach(ngModule('order'));
beforeEach(inject(($componentController, _$window_) => {
$window = _$window_;
const $element = angular.element('<vn-order-index></vn-order-index>');
controller = $componentController('vnOrderIndex', {$element});
}));
describe('compareDate()', () => {
it('should return warning when the date is the present', () => {
let curDate = Date.vnNew();
let result = controller.compareDate(curDate);
expect(result).toEqual('warning');
});
it('should return sucess when the date is in the future', () => {
let futureDate = Date.vnNew();
futureDate = futureDate.setDate(futureDate.getDate() + 10);
let result = controller.compareDate(futureDate);
expect(result).toEqual('success');
});
it('should return undefined when the date is in the past', () => {
let pastDate = Date.vnNew();
pastDate = pastDate.setDate(pastDate.getDate() - 10);
let result = controller.compareDate(pastDate);
expect(result).toEqual(undefined);
});
});
describe('preview()', () => {
it('should show the dialog summary', () => {
controller.$.summary = {show: () => {}};
jest.spyOn(controller.$.summary, 'show');
let event = new MouseEvent('click', {
view: $window,
bubbles: true,
cancelable: true
});
controller.preview(event, orders[0]);
expect(controller.$.summary.show).toHaveBeenCalledWith();
});
});
});

View File

@ -1,96 +0,0 @@
<vn-data-viewer data="$ctrl.rows" class="vn-w-lg">
<vn-card class="vn-pa-lg header" ng-if="$ctrl.rows.length > 0">
<div>
<vn-label translate>Subtotal</vn-label>
{{$ctrl.subtotal | currency: 'EUR':2}}
</div>
<div>
<vn-label translate>VAT</vn-label>
{{$ctrl.VAT | currency: 'EUR':2}}
</div>
<div>
<vn-label>Total</vn-label>
{{$ctrl.order.total | currency: 'EUR':2}}
</div>
</vn-card>
<vn-card class="vn-mt-md">
<vn-table>
<vn-thead>
<vn-tr>
<vn-th></vn-th>
<vn-th number>Id</vn-th>
<vn-th>Description</vn-th>
<vn-th>Warehouse</vn-th>
<vn-th>Shipped</vn-th>
<vn-th number>Quantity</vn-th>
<vn-th number>Price</vn-th>
<vn-th number>Amount</vn-th>
<vn-th ng-if="::!$ctrl.order.isConfirmed"></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="row in $ctrl.rows">
<vn-td shrink>
<img
ng-src="{{::$root.imagePath('catalog', '50x50', row.item.id)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', row.item.id)}}"
on-error-src/>
</vn-td>
<vn-td number>
<span ng-click="itemDescriptor.show($event, row.itemFk)"
class="link">
{{::row.itemFk}}
</span>
</vn-td>
<vn-td vn-fetched-tags>
<div>
<vn-one title="{{::row.item.name}}">{{::row.item.name}}</vn-one>
<vn-one ng-if="::row.item.subName">
<h3 title="{{::row.item.subName}}">{{::row.item.subName}}</h3>
</vn-one>
</div>
<vn-fetched-tags
max-length="6"
item="::row.item"
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td>{{::row.warehouse.name}}</vn-td>
<vn-td>{{::row.shipped | date: 'dd/MM/yyyy'}}</vn-td>
<vn-td number>{{::row.quantity}}</vn-td>
<vn-td number>
{{::row.price | currency: 'EUR':2}}
</vn-td>
<vn-td number>
{{::row.price * row.quantity | currency: 'EUR':2}}
</vn-td>
<vn-td shrink ng-if="::!$ctrl.order.isConfirmed">
<vn-icon-button
vn-tooltip="Remove item"
icon="delete"
ng-click="deleteRow.show($index)"
tabindex="-1">
</vn-icon-button>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card>
</vn-data-viewer>
<vn-float-button
icon="check"
vn-tooltip="Confirm"
ng-click="$ctrl.save()"
ng-if="!$ctrl.order.isConfirmed"
fixed-bottom-right>
</vn-float-button>
<vn-item-descriptor-popover
vn-id="item-descriptor"
warehouse-fk="$ctrl.vnConfig.warehouseFk">
</vn-item-descriptor-popover>
<vn-confirm
vn-id="delete-row"
on-accept="$ctrl.deleteRow($data)"
question="Delete row"
message="Are you sure you want to delete this row?">
</vn-confirm>

View File

@ -1,70 +0,0 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
class Controller extends Section {
$onInit() {
this.getRows();
}
set order(value) {
this._order = value;
this.getVAT();
}
get order() {
return this._order;
}
get subtotal() {
return this.order ? this.order.total - this.VAT : 0;
}
getRows() {
let filter = {
where: {orderFk: this.$params.id},
include: [
{relation: 'item'},
{relation: 'warehouse'}
]
};
this.$http.get(`OrderRows`, {filter})
.then(res => this.rows = res.data);
}
getVAT() {
this.$http.get(`Orders/${this.$params.id}/getVAT`)
.then(res => this.VAT = res.data);
}
deleteRow(index) {
let [row] = this.rows.splice(index, 1);
let params = {
rows: [row.id],
actualOrderId: this.$params.id
};
return this.$http.post(`OrderRows/removes`, params)
.then(() => this.card.reload())
.then(() => this.vnApp.showSuccess(this.$t('Data saved!')));
}
save() {
this.$http.post(`Orders/${this.$params.id}/confirm`).then(() => {
this.vnApp.showSuccess(this.$t('Order confirmed'));
this.$state.go(`ticket.index`, {
q: JSON.stringify({clientFk: this.order.clientFk})
});
});
}
}
ngModule.vnComponent('vnOrderLine', {
template: require('./index.html'),
controller: Controller,
bindings: {
order: '<'
},
require: {
card: '^vnOrderCard'
}
});

View File

@ -1,66 +0,0 @@
import './index.js';
describe('Order', () => {
describe('Component vnOrderLine', () => {
let $state;
let controller;
let $httpBackend;
const vat = 10.5;
const rows = [
{
quantity: 4,
price: 10.5
}, {
quantity: 3,
price: 2.4
}
];
beforeEach(ngModule('order'));
beforeEach(inject(($componentController, _$state_, _$httpBackend_) => {
$state = _$state_;
$httpBackend = _$httpBackend_;
$state.params.id = 1;
$httpBackend.whenGET(`OrderRows`).respond(rows);
$httpBackend.whenRoute('GET', `Orders/:id/getVAT`).respond(200, vat);
controller = $componentController('vnOrderLine', {$element: null});
}));
describe('getRows()', () => {
it('should make a query to get the rows of a given order', () => {
controller.getRows();
$httpBackend.flush();
expect(controller.rows).toEqual(rows);
});
});
describe('getVAT()', () => {
it('should make a query to get the VAT of a given order', () => {
controller.getVAT();
$httpBackend.flush();
expect(controller.VAT).toBe(vat);
});
});
describe('deleteRow()', () => {
it('should remove a row from rows and add save the data if the response is accept', () => {
controller.getRows();
$httpBackend.flush();
controller.card = {reload: jasmine.createSpy('reload')};
$httpBackend.expectPOST(`OrderRows/removes`).respond();
controller.deleteRow(0);
$httpBackend.flush();
expect(controller.rows.length).toBe(1);
expect(controller.card.reload).toHaveBeenCalled();
});
});
});
});

View File

@ -1,3 +0,0 @@
Delete row: Eliminar linea
Order confirmed: Pedido confirmado
Are you sure you want to delete this row?: ¿Estas seguro de que quieres eliminar esta línea?

View File

@ -1,18 +0,0 @@
@import "./variables";
vn-order-line {
vn-table {
img {
border-radius: 50%;
width: 50px;
height: 50px;
}
}
.header {
text-align: right;
& > div {
margin-bottom: $spacing-xs;
}
}
}

View File

@ -2,8 +2,12 @@ import ngModule from '../module';
import ModuleMain from 'salix/components/module-main';
export default class Order extends ModuleMain {
$postLink() {
this.filter = {showEmpty: false};
constructor($element, $) {
super($element, $);
}
async $onInit() {
this.$state.go('home');
window.location.href = await this.vnApp.getUrl(`order/`);
}
}

View File

@ -1,52 +0,0 @@
<default>
<form name="form" class="prices">
<vn-table>
<vn-tbody>
<vn-tr ng-repeat="price in $ctrl.prices">
<vn-td class="warehouse" expand>
<span
class="text"
title="{{::price.warehouse}}">
{{::price.warehouse}}
</span>
</vn-td>
<vn-td number expand>
<div>
<span
ng-click="$ctrl.addQuantity(price)"
class="link unselectable">{{::price.grouping}}</span>
<span> x {{::price.price | currency: 'EUR': 2}}</span>
</div>
<div class="price-kg" ng-show="::price.priceKg">
{{::price.priceKg | currency: 'EUR'}}/Kg
</div>
</vn-td>
<vn-td shrink>
<!-- Focus first element -->
<vn-input-number ng-if="$index === 0"
min="0"
name="quantity"
ng-model="price.quantity"
step="price.grouping"
class="dense"
vn-focus>
</vn-input-number>
<vn-input-number ng-if="$index > 0"
min="0"
name="quantity"
ng-model="price.quantity"
step="price.grouping"
class="dense">
</vn-input-number>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
<div class="footer vn-pa-md">
<vn-submit
label="Add"
ng-click="$ctrl.submit()">
</vn-submit>
</div>
</form>
</default>

View File

@ -1,115 +0,0 @@
import ngModule from '../module';
import Popover from 'core/components/popover';
import './style.scss';
class Controller extends Popover {
constructor(...args) {
super(...args);
this.totalBasquet = 0;
}
set prices(value) {
this._prices = value;
if (value && value[0].grouping)
this.getTotalQuantity();
}
get prices() {
return this._prices;
}
show(parent, item) {
this.id = item.id;
this.item = JSON.parse(JSON.stringify(item));
this.maxQuantity = this.item.available;
this.prices = this.item.prices;
super.show(parent);
}
onClose() {
this.id = null;
this.item = {};
this.tags = {};
this._prices = {};
this.totalQuantity = 0;
super.onClose();
}
getTotalQuantity() {
let total = 0;
for (let price of this.prices) {
if (!price.quantity) price.quantity = 0;
total += price.quantity;
}
this.totalQuantity = total;
}
addQuantity(price) {
this.getTotalQuantity();
const quantity = this.totalQuantity + price.grouping;
if (quantity <= this.maxQuantity)
price.quantity += price.grouping;
}
getGroupings() {
const filledRows = [];
for (let priceOption of this.prices) {
if (priceOption.quantity && priceOption.quantity > 0) {
const priceMatch = filledRows.find(row => {
return row.warehouseFk == priceOption.warehouseFk
&& row.price == priceOption.price;
});
if (!priceMatch)
filledRows.push(Object.assign({}, priceOption));
else priceMatch.quantity += priceOption.quantity;
}
}
return filledRows;
}
submit() {
const filledRows = this.getGroupings();
try {
const hasInvalidGropings = filledRows.some(row =>
row.quantity % row.grouping != 0
);
if (filledRows.length <= 0)
throw new Error('First you must add some quantity');
if (hasInvalidGropings)
throw new Error(`The amounts doesn't match with the grouping`);
const params = {
orderFk: this.order.id,
items: filledRows
};
this.$http.post(`OrderRows/addToOrder`, params)
.then(() => {
this.vnApp.showSuccess(this.$t('Data saved!'));
this.hide();
if (this.card) this.card.reload();
});
} catch (e) {
this.vnApp.showError(this.$t(e.message));
return false;
}
return true;
}
}
ngModule.vnComponent('vnOrderPricesPopover', {
slotTemplate: require('./index.html'),
controller: Controller,
bindings: {
order: '<'
},
require: {
card: '?^vnOrderCard'
}
});

View File

@ -1,171 +0,0 @@
import './index.js';
describe('Order', () => {
describe('Component vnOrderPricesPopover', () => {
let controller;
let $httpBackend;
let orderId = 16;
beforeEach(ngModule('order'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
const $scope = $rootScope.$new();
const $element = angular.element('<vn-order-prices-popover></vn-order-prices-popover>');
const $transclude = {
$$boundTransclude: {
$$slots: []
}
};
controller = $componentController('vnOrderPricesPopover', {$element, $scope, $transclude});
controller._prices = [
{warehouseFk: 1, grouping: 10, quantity: 0},
{warehouseFk: 1, grouping: 100, quantity: 100}
];
controller.item = {available: 1000};
controller.maxQuantity = 1000;
controller.order = {id: orderId};
}));
describe('prices() setter', () => {
it('should call to the getTotalQuantity() method', () => {
controller.getTotalQuantity = jest.fn();
controller.prices = [
{grouping: 10, quantity: 0},
{grouping: 100, quantity: 0},
{grouping: 1000, quantity: 0},
];
expect(controller.getTotalQuantity).toHaveBeenCalledWith();
});
});
describe('getTotalQuantity()', () => {
it('should set the totalQuantity property', () => {
controller.getTotalQuantity();
expect(controller.totalQuantity).toEqual(100);
});
});
describe('addQuantity()', () => {
it('should call to the getTotalQuantity() method and NOT set the quantity property', () => {
jest.spyOn(controller, 'getTotalQuantity');
controller.prices = [
{grouping: 10, quantity: 0},
{grouping: 100, quantity: 0},
{grouping: 1000, quantity: 1000},
];
const oneThousandGrouping = controller.prices[2];
expect(oneThousandGrouping.quantity).toEqual(1000);
controller.addQuantity(oneThousandGrouping);
expect(controller.getTotalQuantity).toHaveBeenCalledWith();
expect(oneThousandGrouping.quantity).toEqual(1000);
});
it('should call to the getTotalQuantity() method and then set the quantity property', () => {
jest.spyOn(controller, 'getTotalQuantity');
const oneHandredGrouping = controller.prices[1];
controller.addQuantity(oneHandredGrouping);
expect(controller.getTotalQuantity).toHaveBeenCalledWith();
expect(oneHandredGrouping.quantity).toEqual(200);
});
});
describe('getGroupings()', () => {
it('should return a row with the total filled quantity', () => {
jest.spyOn(controller, 'getTotalQuantity');
controller.prices = [
{warehouseFk: 1, grouping: 10, quantity: 10},
{warehouseFk: 1, grouping: 100, quantity: 100},
{warehouseFk: 1, grouping: 1000, quantity: 1000},
];
const rows = controller.getGroupings();
const firstRow = rows[0];
expect(rows.length).toEqual(1);
expect(firstRow.quantity).toEqual(1110);
});
it('should return two filled rows with a quantity', () => {
jest.spyOn(controller, 'getTotalQuantity');
controller.prices = [
{warehouseFk: 1, grouping: 10, quantity: 10},
{warehouseFk: 2, grouping: 10, quantity: 10},
{warehouseFk: 1, grouping: 100, quantity: 0},
{warehouseFk: 1, grouping: 1000, quantity: 1000},
];
const rows = controller.getGroupings();
const firstRow = rows[0];
const secondRow = rows[1];
expect(rows.length).toEqual(2);
expect(firstRow.quantity).toEqual(1010);
expect(secondRow.quantity).toEqual(10);
});
});
describe('submit()', () => {
it('should throw an error if none of the rows contains a quantity', () => {
jest.spyOn(controller, 'getTotalQuantity');
jest.spyOn(controller.vnApp, 'showError');
controller.prices = [
{warehouseFk: 1, grouping: 10, quantity: 0},
{warehouseFk: 1, grouping: 100, quantity: 0}
];
controller.submit();
expect(controller.vnApp.showError).toHaveBeenCalledWith(`First you must add some quantity`);
});
it(`should throw an error if the quantity doesn't match the grouping value`, () => {
jest.spyOn(controller, 'getTotalQuantity');
jest.spyOn(controller.vnApp, 'showError');
controller.prices = [
{warehouseFk: 1, grouping: 10, quantity: 0},
{warehouseFk: 1, grouping: 100, quantity: 1101}
];
controller.submit();
expect(controller.vnApp.showError).toHaveBeenCalledWith(`The amounts doesn't match with the grouping`);
});
it('should should make an http query and then show a success message', () => {
jest.spyOn(controller, 'getTotalQuantity');
jest.spyOn(controller.vnApp, 'showSuccess');
controller.prices = [
{warehouseFk: 1, grouping: 10, quantity: 0},
{warehouseFk: 1, grouping: 100, quantity: 100}
];
const params = {
orderFk: orderId,
items: [{warehouseFk: 1, grouping: 100, quantity: 100}]
};
$httpBackend.expectPOST('OrderRows/addToOrder', params).respond(200);
controller.submit();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith(`Data saved!`);
});
});
});
});

View File

@ -1,3 +0,0 @@
Qty.: Cant.
First you must add some quantity: Primero debes agregar alguna cantidad
The amounts doesn't match with the grouping: Las cantidades no coinciden con el grouping

View File

@ -1,18 +0,0 @@
@import "variables";
.vn-order-prices-popover .content {
.prices {
vn-table {
.price-kg {
color: $color-font-secondary;
font-size: .75rem
}
.vn-input-number {
width: 80px;
}
}
.footer {
text-align: center;
}
}
}

View File

@ -28,19 +28,19 @@
"abstract": true,
"component": "vn-order",
"description": "Orders"
},
},
{
"url": "/index?q",
"state": "order.index",
"component": "vn-order-index",
"description": "Orders"
},
},
{
"url": "/:id",
"state": "order.card",
"abstract": true,
"component": "vn-order-card"
},
},
{
"url": "/summary",
"state": "order.card.summary",
@ -49,48 +49,6 @@
"params": {
"order": "$ctrl.order"
}
},
{
"url": "/catalog?q&categoryId&typeId&tagGroups",
"state": "order.card.catalog",
"component": "vn-order-catalog",
"description": "Catalog",
"params": {
"order": "$ctrl.order"
}
},
{
"url": "/volume",
"state": "order.card.volume",
"component": "vn-order-volume",
"description": "Volume",
"params": {
"order": "$ctrl.order"
}
},
{
"url": "/line",
"state": "order.card.line",
"component": "vn-order-line",
"description": "Lines",
"params": {
"order": "$ctrl.order"
}
},
{
"url": "/create?clientFk",
"state": "order.create",
"component": "vn-order-create",
"description": "New order"
},
{
"url": "/basic-data",
"state": "order.card.basicData",
"component": "vn-order-basic-data",
"description": "Basic data",
"params": {
"order": "$ctrl.order"
}
}
]
}
}

View File

@ -1,86 +0,0 @@
<div class="search-panel">
<form ng-submit="$ctrl.onSearch()">
<vn-horizontal>
<vn-textfield
vn-one
label="General search"
ng-model="filter.search"
info="Search orders by ticket id"
vn-focus>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
label="Client id"
ng-model="filter.clientFk">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
vn-one
label="Agency"
ng-model="filter.agencyModeFk"
url="AgencyModes/isActive"
show-field="name"
value-field="id">
</vn-autocomplete>
<vn-worker-autocomplete
vn-one
ng-model="filter.workerFk"
departments="['VT']"
show-field="nickname"
label="Sales person">
</vn-worker-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-date-picker
vn-one
label="From landed"
ng-model="filter.from">
</vn-date-picker>
<vn-date-picker
vn-one
label="To landed"
ng-model="filter.to">
</vn-date-picker>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
label="Order id"
ng-model="filter.orderFk">
</vn-textfield>
<vn-autocomplete
vn-one
label="Application"
ng-model="filter.sourceApp"
url="Orders/getSourceValues"
show-field="value"
value-field="value">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-check
vn-one
label="My team"
ng-model="filter.myTeam"
triple-state="true">
</vn-check>
<vn-check
vn-one
label="Order confirmed"
triple-state="true"
ng-model="filter.isConfirmed">
</vn-check>
<vn-check
vn-one
label="Show empty"
ng-model="filter.showEmpty">
</vn-check>
</vn-horizontal>
<vn-horizontal class="vn-mt-lg">
<vn-submit label="Search"></vn-submit>
</vn-horizontal>
</form>
</div>

View File

@ -1,7 +0,0 @@
import ngModule from '../module';
import SearchPanel from 'core/components/searchbar/search-panel';
ngModule.vnComponent('vnOrderSearchPanel', {
template: require('./index.html'),
controller: SearchPanel
});

View File

@ -1,11 +0,0 @@
Order id: Id cesta
Client id: Id cliente
From landed: Desde f. entrega
To landed: Hasta f. entrega
To: Hasta
Agency: Agencia
Application: Aplicación
SalesPerson: Comercial
Order confirmed: Pedido confirmado
Show empty: Mostrar vacías
Search orders by ticket id: Buscar pedido por id ticket

View File

@ -1,131 +0,0 @@
<vn-card class="summary">
<h5>
<a ng-if="::$ctrl.summary.id"
vn-tooltip="Go to the order"
ui-sref="order.card.summary({id: {{::$ctrl.summary.id}}})"
name="goToSummary">
<vn-icon-button icon="launch"></vn-icon-button>
</a>
<span>
<span translate>Basket</span> #{{$ctrl.summary.id}} - {{$ctrl.summary.client.name}}
({{$ctrl.summary.client.id}})
</span>
<vn-button
disabled="$ctrl.order.isConfirmed"
class="flat"
style="color: inherit;"
label="Confirm"
ng-click="$ctrl.save()"
vn-tooltip="Confirm lines">
</vn-button>
</h5>
<vn-horizontal class="ticketSummary__data">
<vn-one>
<vn-label-value label="Id"
value="{{$ctrl.summary.id}}">
</vn-label-value>
<vn-label-value label="Nickname">
<span
ng-click="clientDescriptor.show($event, $ctrl.summary.clientFk)"
class="link">
{{$ctrl.summary.address.nickname}}
</span>
</vn-label-value>
<vn-label-value label="Company"
value="{{$ctrl.summary.address.companyFk}}">
</vn-label-value>
<vn-check label="Confirmed" disabled="true"
ng-model="$ctrl.summary.isConfirmed">
</vn-check>
</vn-one>
<vn-one>
<vn-label-value label="Created"
value="{{$ctrl.summary.created | date: 'dd/MM/yyyy HH:mm'}}">
</vn-label-value>
<vn-label-value label="Confirmed"
value="{{$ctrl.summary.confirmed | date: 'dd/MM/yyyy HH:mm'}}">
</vn-label-value>
<vn-label-value label="Landed"
value="{{$ctrl.summary.landed | date: 'dd/MM/yyyy HH:mm'}}">
</vn-label-value>
<vn-label-value label="Phone">
<vn-link-phone
phone-number="$ctrl.summary.address.phone"
></vn-link-phone>
</vn-label-value>
<vn-label-value label="Created from"
value="{{$ctrl.summary.sourceApp}}">
</vn-label-value>
<vn-label-value label="Address" no-ellipsize
value="{{$ctrl.formattedAddress}}">
</vn-label-value>
</vn-one>
<vn-one>
<vn-label-value label="Notes" no-ellipsize
value="{{$ctrl.summary.note}}">
</vn-label-value>
</vn-one>
<vn-one class="taxes">
<p><vn-label translate>Subtotal</vn-label> {{$ctrl.summary.subTotal | currency: 'EUR':2}}</p>
<p><vn-label translate>VAT</vn-label> {{$ctrl.summary.VAT | currency: 'EUR':2}}</p>
<p><vn-label><strong>Total</strong></vn-label> <strong>{{$ctrl.summary.total | currency: 'EUR':2}}</strong></p>
</vn-one>
<vn-auto>
<vn-table>
<vn-thead>
<vn-tr>
<vn-th shrink></vn-th>
<vn-th shrink>Item</vn-th>
<vn-th>Description</vn-th>
<vn-th number>Quantity</vn-th>
<vn-th number>Price</vn-th>
<vn-th number>Amount</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="row in $ctrl.summary.rows track by row.id">
<vn-td shrink>
<vn-icon
ng-show="row.visible || row.available"
color-main
icon="warning"
vn-tooltip="Visible: {{::row.visible || 0}} <br> {{::$ctrl.translate.instant('Available')}} {{::row.available || 0}}">
</vn-icon>
<vn-icon ng-show="row.reserved" icon="icon-reserva"></vn-icon>
</vn-td>
<vn-td shrink>
<span
ng-click="itemDescriptor.show($event, row.itemFk)"
class="link">
{{::row.itemFk}}
</span>
</vn-td>
<vn-td vn-fetched-tags>
<div>
<vn-one title="{{::row.item.name}}">{{::row.item.name}}</vn-one>
<vn-one ng-if="::row.item.subName">
<h3 title="{{::row.item.subName}}">{{::row.item.subName}}</h3>
</vn-one>
</div>
<vn-fetched-tags
max-length="6"
item="::row.item"
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td number>{{::row.quantity}}</vn-td>
<vn-td number>{{::row.price | currency: 'EUR':2}}</vn-td>
<vn-td number>{{::row.quantity * row.price | currency: 'EUR':2}}</vn-td>
</vn-tr>
</vn-tbody>
</table>
</vn-auto>
</vn-horizontal>
</vn-card>
<vn-item-descriptor-popover
vn-id="item-descriptor"
warehouse-fk="$ctrl.vnConfig.warehouseFk">
</vn-item-descriptor-popover>
<vn-client-descriptor-popover
vn-id="client-descriptor">
</vn-client-descriptor-popover>

View File

@ -1,41 +0,0 @@
import ngModule from '../module';
import Summary from 'salix/components/summary';
import './style.scss';
class Controller extends Summary {
setSummary() {
this.$http.get(`Orders/${this.order.id}/summary`)
.then(res => this.summary = res.data);
}
get formattedAddress() {
if (!this.summary) return null;
let address = this.summary.address;
let province = address.province ? `(${address.province.name})` : '';
return `${address.street} - ${address.city} ${province}`;
}
$onChanges() {
if (this.order && this.order.id)
this.setSummary();
}
save() {
this.$http.post(`Orders/${this.order.id}/confirm`).then(() => {
this.vnApp.showSuccess(this.$t('Order confirmed'));
this.$state.go(`ticket.index`, {
q: JSON.stringify({clientFk: this.order.clientFk})
});
});
}
}
ngModule.vnComponent('vnOrderSummary', {
template: require('./index.html'),
controller: Controller,
bindings: {
order: '<'
}
});

View File

@ -1,47 +0,0 @@
import './index';
describe('Order', () => {
describe('Component vnOrderSummary', () => {
let controller;
let $httpBackend;
beforeEach(ngModule('order'));
beforeEach(inject(($componentController, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
const $element = angular.element('<vn-order-summary></vn-order-summary>');
controller = $componentController('vnOrderSummary', {$element});
controller.order = {id: 1};
}));
describe('getSummary()', () => {
it('should now perform a GET query and define the summary property', () => {
let res = {
id: 1,
nickname: 'Batman'
};
$httpBackend.expectGET(`Orders/1/summary`).respond(res);
controller.setSummary();
$httpBackend.flush();
expect(controller.summary).toEqual(res);
});
});
describe('formattedAddress()', () => {
it('should return a full fromatted address with city and province', () => {
controller.summary = {
address: {
province: {
name: 'Gotham'
},
street: '1007 Mountain Drive',
city: 'Gotham'
}
};
expect(controller.formattedAddress).toEqual('1007 Mountain Drive - Gotham (Gotham)');
});
});
});
});

View File

@ -1,20 +0,0 @@
@import "./variables";
vn-order-summary .summary{
max-width: $width-lg;
& > vn-horizontal > vn-one {
min-width: 160px;
&.taxes {
border: $border-thin-light;
text-align: right;
padding: 8px;
& > p {
font-size: 1.2rem;
margin: 3px;
}
}
}
}

View File

@ -1,66 +0,0 @@
<vn-crud-model
auto-load="true"
vn-id="model"
url="OrderRows"
filter="::$ctrl.filter"
link="{orderFk: $ctrl.$params.id}"
limit="20"
data="rows"
on-data-change="$ctrl.onDataChange()">
</vn-crud-model>
<mg-ajax path="Orders/{{$ctrl.$params.id}}/getTotalVolume" options="mgEdit"></mg-ajax>
<vn-data-viewer model="model" class="header vn-w-lg">
<vn-card class="vn-pa-lg">
<vn-label-value
label="Total"
value="{{::edit.model.totalVolume}} M³">
</vn-label-value>
<vn-label-value
label="Cajas"
value="{{::edit.model.totalBoxes | dashIfEmpty}} U">
</vn-label-value>
</vn-card>
<vn-card class="vn-mt-md">
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th shrink field="itemFk" number>Item</vn-th>
<vn-th>Description</vn-th>
<vn-th shrink field="quantity" number>Quantity</vn-th>
<vn-th shrink number>m³ per quantity</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="row in rows">
<vn-td shrink number>
<span
ng-click="itemDescriptor.show($event, row.itemFk)"
class="link">
{{::row.itemFk}}
</span>
</vn-td>
<vn-td vn-fetched-tags>
<div>
<vn-one title="{{::row.item.name}}">{{::row.item.name}}</vn-one>
<vn-one ng-if="::row.item.subName">
<h3 title="{{::row.item.subName}}">{{::row.item.subName}}</h3>
</vn-one>
</div>
<vn-fetched-tags
max-length="6"
item="::row.item"
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td shrink number>{{::row.quantity}}</vn-td>
<vn-td shrink number>{{::row.volume | number:3}}</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card>
</vn-data-viewer>
<vn-item-descriptor-popover
vn-id="item-descriptor"
warehouse-fk="$ctrl.vnConfig.warehouseFk">
</vn-item-descriptor-popover>

View File

@ -1,37 +0,0 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
class Controller extends Section {
constructor($element, $) {
super($element, $);
this.filter = {
include: {
relation: 'item'
},
order: 'itemFk'
};
this.order = {};
this.ticketVolumes = [];
}
onDataChange() {
this.$http.get(`Orders/${this.$params.id}/getVolumes`)
.then(res => {
this.$.model.data.forEach(order => {
res.data.volumes.forEach(volume => {
if (order.itemFk === volume.itemFk)
order.volume = volume.volume;
});
});
});
}
}
ngModule.vnComponent('vnOrderVolume', {
template: require('./index.html'),
controller: Controller,
bindings: {
order: '<'
}
});

View File

@ -1,42 +0,0 @@
import './index';
describe('Order', () => {
describe('Component vnOrderVolume', () => {
let controller;
let $httpBackend;
let $scope;
beforeEach(ngModule('order'));
beforeEach(inject(($componentController, $state, _$httpBackend_, $rootScope) => {
$httpBackend = _$httpBackend_;
$scope = $rootScope.$new();
$scope.model = {
data: [
{itemFk: 1},
{itemFk: 2}
]
};
$state.params.id = 1;
const $element = angular.element('<vn-order-volume></vn-order-volume>');
controller = $componentController('vnOrderVolume', {$element, $scope});
}));
it('should join the sale volumes to its respective sale', () => {
let response = {
volumes: [
{itemFk: 1, volume: 0.008},
{itemFk: 2, volume: 0.003}
]
};
$httpBackend.expectGET(`Orders/1/getVolumes`).respond(response);
controller.onDataChange();
$httpBackend.flush();
expect(controller.$.model.data[0].volume).toBe(0.008);
expect(controller.$.model.data[1].volume).toBe(0.003);
});
});
});

View File

@ -1,12 +0,0 @@
@import "./variables";
vn-order-volume {
.header {
text-align: right;
& > div {
margin-bottom: $spacing-xs;
}
}
}

View File

@ -39,8 +39,6 @@ module.exports = Self => {
const {reportMail} = agencyMode();
let user;
let account;
let userEmail;
ctx.args.recipients = reportMail ? reportMail.split(',').map(email => email.trim()) : [];
if (workerFk) {
user = await models.VnUser.findById(workerFk, {
@ -50,17 +48,10 @@ module.exports = Self => {
account = await models.Account.findById(workerFk);
}
if (user?.active && account)
userEmail = user.emailUser().email;
if (userEmail)
ctx.args.recipients.push(userEmail);
ctx.args.recipients = [...new Set(ctx.args.recipients)];
if (!ctx.args.recipients.length)
throw new UserError('An email is necessary');
if (user?.active && account) ctx.args.recipient = user.emailUser().email;
else ctx.args.recipient = reportMail;
if (!ctx.args.recipient) throw new UserError('An email is necessary');
return Self.sendTemplate(ctx, 'driver-route');
};
};

View File

@ -3,7 +3,7 @@
"name": "Travels",
"icon": "local_airport",
"validations": true,
"dependencies": ["worker"],
"dependencies": ["worker", "entry"],
"menus": {
"main": [
{"state": "travel.index", "icon": "local_airport"},