Merge branch 'dev' into 5517-improveLogs+translations
gitea/salix/pipeline/head There was a failure building this commit Details

This commit is contained in:
Juan Ferrer 2023-04-27 18:13:24 +02:00
commit 2cfd2d9dda
86 changed files with 494 additions and 579 deletions

View File

@ -8,7 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [2316.01] - 2023-05-04 ## [2316.01] - 2023-05-04
### Added ### Added
- - (Usuarios -> Histórico) Nueva sección
- (Roles -> Histórico) Nueva sección
### Changed ### Changed
- (Artículo -> Precio fijado) Modificado el buscador superior por uno lateral - (Artículo -> Precio fijado) Modificado el buscador superior por uno lateral

View File

@ -0,0 +1,3 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Receipt', 'receiptEmail', '*', 'ALLOW', 'ROLE', 'salesAssistant');

View File

@ -0,0 +1,3 @@
UPDATE `salix`.`ACL`
SET model = 'InvoiceOut'
WHERE property IN ('negativeBases', 'negativeBasesCsv');

View File

@ -1,21 +0,0 @@
create or replace definer = root@localhost view User as
select `account`.`user`.`id` AS `id`,
`account`.`user`.`realm` AS `realm`,
`account`.`user`.`name` AS `name`,
`account`.`user`.`nickname` AS `nickname`,
`account`.`user`.`bcryptPassword` AS `password`,
`account`.`user`.`role` AS `role`,
`account`.`user`.`active` AS `active`,
`account`.`user`.`email` AS `email`,
`account`.`user`.`emailVerified` AS `emailVerified`,
`account`.`user`.`verificationToken` AS `verificationToken`,
`account`.`user`.`lang` AS `lang`,
`account`.`user`.`lastPassChange` AS `lastPassChange`,
`account`.`user`.`created` AS `created`,
`account`.`user`.`updated` AS `updated`,
`account`.`user`.`image` AS `image`,
`account`.`user`.`recoverPass` AS `recoverPass`,
`account`.`user`.`sync` AS `sync`,
`account`.`user`.`hasGrant` AS `hasGrant`
from `account`.`user`;

View File

@ -0,0 +1,2 @@
DROP PROCEDURE `vn`.`refund`;
DROP PROCEDURE `vn`.`ticket_doRefund`;

View File

@ -0,0 +1,9 @@
-- vn.companyI18n definition
CREATE TABLE `vn`.`companyI18n` (
`companyFk` smallint(5) unsigned NOT NULL,
`lang` char(2) CHARACTER SET utf8mb3 NOT NULL,
`footnotes` longtext COLLATE utf8mb3_unicode_ci DEFAULT NULL,
PRIMARY KEY (`companyFk`,`lang`),
CONSTRAINT `companyI18n_FK` FOREIGN KEY (`companyFk`) REFERENCES `company` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;

View File

@ -0,0 +1 @@
ALTER TABLE `vn`.`company` ADD `web` varchar(100) NULL;

View File

@ -546,7 +546,8 @@ INSERT INTO `vn`.`supplier`(`id`, `name`, `nickname`,`account`,`countryFk`,`nif`
VALUES VALUES
(1, 'Plants SL', 'Plants nick', 4100000001, 1, '06089160W', 0, util.VN_CURDATE(), 1, 'supplier address 1', 'PONTEVEDRA', 1, 15214, 1, 1, 15, 4, 1, 1, 18, 'flowerPlants', 1, '400664487V'), (1, 'Plants SL', 'Plants nick', 4100000001, 1, '06089160W', 0, util.VN_CURDATE(), 1, 'supplier address 1', 'PONTEVEDRA', 1, 15214, 1, 1, 15, 4, 1, 1, 18, 'flowerPlants', 1, '400664487V'),
(2, 'Farmer King', 'The farmer', 4000020002, 1, '87945234L', 0, util.VN_CURDATE(), 1, 'supplier address 2', 'GOTHAM', 2, 43022, 1, 2, 10, 93, 2, 8, 18, 'animals', 1, '400664487V'), (2, 'Farmer King', 'The farmer', 4000020002, 1, '87945234L', 0, util.VN_CURDATE(), 1, 'supplier address 2', 'GOTHAM', 2, 43022, 1, 2, 10, 93, 2, 8, 18, 'animals', 1, '400664487V'),
(442, 'Verdnatura Levante SL', 'Verdnatura', 5115000442, 1, '06815934E', 0, util.VN_CURDATE(), 1, 'supplier address 3', 'GOTHAM', 1, 43022, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V'); (442, 'Verdnatura Levante SL', 'Verdnatura', 5115000442, 1, '06815934E', 0, util.VN_CURDATE(), 1, 'supplier address 3', 'GOTHAM', 1, 43022, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V'),
(1381, 'Ornamentales', 'Ornamentales', 7185000440, 1, '03815934E', 0, util.VN_CURDATE(), 1, 'supplier address 4', 'GOTHAM', 1, 43022, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V');
INSERT INTO `vn`.`supplierContact`(`id`, `supplierFk`, `phone`, `mobile`, `email`, `observation`, `name`) INSERT INTO `vn`.`supplierContact`(`id`, `supplierFk`, `phone`, `mobile`, `email`, `observation`, `name`)
VALUES VALUES
@ -2789,7 +2790,7 @@ INSERT INTO `vn`.`profileType` (`id`, `name`)
INSERT INTO `salix`.`url` (`appName`, `environment`, `url`) INSERT INTO `salix`.`url` (`appName`, `environment`, `url`)
VALUES VALUES
('lilium', 'dev', 'http://localhost:8080/#/'), ('lilium', 'dev', 'http://localhost:9000/#/'),
('salix', 'dev', 'http://localhost:5000/#!/'); ('salix', 'dev', 'http://localhost:5000/#!/');
INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`) INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`)

View File

@ -68,6 +68,7 @@ TABLES=(
time time
volumeConfig volumeConfig
workCenter workCenter
companyI18n
) )
dump_tables ${TABLES[@]} dump_tables ${TABLES[@]}

View File

@ -740,6 +740,7 @@ export default {
anyDocument: 'vn-ticket-dms-index > vn-data-viewer vn-tbody vn-tr' anyDocument: 'vn-ticket-dms-index > vn-data-viewer vn-tbody vn-tr'
}, },
ticketFuture: { ticketFuture: {
searchResult: 'vn-ticket-future tbody tr',
openAdvancedSearchButton: 'vn-searchbar .append vn-icon[icon="arrow_drop_down"]', openAdvancedSearchButton: 'vn-searchbar .append vn-icon[icon="arrow_drop_down"]',
originDated: 'vn-date-picker[label="Origin date"]', originDated: 'vn-date-picker[label="Origin date"]',
futureDated: 'vn-date-picker[label="Destination date"]', futureDated: 'vn-date-picker[label="Destination date"]',
@ -755,7 +756,6 @@ export default {
problems: 'vn-check[label="With problems"]', problems: 'vn-check[label="With problems"]',
tableButtonSearch: 'vn-button[vn-tooltip="Search"]', tableButtonSearch: 'vn-button[vn-tooltip="Search"]',
moveButton: 'vn-button[vn-tooltip="Future tickets"]', moveButton: 'vn-button[vn-tooltip="Future tickets"]',
acceptButton: '.vn-confirm.shown button[response="accept"]',
firstCheck: 'tbody > tr:nth-child(1) > td > vn-check', firstCheck: 'tbody > tr:nth-child(1) > td > vn-check',
multiCheck: 'vn-multi-check', multiCheck: 'vn-multi-check',
tableId: 'vn-textfield[name="id"]', tableId: 'vn-textfield[name="id"]',

View File

@ -81,9 +81,7 @@ describe('SmartTable SearchBar integration', () => {
await page.accessToSection('item.fixedPrice'); await page.accessToSection('item.fixedPrice');
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
const result = await page.waitToGetProperty(selectors.itemFixedPrice.firstItemID, 'value'); await page.waitForTextInField(selectors.itemFixedPrice.firstItemID, '1');
expect(result).toEqual('1');
}); });
it('should order by last id, reload page and have same order', async() => { it('should order by last id, reload page and have same order', async() => {
@ -91,9 +89,7 @@ describe('SmartTable SearchBar integration', () => {
await page.reload({ await page.reload({
waitUntil: 'networkidle2' waitUntil: 'networkidle2'
}); });
const result = await page.waitToGetProperty(selectors.itemFixedPrice.firstItemID, 'value'); await page.waitForTextInField(selectors.itemFixedPrice.firstItemID, '13');
expect(result).toEqual('13');
}); });
}); });
}); });

View File

