3430-ticket_step-two ticket without negatives #823

Merged
joan merged 31 commits from 3430-ticket_step-two into dev 2022-02-01 08:34:41 +00:00
13 changed files with 399 additions and 22 deletions

View File

@ -0,0 +1,43 @@
DROP PROCEDURE IF EXISTS `vn`.`ticket_getMovable`;
DELIMITER $$
$$
CREATE DEFINER=`root`@`%` PROCEDURE `vn`.`ticket_getMovable`(vTicketFk INT, vDatedNew DATETIME, vWarehouseFk INT)
BEGIN
/**
* Cálcula el stock movible para los artículos de un ticket
*
* @param vTicketFk -> Ticket
* @param vDatedNew -> Nueva fecha
* @return Sales con Movible
*/
DECLARE vDatedOld DATETIME;
SELECT t.shipped INTO vDatedOld
FROM ticket t
WHERE t.id = vTicketFk;
CALL itemStock(vWarehouseFk, DATE_SUB(vDatedNew, INTERVAL 1 DAY), NULL);
CALL item_getMinacum(vWarehouseFk, vDatedNew, DATEDIFF(vDatedOld, vDatedNew), NULL);
SELECT s.id,
s.itemFk,
s.quantity,
s.concept,
s.price,
s.reserved,
s.discount,
i.image,
i.subName,
il.stock + IFNULL(im.amount, 0) AS movable
FROM ticket t
JOIN sale s ON s.ticketFk = t.id
JOIN item i ON i.id = s.itemFk
LEFT JOIN tmp.itemMinacum im ON im.itemFk = s.itemFk AND im.warehouseFk = vWarehouseFk
LEFT JOIN tmp.itemList il ON il.itemFk = s.itemFk
WHERE t.id = vTicketFk;
DROP TEMPORARY TABLE IF EXISTS tmp.itemList;
DROP TEMPORARY TABLE IF EXISTS tmp.itemMinacum;
END$$
DELIMITER ;

View File

