Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 3108-item_isBargain_isOnOffer_deprecation
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Carlos Jimenez Ruiz 2021-10-19 09:57:45 +02:00
commit b41a0bd849
39 changed files with 982 additions and 161 deletions

View File

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

View File

@ -47,6 +47,13 @@ export async function getBrowser() {
});
page = extendPage(page);
page.setDefaultTimeout(5000);
await page.addStyleTag({
content: `* {
transition: none!important;
animation: none!important;
}`
});
await page.goto(defaultURL, {waitUntil: 'load'});
return {page, close: browser.close.bind(browser)};
}

View File

@ -18,6 +18,7 @@ describe('InvoiceOut manual invoice path', () => {
it('should open the manual invoice form', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.invoiceOutIndex.createInvoice);
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.invoiceOutIndex.createManualInvoice);
await page.waitForSelector(selectors.invoiceOutIndex.manualInvoiceForm);
});

View File

@ -3,8 +3,8 @@ import Popover from '../popover';
import './style.scss';
export default class Menu extends Popover {
show(parent) {
super.show(parent);
show(parent, direction) {
super.show(parent, direction);
this.windowEl.addEventListener('click', () => this.hide());
}
}

View File

@ -1,7 +1,18 @@
@import "./effects";
@import "variables";
.vn-menu {
vn-item, .vn-item {
@extend %clickable;
}
vn-item.dropdown:after,
.vn-item.dropdown:after {
font-family: 'Material Icons';
content: 'keyboard_arrow_right';
position: absolute;
color: $color-spacer;
font-size: 1.5em;
right: 0
}
}

View File

@ -23,12 +23,15 @@ export default class Popover extends Popup {
* it is shown in a visible relative position to it.
*
* @param {HTMLElement|Event} parent Overrides the parent property
* @param {String} direction - Direction [left]
*/
show(parent) {
show(parent, direction) {
if (parent instanceof Event)
parent = event.target;
if (parent) this.parent = parent;
if (direction) this.direction = direction;
super.show();
this.content = this.popup.querySelector('.content');
this.$timeout(() => this.relocate(), 10);
@ -89,21 +92,40 @@ export default class Popover extends Popup {
let width = clamp(popoverRect.width, parentRect.width, maxWith);
let height = popoverRect.height;
let left = parentRect.left + parentRect.width / 2 - width / 2;
left = clamp(left, margin, maxRight - width);
let left;
if (this.direction == 'left') {
left = parentRect.left + parentRect.width;
left = clamp(left, margin, maxRight - width);
} else {
left = parentRect.left + parentRect.width / 2 - width / 2;
left = clamp(left, margin, maxRight - width);
}
let top = parentRect.top + parentRect.height + arrowOffset;
let top;
if (this.direction == 'left')
top = parentRect.top;
else
top = parentRect.top + parentRect.height + arrowOffset;
let showTop = top + height > maxBottom;
if (showTop) top = parentRect.top - height - arrowOffset;
top = Math.max(top, margin);
if (showTop)
if (this.direction == 'left')
arrowStyle.left = `0`;
else if (showTop)
arrowStyle.bottom = `0`;
else
arrowStyle.top = `0`;
let arrowLeft = (parentRect.left - left) + parentRect.width / 2;
arrowLeft = clamp(arrowLeft, arrowHeight, width - arrowHeight);
let arrowLeft;
if (this.direction == 'left') {
arrowLeft = 0;
let arrowTop = arrowOffset;
arrowStyle.top = `${arrowTop}px`;
} else {
arrowLeft = (parentRect.left - left) + parentRect.width / 2;
arrowLeft = clamp(arrowLeft, arrowHeight, width - arrowHeight);
}
arrowStyle.left = `${arrowLeft}px`;
style.top = `${top}px`;

View File

@ -18,6 +18,18 @@ class Email {
return this.$http.get(`email/${template}`, {params})
.then(() => this.vnApp.showMessage(this.$t('Notification sent!')));
}
/**
* Sends an email displaying a notification when it's sent.
*
* @param {String} template The email report name
* @param {Object} params The email parameters
* @return {Promise} Promise resolved when it's sent
*/
sendCsv(template, params) {
return this.$http.get(`csv/${template}/send`, {params})
.then(() => this.vnApp.showMessage(this.$t('Notification sent!')));
}
}
Email.$inject = ['$http', '$translate', 'vnApp'];

View File

@ -20,6 +20,21 @@ class Report {
const serializedParams = this.$httpParamSerializer(params);
window.open(`api/report/${report}?${serializedParams}`);
}
/**
* Shows a report in another window, automatically adds the authorization
* token to params.
*
* @param {String} report The report name
* @param {Object} params The report parameters
*/
showCsv(report, params) {
params = Object.assign({
authorization: this.vnToken.token
}, params);
const serializedParams = this.$httpParamSerializer(params);
window.open(`api/csv/${report}/download?${serializedParams}`);
}
}
Report.$inject = ['$httpParamSerializer', 'vnToken'];

View File

@ -3,7 +3,7 @@
icon="menu"
class="show-menu"
ng-if="$ctrl.leftMenu"
ng-click="$ctrl.leftMenu.show()">
ng-click="$ctrl.leftMenu.toggle()">
</vn-icon-button>
<div class="side start">
<a class="logo" ui-sref="home" title="{{'Home' | translate}}">

View File

@ -48,6 +48,10 @@ vn-layout {
.show-menu {
display: none;
}
& > .show-menu {
margin-right: 5px;
display: block
}
.vn-button {
color: inherit;
font-size: 1.05rem;
@ -71,6 +75,10 @@ vn-layout {
& > .main-view {
padding-left: $menu-width;
}
&.shown > .main-view {
padding-left: 0;
}
}
&.right-menu {
& > vn-topbar > .end {
@ -85,6 +93,8 @@ vn-layout {
}
& > .main-view {
padding-top: $topbar-height;
transition: padding-left 200ms ease-out;
}
ui-view {
& > * {
@ -134,7 +144,8 @@ vn-layout {
& > vn-topbar {
left: 0;
}
& > .main-view {
& > .main-view,
&.shown > .main-view {
padding-left: 0;
}
}

View File

@ -52,12 +52,18 @@ export default class SideMenu extends Component {
this.hide();
}
toggle() {
if (this.shown) this.hide();
else this.show();
}
show() {
if (this.shown) return;
this.shown = true;
this.handler = e => this.onEscape(e);
this.$window.addEventListener('keydown', this.handler);
this.stateHandler = this.$transitions.onStart({}, t => this.onTransition(t));
this.layout.element.classList.add('shown');
}
hide() {
@ -65,6 +71,7 @@ export default class SideMenu extends Component {
this.$window.removeEventListener('keydown', this.handler);
this.stateHandler();
this.shown = false;
this.layout.element.classList.remove('shown');
}
}

View File

@ -14,16 +14,20 @@ vn-side-menu > .menu {
box-shadow: 0 1px 3px $color-shadow;
overflow: auto;
top: $topbar-height;
transition: transform 200ms ease-out;
&.left {
left: 0;
left: 0
}
&.right {
right: 0;
}
&.shown {
transform: translateZ(0) translateX(-$menu-width);
}
@media screen and (max-width: $mobile-width) {
transition: transform 200ms ease-out;
z-index: 15;
top: 0;

View File

@ -61,7 +61,7 @@
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
label="Type"
label="Orientation"
ng-model="$ctrl.viewportType"
data="$ctrl.viewportTypes"
selection="$ctrl.viewportSelection"

View File

@ -36,6 +36,18 @@ export default class UploadPhoto extends Component {
width: 1350,
height: 900
}
},
{
code: 'vertical',
description: this.$t('Vertical'),
viewport: {
width: 306.66,
height: 533.33
},
output: {
width: 460,
height: 800
}
}
];
this.viewportType = 'normal';
@ -103,8 +115,17 @@ export default class UploadPhoto extends Component {
const reader = new FileReader();
reader.onload = e => this.editor.bind({url: e.target.result});
reader.readAsDataURL(value);
} else if (this.uploadMethod == 'URL')
this.editor.bind({url: value});
} else if (this.uploadMethod == 'URL') {
const img = new Image();
img.crossOrigin = 'Anonymous';
img.src = value;
img.onload = () => this.editor.bind({url: value});
img.onerror = () => {
this.vnApp.showError(
this.$t(`This photo provider doesn't allow remote downloads`)
);
};
}
}
}

View File

@ -3,6 +3,8 @@ Select an image: Selecciona una imagen
File name: Nombre del fichero
Rotate left: Girar a la izquierda
Rotate right: Girar a la derecha
Panoramic: Panorámico
Panoramic: Panorámica
Orientation: Orientación
Select from computer: Seleccionar desde ordenador
Import from external URL: Importar desde URL externa
This photo provider doesn't allow remote downloads: Este proveedor de fotos no permite descargas remotas

View File

@ -2,18 +2,49 @@
module="invoiceOut"
description="$ctrl.invoiceOut.ref">
<slot-menu>
<a class="vn-item"
href="api/InvoiceOuts/{{$ctrl.id}}/download?access_token={{$ctrl.vnToken.token}}"
target="_blank"
<vn-item class="dropdown"
vn-click-stop="showInvoiceMenu.show($event, 'left')"
name="showInvoicePdf"
translate>
Show invoice PDF
</a>
<vn-item
ng-click="invoiceConfirmation.show({email: $ctrl.invoiceOut.client.email})"
Show invoice...
<vn-menu vn-id="showInvoiceMenu">
<vn-list>
<a class="vn-item"
href="api/InvoiceOuts/{{$ctrl.id}}/download?access_token={{$ctrl.vnToken.token}}"
target="_blank"
name="showInvoicePdf"
translate>
Show as PDF
</a>
<vn-item
ng-click="$ctrl.showCsvInvoice()"
translate>
Show as CSV
</vn-item>
</vn-list>
</vn-menu>
</vn-item>
<vn-item class="dropdown"
vn-click-stop="sendInvoiceMenu.show($event, 'left')"
name="sendInvoice"
translate>
Send invoice PDF
Send invoice...
<vn-menu vn-id="sendInvoiceMenu">
<vn-list>
<vn-item
ng-click="sendPdfConfirmation.show({email: $ctrl.invoiceOut.client.email})"
translate>
Send PDF
</vn-item>
<vn-item
ng-click="sendCsvConfirmation.show({email: $ctrl.invoiceOut.client.email})"
translate>
Send CSV
</vn-item>
</vn-list>
</vn-menu>
</vn-item>
<vn-item
ng-click="deleteConfirmation.show()"
@ -104,15 +135,32 @@
message="Generate PDF invoice document">
</vn-confirm>
<!-- Send invoice confirmation popup -->
<vn-dialog class="edit"
vn-id="invoiceConfirmation"
on-accept="$ctrl.sendInvoice($data)"
message="Send invoice PDF">
<!-- Send PDF invoice confirmation popup -->
<vn-dialog
vn-id="sendPdfConfirmation"
on-accept="$ctrl.sendPdfInvoice($data)"
message="Send PDF invoice">
<tpl-body>
<span translate>Are you sure you want to send it?</span>
<vn-textfield vn-one
ng-model="invoiceConfirmation.data.email">
ng-model="sendPdfConfirmation.data.email">
</vn-textfield>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Confirm</button>
</tpl-buttons>
</vn-dialog>
<!-- Send CSV invoice confirmation popup -->
<vn-dialog
vn-id="sendCsvConfirmation"
on-accept="$ctrl.sendCsvInvoice($data)"
message="Send CSV invoice">
<tpl-body>
<span translate>Are you sure you want to send it?</span>
<vn-textfield vn-one
ng-model="sendCsvConfirmation.data.email">
</vn-textfield>
</tpl-body>
<tpl-buttons>

View File

@ -14,29 +14,6 @@ class Controller extends Descriptor {
return this.aclService.hasAny(['invoicing']);
}
deleteInvoiceOut() {
return this.$http.post(`InvoiceOuts/${this.id}/delete`)
.then(() => this.$state.go('invoiceOut.index'))
.then(() => this.vnApp.showSuccess(this.$t('InvoiceOut deleted')));
}
bookInvoiceOut() {
return this.$http.post(`InvoiceOuts/${this.invoiceOut.ref}/book`)
.then(() => this.$state.reload())
.then(() => this.vnApp.showSuccess(this.$t('InvoiceOut booked')));
}
createInvoicePdf() {
const invoiceId = this.invoiceOut.id;
return this.$http.post(`InvoiceOuts/${invoiceId}/createPdf`)
.then(() => this.reload())
.then(() => {
const snackbarMessage = this.$t(
`The invoice PDF document has been regenerated`);
this.vnApp.showSuccess(snackbarMessage);
});
}
get filter() {
if (this.invoiceOut)
return JSON.stringify({refFk: this.invoiceOut.ref});
@ -55,7 +32,7 @@ class Controller extends Descriptor {
}, {
relation: 'client',
scope: {
fields: ['id', 'name']
fields: ['id', 'name', 'email']
}
}
]
@ -76,13 +53,51 @@ class Controller extends Descriptor {
// Prevents error when not defined
}
sendInvoice($data) {
deleteInvoiceOut() {
return this.$http.post(`InvoiceOuts/${this.id}/delete`)
.then(() => this.$state.go('invoiceOut.index'))
.then(() => this.vnApp.showSuccess(this.$t('InvoiceOut deleted')));
}
bookInvoiceOut() {
return this.$http.post(`InvoiceOuts/${this.invoiceOut.ref}/book`)
.then(() => this.$state.reload())
.then(() => this.vnApp.showSuccess(this.$t('InvoiceOut booked')));
}
createPdfInvoice() {
const invoiceId = this.invoiceOut.id;
return this.$http.post(`InvoiceOuts/${invoiceId}/createPdf`)
.then(() => this.reload())
.then(() => {
const snackbarMessage = this.$t(
`The invoice PDF document has been regenerated`);
this.vnApp.showSuccess(snackbarMessage);
});
}
showCsvInvoice() {
this.vnReport.showCsv('invoice', {
recipientId: this.invoiceOut.client.id,
invoiceId: this.id,
});
}
sendPdfInvoice($data) {
return this.vnEmail.send('invoice', {
recipientId: this.invoiceOut.client.id,
recipient: $data.email,
invoiceId: this.id
});
}
sendCsvInvoice($data) {
return this.vnEmail.sendCsv('invoice', {
recipientId: this.invoiceOut.client.id,
recipient: $data.email,
invoiceId: this.id
});
}
}
ngModule.vnComponent('vnInvoiceOutDescriptor', {

View File

@ -3,30 +3,20 @@ import './index';
describe('vnInvoiceOutDescriptor', () => {
let controller;
let $httpBackend;
const invoiceOut = {id: 1};
let $httpParamSerializer;
const invoiceOut = {
id: 1,
client: {id: 1101}
};
beforeEach(ngModule('invoiceOut'));
beforeEach(inject(($componentController, _$httpBackend_) => {
beforeEach(inject(($componentController, _$httpParamSerializer_, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
controller = $componentController('vnInvoiceOutDescriptor', {$element: null});
}));
describe('createInvoicePdf()', () => {
it('should make a query and show a success snackbar', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
controller.invoiceOut = invoiceOut;
$httpBackend.whenGET(`InvoiceOuts/${invoiceOut.id}`).respond();
$httpBackend.expectPOST(`InvoiceOuts/${invoiceOut.id}/createPdf`).respond();
controller.createInvoicePdf();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
describe('loadData()', () => {
it(`should perform a get query to store the invoice in data into the controller`, () => {
const id = 1;
@ -39,4 +29,81 @@ describe('vnInvoiceOutDescriptor', () => {
expect(controller.invoiceOut).toEqual(response);
});
});
describe('createPdfInvoice()', () => {
it('should make a query to the createPdf() endpoint and show a success snackbar', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
controller.invoiceOut = invoiceOut;
$httpBackend.whenGET(`InvoiceOuts/${invoiceOut.id}`).respond();
$httpBackend.expectPOST(`InvoiceOuts/${invoiceOut.id}/createPdf`).respond();
controller.createPdfInvoice();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
describe('showCsvInvoice()', () => {
it('should make a query to the csv invoice download endpoint and show a message snackbar', () => {
jest.spyOn(window, 'open').mockReturnThis();
controller.invoiceOut = invoiceOut;
const expectedParams = {
invoiceId: invoiceOut.id,
recipientId: invoiceOut.client.id
};
const serializedParams = $httpParamSerializer(expectedParams);
const expectedPath = `api/csv/invoice/download?${serializedParams}`;
controller.showCsvInvoice();
expect(window.open).toHaveBeenCalledWith(expectedPath);
});
});
describe('sendPdfInvoice()', () => {
it('should make a query to the email invoice endpoint and show a message snackbar', () => {
jest.spyOn(controller.vnApp, 'showMessage');
controller.invoiceOut = invoiceOut;
const $data = {email: 'brucebanner@gothamcity.com'};
const expectedParams = {
invoiceId: invoiceOut.id,
recipient: $data.email,
recipientId: invoiceOut.client.id
};
const serializedParams = $httpParamSerializer(expectedParams);
$httpBackend.expectGET(`email/invoice?${serializedParams}`).respond();
controller.sendPdfInvoice($data);
$httpBackend.flush();
expect(controller.vnApp.showMessage).toHaveBeenCalled();
});
});
describe('sendCsvInvoice()', () => {
it('should make a query to the csv invoice send endpoint and show a message snackbar', () => {
jest.spyOn(controller.vnApp, 'showMessage');
controller.invoiceOut = invoiceOut;
const $data = {email: 'brucebanner@gothamcity.com'};
const expectedParams = {
invoiceId: invoiceOut.id,
recipient: $data.email,
recipientId: invoiceOut.client.id
};
const serializedParams = $httpParamSerializer(expectedParams);
$httpBackend.expectGET(`csv/invoice/send?${serializedParams}`).respond();
controller.sendCsvInvoice($data);
$httpBackend.flush();
expect(controller.vnApp.showMessage).toHaveBeenCalled();
});
});
});

View File

@ -2,8 +2,10 @@ Volume exceded: Volumen excedido
Volume: Volumen
Client card: Ficha del cliente
Invoice ticket list: Listado de tickets de la factura
Show invoice PDF: Ver factura en PDF
Send invoice PDF: Enviar factura en PDF
Show invoice...: Ver factura...
Send invoice...: Enviar factura...
Send PDF invoice: Enviar factura en PDF
Send CSV invoice: Enviar factura en CSV
Delete Invoice: Eliminar factura
Clone Invoice: Clonar factura
InvoiceOut deleted: Factura eliminada

View File

@ -29,6 +29,7 @@
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th shrink></vn-th>
<vn-th expand>Date</vn-th>
<vn-th number order="DESC" shrink>Id</vn-th>
<vn-th>State</vn-th>
@ -48,6 +49,14 @@
vn-repeat-last
on-last="$ctrl.scrollToLine(sale.lastPreparedLineFk)"
ng-attr-id="vnItemDiary-{{::sale.lineFk}}">
<vn-td shrink>
<a ui-sref="claim.card.basicData({id: sale.claimFk})">
<vn-icon icon="icon-claims"
ng-show="sale.claimFk"
vn-tooltip="{{::$ctrl.$t('Claim')}}: {{::sale.claimFk}}">
</vn-icon>
</a>
</vn-td>
<vn-td expand>
<span class="chip"
ng-class="::{warning: $ctrl.today == sale.shipped}">

View File

@ -7,17 +7,26 @@
order="landed DESC, buyFk DESC"
limit="20">
</vn-crud-model>
<vn-card class="vn-mb-md vn-w-xl vn-pa-lg">
<vn-date-picker
vn-none
label="Since"
ng-model="$ctrl.date">
</vn-date-picker>
<vn-card class="vn-w-md vn-pa-md">
<vn-horizontal>
<vn-date-picker class="vn-pa-xs"
vn-one
label="Since"
ng-model="$ctrl.dateFrom">
</vn-date-picker>
<vn-date-picker class="vn-pa-xs"
vn-one
label="To"
ng-model="$ctrl.dateTo">
</vn-date-picker>
</vn-horizontal>
</vn-card>
<vn-data-viewer
model="model"
class="vn-mb-xl vn-w-xl">
class="vn-mb-xl vn-w-xl vn-pa-md">
<vn-card class="vn-pa-lg">
<vn-vertical>
<vn-table model="model">

View File

@ -1,17 +1,16 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
class Controller extends Section {
constructor($element, $) {
super($element, $);
const from = new Date();
from.setDate(from.getDate() - 75);
from.setDate(from.getDate());
from.setHours(0, 0, 0, 0);
const to = new Date();
to.setDate(to.getDate() + 60);
to.setDate(to.getDate() + 10);
to.setHours(23, 59, 59, 59);
this.filter = {
@ -22,19 +21,19 @@ class Controller extends Section {
}
}
};
this._date = from;
this._dateFrom = from;
this._dateTo = to;
}
set date(value) {
this._date = value;
set dateFrom(value) {
this._dateFrom = value;
if (!value) return;
const from = new Date(value);
from.setHours(0, 0, 0, 0);
const to = new Date();
to.setDate(to.getDate() + 60);
const to = new Date(this._dateTo);
to.setHours(23, 59, 59, 59);
this.filter.where.shipped = {
@ -43,8 +42,29 @@ class Controller extends Section {
this.$.model.refresh();
}
get date() {
return this._date;
set dateTo(value) {
this._dateTo = value;
if (!value) return;
const from = new Date(this._dateFrom);
from.setHours(0, 0, 0, 0);
const to = new Date(value);
to.setHours(23, 59, 59, 59);
this.filter.where.shipped = {
between: [from, to]
};
this.$.model.refresh();
}
get dateFrom() {
return this._dateFrom;
}
get dateTo() {
return this._dateTo;
}
exprBuilder(param, value) {

View File

@ -1,24 +0,0 @@
@import "variables";
vn-item-last-entries {
.round {
background-color: $color-spacer;
border-radius: 25px;
float: right;
width: 25px;
color: $color-font-dark;
text-align: center;
font-weight: bold;
}
vn-horizontal {
justify-content: center;
}
vn-date-picker {
flex: none !important;
}
@media screen and (max-width: 1440px) {
.expendable {
display: none;
}
}
}

View File

@ -2,6 +2,7 @@
icon="more_vert"
vn-popover="menu">
</vn-icon-button>
<vn-menu vn-id="menu">
<vn-list>
<vn-item
@ -12,15 +13,44 @@
translate>
Add turn
</vn-item>
<vn-item
ng-click="$ctrl.showDeliveryNote()"
<vn-item class="dropdown"
vn-click-stop="showDeliveryNoteMenu.show($event, 'left')"
translate>
Show Delivery Note
Show Delivery Note...
<vn-menu vn-id="showDeliveryNoteMenu">
<vn-list>
<vn-item
ng-click="$ctrl.showPdfDeliveryNote()"
translate>
Show as PDF
</vn-item>
<vn-item
ng-click="$ctrl.showCsvDeliveryNote()"
translate>
Show as CSV
</vn-item>
</vn-list>
</vn-menu>
</vn-item>
<vn-item
ng-click="confirmDeliveryNote.show()"
<vn-item class="dropdown"
vn-click-stop="sendDeliveryNoteMenu.show($event, 'left')"
translate>
Send Delivery Note
Send Delivery Note...
<vn-menu vn-id="sendDeliveryNoteMenu">
<vn-list>
<vn-item
ng-click="sendPdfConfirmation.show({email: $ctrl.ticket.client.email})"
translate>
Send PDF
</vn-item>
<vn-item
ng-click="sendCsvConfirmation.show({email: $ctrl.ticket.client.email})"
translate>
Send CSV
</vn-item>
</vn-list>
</vn-menu>
</vn-item>
<vn-item
ng-click="deleteConfirmation.show()"
@ -79,7 +109,7 @@
Make invoice
</vn-item>
<vn-item
ng-click="createInvoicePdfConfirmation.show()"
ng-click="createPdfConfirmation.show()"
ng-show="$ctrl.isInvoiced && ($ctrl.hasInvoicing || !$ctrl.ticket.invoiceOut.hasPdf)"
name="regenerateInvoice"
translate>
@ -133,13 +163,39 @@
</div>
</vn-popup>
<!-- Send delivery note confirmation popup -->
<vn-confirm
vn-id="confirmDeliveryNote"
on-accept="$ctrl.sendDeliveryNote()"
question="Are you sure you want to send it?"
message="Send Delivery Note">
</vn-confirm>
<!-- Send PDF delivery note confirmation popup -->
<vn-dialog
vn-id="sendPdfConfirmation"
on-accept="$ctrl.sendPdfDeliveryNote($data)"
message="Send PDF Delivery Note">
<tpl-body>
<span translate>Are you sure you want to send it?</span>
<vn-textfield vn-one
ng-model="sendPdfConfirmation.data.email">
</vn-textfield>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Confirm</button>
</tpl-buttons>
</vn-dialog>
<!-- Send CSV delivery note confirmation popup -->
<vn-dialog
vn-id="sendCsvConfirmation"
on-accept="$ctrl.sendCsvDeliveryNote($data)"
message="Send CSV Delivery Note">
<tpl-body>
<span translate>Are you sure you want to send it?</span>
<vn-textfield vn-one
ng-model="sendCsvConfirmation.data.email">
</vn-textfield>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Confirm</button>
</tpl-buttons>
</vn-dialog>
<!-- Delete ticket confirmation popup -->
<vn-confirm
@ -206,8 +262,8 @@
<!-- Create invoice PDF confirmation dialog -->
<vn-confirm
vn-id="createInvoicePdfConfirmation"
on-accept="$ctrl.createInvoicePdf()"
vn-id="createPdfConfirmation"
on-accept="$ctrl.createPdfInvoice()"
question="Are you sure you want to generate/regenerate the PDF invoice?"
message="Generate PDF invoice document">
</vn-confirm>

View File

@ -115,17 +115,32 @@ class Controller extends Section {
});
}
showDeliveryNote() {
showPdfDeliveryNote() {
this.vnReport.show('delivery-note', {
recipientId: this.ticket.client.id,
ticketId: this.id,
});
}
sendDeliveryNote() {
showCsvDeliveryNote() {
this.vnReport.showCsv('delivery-note', {
recipientId: this.ticket.client.id,
ticketId: this.id,
});
}
sendPdfDeliveryNote($data) {
return this.vnEmail.send('delivery-note', {
recipientId: this.ticket.client.id,
recipient: this.ticket.client.email,
recipient: $data.email,
ticketId: this.id
});
}
sendCsvDeliveryNote($data) {
return this.vnEmail.sendCsv('delivery-note', {
recipientId: this.ticket.client.id,
recipient: $data.email,
ticketId: this.id
});
}
@ -227,7 +242,7 @@ class Controller extends Section {
.then(() => this.vnApp.showSuccess(this.$t('Ticket invoiced')));
}
createInvoicePdf() {
createPdfInvoice() {
const invoiceId = this.ticket.invoiceOut.id;
return this.$http.post(`InvoiceOuts/${invoiceId}/createPdf`)
.then(() => this.reload())

View File

@ -2,6 +2,7 @@ import './index.js';
describe('Ticket Component vnTicketDescriptorMenu', () => {
let $httpBackend;
let $httpParamSerializer;
let controller;
let $state;
@ -25,8 +26,9 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
beforeEach(ngModule('ticket'));
beforeEach(inject(($componentController, _$httpBackend_, _$state_) => {
beforeEach(inject(($componentController, _$httpBackend_, _$httpParamSerializer_, _$state_) => {
$httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
$state = _$state_;
$state.params.id = 16;
$state.getCurrentPath = () => [null, {state: {name: 'ticket'}}];
@ -104,36 +106,74 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
});
});
describe('showDeliveryNote()', () => {
describe('showPdfDeliveryNote()', () => {
it('should open a new window showing a delivery note PDF document', () => {
jest.spyOn(controller.vnReport, 'show');
jest.spyOn(window, 'open').mockReturnThis();
window.open = jasmine.createSpy('open');
const params = {
recipientId: ticket.client.id,
ticketId: ticket.id
const expectedParams = {
ticketId: ticket.id,
recipientId: ticket.client.id
};
controller.showDeliveryNote();
const serializedParams = $httpParamSerializer(expectedParams);
const expectedPath = `api/report/delivery-note?${serializedParams}`;
controller.showPdfDeliveryNote();
expect(controller.vnReport.show).toHaveBeenCalledWith('delivery-note', params);
expect(window.open).toHaveBeenCalledWith(expectedPath);
});
});
describe('sendDeliveryNote()', () => {
describe('sendPdfDeliveryNote()', () => {
it('should make a query and call vnApp.showMessage()', () => {
jest.spyOn(controller.vnEmail, 'send');
const $data = {email: 'brucebanner@gothamcity.com'};
const params = {
recipient: ticket.client.email,
recipient: $data.email,
recipientId: ticket.client.id,
ticketId: ticket.id
};
controller.sendDeliveryNote();
controller.sendPdfDeliveryNote($data);
expect(controller.vnEmail.send).toHaveBeenCalledWith('delivery-note', params);
});
});
describe('showCsvDeliveryNote()', () => {
it('should make a query to the csv delivery-note download endpoint and show a message snackbar', () => {
jest.spyOn(window, 'open').mockReturnThis();
const expectedParams = {
ticketId: ticket.id,
recipientId: ticket.client.id
};
const serializedParams = $httpParamSerializer(expectedParams);
const expectedPath = `api/csv/delivery-note/download?${serializedParams}`;
controller.showCsvDeliveryNote();
expect(window.open).toHaveBeenCalledWith(expectedPath);
});
});
describe('sendCsvDeliveryNote()', () => {
it('should make a query to the csv delivery-note send endpoint and show a message snackbar', () => {
jest.spyOn(controller.vnApp, 'showMessage');
const $data = {email: 'brucebanner@gothamcity.com'};
const expectedParams = {
ticketId: ticket.id,
recipient: $data.email,
recipientId: ticket.client.id,
};
const serializedParams = $httpParamSerializer(expectedParams);
$httpBackend.expectGET(`csv/delivery-note/send?${serializedParams}`).respond();
controller.sendCsvDeliveryNote($data);
$httpBackend.flush();
expect(controller.vnApp.showMessage).toHaveBeenCalled();
});
});
describe('makeInvoice()', () => {
it('should make a query and call $state.reload() method', () => {
jest.spyOn(controller, 'reload').mockReturnThis();
@ -149,13 +189,13 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
});
});
describe('createInvoicePdf()', () => {
describe('createPdfInvoice()', () => {
it('should make a query and show a success snackbar', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
$httpBackend.whenGET(`Tickets/16`).respond();
$httpBackend.expectPOST(`InvoiceOuts/${ticket.invoiceOut.id}/createPdf`).respond();
controller.createInvoicePdf();
controller.createPdfInvoice();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();

View File

@ -0,0 +1,8 @@
Show Delivery Note...: Ver albarán...
Send Delivery Note...: Enviar albarán...
Show as PDF: Ver como PDF
Show as CSV: Ver como CSV
Send PDF: Enviar PDF
Send CSV: Enviar CSV
Send CSV Delivery Note: Enviar albarán en CSV
Send PDF Delivery Note: Enviar albarán en PDF

View File

@ -8,8 +8,6 @@ Add stowaway: Añadir polizón
Delete stowaway: Eliminar polizón
Are you sure you want to delete this stowaway?: ¿Seguro que quieres eliminar este polizón?
Are you sure you want to send it?: ¿Seguro que quieres enviarlo?
Show Delivery Note: Ver albarán
Send Delivery Note: Enviar albarán
Show pallet report: Ver hoja de pallet
Change shipped hour: Cambiar hora de envío
Shipped hour: Hora de envío

View File

@ -37,20 +37,30 @@ class Email extends Component {
return userTranslations.subject;
}
async send() {
/**
* @param {Object} [options] - Additional options
* @param {Boolean} [options.overrideAttachments] - Overrides default PDF attachments
* @param {Array} [options.attachments] - Array containing attachment objects
* @return {Promise} SMTP Promise
*/
async send(options = {}) {
const instance = this.build();
const rendered = await this.render();
const attachments = [];
const getAttachments = async(componentPath, files) => {
for (file of files) {
const fileCopy = Object.assign({}, file);
const fileName = fileCopy.filename;
if (options.overrideAttachments && !fileName.includes('.png')) continue;
if (fileCopy.cid) {
const templatePath = `${componentPath}/${file.path}`;
const fullFilePath = path.resolve(__dirname, templatePath);
fileCopy.path = path.resolve(__dirname, fullFilePath);
} else {
const reportName = fileCopy.filename.replace('.pdf', '');
const reportName = fileName.replace('.pdf', '');
const report = new Report(reportName, this.args);
fileCopy.content = await report.toPdfStream();
}
@ -71,9 +81,14 @@ class Email extends Component {
if (this.attachments)
await getAttachments(this.path, this.attachments);
if (options.attachments) {
for (let attachment of options.attachments)
attachments.push(attachment);
}
const localeSubject = await this.getSubject();
const replyTo = this.args.replyTo || this.args.auth.email;
const options = {
const mailOptions = {
to: this.args.recipient,
replyTo: replyTo,
subject: localeSubject,
@ -81,7 +96,7 @@ class Email extends Component {
attachments: attachments
};
return smtp.send(options);
return smtp.send(mailOptions);
}
}

View File

@ -8,9 +8,10 @@ module.exports = app => {
const methods = [];
// Get all methods
methodsDir.forEach(method => {
methods.push(method.replace('.js', ''));
});
for (let method of methodsDir) {
if (method.includes('.js'))
methods.push(method.replace('.js', ''));
}
// Auth middleware
const paths = [];

31
print/methods/csv.js Normal file
View File

@ -0,0 +1,31 @@
module.exports = app => {
app.use('/api/csv/delivery-note', require('./csv/delivery-note')(app));
app.use('/api/csv/invoice', require('./csv/invoice')(app));
app.toCSV = function toCSV(rows) {
const [columns] = rows;
let content = Object.keys(columns).join('\t');
for (let row of rows) {
const values = Object.values(row);
const finalValues = values.map(value => {
if (value instanceof Date) return formatDate(value);
if (value === null) return '';
return value;
});
content += '\n';
content += finalValues.join('\t');
}
return content;
};
function formatDate(date) {
return new Intl.DateTimeFormat('es', {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).format(date);
}
};

View File

@ -0,0 +1,82 @@
const express = require('express');
const router = new express.Router();
const path = require('path');
const db = require('../../../core/database');
const sqlPath = path.join(__dirname, 'sql');
module.exports = app => {
router.get('/preview', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.ticketId)
throw new Error('The argument ticketId is required');
const ticketId = reqArgs.ticketId;
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [ticketId]);
const content = app.toCSV(sales);
const fileName = `ticket_${ticketId}.csv`;
res.setHeader('Content-type', 'application/json; charset=utf-8');
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
res.end(content);
} catch (error) {
next(error);
}
});
router.get('/download', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.ticketId)
throw new Error('The argument ticketId is required');
const ticketId = reqArgs.ticketId;
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [ticketId]);
const content = app.toCSV(sales);
const fileName = `ticket_${ticketId}.csv`;
res.setHeader('Content-type', 'text/csv');
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
res.end(content);
} catch (error) {
next(error);
}
});
const Email = require('../../../core/email');
router.get('/send', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.ticketId)
throw new Error('The argument ticketId is required');
const ticketId = reqArgs.ticketId;
const ticket = await db.findOneFromDef(`${sqlPath}/ticket`, [ticketId]);
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [ticketId]);
const args = Object.assign({
ticketId: (String(ticket.id)),
recipientId: ticket.clientFk,
recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail
}, reqArgs);
const content = app.toCSV(sales);
const fileName = `ticket_${ticketId}.csv`;
const email = new Email('delivery-note', args);
await email.send({
overrideAttachments: true,
attachments: [{
filename: fileName,
content: content
}]
});
res.status(200).json({message: 'ok'});
} catch (error) {
next(error);
}
});
return router;
};

View File

@ -0,0 +1,35 @@
SELECT io.ref Invoice,
io.issued InvoiceDate,
s.ticketFk Ticket,
s.itemFk Item,
s.concept Description,
i.size,
i.subName Producer,
s.quantity Quantity,
s.price Price,
s.discount Discount,
s.created Created,
tc.code Taxcode,
tc.description TaxDescription,
i.tag5,
i.value5,
i.tag6,
i.value6,
i.tag7,
i.value7,
i.tag8,
i.value8,
i.tag9,
i.value9,
i.tag10,
i.value10
FROM vn.sale s
JOIN vn.ticket t ON t.id = s.ticketFk
JOIN vn.item i ON i.id = s.itemFk
JOIN vn.supplier s2 ON s2.id = t.companyFk
JOIN vn.itemTaxCountry itc ON itc.itemFk = i.id
AND itc.countryFk = s2.countryFk
JOIN vn.taxClass tc ON tc.id = itc.taxClassFk
LEFT JOIN vn.invoiceOut io ON io.id = t.refFk
WHERE s.ticketFk = ?
ORDER BY s.ticketFk, s.created

View File

@ -0,0 +1,9 @@
SELECT
t.id,
t.clientFk,
c.email recipient,
eu.email salesPersonEmail
FROM ticket t
JOIN client c ON c.id = t.clientFk
LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
WHERE t.id = ?

View File

@ -0,0 +1,82 @@
const express = require('express');
const router = new express.Router();
const path = require('path');
const db = require('../../../core/database');
const sqlPath = path.join(__dirname, 'sql');
module.exports = app => {
router.get('/preview', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.invoiceId)
throw new Error('The argument invoiceId is required');
const invoiceId = reqArgs.invoiceId;
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [invoiceId]);
const content = app.toCSV(sales);
const fileName = `invoice_${invoiceId}.csv`;
res.setHeader('Content-type', 'application/json; charset=utf-8');
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
res.end(content);
} catch (error) {
next(error);
}
});
router.get('/download', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.invoiceId)
throw new Error('The argument invoiceId is required');
const invoiceId = reqArgs.invoiceId;
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [invoiceId]);
const content = app.toCSV(sales);
const fileName = `invoice_${invoiceId}.csv`;
res.setHeader('Content-type', 'text/csv');
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
res.end(content);
} catch (error) {
next(error);
}
});
const Email = require('../../../core/email');
router.get('/send', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.invoiceId)
throw new Error('The argument invoiceId is required');
const invoiceId = reqArgs.invoiceId;
const invoice = await db.findOneFromDef(`${sqlPath}/invoice`, [invoiceId]);
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [invoiceId]);
const args = Object.assign({
invoiceId: (String(invoice.id)),
recipientId: invoice.clientFk,
recipient: invoice.recipient,
replyTo: invoice.salesPersonEmail
}, reqArgs);
const content = app.toCSV(sales);
const fileName = `invoice_${invoiceId}.csv`;
const email = new Email('invoice', args);
await email.send({
overrideAttachments: true,
attachments: [{
filename: fileName,
content: content
}]
});
res.status(200).json({message: 'ok'});
} catch (error) {
next(error);
}
});
return router;
};

View File

@ -0,0 +1,9 @@
SELECT
io.id,
io.clientFk,
c.email recipient,
eu.email salesPersonEmail
FROM invoiceOut io
JOIN client c ON c.id = io.clientFk
LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
WHERE io.id = ?

View File

@ -0,0 +1,35 @@
SELECT io.ref Invoice,
io.issued InvoiceDate,
s.ticketFk Ticket,
s.itemFk Item,
s.concept Description,
i.size,
i.subName Producer,
s.quantity Quantity,
s.price Price,
s.discount Discount,
s.created Created,
tc.code Taxcode,
tc.description TaxDescription,
i.tag5,
i.value5,
i.tag6,
i.value6,
i.tag7,
i.value7,
i.tag8,
i.value8,
i.tag9,
i.value9,
i.tag10,
i.value10
FROM sale s
JOIN ticket t ON t.id = s.ticketFk
JOIN item i ON i.id = s.itemFk
JOIN supplier s2 ON s2.id = t.companyFk
JOIN itemTaxCountry itc ON itc.itemFk = i.id
AND itc.countryFk = s2.countryFk
JOIN taxClass tc ON tc.id = itc.taxClassFk
JOIN invoiceOut io ON io.ref = t.refFk
WHERE io.id = ?
ORDER BY s.ticketFk, s.created

View File

@ -29,6 +29,9 @@ module.exports = {
const hash = md5(this.signature.id.toString()).substring(0, 3);
const file = `${config.storage.root}/${hash}/${this.signature.id}.png`;
if (!fs.existsSync(file)) return null;
const src = fs.readFileSync(file);
const base64 = Buffer.from(src, 'utf8').toString('base64');

View File

@ -95,7 +95,6 @@ module.exports = {
},
ticketSubtotal(ticket) {
let subTotal = 0.00;
console.log(ticket.sales);
for (let sale of ticket.sales)
subTotal += this.saleImport(sale);