@ -246,6 +246,7 @@ describe('Ticket Edit sale path', () => {
it('should select the third sale and create a claim of it', async() => { it('should select the third sale and create a claim of it', async() => {
await page.accessToSearchResult('16'); await page.accessToSearchResult('16');
await page.accessToSection('ticket.card.sale'); await page.accessToSection('ticket.card.sale');
await page.waitToClick(selectors.ticketSales.firstSaleCheckbox);
await page.waitToClick(selectors.ticketSales.thirdSaleCheckbox); await page.waitToClick(selectors.ticketSales.thirdSaleCheckbox);
await page.waitToClick(selectors.ticketSales.moreMenu); await page.waitToClick(selectors.ticketSales.moreMenu);
await page.waitToClick(selectors.ticketSales.moreMenuCreateClaim); await page.waitToClick(selectors.ticketSales.moreMenuCreateClaim);

View File

@ -126,10 +126,11 @@ describe('Ticket Future path', () => {
}); });
it('should check the three last tickets and move to the future', async() => { it('should check the three last tickets and move to the future', async() => {
await page.waitForNumberOfElements(selectors.ticketFuture.searchResult, 4);
await page.waitToClick(selectors.ticketFuture.multiCheck); await page.waitToClick(selectors.ticketFuture.multiCheck);
await page.waitToClick(selectors.ticketFuture.firstCheck); await page.waitToClick(selectors.ticketFuture.firstCheck);
await page.waitToClick(selectors.ticketFuture.moveButton); await page.waitToClick(selectors.ticketFuture.moveButton);
await page.waitToClick(selectors.ticketFuture.acceptButton); await page.waitToClick(selectors.globalItems.acceptButton);
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();
expect(message.text).toContain('Tickets moved successfully!'); expect(message.text).toContain('Tickets moved successfully!');

View File

@ -35,7 +35,7 @@ describe('InvoiceIn serial path', () => {
}); });
it('should go to index and check if the search-panel has the correct params', async() => { it('should go to index and check if the search-panel has the correct params', async() => {
await page.click(selectors.invoiceInSerial.goToIndex); await page.waitToClick(selectors.invoiceInSerial.goToIndex);
const params = await page.$$(selectors.invoiceInIndex.topbarSearchParams); const params = await page.$$(selectors.invoiceInIndex.topbarSearchParams);
const serial = await params[0].getProperty('title'); const serial = await params[0].getProperty('title');
const isBooked = await params[1].getProperty('title'); const isBooked = await params[1].getProperty('title');

View File

@ -1,6 +1,6 @@
import getBrowser from '../../helpers/puppeteer'; import getBrowser from '../../helpers/puppeteer';
describe('InvoiceIn negative bases path', () => { describe('InvoiceOut negative bases path', () => {
let browser; let browser;
let page; let page;
const httpRequests = []; const httpRequests = [];
@ -9,11 +9,11 @@ describe('InvoiceIn negative bases path', () => {
browser = await getBrowser(); browser = await getBrowser();
page = browser.page; page = browser.page;
page.on('request', req => { page.on('request', req => {
if (req.url().includes(`InvoiceIns/negativeBases`)) if (req.url().includes(`InvoiceOuts/negativeBases`))
httpRequests.push(req.url()); httpRequests.push(req.url());
}); });
await page.loginAndModule('administrative', 'invoiceIn'); await page.loginAndModule('administrative', 'invoiceOut');
await page.accessToSection('invoiceIn.negative-bases'); await page.accessToSection('invoiceOut.negative-bases');
}); });
afterAll(async() => { afterAll(async() => {

View File

@ -156,6 +156,19 @@
"Component cost not set": "Componente coste no está estabecido", "Component cost not set": "Componente coste no está estabecido",
"Tickets with associated refunds can't be deleted. This ticket is associated with refund Nº 2": "Tickets with associated refunds can't be deleted. This ticket is associated with refund Nº 2", "Tickets with associated refunds can't be deleted. This ticket is associated with refund Nº 2": "Tickets with associated refunds can't be deleted. This ticket is associated with refund Nº 2",
"Description cannot be blank": "Description cannot be blank", "Description cannot be blank": "Description cannot be blank",
"company": "Company",
"country": "Country",
"clientId": "Id client",
"clientSocialName": "Client",
"amount": "Amount",
"taxableBase": "Taxable base",
"ticketFk": "Id ticket",
"isActive": "Active",
"hasToInvoice": "Invoice",
"isTaxDataChecked": "Data checked",
"comercialId": "Id Comercial",
"comercialName": "Comercial",
"Added observation": "Added observation", "Added observation": "Added observation",
"Comment added to client": "Comment added to client" "Comment added to client": "Comment added to client",
"This ticket is already a refund": "This ticket is already a refund"
} }

View File

@ -277,5 +277,17 @@
"Insert a date range": "Inserte un rango de fechas", "Insert a date range": "Inserte un rango de fechas",
"Added observation": "{{user}} añadió esta observacion: {{text}}", "Added observation": "{{user}} añadió esta observacion: {{text}}",
"Comment added to client": "Observación añadida al cliente {{clientFk}}", "Comment added to client": "Observación añadida al cliente {{clientFk}}",
"Cannot create a new claimBeginning from a different ticket": "No se puede crear una línea de reclamación de un ticket diferente al origen" "Cannot create a new claimBeginning from a different ticket": "No se puede crear una línea de reclamación de un ticket diferente al origen",
"company": "Compañía",
"country": "País",
"clientId": "Id cliente",
"clientSocialName": "Cliente",
"amount": "Importe",
"taxableBase": "Base",
"ticketFk": "Id ticket",
"isActive": "Activo",
"hasToInvoice": "Facturar",
"isTaxDataChecked": "Datos comprobados",
"comercialId": "Id comercial",
"comercialName": "Comercial"
} }

View File

@ -6,6 +6,7 @@
</vn-crud-model> </vn-crud-model>
<vn-portal slot="topbar"> <vn-portal slot="topbar">
<vn-searchbar <vn-searchbar
vn-focus
panel="vn-user-search-panel" panel="vn-user-search-panel"
info="Search user by id, name or nickname" info="Search user by id, name or nickname"
model="model" model="model"

View File

@ -109,6 +109,11 @@ module.exports = Self => {
zoneFk: zone.id zoneFk: zone.id
}, myOptions); }, myOptions);
await models.TicketRefund.create({
refundTicketFk: newRefundTicket.id,
originalTicketFk: claim.ticket().id
}, myOptions);
await saveObservation({ await saveObservation({
description: `Reclama ticket: ${claim.ticketFk}`, description: `Reclama ticket: ${claim.ticketFk}`,
ticketFk: newRefundTicket.id, ticketFk: newRefundTicket.id,

View File

@ -16,7 +16,7 @@
value="{{$ctrl.claimedTotal | currency: 'EUR':2}}"> value="{{$ctrl.claimedTotal | currency: 'EUR':2}}">
</vn-label-value> </vn-label-value>
</vn-card> </vn-card>
<vn-card class="vn-pa-lg vn-w-lg"> <vn-card class="vn-pa-md vn-w-lg">
<smart-table <smart-table
model="model" model="model"
options="$ctrl.smartTableOptions" options="$ctrl.smartTableOptions"
@ -45,13 +45,13 @@
step="1" step="1"
on-change="$ctrl.save({responsibility: value})"> on-change="$ctrl.save({responsibility: value})">
</vn-range> </vn-range>
</vn-tool-bar>
<vn-check class="right" <vn-check class="right"
vn-one vn-one
label="Is paid with mana" label="Is paid with mana"
ng-model="$ctrl.claim.isChargedToMana" ng-model="$ctrl.claim.isChargedToMana"
on-change="$ctrl.save({isChargedToMana: value})"> on-change="$ctrl.save({isChargedToMana: value})">
</vn-check> </vn-check>
</vn-tool-bar>
</section> </section>
</slot-actions> </slot-actions>
<slot-table> <slot-table>

View File

@ -17,6 +17,10 @@ class Controller extends Descriptor {
} }
sendPickupOrder() { sendPickupOrder() {
if (!this.claim.client.email) {
this.vnApp.showError(this.$t('The client does not have an email'));
return;
}
return this.vnEmail.send(`Claims/${this.claim.id}/claim-pickup-email`, { return this.vnEmail.send(`Claims/${this.claim.id}/claim-pickup-email`, {
recipient: this.claim.client.email, recipient: this.claim.client.email,
recipientId: this.claim.clientFk recipientId: this.claim.clientFk

View File

@ -20,3 +20,4 @@ Photos: Fotos
Go to the claim: Ir a la reclamación Go to the claim: Ir a la reclamación
Sale tracking: Líneas preparadas Sale tracking: Líneas preparadas
Ticket tracking: Estados del ticket Ticket tracking: Estados del ticket
The client does not have an email: El cliente no tiene email

View File

@ -1,48 +1 @@
<vn-crud-model
vn-id="model"
auto-load="true"
filter="::$ctrl.filter"
url="ClaimDms"
link="{claimFk: $ctrl.$params.id}"
limit="20"
data="$ctrl.photos">
</vn-crud-model>
<vn-horizontal class="photo-list drop-zone" vn-droppable="$ctrl.onDrop($event)">
<section class="empty-rows" ng-if="!$ctrl.photos.length">
<section><vn-icon icon="image"></vn-icon></section>
<section translate>Drag & Drop photos here...</section>
</section>
<section class="photo" ng-repeat="photo in $ctrl.photos">
<section class="image vn-shadow" on-error-src
ng-style="{'background': 'url(' + $ctrl.getImagePath(photo.dmsFk) + ')'}"
zoom-image="{{$ctrl.getImagePath(photo.dmsFk)}}"
ng-if="photo.dms.contentType != 'video/mp4'">
</section>
<video id="videobcg" muted="muted" controls ng-if="photo.dms.contentType == 'video/mp4'"
class="video">
<source src="{{$ctrl.getImagePath(photo.dmsFk)}}" type="video/mp4">
</video>
<section class="actions">
<vn-button
class="round"
ng-click="confirm.show($index)"
title="{{'Remove file' | translate}}"
tabindex="-1"
icon="delete">
</vn-button>
</section>
</section>
</vn-horizontal>
<vn-confirm
vn-id="confirm"
message="This file will be deleted"
question="Are you sure you want to continue?"
on-accept="$ctrl.deleteDms($data)">
</vn-confirm>
<vn-float-button
icon="add"
vn-tooltip="Select file"
vn-bind="+"
ng-click="$ctrl.openUploadDialog()"
fixed-bottom-right>
</vn-float-button>

View File

@ -1,105 +1,17 @@
import ngModule from '../module'; import ngModule from '../module';
import Section from 'salix/components/section'; import Section from 'salix/components/section';
import './style.scss';
class Controller extends Section { class Controller extends Section {
constructor($element, $, vnFile) { constructor($element, $) {
super($element, $); super($element, $);
this.vnFile = vnFile;
this.filter = {
include: [
{
relation: 'dms'
}
]
};
} }
deleteDms(index) { async $onInit() {
const dmsFk = this.photos[index].dmsFk; const url = await this.vnApp.getUrl(`claim/${this.$params.id}/photos`);
return this.$http.post(`ClaimDms/${dmsFk}/removeFile`) window.location.href = url;
.then(() => {
this.$.model.remove(index);
this.vnApp.showSuccess(this.$t('File deleted'));
});
}
onDrop($event) {
const files = $event.dataTransfer.files;
this.setDefaultParams().then(() => {
this.dms.files = files;
this.create();
});
}
setDefaultParams() {
const filter = {
where: {code: 'claim'}
};
return this.$http.get('DmsTypes/findOne', {filter}).then(res => {
const dmsTypeId = res.data && res.data.id;
const companyId = this.vnConfig.companyFk;
const warehouseId = this.vnConfig.warehouseFk;
this.dms = {
hasFile: false,
hasFileAttached: false,
reference: this.claim.id,
warehouseId: warehouseId,
companyId: companyId,
dmsTypeId: dmsTypeId,
description: this.$t('FileDescription', {
claimId: this.claim.id,
clientId: this.claim.client.id,
clientName: this.claim.client.name
}).toUpperCase()
};
});
}
openUploadDialog() {
const element = document.createElement('input');
element.setAttribute('type', 'file');
element.setAttribute('multiple', true);
element.click();
element.addEventListener('change', () =>
this.setDefaultParams().then(() => {
this.dms.files = element.files;
this.create();
})
);
}
create() {
const query = `claims/${this.claim.id}/uploadFile`;
const options = {
method: 'POST',
url: query,
params: this.dms,
headers: {'Content-Type': undefined},
transformRequest: files => {
const formData = new FormData();
for (let i = 0; i < files.length; i++)
formData.append(files[i].name, files[i]);
return formData;
},
data: this.dms.files
};
this.$http(options).then(() => {
this.vnApp.showSuccess(this.$t('File uploaded!'));
this.$.model.refresh();
});
}
getImagePath(dmsId) {
return this.vnFile.getPath(`/api/Claims/${dmsId}/downloadFile`);
} }
} }
Controller.$inject = ['$element', '$scope', 'vnFile'];
ngModule.vnComponent('vnClaimPhotos', { ngModule.vnComponent('vnClaimPhotos', {
template: require('./index.html'), template: require('./index.html'),
controller: Controller, controller: Controller,

View File

@ -1,70 +0,0 @@
import './index';
import crudModel from 'core/mocks/crud-model';
describe('Claim', () => {
describe('Component vnClaimPhotos', () => {
let $scope;
let $httpBackend;
let controller;
beforeEach(ngModule('claim'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
$scope = $rootScope.$new();
controller = $componentController('vnClaimPhotos', {$element: null, $scope});
controller.$.model = crudModel;
controller.claim = {
id: 1,
client: {id: 1101, name: 'Bruce Wayne'}
};
}));
describe('deleteDms()', () => {
it('should make an HTTP Post query', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
jest.spyOn(controller.$.model, 'remove');
const dmsId = 1;
const dmsIndex = 0;
controller.photos = [{dmsFk: 1}];
$httpBackend.expectPOST(`ClaimDms/${dmsId}/removeFile`).respond();
controller.deleteDms(dmsIndex);
$httpBackend.flush();
expect(controller.$.model.remove).toHaveBeenCalledWith(dmsIndex);
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
describe('setDefaultParams()', () => {
it('should make an HTTP GET query, then set all dms properties', () => {
$httpBackend.expectRoute('GET', `DmsTypes/findOne`).respond({});
controller.setDefaultParams();
$httpBackend.flush();
expect(controller.dms).toBeDefined();
});
});
describe('create()', () => {
it('should make an HTTP Post query, then refresh the model data', () => {
const claimId = 1;
const dmsIndex = 0;
jest.spyOn(controller.vnApp, 'showSuccess');
jest.spyOn(controller.$.model, 'refresh');
controller.photos = [{dmsFk: 1}];
controller.dmsIndex = dmsIndex;
controller.dms = {files: []};
$httpBackend.expectPOST(`claims/${claimId}/uploadFile`).respond({});
controller.create();
$httpBackend.flush();
expect(controller.$.model.refresh).toHaveBeenCalled();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
});
});

View File

@ -1,5 +0,0 @@
Are you sure you want to continue?: ¿Seguro que quieres continuar?
Drag & Drop photos here...: Arrastra y suelta fotos aquí...
File deleted: Archivo eliminado
File uploaded!: Archivo subido!
Select file: Seleccionar fichero

View File

@ -1,47 +0,0 @@
@import "./variables";
vn-claim-photos {
height: 100%;
.drop-zone {
color: $color-font-secondary;
box-sizing: border-box;
border-radius: 8px;
text-align: center;
min-height: 100%;
.empty-rows {
padding: 80px $spacing-md;
font-size: 1.375rem
}
vn-icon {
font-size: 3rem
}
}
.photo-list {
padding: $spacing-md;
min-height: 100%;
.photo {
width: 512px;
height: 288px;
}
}
.video {
width: 100%;
height: 100%;
object-fit: cover;
cursor: pointer;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),
0 3px 1px -2px rgba(0,0,0,.2),
0 1px 5px 0 rgba(0,0,0,.12);
border: 2px solid transparent;
}
.video:hover {
border: 2px solid $color-primary
}
}

View File

@ -76,7 +76,7 @@ module.exports = function(Self) {
const date = Date.vnNew(); const date = Date.vnNew();
date.setHours(0, 0, 0, 0); date.setHours(0, 0, 0, 0);
const query = `SELECT vn.clientGetDebt(?, ?) AS debt`; const query = `SELECT vn.client_getDebt(?, ?) AS debt`;
const data = await Self.rawSql(query, [id, date], myOptions); const data = await Self.rawSql(query, [id, date], myOptions);
client.debt = data[0].debt; client.debt = data[0].debt;

View File

@ -27,7 +27,7 @@ module.exports = Self => {
const date = Date.vnNew(); const date = Date.vnNew();
date.setHours(0, 0, 0, 0); date.setHours(0, 0, 0, 0);
const query = `SELECT vn.clientGetDebt(?, ?) AS debt`; const query = `SELECT vn.client_getDebt(?, ?) AS debt`;
const [debt] = await Self.rawSql(query, [clientFk, date], myOptions); const [debt] = await Self.rawSql(query, [clientFk, date], myOptions);
return debt; return debt;

View File

@ -0,0 +1,57 @@
const {Email} = require('vn-print');
module.exports = Self => {
Self.remoteMethodCtx('receiptEmail', {
description: 'Returns the receipt pdf',
accepts: [
{
arg: 'id',
type: 'number',
required: true,
description: 'The claim id',
http: {source: 'path'}
},
{
arg: 'recipient',
type: 'string',
description: 'The recipient email',
required: true,
}
],
returns: [
{
arg: 'body',
type: 'file',
root: true
}, {
arg: 'Content-Type',
type: 'String',
http: {target: 'header'}
}, {
arg: 'Content-Disposition',
type: 'String',
http: {target: 'header'}
}
],
http: {
path: '/:id/receipt-email',
verb: 'POST'
}
});
Self.receiptEmail = async(ctx, id) => {
const args = Object.assign({}, ctx.args);
const params = {
recipient: args.recipient,
lang: ctx.req.getLocale()
};
delete args.ctx;
for (const param in args)
params[param] = args[param];
const email = new Email('receipt', params);
return email.send();
};
};

View File

@ -1,12 +1,12 @@
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('receiptPdf', { Self.remoteMethodCtx('receiptPdf', {
description: 'Returns the receipt pdf', description: 'Send the receipt pdf to client',
accepts: [ accepts: [
{ {
arg: 'id', arg: 'id',
type: 'number', type: 'number',
required: true, required: true,
description: 'The claim id', description: 'The receipt id',
http: {source: 'path'} http: {source: 'path'}
}, },
{ {

View File

@ -5,6 +5,7 @@ module.exports = function(Self) {
require('../methods/receipt/balanceCompensationEmail')(Self); require('../methods/receipt/balanceCompensationEmail')(Self);
require('../methods/receipt/balanceCompensationPdf')(Self); require('../methods/receipt/balanceCompensationPdf')(Self);
require('../methods/receipt/receiptPdf')(Self); require('../methods/receipt/receiptPdf')(Self);
require('../methods/receipt/receiptEmail')(Self);
Self.validateBinded('amountPaid', isNotZero, { Self.validateBinded('amountPaid', isNotZero, {
message: 'Amount cannot be zero', message: 'Amount cannot be zero',

View File

@ -84,6 +84,10 @@
label="View receipt" label="View receipt"
ng-model="$ctrl.viewReceipt"> ng-model="$ctrl.viewReceipt">
</vn-check> </vn-check>
<vn-check
label="Send email"
ng-model="$ctrl.sendEmail">
</vn-check>
</vn-horizontal> </vn-horizontal>
</tpl-body> </tpl-body>
<tpl-buttons> <tpl-buttons>

View File

@ -2,9 +2,10 @@ import ngModule from '../../module';
import Dialog from 'core/components/dialog'; import Dialog from 'core/components/dialog';
class Controller extends Dialog { class Controller extends Dialog {
constructor($element, $, $transclude, vnReport) { constructor($element, $, $transclude, vnReport, vnEmail) {
super($element, $, $transclude); super($element, $, $transclude);
this.vnReport = vnReport; this.vnReport = vnReport;
this.vnEmail = vnEmail;
this.receipt = {}; this.receipt = {};
} }
@ -23,6 +24,18 @@ class Controller extends Dialog {
set clientFk(value) { set clientFk(value) {
this.receipt.clientFk = value; this.receipt.clientFk = value;
const filter = {
fields: ['email'],
where: {
id: value
}
};
this.$http.get(`Clients/findOne`, {filter})
.then(res => {
this.receipt.email = res.data.email;
});
} }
get clientFk() { get clientFk() {
@ -154,10 +167,13 @@ class Controller extends Dialog {
return super.responseHandler(response); return super.responseHandler(response);
const exceededAmount = this.receipt.amountPaid > this.maxAmount; const exceededAmount = this.receipt.amountPaid > this.maxAmount;
const isCash = this.bankSelection.accountingType.code == 'cash';
if (this.bankSelection.accountingType.code == 'cash' && exceededAmount) if (isCash && exceededAmount)
return this.vnApp.showError(this.$t('Amount exceeded', {maxAmount: this.maxAmount})); return this.vnApp.showError(this.$t('Amount exceeded', {maxAmount: this.maxAmount}));
if (isCash && this.sendEmail && !this.receipt.email)
return this.vnApp.showError(this.$t('There is no assigned email for this client'));
let receiptId; let receiptId;
return this.$http.post(`Clients/${this.clientFk}/createReceipt`, this.receipt) return this.$http.post(`Clients/${this.clientFk}/createReceipt`, this.receipt)
.then(res => { .then(res => {
@ -165,6 +181,13 @@ class Controller extends Dialog {
super.responseHandler(response); super.responseHandler(response);
}) })
.then(() => this.vnApp.showSuccess(this.$t('Data saved!'))) .then(() => this.vnApp.showSuccess(this.$t('Data saved!')))
.then(() => {
if (!this.sendEmail || !isCash) return;
const params = {
recipient: this.receipt.email
};
this.vnEmail.send(`Receipts/${receiptId}/receipt-email`, params);
})
.then(() => { .then(() => {
if (this.viewReceipt) if (this.viewReceipt)
this.vnReport.show(`Receipts/${receiptId}/receipt-pdf`); this.vnReport.show(`Receipts/${receiptId}/receipt-pdf`);
@ -178,7 +201,7 @@ class Controller extends Dialog {
} }
} }
Controller.$inject = ['$element', '$scope', '$transclude', 'vnReport']; Controller.$inject = ['$element', '$scope', '$transclude', 'vnReport', 'vnEmail'];
ngModule.vnComponent('vnClientBalanceCreate', { ngModule.vnComponent('vnClientBalanceCreate', {
slotTemplate: require('./index.html'), slotTemplate: require('./index.html'),

View File

@ -192,19 +192,19 @@
{{::buy.entryFk}} {{::buy.entryFk}}
</span> </span>
</td> </td>
<td number>{{::buy.buyingValue | currency: 'EUR':2}}</td> <td number>{{::buy.buyingValue | currency: 'EUR':3}}</td>
<td number>{{::buy.freightValue | currency: 'EUR':2}}</td> <td number>{{::buy.freightValue | currency: 'EUR':3}}</td>
<td number>{{::buy.comissionValue | currency: 'EUR':2}}</td> <td number>{{::buy.comissionValue | currency: 'EUR':3}}</td>
<td number>{{::buy.packageValue | currency: 'EUR':2}}</td> <td number>{{::buy.packageValue | currency: 'EUR':3}}</td>
<td> <td>
<vn-check <vn-check
disabled="true" disabled="true"
ng-model="::buy.isIgnored"> ng-model="::buy.isIgnored">
</vn-check> </vn-check>
</td> </td>
<td number>{{::buy.price2 | currency: 'EUR':2}}</td> <td number>{{::buy.price2 | currency: 'EUR':3}}</td>
<td number>{{::buy.price3 | currency: 'EUR':2}}</td> <td number>{{::buy.price3 | currency: 'EUR':3}}</td>
<td number>{{::buy.minPrice | currency: 'EUR':2}}</td> <td number>{{::buy.minPrice | currency: 'EUR':3}}</td>
<td>{{::buy.ektFk | dashIfEmpty}}</td> <td>{{::buy.ektFk | dashIfEmpty}}</td>
<td>{{::buy.weight}}</td> <td>{{::buy.weight}}</td>
<td>{{::buy.packageFk}}</td> <td>{{::buy.packageFk}}</td>

View File

@ -1,53 +0,0 @@
const {toCSV} = require('vn-loopback/util/csv');
module.exports = Self => {
Self.remoteMethodCtx('negativeBasesCsv', {
description: 'Returns the negative bases as .csv',
accessType: 'READ',
accepts: [{
arg: 'negativeBases',
type: ['object'],
required: true
},
{
arg: 'from',
type: 'date',
description: 'From date'
},
{
arg: 'to',
type: 'date',
description: 'To date'
}],
returns: [
{
arg: 'body',
type: 'file',
root: true
}, {
arg: 'Content-Type',
type: 'String',
http: {target: 'header'}
}, {
arg: 'Content-Disposition',
type: 'String',
http: {target: 'header'}
}
],
http: {
path: '/negativeBasesCsv',
verb: 'GET'
}
});
Self.negativeBasesCsv = async ctx => {
const args = ctx.args;
const content = toCSV(args.negativeBases);
return [
content,
'text/csv',
`attachment; filename="negative-bases-${new Date(args.from).toLocaleDateString()}-${new Date(args.to).toLocaleDateString()}.csv"`
];
};
};

View File

@ -7,6 +7,4 @@ module.exports = Self => {
require('../methods/invoice-in/invoiceInPdf')(Self); require('../methods/invoice-in/invoiceInPdf')(Self);
require('../methods/invoice-in/invoiceInEmail')(Self); require('../methods/invoice-in/invoiceInEmail')(Self);
require('../methods/invoice-in/getSerial')(Self); require('../methods/invoice-in/getSerial')(Self);
require('../methods/invoice-in/negativeBases')(Self);
require('../methods/invoice-in/negativeBasesCsv')(Self);
}; };

View File

@ -15,4 +15,3 @@ import './create';
import './log'; import './log';
import './serial'; import './serial';
import './serial-search-panel'; import './serial-search-panel';
import './negative-bases';

View File

@ -1,14 +0,0 @@
Has To Invoice: Facturar
Download as CSV: Descargar como CSV
company: Compañía
country: País
clientId: Id Cliente
clientSocialName: Cliente
amount: Importe
taxableBase: Base
ticketFk: Id Ticket
isActive: Activo
hasToInvoice: Facturar
isTaxDataChecked: Datos comprobados
comercialId: Id Comercial
comercialName: Comercial

View File

@ -10,8 +10,7 @@
"menus": { "menus": {
"main": [ "main": [
{ "state": "invoiceIn.index", "icon": "icon-invoice-in"}, { "state": "invoiceIn.index", "icon": "icon-invoice-in"},
{ "state": "invoiceIn.serial", "icon": "icon-invoice-in"}, { "state": "invoiceIn.serial", "icon": "icon-invoice-in"}
{ "state": "invoiceIn.negative-bases", "icon": "icon-ticket"}
], ],
"card": [ "card": [
{ {
@ -53,15 +52,6 @@
"administrative" "administrative"
] ]
}, },
{
"url": "/negative-bases",
"state": "invoiceIn.negative-bases",
"component": "vn-negative-bases",
"description": "Negative bases",
"acl": [
"administrative"
]
},
{ {
"url": "/serial", "url": "/serial",
"state": "invoiceIn.serial", "state": "invoiceIn.serial",

View File

@ -96,16 +96,18 @@ module.exports = Self => {
SELECT f.* SELECT f.*
FROM tmp.filter f`); FROM tmp.filter f`);
if (args.filter) {
stmt.merge(conn.makeWhere(args.filter.where)); stmt.merge(conn.makeWhere(args.filter.where));
stmt.merge(conn.makeOrderBy(args.filter.order)); stmt.merge(conn.makeOrderBy(args.filter.order));
stmt.merge(conn.makeLimit(args.filter)); stmt.merge(conn.makeLimit(args.filter));
}
const negativeBasesIndex = stmts.push(stmt) - 1; const negativeBasesIndex = stmts.push(stmt) - 1;
stmts.push(`DROP TEMPORARY TABLE tmp.filter, tmp.ticket, tmp.ticketTax, tmp.ticketAmount`); stmts.push(`DROP TEMPORARY TABLE tmp.filter, tmp.ticket, tmp.ticketTax, tmp.ticketAmount`);
const sql = ParameterizedSQL.join(stmts, ';'); const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql, myOptions); const result = await conn.executeStmt(sql);
return negativeBasesIndex === 0 ? result : result[negativeBasesIndex]; return negativeBasesIndex === 0 ? result : result[negativeBasesIndex];
}; };

View File

@ -0,0 +1,68 @@
const {toCSV} = require('vn-loopback/util/csv');
module.exports = Self => {
Self.remoteMethodCtx('negativeBasesCsv', {
description: 'Returns the negative bases as .csv',
accessType: 'READ',
accepts: [
{
arg: 'from',
type: 'date',
description: 'From date',
required: true
},
{
arg: 'to',
type: 'date',
description: 'To date',
required: true
}],
returns: [
{
arg: 'body',
type: 'file',
root: true
}, {
arg: 'Content-Type',
type: 'String',
http: {target: 'header'}
}, {
arg: 'Content-Disposition',
type: 'String',
http: {target: 'header'}
}
],
http: {
path: '/negativeBasesCsv',
verb: 'GET'
}
});
Self.negativeBasesCsv = async(ctx, options) => {
const $t = ctx.req.__; // $translate
const args = ctx.args;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const negativeBases = await Self.app.models.InvoiceOut.negativeBases(ctx, myOptions);
const locatedFields = [];
negativeBases.forEach(element => {
locatedFields.push(Object.keys(element).map(key => {
return {newName: $t(key), value: element[key]};
}).filter(item => item !== null)
.reduce((result, item) => {
result[item.newName] = item.value;
return result;
}, {}));
});
const content = toCSV(locatedFields);
return [
content,
'text/csv',
`attachment; filename="negative-bases-${new Date(args.from).toLocaleDateString()}-${new Date(args.to).toLocaleDateString()}.csv"`
];
};
};

View File

@ -35,7 +35,7 @@ module.exports = Self => {
const tickets = await models.Ticket.find(filter, myOptions); const tickets = await models.Ticket.find(filter, myOptions);
const ticketsIds = tickets.map(ticket => ticket.id); const ticketsIds = tickets.map(ticket => ticket.id);
const refundedTickets = await models.Ticket.refund(ticketsIds, true, myOptions); const refundedTickets = await models.Ticket.refund(ticketsIds, myOptions);
if (tx) await tx.commit(); if (tx) await tx.commit();

View File

@ -1,19 +1,18 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
describe('invoiceIn negativeBases()', () => { describe('invoiceOut negativeBases()', () => {
it('should return all negative bases in a date range', async() => { it('should return all negative bases in a date range', async() => {
const tx = await models.InvoiceIn.beginTransaction({}); const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx}; const options = {transaction: tx};
const ctx = { const ctx = {
args: { args: {
from: new Date().setMonth(new Date().getMonth() - 12), from: new Date().setMonth(new Date().getMonth() - 12),
to: new Date(), to: new Date()
filter: {}
} }
}; };
try { try {
const result = await models.InvoiceIn.negativeBases(ctx, options); const result = await models.InvoiceOut.negativeBases(ctx, options);
expect(result.length).toBeGreaterThan(0); expect(result.length).toBeGreaterThan(0);
@ -26,7 +25,7 @@ describe('invoiceIn negativeBases()', () => {
it('should throw an error if a date range is not in args', async() => { it('should throw an error if a date range is not in args', async() => {
let error; let error;
const tx = await models.InvoiceIn.beginTransaction({}); const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx}; const options = {transaction: tx};
const ctx = { const ctx = {
args: { args: {
@ -35,7 +34,7 @@ describe('invoiceIn negativeBases()', () => {
}; };
try { try {
await models.InvoiceIn.negativeBases(ctx, options); await models.InvoiceOut.negativeBases(ctx, options);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
error = e; error = e;

View File

@ -17,7 +17,7 @@ describe('InvoiceOut refund()', () => {
try { try {
const result = await models.InvoiceOut.refund('T1111111', options); const result = await models.InvoiceOut.refund('T1111111', options);
expect(result.length).toEqual(1); expect(result).toBeDefined();
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {

View File

@ -17,4 +17,6 @@ module.exports = Self => {
require('../methods/invoiceOut/invoiceCsvEmail')(Self); require('../methods/invoiceOut/invoiceCsvEmail')(Self);
require('../methods/invoiceOut/invoiceOutPdf')(Self); require('../methods/invoiceOut/invoiceOutPdf')(Self);
require('../methods/invoiceOut/getInvoiceDate')(Self); require('../methods/invoiceOut/getInvoiceDate')(Self);
require('../methods/invoiceOut/negativeBases')(Self);
require('../methods/invoiceOut/negativeBasesCsv')(Self);
}; };

View File

@ -118,8 +118,11 @@ class Controller extends Section {
const query = 'InvoiceOuts/refund'; const query = 'InvoiceOuts/refund';
const params = {ref: this.invoiceOut.ref}; const params = {ref: this.invoiceOut.ref};
this.$http.post(query, params).then(res => { this.$http.post(query, params).then(res => {
const ticketIds = res.data.map(ticket => ticket.id).join(', '); const refundTicket = res.data;
this.vnApp.showSuccess(this.$t('The following refund tickets have been created', {ticketIds})); this.vnApp.showSuccess(this.$t('The following refund ticket have been created', {
ticketId: refundTicket.id
}));
this.$state.go('ticket.card.sale', {id: refundTicket.id});
}); });
} }
} }

View File

@ -10,3 +10,4 @@ import './descriptor-popover';
import './descriptor-menu'; import './descriptor-menu';
import './index/manual'; import './index/manual';
import './global-invoicing'; import './global-invoicing';
import './negative-bases';

View File

@ -1,6 +1,6 @@
<vn-crud-model <vn-crud-model
vn-id="model" vn-id="model"
url="InvoiceIns/negativeBases" url="InvoiceOuts/negativeBases"
auto-load="true" auto-load="true"
params="$ctrl.params" params="$ctrl.params"
limit="20"> limit="20">
@ -43,7 +43,7 @@
<span translate>Country</span> <span translate>Country</span>
</th> </th>
<th field="clientId"> <th field="clientId">
<span translate>Id Client</span> <span translate>Client id</span>
</th> </th>
<th field="clientSocialName"> <th field="clientSocialName">
<span translate>Client</span> <span translate>Client</span>
@ -55,7 +55,7 @@
<span translate>Base</span> <span translate>Base</span>
</th> </th>
<th field="ticketFk"> <th field="ticketFk">
<span translate>Id Ticket</span> <span translate>Ticket id</span>
</th> </th>
<th field="isActive"> <th field="isActive">
<span translate>Active</span> <span translate>Active</span>

View File

@ -58,18 +58,7 @@ export default class Controller extends Section {
} }
downloadCSV() { downloadCSV() {
const data = []; this.vnReport.show('InvoiceOuts/negativeBasesCsv', {
this.$.model._orgData.forEach(element => {
data.push(Object.keys(element).map(key => {
return {newName: this.$t(key), value: element[key]};
}).filter(item => item !== null)
.reduce((result, item) => {
result[item.newName] = item.value;
return result;
}, {}));
});
this.vnReport.show('InvoiceIns/negativeBasesCsv', {
negativeBases: data,
from: this.params.from, from: this.params.from,
to: this.params.to to: this.params.to
}); });

View File

@ -0,0 +1,2 @@
Has To Invoice: Facturar
Download as CSV: Descargar como CSV

View File

@ -7,8 +7,8 @@
"menus": { "menus": {
"main": [ "main": [
{"state": "invoiceOut.index", "icon": "icon-invoice-out"}, {"state": "invoiceOut.index", "icon": "icon-invoice-out"},
{"state": "invoiceOut.global-invoicing", "icon": "contact_support"} {"state": "invoiceOut.global-invoicing", "icon": "contact_support"},
{ "state": "invoiceOut.negative-bases", "icon": "icon-ticket"}
] ]
}, },
"routes": [ "routes": [
@ -46,6 +46,15 @@
"state": "invoiceOut.card", "state": "invoiceOut.card",
"abstract": true, "abstract": true,
"component": "vn-invoice-out-card" "component": "vn-invoice-out-card"
},
{
"url": "/negative-bases",
"state": "invoiceOut.negative-bases",
"component": "vn-negative-bases",
"description": "Negative bases",
"acl": [
"administrative"
]
} }
] ]
} }

View File

@ -42,7 +42,7 @@
<vn-autocomplete <vn-autocomplete
vn-one vn-one
ng-model="filter.requesterFk" ng-model="filter.requesterFk"
url="Workers/activeWithRole" url="Workers/activeWithInheritedRole"
search-function="{firstName: $search}" search-function="{firstName: $search}"
value-field="id" value-field="id"
where="{role: 'salesPerson'}" where="{role: 'salesPerson'}"

View File

@ -238,7 +238,7 @@ module.exports = Self => {
ENGINE = MEMORY ENGINE = MEMORY
SELECT DISTINCT clientFk FROM tmp.filter`); SELECT DISTINCT clientFk FROM tmp.filter`);
stmt = new ParameterizedSQL('CALL clientGetDebt(?)', [args.to]); stmt = new ParameterizedSQL('CALL client_getDebt(?)', [args.to]);
stmts.push(stmt); stmts.push(stmt);
stmts.push('DROP TEMPORARY TABLE tmp.clientGetDebt'); stmts.push('DROP TEMPORARY TABLE tmp.clientGetDebt');

View File

@ -23,6 +23,6 @@ describe('Supplier filter()', () => {
let result = await app.models.Supplier.filter(ctx); let result = await app.models.Supplier.filter(ctx);
expect(result.length).toEqual(2); expect(result.length).toEqual(3);
}); });
}); });

View File

@ -53,7 +53,7 @@ module.exports = Self => {
let start = new Date(expedition.created); let start = new Date(expedition.created);
let end = new Date(start.getTime() + (packingSiteConfig.avgBoxingTime * 1000)); let end = new Date(start.getTime() + (packingSiteConfig.avgBoxingTime * 1000));
if (from && to) { if (from != undefined && to != undefined) {
start.setHours(from, 0, 0); start.setHours(from, 0, 0);
end.setHours(to, 0, 0); end.setHours(to, 0, 0);
} }

View File

@ -11,11 +11,6 @@ module.exports = Self => {
{ {
arg: 'servicesIds', arg: 'servicesIds',
type: ['number'] type: ['number']
},
{
arg: 'createSingleTicket',
type: 'boolean',
required: false
} }
], ],
returns: { returns: {
@ -28,7 +23,7 @@ module.exports = Self => {
} }
}); });
Self.refund = async(salesIds, servicesIds, createSingleTicket = false, options) => { Self.refund = async(salesIds, servicesIds, options) => {
const models = Self.app.models; const models = Self.app.models;
const myOptions = {}; const myOptions = {};
let tx; let tx;
@ -67,40 +62,14 @@ module.exports = Self => {
const sales = await models.Sale.find(salesFilter, myOptions); const sales = await models.Sale.find(salesFilter, myOptions);
const ticketsIds = [...new Set(sales.map(sale => sale.ticketFk))]; const ticketsIds = [...new Set(sales.map(sale => sale.ticketFk))];
const refundTickets = [];
const mappedTickets = new Map();
const now = Date.vnNew(); const now = Date.vnNew();
const [firstTicketId] = ticketsIds; const [firstTicketId] = ticketsIds;
if (createSingleTicket) {
await createTicketRefund( const refundTicket = await createTicketRefund(firstTicketId, now, refundAgencyMode, refoundZoneId, myOptions);
firstTicketId,
refundTickets,
mappedTickets,
now,
refundAgencyMode,
refoundZoneId,
myOptions
);
} else {
for (let ticketId of ticketsIds) {
await createTicketRefund(
ticketId,
refundTickets,
mappedTickets,
now,
refundAgencyMode,
refoundZoneId,
myOptions
);
}
}
for (const sale of sales) { for (const sale of sales) {
const refundTicketId = await getTicketRefundId(createSingleTicket, sale.ticketFk, refundTickets, mappedTickets);
const createdSale = await models.Sale.create({ const createdSale = await models.Sale.create({
ticketFk: refundTicketId, ticketFk: refundTicket.id,
itemFk: sale.itemFk, itemFk: sale.itemFk,
quantity: - sale.quantity, quantity: - sale.quantity,
concept: sale.concept, concept: sale.concept,
@ -120,16 +89,13 @@ module.exports = Self => {
where: {id: {inq: servicesIds}} where: {id: {inq: servicesIds}}
}; };
const services = await models.TicketService.find(servicesFilter, myOptions); const services = await models.TicketService.find(servicesFilter, myOptions);
for (const service of services) { for (const service of services) {
const refundTicketId = await getTicketRefundId(createSingleTicket, service.ticketFk, refundTickets, mappedTickets);
await models.TicketService.create({ await models.TicketService.create({
description: service.description, description: service.description,
quantity: - service.quantity, quantity: - service.quantity,
price: service.price, price: service.price,
taxClassFk: service.taxClassFk, taxClassFk: service.taxClassFk,
ticketFk: refundTicketId, ticketFk: refundTicket.id,
ticketServiceTypeFk: service.ticketServiceTypeFk, ticketServiceTypeFk: service.ticketServiceTypeFk,
}, myOptions); }, myOptions);
} }
@ -137,22 +103,14 @@ module.exports = Self => {
if (tx) await tx.commit(); if (tx) await tx.commit();
return refundTickets; return refundTicket;
} catch (e) { } catch (e) {
if (tx) await tx.rollback(); if (tx) await tx.rollback();
throw e; throw e;
} }
}; };
async function createTicketRefund( async function createTicketRefund(ticketId, now, refundAgencyMode, refoundZoneId, myOptions) {
ticketId,
refundTickets,
mappedTickets,
now,
refundAgencyMode,
refoundZoneId,
myOptions
) {
const models = Self.app.models; const models = Self.app.models;
const filter = {include: {relation: 'address'}}; const filter = {include: {relation: 'address'}};
@ -170,20 +128,11 @@ module.exports = Self => {
zoneFk: refoundZoneId zoneFk: refoundZoneId
}, myOptions); }, myOptions);
refundTickets.push(refundTicket);
mappedTickets.set(ticketId, refundTicket.id);
await models.TicketRefund.create({ await models.TicketRefund.create({
refundTicketFk: refundTicket.id, refundTicketFk: refundTicket.id,
originalTicketFk: ticket.id, originalTicketFk: ticket.id,
}, myOptions); }, myOptions);
}
async function getTicketRefundId(createSingleTicket, ticketId, refundTickets, mappedTickets) { return refundTicket;
if (createSingleTicket) {
const [firstRefundTicket] = refundTickets;
return firstRefundTicket.id;
} else return mappedTickets.get(ticketId);
} }
}; };

View File

@ -22,9 +22,9 @@ describe('Sale refund()', () => {
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const response = await models.Sale.refund(salesIds, servicesIds, false, options); const refundedTicket = await models.Sale.refund(salesIds, servicesIds, options);
expect(response.length).toBeGreaterThanOrEqual(1); expect(refundedTicket).toBeDefined();
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
@ -33,23 +33,18 @@ describe('Sale refund()', () => {
} }
}); });
it('should create a ticket for each unique ticketFk in the sales', async() => { it('should create one ticket for each unique ticketFk in the sales', async() => {
const tx = await models.Sale.beginTransaction({}); const tx = await models.Sale.beginTransaction({});
const salesIds = [6, 7]; const salesIds = [6, 7];
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const createSingleTicket = false; const ticket = await models.Sale.refund(salesIds, servicesIds, options);
const tickets = await models.Sale.refund(salesIds, servicesIds, createSingleTicket, options);
const ticketsIds = tickets.map(ticket => ticket.id); const refundedTicket = await models.Ticket.findOne({
const refundedTickets = await models.Ticket.find({
where: { where: {
id: { id: ticket.id
inq: ticketsIds
}
}, },
include: [ include: [
{ {
@ -66,16 +61,12 @@ describe('Sale refund()', () => {
] ]
}, options); }, options);
const firstRefoundedTicket = refundedTickets[0]; const salesLength = refundedTicket.ticketSales().length;
const secondRefoundedTicket = refundedTickets[1]; const componentsLength = refundedTicket.ticketSales()[0].components().length;
const salesLength = firstRefoundedTicket.ticketSales().length;
const componentsLength = firstRefoundedTicket.ticketSales()[0].components().length;
const servicesLength = secondRefoundedTicket.ticketServices().length;
expect(refundedTickets.length).toEqual(2); expect(refundedTicket).toBeDefined();
expect(salesLength).toEqual(1); expect(salesLength).toEqual(2);
expect(componentsLength).toEqual(4); expect(componentsLength).toEqual(4);
expect(servicesLength).toBeGreaterThanOrEqual(1);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {

View File

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

View File

@ -300,7 +300,7 @@ class Controller extends Section {
const params = {ticketsIds: [this.id]}; const params = {ticketsIds: [this.id]};
const query = 'Tickets/refund'; const query = 'Tickets/refund';
return this.$http.post(query, params).then(res => { return this.$http.post(query, params).then(res => {
const [refundTicket] = res.data; const refundTicket = res.data;
this.vnApp.showSuccess(this.$t('The following refund ticket have been created', { this.vnApp.showSuccess(this.$t('The following refund ticket have been created', {
ticketId: refundTicket.id ticketId: refundTicket.id
})); }));
@ -326,8 +326,13 @@ class Controller extends Section {
return this.$http.post(`Docuwares/${this.id}/upload`, {fileCabinet: 'deliveryNote'}) return this.$http.post(`Docuwares/${this.id}/upload`, {fileCabinet: 'deliveryNote'})
.then(() => { .then(() => {
this.vnApp.showSuccess(this.$t('PDF sent!')); this.$.balanceCreate.amountPaid = this.ticket.totalWithVat;
this.$.balanceCreate.clientFk = this.ticket.clientFk;
this.$.balanceCreate.description = 'Albaran: ';
this.$.balanceCreate.description += this.ticket.id;
this.$.balanceCreate.show(); this.$.balanceCreate.show();
this.vnApp.showSuccess(this.$t('PDF sent!'));
}); });
} }
} }

View File

@ -250,7 +250,7 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
const params = { const params = {
ticketsIds: [16] ticketsIds: [16]
}; };
$httpBackend.expectPOST('Tickets/refund', params).respond([{id: 99}]); $httpBackend.expectPOST('Tickets/refund', params).respond({id: 99});
controller.refund(); controller.refund();
$httpBackend.flush(); $httpBackend.flush();

View File

@ -516,7 +516,7 @@ class Controller extends Section {
const params = {salesIds: salesIds}; const params = {salesIds: salesIds};
const query = 'Sales/refund'; const query = 'Sales/refund';
this.$http.post(query, params).then(res => { this.$http.post(query, params).then(res => {
const [refundTicket] = res.data; const refundTicket = res.data;
this.vnApp.showSuccess(this.$t('The following refund ticket have been created', { this.vnApp.showSuccess(this.$t('The following refund ticket have been created', {
ticketId: refundTicket.id ticketId: refundTicket.id
})); }));

View File

@ -726,8 +726,7 @@ describe('Ticket', () => {
salesIds: [1, 4], salesIds: [1, 4],
}; };
const refundTicket = {id: 99}; const refundTicket = {id: 99};
const createdTickets = [refundTicket]; $httpBackend.expect('POST', 'Sales/refund', params).respond(200, refundTicket);
$httpBackend.expect('POST', 'Sales/refund', params).respond(200, createdTickets);
controller.createRefund(); controller.createRefund();
$httpBackend.flush(); $httpBackend.flush();

View File

@ -4,6 +4,7 @@
margin-right: 2cm; margin-right: 2cm;
font-size: 10px; font-size: 10px;
color: #555; color: #555;
width: 100%;
zoom: 0.65 zoom: 0.65
} }

View File

@ -1,8 +1 @@
numPages: Page <span class="pageNumber"></span> of <span class="totalPages"></span> numPages: Page <span class="pageNumber"></span> of <span class="totalPages"></span>
law:
privacy: 'In compliance with the provisions of Organic Law 15/1999, on the
Protection of Personal Data, we inform you that the personal data you provide
will be included in automated files of VERDNATURA LEVANTE SL, being able at all
times to exercise the rights of access, rectification, cancellation and opposition,
communicating it in writing to the registered office of the entity.
The purpose of the file is administrative management, accounting, and billing.'

View File

@ -1,8 +1 @@
numPages: Página <span class="pageNumber"></span> de <span class="totalPages"></span> numPages: Página <span class="pageNumber"></span> de <span class="totalPages"></span>
law:
privacy: En cumplimiento de lo dispuesto en la Ley Orgánica 15/1999, de Protección
de Datos de Carácter Personal, le comunicamos que los datos personales que facilite
se incluirán en ficheros automatizados de VERDNATURA LEVANTE S.L., pudiendo en
todo momento ejercitar los derechos de acceso, rectificación, cancelación y oposición,
comunicándolo por escrito al domicilio social de la entidad. La finalidad del
fichero es la gestión administrativa, contabilidad, y facturación.

View File

@ -1,8 +1 @@
numPages: Page <span class="pageNumber"></span> de <span class="totalPages"></span> numPages: Page <span class="pageNumber"></span> de <span class="totalPages"></span>
law:
privacy: Conformément aux dispositions de la loi organique 15/1999 sur la protection
des données personnelles, nous vous informons que les données personnelles que
vous fournissez seront incluses dans des dossiers. VERDNATURA LEVANTE S.L., vous
pouvez à tout moment, exercer les droits d'accès, de rectification, d'annulation
et d'opposition, en communiquant par écrit au siège social de la société. Le dossier
a pour objet la gestion administrative, la comptabilité et la facturation.

View File

@ -1,8 +1 @@
numPages: Página <span class="pageNumber"></span> de <span class="totalPages"></span> numPages: Página <span class="pageNumber"></span> de <span class="totalPages"></span>
law:
privacy: Em cumprimento do disposto na lei Orgânica 15/1999, de Protecção de Dados
de Carácter Pessoal, comunicamos que os dados pessoais que facilite se incluirão
nos ficheiros automatizados de VERDNATURA LEVANTE S.L., podendo em todo momento
exercer os direitos de acesso, rectificação, cancelação e oposição, comunicando
por escrito ao domicílio social da entidade. A finalidade do ficheiro é a gestão
administrativa, contabilidade e facturação.

View File

@ -5,6 +5,11 @@
<div class="centerText" v-if="centerText" class="uppercase">{{centerText}}</div> <div class="centerText" v-if="centerText" class="uppercase">{{centerText}}</div>
<div class="pageCount" v-html="$t('numPages')"></div> <div class="pageCount" v-html="$t('numPages')"></div>
</div> </div>
<p class="privacy" v-html="$t('law.privacy')"></p> <p
v-if="company?.footnotes"
v-html="company.footnotes">
</p>
</div> </div>
</div> </div>

View File

@ -1,4 +1,17 @@
/* eslint-disable no-tabs */
const db = require('../../database');
module.exports = { module.exports = {
name: 'report-footer', name: 'report-footer',
props: ['leftText', 'centerText'] async serverPrefetch() {
this.company = await db.findOne(
`SELECT
ci.footnotes
FROM companyI18n ci
JOIN company c ON c.id = ci.companyFk
WHERE c.code = ? AND ci.lang = (SELECT lang FROM account.user WHERE id = ?)`,
[this.companyCode, this.recipientId]);
},
props: ['leftText', 'companyCode', 'recipientId', 'centerText']
}; };

View File

@ -8,7 +8,7 @@
{{companyName}}. {{company.street}}. {{companyName}}. {{company.street}}.
{{company.postCode}} {{company.city}}. {{company.postCode}} {{company.city}}.
&#9742; {{companyPhone}} &#9742; {{companyPhone}}
· {{$t('company.contactData')}} · {{company.web}} - {{company.email}}
</section> </section>
<section>CIF: {{fiscalAddress.nif}} {{fiscalAddress.register}}</section> <section>CIF: {{fiscalAddress.nif}} {{fiscalAddress.register}}</section>
</header> </header>

View File

@ -43,7 +43,9 @@ module.exports = {
s.postCode, s.postCode,
s.city, s.city,
s.phone, s.phone,
cg.code AS groupName cg.code AS groupName,
c.email,
c.web
FROM company c FROM company c
JOIN companyGroup cg ON cg.id = c.companyGroupFk JOIN companyGroup cg ON cg.id = c.companyGroupFk
JOIN supplier s ON s.id = c.id JOIN supplier s ON s.id = c.id

View File

@ -0,0 +1,11 @@
const Stylesheet = require(`vn-print/core/stylesheet`);
const path = require('path');
const vnPrintPath = path.resolve('print');
module.exports = new Stylesheet([
`${vnPrintPath}/common/css/spacing.css`,
`${vnPrintPath}/common/css/misc.css`,
`${vnPrintPath}/common/css/layout.css`,
`${vnPrintPath}/common/css/email.css`])
.mergeStyles();

View File

@ -0,0 +1,15 @@
<email-body v-bind="$props">
<div class="grid-row">
<div class="grid-block vn-px-ml centered">
<h1>{{ $t('total') }}: {{tickets.length}}</h1>
<hr>
</div>
<div v-for="ticket in tickets" class="grid-block vn-px-ml">
<p v-if="ticket.ticketId"><b>{{ $t('ticketId') }}:</b> {{ticket.ticketId}}</p>
<p><b>{{ $t('clientId') }}:</b> <a :href="clientGreugeUrl(ticket.clientId)" target="_blank">{{ticket.clientId}}</a></p>
<p v-if="ticket.description"><b>{{ $t('description') }}:</b> {{ticket.description}}</p>
<p><b>{{ $t('amount') }}:</b> {{ticket.amount}} €</p>
<hr>
</div>
</div>
</email-body>

View File

@ -0,0 +1,36 @@
const Component = require(`vn-print/core/component`);
const emailBody = new Component('email-body');
const models = require('vn-loopback/server/server').models;
module.exports = {
name: 'greuge-wrong',
async serverPrefetch() {
this.url = await this.salixUrl();
if (!this.url)
throw new Error('Something went wrong');
},
components: {
'email-body': emailBody.build(),
},
methods: {
async salixUrl() {
const salix = await models.Url.findOne({
where: {
appName: 'salix',
environment: process.env.NODE_ENV || 'dev'
}
});
return salix.url;
},
clientGreugeUrl(clientId) {
return `${this.url}client/${clientId}/greuge/index`
},
},
props: {
tickets: {
type: Array,
required: true
},
},
};

View File

@ -0,0 +1,6 @@
subject: Abnormal greuges have been created
total: Total number of abnormal greuges
ticketId: Ticket
clientId: Client
description: Description
amount: Amount

View File

@ -0,0 +1,6 @@
subject: Se han creado greuges anormales
total: Número total de greuges anormales
ticketId: Ticket
clientId: Cliente
description: Descipción
amount: Importe

View File

@ -0,0 +1,11 @@
const Stylesheet = require(`vn-print/core/stylesheet`);
const path = require('path');
const vnPrintPath = path.resolve('print');
module.exports = new Stylesheet([
`${vnPrintPath}/common/css/spacing.css`,
`${vnPrintPath}/common/css/misc.css`,
`${vnPrintPath}/common/css/layout.css`,
`${vnPrintPath}/common/css/email.css`])
.mergeStyles();

View File

@ -0,0 +1,6 @@
[
{
"filename": "receipt.pdf",
"component": "receipt"
}
]

View File

@ -0,0 +1,5 @@
subject: Recibo
title: Recibo
dear: Estimado cliente
description: Ya está disponible el recibo <strong>{0}</strong>. <br/>
Puedes descargarlo haciendo clic en el adjunto de este correo.

View File

@ -0,0 +1,9 @@
<email-body v-bind="$props">
<div class="grid-row">
<div class="grid-block vn-pa-ml">
<h1>{{ $t('title') }}</h1>
<p>{{$t('dear')}},</p>
<p v-html="$t('description', [id])"></p>
</div>
</div>
</email-body>

View File

@ -0,0 +1,15 @@
const Component = require(`vn-print/core/component`);
const emailBody = new Component('email-body');
module.exports = {
name: 'receipt',
components: {
'email-body': emailBody.build(),
},
props: {
id: {
type: Number,
required: true
}
}
};

View File

@ -242,7 +242,7 @@
</tfoot> </tfoot>
</table> </table>
</div> </div>
<div class="columns vn-mt-xl" v-if="invoice.payMethodCode == 'wireTransfer' || ticketObservations"> <div class="columns vn-mt-xl" v-if="invoice.payMethodCode == 'wireTransfer' && invoice.iban">
<div class="size50 pull-left no-page-break"> <div class="size50 pull-left no-page-break">
<div class="panel"> <div class="panel">
<div class="header">{{$t('observations')}}</div> <div class="header">{{$t('observations')}}</div>
@ -266,7 +266,9 @@
v-bind:company-code="invoice.companyCode" v-bind:company-code="invoice.companyCode"
v-bind:left-text="$t('invoiceRef', [invoice.ref])" v-bind:left-text="$t('invoiceRef', [invoice.ref])"
v-bind:center-text="client.socialName" v-bind:center-text="client.socialName"
v-bind:recipient-id="client.id"
v-bind="$props" v-bind="$props"
> >
</report-footer> </report-footer>
</template> </template>

View File

@ -11,8 +11,12 @@ module.exports = {
this.client = await this.findOneFromDef('client', [this.reference]); this.client = await this.findOneFromDef('client', [this.reference]);
this.taxes = await this.rawSqlFromDef(`taxes`, [this.reference]); this.taxes = await this.rawSqlFromDef(`taxes`, [this.reference]);
this.hasIntrastat = await this.findValueFromDef(`hasIntrastat`, [this.reference]); this.hasIntrastat = await this.findValueFromDef(`hasIntrastat`, [this.reference]);
this.intrastat = await this.rawSqlFromDef(`intrastat`, this.intrastat = await this.rawSqlFromDef(`intrastat`, [
[this.reference, this.reference, this.reference, this.reference]); this.reference,
this.reference,
this.reference,
this.reference
]);
this.rectified = await this.rawSqlFromDef(`rectified`, [this.reference]); this.rectified = await this.rawSqlFromDef(`rectified`, [this.reference]);
this.hasIncoterms = await this.findValueFromDef(`hasIncoterms`, [this.reference]); this.hasIncoterms = await this.findValueFromDef(`hasIncoterms`, [this.reference]);

View File

@ -11,7 +11,7 @@ FROM invoiceOut io
JOIN client c ON c.id = io.clientFk JOIN client c ON c.id = io.clientFk
JOIN payMethod pm ON pm.id = c.payMethodFk JOIN payMethod pm ON pm.id = c.payMethodFk
JOIN company cny ON cny.id = io.companyFk JOIN company cny ON cny.id = io.companyFk
JOIN supplierAccount sa ON sa.id = cny.supplierAccountFk LEFT JOIN supplierAccount sa ON sa.id = cny.supplierAccountFk
LEFT JOIN invoiceOutSerial ios ON ios.code = io.serial LEFT JOIN invoiceOutSerial ios ON ios.code = io.serial
LEFT JOIN ticket t ON t.refFk = io.ref LEFT JOIN ticket t ON t.refFk = io.ref
WHERE t.refFk = ? WHERE t.refFk = ?