@ -625,6 +625,7 @@ INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeF
(25 ,NULL, 8, 1, NULL, CURDATE(), CURDATE(), 1101, 'Bruce Wayne', 1, NULL, 0, 1, 5, 1, CURDATE()), (25 ,NULL, 8, 1, NULL, CURDATE(), CURDATE(), 1101, 'Bruce Wayne', 1, NULL, 0, 1, 5, 1, CURDATE()),
(26 ,NULL, 8, 1, NULL, CURDATE(), CURDATE(), 1101, 'An incredibly long alias for testing purposes', 1, NULL, 0, 1, 5, 1, CURDATE()), (26 ,NULL, 8, 1, NULL, CURDATE(), CURDATE(), 1101, 'An incredibly long alias for testing purposes', 1, NULL, 0, 1, 5, 1, CURDATE()),
(27 ,NULL, 8, 1, NULL, CURDATE(), CURDATE(), 1101, 'Wolverine', 1, NULL, 0, 1, 5, 1, CURDATE()); (27 ,NULL, 8, 1, NULL, CURDATE(), CURDATE(), 1101, 'Wolverine', 1, NULL, 0, 1, 5, 1, CURDATE());
INSERT INTO `vn`.`ticketObservation`(`id`, `ticketFk`, `observationTypeFk`, `description`) INSERT INTO `vn`.`ticketObservation`(`id`, `ticketFk`, `observationTypeFk`, `description`)
VALUES VALUES
(1, 11, 1, 'ready'), (1, 11, 1, 'ready'),
@ -899,7 +900,8 @@ INSERT INTO `vn`.`sale`(`id`, `itemFk`, `ticketFk`, `concept`, `quantity`, `pric
(29, 4, 17, 'Melee weapon heavy shield 1x0.5m', 20, 1.72, 0, 0, 0, CURDATE()), (29, 4, 17, 'Melee weapon heavy shield 1x0.5m', 20, 1.72, 0, 0, 0, CURDATE()),
(30, 4, 18, 'Melee weapon heavy shield 1x0.5m', 20, 1.72, 0, 0, 0, CURDATE()), (30, 4, 18, 'Melee weapon heavy shield 1x0.5m', 20, 1.72, 0, 0, 0, CURDATE()),
(31, 2, 23, 'Melee weapon combat fist 15cm', -5, 7.08, 0, 0, 0, CURDATE()), (31, 2, 23, 'Melee weapon combat fist 15cm', -5, 7.08, 0, 0, 0, CURDATE()),
(32, 1, 24, 'Ranged weapon longbow 2m', -1, 8.07, 0, 0, 0, CURDATE()); (32, 1, 24, 'Ranged weapon longbow 2m', -1, 8.07, 0, 0, 0, CURDATE()),
(33, 5, 14, 'Ranged weapon pistol 9mm', 50, 1.79, 0, 0, 0, CURDATE());
INSERT INTO `vn`.`saleChecked`(`saleFk`, `isChecked`) INSERT INTO `vn`.`saleChecked`(`saleFk`, `isChecked`)
VALUES VALUES

View File

@ -526,6 +526,7 @@ export default {
acceptDialog: '.vn-dialog.shown button[response="accept"]', acceptDialog: '.vn-dialog.shown button[response="accept"]',
acceptChangeHourButton: '.vn-dialog.shown button[response="accept"]', acceptChangeHourButton: '.vn-dialog.shown button[response="accept"]',
descriptorDeliveryDate: 'vn-ticket-descriptor slot-body > .attributes > vn-label-value:nth-child(4) > section > span', descriptorDeliveryDate: 'vn-ticket-descriptor slot-body > .attributes > vn-label-value:nth-child(4) > section > span',
descriptorDeliveryAgency: 'vn-ticket-descriptor slot-body > .attributes > vn-label-value:nth-child(5) > section > span',
acceptInvoiceOutButton: '.vn-confirm.shown button[response="accept"]', acceptInvoiceOutButton: '.vn-confirm.shown button[response="accept"]',
acceptDeleteStowawayButton: '.vn-dialog.shown button[response="accept"]' acceptDeleteStowawayButton: '.vn-dialog.shown button[response="accept"]'
}, },
@ -603,10 +604,12 @@ export default {
ticketBasicData: { ticketBasicData: {
agency: 'vn-autocomplete[ng-model="$ctrl.agencyModeId"]', agency: 'vn-autocomplete[ng-model="$ctrl.agencyModeId"]',
zone: 'vn-autocomplete[ng-model="$ctrl.zoneId"]', zone: 'vn-autocomplete[ng-model="$ctrl.zoneId"]',
shipped: 'vn-date-picker[ng-model="$ctrl.shipped"]',
nextStepButton: 'vn-step-control .buttons > section:last-child vn-button', nextStepButton: 'vn-step-control .buttons > section:last-child vn-button',
finalizeButton: 'vn-step-control .buttons > section:last-child button[type=submit]', finalizeButton: 'vn-step-control .buttons > section:last-child button[type=submit]',
stepTwoTotalPriceDif: 'vn-ticket-basic-data-step-two > vn-side-menu div:nth-child(4)', stepTwoTotalPriceDif: 'vn-ticket-basic-data-step-two > vn-side-menu div:nth-child(4)',
chargesReason: 'vn-ticket-basic-data-step-two div:nth-child(3) > vn-radio', chargesReason: 'vn-ticket-basic-data-step-two div:nth-child(3) > vn-radio',
withoutNegatives: 'vn-check[ng-model="$ctrl.ticket.withoutNegatives"]',
}, },
ticketComponents: { ticketComponents: {
base: 'vn-ticket-components > vn-side-menu div:nth-child(1) > div:nth-child(2)' base: 'vn-ticket-components > vn-side-menu div:nth-child(1) > div:nth-child(2)'

View File

@ -83,4 +83,62 @@ describe('Ticket Edit basic data path', () => {
await page.waitToClick(selectors.ticketBasicData.finalizeButton); await page.waitToClick(selectors.ticketBasicData.finalizeButton);
await page.waitForState('ticket.card.summary'); await page.waitForState('ticket.card.summary');
}); });
it(`should not find ticket`, async() => {
await page.doSearch('29');
const count = await page.countElement(selectors.ticketsIndex.searchResult);
expect(count).toEqual(0);
});
it(`should split ticket without negatives`, async() => {
const newAgency = 'Silla247';
const newDate = new Date();
newDate.setDate(newDate.getDate() + 1);
await page.accessToSearchResult('14');
await page.accessToSection('ticket.card.basicData.stepOne');
await page.autocompleteSearch(selectors.ticketBasicData.agency, newAgency);
await page.pickDate(selectors.ticketBasicData.shipped, newDate);
await page.waitToClick(selectors.ticketBasicData.nextStepButton);
await page.waitToClick(selectors.ticketBasicData.withoutNegatives);
await page.waitToClick(selectors.ticketBasicData.finalizeButton);
await page.waitForState('ticket.card.summary');
const newTicketAgency = await page
.waitToGetProperty(selectors.ticketDescriptor.descriptorDeliveryAgency, 'innerText');
const newTicketDate = await page
.waitToGetProperty(selectors.ticketDescriptor.descriptorDeliveryDate, 'innerText');
expect(newAgency).toEqual(newTicketAgency);
expect(newTicketDate).toContain(newDate.getDate());
});
it(`should new ticket have sale of old ticket`, async() => {
await page.accessToSection('ticket.card.sale');
await page.waitForState('ticket.card.sale');
const item = await page.waitToGetProperty(selectors.ticketSales.firstSaleId, 'innerText');
expect(item).toEqual('4');
});
it(`should old ticket have old date and agency`, async() => {
const oldDate = new Date();
const oldAgency = 'Super-Man delivery';
await page.accessToSearchResult('14');
const oldTicketAgency = await page
.waitToGetProperty(selectors.ticketDescriptor.descriptorDeliveryAgency, 'innerText');
const oldTicketDate = await page
.waitToGetProperty(selectors.ticketDescriptor.descriptorDeliveryDate, 'innerText');
expect(oldTicketAgency).toEqual(oldAgency);
expect(oldTicketDate).toContain(oldDate.getDate());
});
}); });

View File

@ -77,6 +77,12 @@ module.exports = Self => {
type: 'number', type: 'number',
description: 'Action id', description: 'Action id',
required: true required: true
},
{
arg: 'isWithoutNegatives',
type: 'boolean',
description: 'Is whithout negatives',
required: true
}], }],
returns: { returns: {
type: ['object'], type: ['object'],
@ -127,6 +133,18 @@ module.exports = Self => {
} }
} }
if (args.isWithoutNegatives) {
const query = `CALL ticket_getMovable(?,?,?)`;
const params = [args.id, args.shipped, args.warehouseFk];
const [salesMovable] = await Self.rawSql(query, params, myOptions);
const salesNewTicket = salesMovable.filter(sale => (sale.movable ?? 0) >= sale.quantity);
if (salesNewTicket.length) {
const newTicket = await models.Ticket.transferSales(ctx, args.id, null, salesNewTicket, myOptions);
args.id = newTicket.id;
}
}
const originalTicket = await models.Ticket.findOne({ const originalTicket = await models.Ticket.findOne({
where: {id: args.id}, where: {id: args.id},
fields: [ fields: [
@ -230,8 +248,9 @@ module.exports = Self => {
await models.Chat.sendCheckingPresence(ctx, salesPersonId, message); await models.Chat.sendCheckingPresence(ctx, salesPersonId, message);
} }
res.id = args.id;
if (tx) await tx.commit(); if (tx) await tx.commit();
return res; return res;
} catch (e) { } catch (e) {
if (tx) await tx.rollback(); if (tx) await tx.rollback();

View File

@ -40,6 +40,12 @@ module.exports = Self => {
type: 'number', type: 'number',
description: 'The warehouse id', description: 'The warehouse id',
required: true required: true
},
{
arg: 'shipped',
type: 'date',
description: 'shipped',
required: true
}], }],
returns: { returns: {
type: ['object'], type: ['object'],
@ -104,19 +110,32 @@ module.exports = Self => {
totalDifference: 0.00, totalDifference: 0.00,
}; };
const query = `CALL vn.ticket_priceDifference(?, ?, ?, ?, ?)`; // Get items movable
const params = [args.id, args.landed, args.addressId, args.zoneId, args.warehouseId]; const ticketOrigin = await models.Ticket.findById(args.id, null, myOptions);
const differenceShipped = ticketOrigin.shipped.getTime() != args.shipped.getTime();
const differenceWarehouse = ticketOrigin.warehouseFk != args.warehouseId;
salesObj.haveDifferences = differenceShipped || differenceWarehouse;
let query = `CALL ticket_getMovable(?,?,?)`;
let params = [args.id, args.shipped, args.warehouseId];
const [salesMovable] = await Self.rawSql(query, params, myOptions);
const itemMovable = new Map();
for (sale of salesMovable)
itemMovable.set(sale.id, sale.movable ?? 0);
// Sale price component, one per sale
query = `CALL vn.ticket_priceDifference(?, ?, ?, ?, ?)`;
params = [args.id, args.landed, args.addressId, args.zoneId, args.warehouseId];
const [difComponents] = await Self.rawSql(query, params, myOptions); const [difComponents] = await Self.rawSql(query, params, myOptions);
const map = new Map(); const map = new Map();
// Sale price component, one per sale
for (difComponent of difComponents) for (difComponent of difComponents)
map.set(difComponent.saleFk, difComponent); map.set(difComponent.saleFk, difComponent);
for (sale of salesObj.items) { for (sale of salesObj.items) {
const difComponent = map.get(sale.id); const difComponent = map.get(sale.id);
if (difComponent) { if (difComponent) {
sale.component = difComponent; sale.component = difComponent;
@ -129,10 +148,11 @@ module.exports = Self => {
salesObj.totalUnitPrice += sale.price; salesObj.totalUnitPrice += sale.price;
salesObj.totalUnitPrice = round(salesObj.totalUnitPrice); salesObj.totalUnitPrice = round(salesObj.totalUnitPrice);
sale.movable = itemMovable.get(sale.id);
} }
if (tx) await tx.commit(); if (tx) await tx.commit();
return salesObj; return salesObj;
} catch (e) { } catch (e) {
if (tx) await tx.rollback(); if (tx) await tx.rollback();

View File

@ -45,7 +45,8 @@ describe('ticket componentUpdate()', () => {
shipped: today, shipped: today,
landed: tomorrow, landed: tomorrow,
isDeleted: false, isDeleted: false,
option: 1 option: 1,
isWithoutNegatives: false
}; };
let ctx = { let ctx = {
@ -94,7 +95,8 @@ describe('ticket componentUpdate()', () => {
shipped: today, shipped: today,
landed: tomorrow, landed: tomorrow,
isDeleted: false, isDeleted: false,
option: 1 option: 1,
isWithoutNegatives: false
}; };
const ctx = { const ctx = {
@ -134,4 +136,60 @@ describe('ticket componentUpdate()', () => {
throw e; throw e;
} }
}); });
it('should change warehouse and without negatives', async() => {
const tx = await models.SaleComponent.beginTransaction({});
try {
const options = {transaction: tx};
const saleToTransfer = 27;
const originDate = today;
const newDate = tomorrow;
const ticketID = 14;
newDate.setHours(0, 0, 0, 0, 0);
originDate.setHours(0, 0, 0, 0, 0);
const args = {
id: ticketID,
clientFk: 1104,
agencyModeFk: 2,
addressFk: 4,
zoneFk: 9,
warehouseFk: 1,
companyFk: 442,
shipped: newDate,
landed: tomorrow,
isDeleted: false,
option: 1,
isWithoutNegatives: true
};
const ctx = {
args: args,
req: {
accessToken: {userId: 9},
headers: {origin: 'http://localhost'},
__: value => {
return value;
}
}
};
await models.Ticket.componentUpdate(ctx, options);
const [newTicketID] = await models.Ticket.rawSql('SELECT MAX(id) as id FROM ticket', null, options);
const oldTicket = await models.Ticket.findById(ticketID, null, options);
const newTicket = await models.Ticket.findById(newTicketID.id, null, options);
const newTicketSale = await models.Sale.findOne({where: {ticketFk: args.id}}, options);
expect(oldTicket.shipped).toEqual(originDate);
expect(newTicket.shipped).toEqual(newDate);
expect(newTicketSale.id).toEqual(saleToTransfer);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
}); });

View File

@ -15,6 +15,7 @@ describe('sale priceDifference()', () => {
ctx.args = { ctx.args = {
id: 16, id: 16,
landed: tomorrow, landed: tomorrow,
shipped: tomorrow,
addressId: 126, addressId: 126,
agencyModeId: 7, agencyModeId: 7,
zoneId: 3, zoneId: 3,
@ -45,6 +46,7 @@ describe('sale priceDifference()', () => {
ctx.args = { ctx.args = {
id: 1, id: 1,
landed: new Date(), landed: new Date(),
shipped: new Date(),
addressId: 121, addressId: 121,
zoneId: 3, zoneId: 3,
warehouseId: 1 warehouseId: 1
@ -59,4 +61,38 @@ describe('sale priceDifference()', () => {
expect(error).toEqual(new UserError(`The sales of this ticket can't be modified`)); expect(error).toEqual(new UserError(`The sales of this ticket can't be modified`));
}); });
it('should return ticket movable', async() => {
const tx = await models.Ticket.beginTransaction({});
try {
const options = {transaction: tx};
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const ctx = {req: {accessToken: {userId: 1106}}};
ctx.args = {
id: 11,
shipped: tomorrow,
landed: tomorrow,
addressId: 122,
agencyModeId: 7,
zoneId: 3,
warehouseId: 1
};
const result = await models.Ticket.priceDifference(ctx, options);
const firstItem = result.items[0];
const secondtItem = result.items[1];
expect(firstItem.movable).toEqual(440);
expect(secondtItem.movable).toEqual(1980);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
}); });

View File

@ -201,7 +201,8 @@ class Controller extends Component {
addressId: this.ticket.addressFk, addressId: this.ticket.addressFk,
agencyModeId: this.ticket.agencyModeFk, agencyModeId: this.ticket.agencyModeFk,
zoneId: this.ticket.zoneFk, zoneId: this.ticket.zoneFk,
warehouseId: this.ticket.warehouseFk warehouseId: this.ticket.warehouseFk,
shipped: this.ticket.shipped
}; };
return this.$http.post(query, params).then(res => { return this.$http.post(query, params).then(res => {

View File

@ -9,6 +9,7 @@
<vn-tr> <vn-tr>
<vn-th number>Item</vn-th> <vn-th number>Item</vn-th>
<vn-th class="align-center">Description</vn-th> <vn-th class="align-center">Description</vn-th>
<vn-th ng-if="$ctrl.ticket.sale.haveDifferences" number>Movable</vn-th>
<vn-th number>Quantity</vn-th> <vn-th number>Quantity</vn-th>
<vn-th number>Price (PPU)</vn-th> <vn-th number>Price (PPU)</vn-th>
<vn-th number>New (PPU)</vn-th> <vn-th number>New (PPU)</vn-th>
@ -31,6 +32,13 @@
tabindex="-1"> tabindex="-1">
</vn-fetched-tags> </vn-fetched-tags>
</vn-td> </vn-td>
<vn-td ng-if="$ctrl.ticket.sale.haveDifferences" number>
<span
class="chip"
ng-class="{'alert': sale.quantity>sale.movable}">
{{::sale.movable}}
</span>
</vn-td>
<vn-td number>{{::sale.quantity}}</vn-td> <vn-td number>{{::sale.quantity}}</vn-td>
<vn-td number>{{::sale.price | currency: 'EUR': 2}}</vn-td> <vn-td number>{{::sale.price | currency: 'EUR': 2}}</vn-td>
<vn-td number>{{::sale.component.newPrice | currency: 'EUR': 2}}</vn-td> <vn-td number>{{::sale.component.newPrice | currency: 'EUR': 2}}</vn-td>
@ -66,6 +74,13 @@
</div> </div>
</div> </div>
</vn-card> </vn-card>
<div class="totalBox align-left" ng-show="::$ctrl.haveNegatives">
<vn-check
ng-model="$ctrl.ticket.withoutNegatives"
label="Create without negatives"
info="Clone this ticket with the changes and only sales availables">
</vn-check>
</div>
</div> </div>
</vn-side-menu> </vn-side-menu>

View File

@ -20,6 +20,7 @@ class Controller extends Component {
this.getTotalNewPrice(); this.getTotalNewPrice();
this.getTotalDifferenceOfPrice(); this.getTotalDifferenceOfPrice();
this.loadDefaultTicketAction(); this.loadDefaultTicketAction();
this.ticketHaveNegatives();
} }
loadDefaultTicketAction() { loadDefaultTicketAction() {
@ -63,6 +64,22 @@ class Controller extends Component {
this.totalPriceDifference = totalPriceDifference; this.totalPriceDifference = totalPriceDifference;
} }
ticketHaveNegatives() {
let haveNegatives = false;
let haveNotNegatives = false;
const haveDifferences = this.ticket.sale.haveDifferences;
this.ticket.sale.items.forEach(item => {
if (item.quantity > item.movable)
haveNegatives = true;
else
haveNotNegatives = true;
});
this.ticket.withoutNegatives = false;
this.haveNegatives = (haveNegatives && haveNotNegatives && haveDifferences);
}
onSubmit() { onSubmit() {
if (!this.ticket.option) { if (!this.ticket.option) {
return this.vnApp.showError( return this.vnApp.showError(
@ -70,8 +87,8 @@ class Controller extends Component {
); );
} }
let query = `tickets/${this.ticket.id}/componentUpdate`; const query = `tickets/${this.ticket.id}/componentUpdate`;
let params = { const params = {
clientFk: this.ticket.clientFk, clientFk: this.ticket.clientFk,
nickname: this.ticket.nickname, nickname: this.ticket.nickname,
agencyModeFk: this.ticket.agencyModeFk, agencyModeFk: this.ticket.agencyModeFk,
@ -82,16 +99,20 @@ class Controller extends Component {
shipped: this.ticket.shipped, shipped: this.ticket.shipped,
landed: this.ticket.landed, landed: this.ticket.landed,
isDeleted: this.ticket.isDeleted, isDeleted: this.ticket.isDeleted,
option: parseInt(this.ticket.option) option: parseInt(this.ticket.option),
isWithoutNegatives: this.ticket.withoutNegatives
}; };
this.$http.post(query, params).then(res => { this.$http.post(query, params)
this.vnApp.showMessage( .then(res => {
this.$t(`The ticket has been unrouted`) this.ticketToMove = res.data.id;
); this.vnApp.showMessage(
this.card.reload(); this.$t(`The ticket has been unrouted`)
this.$state.go('ticket.card.summary', {id: this.$params.id}); );
}); })
.finally(() => {
this.$state.go('ticket.card.summary', {id: this.ticketToMove});
});
} }
} }

View File

@ -64,5 +64,103 @@ describe('Ticket', () => {
expect(controller.totalPriceDifference).toEqual(0.3); expect(controller.totalPriceDifference).toEqual(0.3);
}); });
}); });
describe('ticketHaveNegatives()', () => {
it('should show if ticket have any negative, have differences, but not all sale are negative', () => {
controller.ticket = {
sale: {
items: [
{
item: 1,
quantity: 2,
movable: 1
},
{
item: 2,
quantity: 1,
movable: 5
}
],
haveDifferences: true
}
};
controller.ticketHaveNegatives();
expect(controller.haveNegatives).toEqual(true);
});
it('should not show if ticket not have any negative', () => {
controller.ticket = {
sale: {
items: [
{
item: 1,
quantity: 2,
movable: 1
},
{
item: 2,
quantity: 2,
movable: 1
}
],
haveDifferences: true
}
};
controller.ticketHaveNegatives();
expect(controller.haveNegatives).toEqual(false);
});
it('should not show if all sale are negative', () => {
controller.ticket = {
sale: {
items: [
{
item: 1,
quantity: 2,
movable: 1
},
{
item: 2,
quantity: 2,
movable: 1
}
],
haveDifferences: true
}
};
controller.ticketHaveNegatives();
expect(controller.haveNegatives).toEqual(false);
});
it('should not show if ticket not have differences', () => {
controller.ticket = {
sale: {
items: [
{
item: 1,
quantity: 2,
movable: 1
},
{
item: 2,
quantity: 1,
movable: 2
}
],
haveDifferences: false
}
};
controller.ticketHaveNegatives();
expect(controller.haveNegatives).toEqual(false);
});
});
}); });
}); });

View File

@ -5,4 +5,7 @@ Charge difference to: Cargar diferencia a
The ticket has been unrouted: El ticket ha sido desenrutado The ticket has been unrouted: El ticket ha sido desenrutado
Price: Precio Price: Precio
New price: Nuevo precio New price: Nuevo precio
Price difference: Diferencia de precio Price difference: Diferencia de precio
Create without negatives: Crear sin negativos
Clone this ticket with the changes and only sales availables: Clona este ticket con los cambios y solo las ventas disponibles.
Movable: Movible