Initial commit: Sale packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:49 +02:00
commit 14e3d26998
6469 changed files with 2479670 additions and 0 deletions

View file

@ -0,0 +1,55 @@
odoo.define('point_of_sale.tour.BarcodeScanning', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
const Tour = require('web_tour.tour');
startSteps();
// Add a product with its barcode
ProductScreen.do.scan_barcode("0123456789");
ProductScreen.check.selectedOrderlineHas('Monitor Stand');
ProductScreen.do.scan_barcode("0123456789");
ProductScreen.check.selectedOrderlineHas('Monitor Stand', 2);
// Test "Prices product" EAN-13 `23.....{NNNDD}` barcode pattern
ProductScreen.do.scan_ean13_barcode("2305000000004");
ProductScreen.check.selectedOrderlineHas('Magnetic Board', 1, "0.00");
ProductScreen.do.scan_ean13_barcode("2305000123451");
ProductScreen.check.selectedOrderlineHas('Magnetic Board', 1, "123.45");
// Test "Weighted product" EAN-13 `21.....{NNDDD}` barcode pattern
ProductScreen.do.scan_ean13_barcode("2100005000000");
ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', 0, "0.00");
ProductScreen.do.scan_ean13_barcode("2100005080002");
ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', 8);
Tour.register('BarcodeScanningTour', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
// Add the Product 1 with GS1 barcode
ProductScreen.do.scan_barcode("0108431673020125100000001");
ProductScreen.check.selectedOrderlineHas('Product 1');
ProductScreen.do.scan_barcode("0108431673020125100000001");
ProductScreen.check.selectedOrderlineHas('Product 1', 2);
// Add the Product 2 with normal barcode
ProductScreen.do.scan_barcode("08431673020126");
ProductScreen.check.selectedOrderlineHas('Product 2');
ProductScreen.do.scan_barcode("08431673020126");
ProductScreen.check.selectedOrderlineHas('Product 2', 2);
// Add the Product 3 with normal barcode
ProductScreen.do.scan_barcode("3760171283370");
ProductScreen.check.selectedOrderlineHas('Product 3');
ProductScreen.do.scan_barcode("3760171283370");
ProductScreen.check.selectedOrderlineHas('Product 3', 2);
Tour.register('GS1BarcodeScanningTour', { test: true, url: '/pos/ui' }, getSteps());
});

View file

@ -0,0 +1,105 @@
odoo.define('point_of_sale.tour.Chrome', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods');
const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods');
const { TicketScreen } = require('point_of_sale.tour.TicketScreenTourMethods');
const { Chrome } = require('point_of_sale.tour.ChromeTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
var Tour = require('web_tour.tour');
startSteps();
// Order 1 is at Product Screen
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Desk Pad', '1', '2', '2.0');
Chrome.do.clickTicketButton();
TicketScreen.check.checkStatus('-0001', 'Ongoing');
// Order 2 is at Payment Screen
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline('Monitor Stand', '3', '4', '12.0');
ProductScreen.do.clickPayButton();
PaymentScreen.check.isShown();
Chrome.do.clickTicketButton();
TicketScreen.check.checkStatus('-0002', 'Payment');
// Order 3 is at Receipt Screen
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline('Whiteboard Pen', '5', '6', '30.0');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.validateButtonIsHighlighted(true);
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
Chrome.do.clickTicketButton();
TicketScreen.check.checkStatus('-0003', 'Receipt');
// Select order 1, should be at Product Screen
TicketScreen.do.selectOrder('-0001');
ProductScreen.check.productIsDisplayed('Desk Pad');
ProductScreen.check.selectedOrderlineHas('Desk Pad', '1.0', '2.0');
// Select order 2, should be at Payment Screen
Chrome.do.clickTicketButton();
TicketScreen.do.selectOrder('-0002');
PaymentScreen.check.emptyPaymentlines('12.0');
PaymentScreen.check.validateButtonIsHighlighted(false);
// Select order 3, should be at Receipt Screen
Chrome.do.clickTicketButton();
TicketScreen.do.selectOrder('-0003');
ReceiptScreen.check.totalAmountContains('30.0');
// Pay order 1, with change
Chrome.do.clickTicketButton();
TicketScreen.do.selectOrder('-0001');
ProductScreen.check.isShown();
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.pressNumpad('2 0');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('18.0');
PaymentScreen.check.validateButtonIsHighlighted(true);
PaymentScreen.do.clickValidate();
ReceiptScreen.check.totalAmountContains('2.0');
// Order 1 now should have Receipt status
Chrome.do.clickTicketButton();
TicketScreen.check.checkStatus('-0001', 'Receipt');
// Select order 3, should still be at Receipt Screen
// and the total amount doesn't change.
TicketScreen.do.selectOrder('-0003');
ReceiptScreen.check.totalAmountContains('30.0');
// click next screen on order 3
// then delete the new empty order
ReceiptScreen.do.clickNextOrder();
ProductScreen.check.orderIsEmpty();
Chrome.do.clickTicketButton();
TicketScreen.do.deleteOrder('-0004');
TicketScreen.do.deleteOrder('-0001');
// After deleting order 1 above, order 2 became
// the 2nd-row order and it has payment status
TicketScreen.check.nthRowContains(2, 'Payment')
TicketScreen.do.deleteOrder('-0002');
Chrome.do.confirmPopup();
TicketScreen.do.clickNewTicket();
// Invoice an order
ProductScreen.exec.addOrderline('Whiteboard Pen', '5', '6');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Nicole Ford');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.clickInvoiceButton();
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
Tour.register('ChromeTour', { test: true, url: '/pos/ui' }, getSteps());
});

View file

@ -0,0 +1,291 @@
odoo.define('point_of_sale.tour.PaymentScreen', function (require) {
'use strict';
const { Chrome } = require('point_of_sale.tour.ChromeTourMethods');
const { ErrorPopup } = require('point_of_sale.tour.ErrorPopupTourMethods');
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods');
const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods');
const { TicketScreen } = require('point_of_sale.tour.TicketScreenTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
var Tour = require('web_tour.tour');
startSteps();
ProductScreen.exec.addOrderline('Letter Tray', '10');
ProductScreen.check.selectedOrderlineHas('Letter Tray', '10.0');
ProductScreen.do.clickPayButton();
PaymentScreen.check.emptyPaymentlines('52.8');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.pressNumpad('1 1');
PaymentScreen.check.selectedPaymentlineHas('Cash', '11.00');
PaymentScreen.check.remainingIs('41.8');
PaymentScreen.check.changeIs('0.0');
PaymentScreen.check.validateButtonIsHighlighted(false);
// remove the selected paymentline with multiple backspace presses
PaymentScreen.do.pressNumpad('Backspace Backspace');
PaymentScreen.check.selectedPaymentlineHas('Cash', '0.00');
PaymentScreen.do.pressNumpad('Backspace');
PaymentScreen.check.emptyPaymentlines('52.8');
// Pay with bank, the selected line should have full amount
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
PaymentScreen.check.validateButtonIsHighlighted(true);
// remove the line using the delete button
PaymentScreen.do.clickPaymentlineDelButton('Bank', '52.8');
// Use +10 and +50 to increment the amount of the paymentline
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.pressNumpad('+10');
PaymentScreen.check.remainingIs('42.8');
PaymentScreen.check.changeIs('0.0');
PaymentScreen.check.validateButtonIsHighlighted(false);
PaymentScreen.do.pressNumpad('+50');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('7.2');
PaymentScreen.check.validateButtonIsHighlighted(true);
PaymentScreen.do.clickPaymentlineDelButton('Cash', '60.0');
// Multiple paymentlines
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.pressNumpad('1');
PaymentScreen.check.remainingIs('51.8');
PaymentScreen.check.changeIs('0.0');
PaymentScreen.check.validateButtonIsHighlighted(false);
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.pressNumpad('5');
PaymentScreen.check.remainingIs('46.8');
PaymentScreen.check.changeIs('0.0');
PaymentScreen.check.validateButtonIsHighlighted(false);
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.pressNumpad('2 0');
PaymentScreen.check.remainingIs('26.8');
PaymentScreen.check.changeIs('0.0');
PaymentScreen.check.validateButtonIsHighlighted(false);
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
PaymentScreen.check.validateButtonIsHighlighted(true);
Tour.register('PaymentScreenTour', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Letter Tray', '1', '10');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.pressNumpad('1 0 0 0');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
Tour.register('PaymentScreenTour2', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Product Test', '1');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('2.00');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
Chrome.do.clickTicketButton();
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline('Product Test', '-1');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('-2.00');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
Tour.register('PaymentScreenRoundingUp', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Product Test', '1');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('1.95');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
Chrome.do.clickTicketButton();
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline('Product Test', '-1');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('-1.95');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
Tour.register('PaymentScreenRoundingDown', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Product Test 1.2', '1');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('1.00');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
Chrome.do.clickTicketButton();
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline('Product Test 1.25', '1');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('1.5');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
Chrome.do.clickTicketButton();
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline('Product Test 1.4', '1');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('1.5');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
Chrome.do.clickTicketButton();
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline('Product Test 1.2', '1');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('1.00');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.pressNumpad('2');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('1.0');
Tour.register('PaymentScreenRoundingHalfUp', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Product Test 40', '1');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Nicole Ford');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('40.00');
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.pressNumpad('3 8');
PaymentScreen.check.remainingIs('2.0');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
PaymentScreen.do.clickInvoiceButton();
PaymentScreen.do.clickValidate();
ReceiptScreen.check.receiptIsThere();
ReceiptScreen.do.clickNextOrder();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Product Test 41', '1');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Nicole Ford');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('41.00');
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.pressNumpad('3 8');
PaymentScreen.check.remainingIs('3.0');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
PaymentScreen.do.clickInvoiceButton();
PaymentScreen.do.clickValidate();
ReceiptScreen.check.receiptIsThere();
Tour.register('PaymentScreenRoundingHalfUpCashAndBank', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Product Test', '1');
ProductScreen.do.clickPayButton();
PaymentScreen.check.totalIs('1.95');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.pressNumpad('5');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('3.05');
PaymentScreen.check.totalDueIs('1.95');
Tour.register('PaymentScreenTotalDueWithOverPayment', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Magnetic Board', '1');
ProductScreen.do.clickPayButton();
// Check the popup error is shown when selecting another payment method
PaymentScreen.check.totalIs('1.90');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.pressNumpad('1 .');
PaymentScreen.check.selectedPaymentlineHas('Cash', '1.00');
PaymentScreen.do.pressNumpad('2 4');
PaymentScreen.check.selectedPaymentlineHas('Cash', '1.24');
PaymentScreen.do.clickPaymentMethod('Bank');
ErrorPopup.check.isShown();
ErrorPopup.check.messageBodyContains( // Verify the value displayed are as expected
'The rounding precision is 0.10 so you should set 1.20 or 1.30 as payment amount instead of 1.24.'
);
Tour.register('CashRoundingPayment', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.exec.addOrderline('Letter Tray', '5');
ProductScreen.check.selectedOrderlineHas('Letter Tray', '5.0');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Nicole Ford');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('New Cash');
PaymentScreen.do.pressNumpad('5 5');
PaymentScreen.check.selectedPaymentlineHas('New Cash', '55');
PaymentScreen.do.clickInvoiceButton();
PaymentScreen.do.clickValidate();
ReceiptScreen.check.receiptIsThere();
Tour.register('MultipleCashPaymentMethod', { test: true, url: '/pos/ui' }, getSteps());
});

View file

@ -0,0 +1,78 @@
odoo.define('point_of_sale.tour.ProductConfigurator', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { ProductConfigurator } = require('point_of_sale.tour.ProductConfiguratorTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
var Tour = require('web_tour.tour');
// signal to start generating steps
// when finished, steps can be taken from getSteps
startSteps();
ProductScreen.do.confirmOpeningPopup();
// Go by default to home category
ProductScreen.do.clickHomeCategory();
// Click on Configurable Chair product
ProductScreen.do.clickDisplayedProduct('Configurable Chair');
ProductConfigurator.check.isShown();
// Cancel configuration, not product should be in order
ProductConfigurator.do.cancelAttributes();
ProductScreen.check.orderIsEmpty();
// Click on Configurable Chair product
ProductScreen.do.clickDisplayedProduct('Configurable Chair');
ProductConfigurator.check.isShown();
// Pick Color
ProductConfigurator.do.pickColor('Red');
// Pick Radio
ProductConfigurator.do.pickSelect('Metal');
// Pick Select
ProductConfigurator.do.pickRadio('Other');
// Fill in custom attribute
ProductConfigurator.do.fillCustomAttribute('Custom Fabric');
// Confirm configuration
ProductConfigurator.do.confirmAttributes();
// Check that the product has been added to the order with correct attributes and price
ProductScreen.check.selectedOrderlineHas('Configurable Chair (Red, Metal, Other: Custom Fabric)', '1.0', '11.0');
// Orderlines with the same attributes should be merged
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Configurable Chair');
ProductConfigurator.do.pickColor('Red');
ProductConfigurator.do.pickSelect('Metal');
ProductConfigurator.do.pickRadio('Other');
ProductConfigurator.do.fillCustomAttribute('Custom Fabric');
ProductConfigurator.do.confirmAttributes();
ProductScreen.check.selectedOrderlineHas('Configurable Chair (Red, Metal, Other: Custom Fabric)', '2.0', '22.0');
// Orderlines with different attributes shouldn't be merged
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Configurable Chair');
ProductConfigurator.do.pickColor('Blue');
ProductConfigurator.do.pickSelect('Metal');
ProductConfigurator.do.pickRadio('Leather');
ProductConfigurator.do.confirmAttributes();
ProductScreen.check.selectedOrderlineHas('Configurable Chair (Blue, Metal, Leather)', '1.0', '10.0');
Tour.register('ProductConfiguratorTour', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Configurable Chair');
ProductConfigurator.check.isShown();
// Option Other is active, Leather is not -> only 1 option available
ProductConfigurator.check.numberRadioOptions(1);
Tour.register('InactiveAttributeValueTour', { test: true, url: '/pos/ui' }, getSteps());
});

View file

@ -0,0 +1,258 @@
odoo.define('point_of_sale.tour.ProductScreen', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods');
const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods');
const { TextAreaPopup } = require('point_of_sale.tour.TextAreaPopupTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
var Tour = require('web_tour.tour');
// signal to start generating steps
// when finished, steps can be taken from getSteps
startSteps();
// Go by default to home category
ProductScreen.do.clickHomeCategory();
// Clicking product multiple times should increment quantity
ProductScreen.do.clickDisplayedProduct('Desk Organizer');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '1.0', '5.10');
ProductScreen.do.clickDisplayedProduct('Desk Organizer');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '2.0', '10.20');
// Clicking product should add new orderline and select the orderline
// If orderline exists, increment the quantity
ProductScreen.do.clickDisplayedProduct('Letter Tray');
ProductScreen.check.selectedOrderlineHas('Letter Tray', '1.0', '5.28');
ProductScreen.do.clickDisplayedProduct('Desk Organizer');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '3.0', '15.30');
// Check effects of clicking numpad buttons
ProductScreen.do.clickOrderline('Letter Tray', '1');
ProductScreen.check.selectedOrderlineHas('Letter Tray', '1.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Letter Tray', '0.0', '0.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '3', '15.30');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '0.0', '0.0');
ProductScreen.do.pressNumpad('1');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '1.0', '5.1');
ProductScreen.do.pressNumpad('2');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '12.0', '61.2');
ProductScreen.do.pressNumpad('3');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '123.0', '627.3');
ProductScreen.do.pressNumpad('. 5');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '123.5', '629.85');
ProductScreen.do.pressNumpad('Price');
ProductScreen.do.pressNumpad('1');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '123.5', '123.5');
ProductScreen.do.pressNumpad('1 .');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '123.5', '1,358.5');
ProductScreen.do.pressNumpad('Disc');
ProductScreen.do.pressNumpad('5 .');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '123.5', '1,290.58');
ProductScreen.do.pressNumpad('Qty');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.orderIsEmpty();
// Check different subcategories
ProductScreen.do.clickSubcategory('Desks');
ProductScreen.check.productIsDisplayed('Desk Pad');
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickSubcategory('Miscellaneous');
ProductScreen.check.productIsDisplayed('Whiteboard Pen');
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickSubcategory('Chairs');
ProductScreen.check.productIsDisplayed('Letter Tray');
ProductScreen.do.clickHomeCategory();
// Add two orderlines and update quantity
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
ProductScreen.do.clickDisplayedProduct('Wall Shelf Unit');
ProductScreen.do.clickOrderline('Whiteboard Pen', '1.0');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '1.0');
ProductScreen.do.pressNumpad('2');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '2.0');
ProductScreen.do.clickOrderline('Wall Shelf Unit', '1.0');
ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', '1.0');
ProductScreen.do.pressNumpad('2');
ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', '2.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', '0.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '2.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '0.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.orderIsEmpty();
// Add multiple orderlines then delete each of them until empty
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
ProductScreen.do.clickDisplayedProduct('Wall Shelf Unit');
ProductScreen.do.clickDisplayedProduct('Small Shelf');
ProductScreen.do.clickDisplayedProduct('Magnetic Board');
ProductScreen.do.clickDisplayedProduct('Monitor Stand');
ProductScreen.do.clickOrderline('Whiteboard Pen', '1.0');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '1.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '0.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Monitor Stand', '1.0');
ProductScreen.do.clickOrderline('Wall Shelf Unit', '1.0');
ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', '1.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', '0.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Monitor Stand', '1.0');
ProductScreen.do.clickOrderline('Small Shelf', '1.0');
ProductScreen.check.selectedOrderlineHas('Small Shelf', '1.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Small Shelf', '0.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Monitor Stand', '1.0');
ProductScreen.do.clickOrderline('Magnetic Board', '1.0');
ProductScreen.check.selectedOrderlineHas('Magnetic Board', '1.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Magnetic Board', '0.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Monitor Stand', '1.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Monitor Stand', '0.0');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.orderIsEmpty();
// Test OrderlineCustomerNoteButton
ProductScreen.do.clickDisplayedProduct('Desk Organizer');
ProductScreen.do.clickOrderlineCustomerNoteButton();
TextAreaPopup.check.isShown();
TextAreaPopup.do.inputText('Test customer note');
TextAreaPopup.do.clickConfirm();
ProductScreen.check.orderlineHasCustomerNote('Desk Organizer', '1', 'Test customer note');
Tour.register('ProductScreenTour', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Test Product');
ProductScreen.check.totalAmountIs('100.00');
ProductScreen.do.changeFiscalPosition('No Tax');
ProductScreen.check.noDiscountApplied("100.00");
ProductScreen.check.totalAmountIs('86.96');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.check.remainingIs('0.00');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
ReceiptScreen.check.noOrderlineContainsDiscount();
Tour.register('FiscalPositionNoTax', { test: true, url: '/pos/ui' }, getSteps());
});
odoo.define('point_of_sale.tour.FixedPriceNegativeQty', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods');
const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
var Tour = require('web_tour.tour');
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Zero Amount Product');
ProductScreen.check.selectedOrderlineHas('Zero Amount Product', '1.0', '1.0');
ProductScreen.do.pressNumpad('+/- 1');
ProductScreen.check.selectedOrderlineHas('Zero Amount Product', '-1.0', '-1.0');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.check.remainingIs('0.00');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.receiptIsThere();
Tour.register('FixedTaxNegativeQty', { test: true, url: '/pos/ui' }, getSteps());
});
odoo.define('point_of_sale.tour.OpenCloseCashCount', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
var Tour = require('web_tour.tour');
startSteps();
ProductScreen.do.enterOpeningAmount('90');
ProductScreen.do.confirmOpeningPopup();
ProductScreen.check.checkSecondCashClosingDetailsLineAmount('10.00', '-');
Tour.register('CashClosingDetails', { test: true, url: '/pos/ui' }, getSteps());
});
odoo.define('point_of_sale.tour.RoundGloballyTax', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
var Tour = require('web_tour.tour');
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Test Product');
ProductScreen.check.totalAmountIs('115.00');
Tour.register('RoundGloballyAmoundTour', { test: true, url: '/pos/ui' }, getSteps());
});
odoo.define('point_of_sale.tour.ShowTaxExcludedTour', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
var Tour = require('web_tour.tour');
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Test Product');
ProductScreen.check.selectedOrderlineHas('Test Product', '1.0', '100.0');
ProductScreen.check.totalAmountIs('110.0');
Tour.register('ShowTaxExcludedTour', { test: true, url: '/pos/ui' }, getSteps());
});
odoo.define('point_of_sale.tour.limitedProductPricelistLoading', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
var Tour = require('web_tour.tour');
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.scan_barcode("0100100");
ProductScreen.check.selectedOrderlineHas('Test Product 1', '1.0', '80.0');
ProductScreen.do.scan_barcode("0100200");
ProductScreen.check.selectedOrderlineHas('Test Product 2', '1.0', '100.0');
ProductScreen.do.scan_barcode("0100300");
ProductScreen.check.selectedOrderlineHas('Test Product 3', '1.0', '50.0');
Tour.register('limitedProductPricelistLoading', { test: true, url: '/pos/ui' }, getSteps());
});

View file

@ -0,0 +1,104 @@
odoo.define('point_of_sale.tour.ReceiptScreen', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods');
const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods');
const { NumberPopup } = require('point_of_sale.tour.NumberPopupTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
const Tour = require('web_tour.tour');
startSteps();
// press close button in receipt screen
ProductScreen.exec.addOrderline('Letter Tray', '10', '5');
ProductScreen.check.selectedOrderlineHas('Letter Tray', '10');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.check.validateButtonIsHighlighted(true);
PaymentScreen.do.clickValidate();
ReceiptScreen.check.receiptIsThere();
// letter tray has 10% tax (search SRC)
ReceiptScreen.check.totalAmountContains('55.0');
ReceiptScreen.do.clickNextOrder();
// send email in receipt screen
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Desk Pad', '6', '5', '30.0');
ProductScreen.exec.addOrderline('Whiteboard Pen', '6', '6', '36.0');
ProductScreen.exec.addOrderline('Monitor Stand', '6', '1', '6.0');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.pressNumpad('7 0');
PaymentScreen.check.remainingIs('2.0');
PaymentScreen.do.pressNumpad('0');
PaymentScreen.check.remainingIs('0.00');
PaymentScreen.check.changeIs('628.0');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.receiptIsThere();
ReceiptScreen.check.totalAmountContains('72.0');
ReceiptScreen.do.setEmail('test@receiptscreen.com');
ReceiptScreen.do.clickSend();
ReceiptScreen.check.emailIsSuccessful();
ReceiptScreen.do.clickNextOrder();
// order with tip
// check if tip amount is displayed
ProductScreen.exec.addOrderline('Desk Pad', '6', '5');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickTipButton();
NumberPopup.do.pressNumpad('1');
NumberPopup.check.inputShownIs('1');
NumberPopup.do.clickConfirm();
PaymentScreen.check.emptyPaymentlines('31.0');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.receiptIsThere();
ReceiptScreen.check.totalAmountContains('$ 30.00 + $ 1.00 tip');
ReceiptScreen.do.clickNextOrder();
// Test customer note in receipt
ProductScreen.exec.addOrderline('Desk Pad', '1', '5');
ProductScreen.exec.addCustomerNote('Test customer note')
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.customerNoteIsThere('Test customer note');
Tour.register('ReceiptScreenTour', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Test Product', '1');
ProductScreen.do.clickPricelistButton();
ProductScreen.do.selectPriceList('special_pricelist');
ProductScreen.check.discountOriginalPriceIs('7.0');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.discountAmountIs('0.7');
ReceiptScreen.do.clickNextOrder();
ProductScreen.exec.addOrderline('Test Product', '1');
ProductScreen.do.pressNumpad('Price');
ProductScreen.do.pressNumpad('9');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.noDiscountAmount();
Tour.register('ReceiptScreenDiscountWithPricelistTour', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Product A');
ProductScreen.do.enterLotNumber('123456789');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.trackingMethodIsLot();
Tour.register('ReceiptTrackingMethodTour', { test: true, url: '/pos/ui' }, getSteps());
});

View file

@ -0,0 +1,204 @@
odoo.define('point_of_sale.tour.TicketScreen', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods');
const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods');
const { PartnerListScreen } = require('point_of_sale.tour.PartnerListScreenTourMethods');
const { TicketScreen } = require('point_of_sale.tour.TicketScreenTourMethods');
const { ErrorPopup } = require('point_of_sale.tour.ErrorPopupTourMethods');
const { Chrome } = require('point_of_sale.tour.ChromeTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
var Tour = require('web_tour.tour');
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Desk Pad', '1', '2');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Nicole Ford');
Chrome.do.clickTicketButton();
TicketScreen.check.nthRowContains(2, 'Nicole Ford');
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline('Desk Pad', '1', '3');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Brandon Freeman');
ProductScreen.do.clickPayButton();
PaymentScreen.check.isShown();
Chrome.do.clickTicketButton();
TicketScreen.check.nthRowContains(3, 'Brandon Freeman');
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline('Desk Pad', '2', '4');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
Chrome.do.clickTicketButton();
TicketScreen.check.nthRowContains(4, 'Receipt');
TicketScreen.do.selectFilter('Receipt');
TicketScreen.check.nthRowContains(2, 'Receipt');
TicketScreen.do.selectFilter('Payment');
TicketScreen.check.nthRowContains(2, 'Payment');
TicketScreen.do.selectFilter('Ongoing');
TicketScreen.check.nthRowContains(2, 'Ongoing');
TicketScreen.do.selectFilter('All active orders');
TicketScreen.check.nthRowContains(4, 'Receipt');
TicketScreen.do.search('Customer', 'Nicole');
TicketScreen.check.nthRowContains(2, 'Nicole');
TicketScreen.do.search('Customer', 'Brandon');
TicketScreen.check.nthRowContains(2, 'Brandon');
TicketScreen.do.search('Receipt Number', '-0003');
TicketScreen.check.nthRowContains(2, 'Receipt');
// Close the TicketScreen to see the current order which is in ReceiptScreen.
// This is just to remove the search string in the search bar.
TicketScreen.do.clickDiscard();
ReceiptScreen.check.isShown();
// Open again the TicketScreen to check the Paid filter.
Chrome.do.clickTicketButton();
TicketScreen.do.selectFilter('Paid');
TicketScreen.check.nthRowContains(2, '-0003');
// Pay the order that was in PaymentScreen.
TicketScreen.do.selectFilter('Payment');
TicketScreen.do.selectOrder('-0002');
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
ReceiptScreen.do.clickNextOrder();
ProductScreen.check.isShown();
// Check that the Paid filter will show the 2 synced orders.
Chrome.do.clickTicketButton();
TicketScreen.do.selectFilter('Paid');
TicketScreen.check.nthRowContains(2, 'Brandon Freeman');
TicketScreen.check.nthRowContains(3, '-0003');
// Invoice order
TicketScreen.do.selectOrder('-0003');
TicketScreen.check.orderWidgetIsNotEmpty();
TicketScreen.do.clickControlButton('Invoice');
Chrome.do.confirmPopup();
PartnerListScreen.check.isShown();
PartnerListScreen.do.clickPartner('Colleen Diaz');
TicketScreen.check.partnerIs('Colleen Diaz');
// Reprint receipt
TicketScreen.do.clickControlButton('Print Receipt');
ReceiptScreen.check.isShown();
ReceiptScreen.do.clickBack();
// When going back, the ticket screen should be in its previous state.
TicketScreen.check.filterIs('Paid');
// Test refund //
TicketScreen.do.clickDiscard();
ProductScreen.check.isShown();
ProductScreen.check.orderIsEmpty();
ProductScreen.do.clickRefund();
// Filter should be automatically 'Paid'.
TicketScreen.check.filterIs('Paid');
TicketScreen.do.selectOrder('-0003');
TicketScreen.check.partnerIs('Colleen Diaz');
TicketScreen.do.clickOrderline('Desk Pad');
TicketScreen.do.pressNumpad('3');
// Error should show because 2 is more than the number
// that can be refunded.
ErrorPopup.do.clickConfirm();
TicketScreen.do.clickDiscard();
ProductScreen.check.isShown();
ProductScreen.check.orderIsEmpty();
ProductScreen.do.clickRefund();
TicketScreen.do.selectOrder('-0003');
TicketScreen.do.clickOrderline('Desk Pad');
TicketScreen.do.pressNumpad('1');
TicketScreen.check.toRefundTextContains('To Refund: 1.00');
TicketScreen.do.confirmRefund();
ProductScreen.check.isShown();
ProductScreen.check.selectedOrderlineHas('Desk Pad', '-1.00');
// Try changing the refund line to positive number.
// Error popup should show.
ProductScreen.do.pressNumpad('2');
ErrorPopup.do.clickConfirm();
// Change the refund line quantity to -3 -- not allowed
// so error popup.
ProductScreen.do.pressNumpad('+/- 3');
ErrorPopup.do.clickConfirm();
// Change the refund line quantity to -2 -- allowed.
ProductScreen.do.pressNumpad('+/- 2');
ProductScreen.check.selectedOrderlineHas('Desk Pad', '-2.00');
// Check if the amount being refunded changed to 2.
ProductScreen.do.clickRefund();
TicketScreen.do.selectOrder('-0003');
TicketScreen.check.toRefundTextContains('Refunding 2.00');
TicketScreen.do.clickDiscard();
// Pay the refund order.
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
ReceiptScreen.do.clickNextOrder();
// Check refunded quantity.
ProductScreen.do.clickRefund();
TicketScreen.do.selectOrder('-0003');
TicketScreen.check.refundedNoteContains('2.00 Refunded');
Tour.register('TicketScreenTour', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Product Test');
ProductScreen.check.totalAmountIs('100.00');
ProductScreen.do.changeFiscalPosition('No Tax');
ProductScreen.check.totalAmountIs('86.96');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.check.remainingIs('0.00');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
ReceiptScreen.do.clickNextOrder();
ProductScreen.do.clickRefund();
TicketScreen.do.selectOrder('-0001');
TicketScreen.do.clickOrderline('Product Test');
TicketScreen.do.pressNumpad('1');
TicketScreen.check.toRefundTextContains('To Refund: 1.00');
TicketScreen.do.confirmRefund();
ProductScreen.check.isShown();
ProductScreen.check.totalAmountIs('-86.96');
Tour.register('FiscalPositionNoTaxRefund', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Product A');
ProductScreen.do.enterLotNumber('123456789');
ProductScreen.check.selectedOrderlineHas('Product A', '1.00');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
ReceiptScreen.do.clickNextOrder();
ProductScreen.do.clickRefund();
TicketScreen.do.selectOrder('-0001');
TicketScreen.do.clickOrderline('Product A');
TicketScreen.do.pressNumpad('1');
TicketScreen.check.toRefundTextContains('To Refund: 1.00');
TicketScreen.do.confirmRefund();
ProductScreen.check.isShown();
ProductScreen.do.clickLotIcon();
ProductScreen.check.checkFirstLotNumber('123456789');
Tour.register('LotRefundTour', { test: true, url: '/pos/ui' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct("Test Product");
ProductScreen.check.checkTaxAmount("9.09");
ProductScreen.check.totalAmountIs("100.00");
ProductScreen.do.changeFiscalPosition("test fp");
ProductScreen.check.totalAmountIs("100.00");
ProductScreen.check.checkTaxAmount("4.76");
Tour.register("FiscalPositionTwoTaxIncluded", { test: true, url: "/pos/ui" }, getSteps());
});

View file

@ -0,0 +1,29 @@
odoo.define('point_of_sale.tour.ChromeTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
confirmPopup() {
return [
{
content: 'confirm popup',
trigger: '.popups .modal-dialog .button.confirm',
},
];
}
clickTicketButton() {
return [
{
trigger: '.pos-topheader .ticket-button',
},
{
trigger: '.subwindow .ticket-screen',
run: () => {},
},
];
}
}
return createTourMethods('Chrome', Do);
});

View file

@ -0,0 +1,39 @@
odoo.define('point_of_sale.tour.ErrorPopupTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
clickConfirm() {
return [
{
content: 'click confirm button',
trigger: '.popup-error .footer .cancel',
},
];
}
}
class Check {
isShown() {
return [
{
content: 'error popup is shown',
trigger: '.modal-dialog .popup-error',
run: () => {},
},
];
}
messageBodyContains(text) {
return [
{
content: `check '${text}' is in the body of the popup`,
trigger: `.modal-dialog .popup-error .body:contains(${text})`,
}
];
}
}
return createTourMethods('ErrorPopup', Do, Check);
});

View file

@ -0,0 +1,72 @@
odoo.define('point_of_sale.tour.NumberPopupTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
/**
* Note: Maximum of 2 characters because NumberBuffer only allows 2 consecutive
* fast inputs. Fast inputs is the case in tours.
*
* @param {String} keys space-separated input keys
*/
pressNumpad(keys) {
const numberChars = '0 1 2 3 4 5 6 7 8 9 C'.split(' ');
const modeButtons = '+1 +10 +2 +20 +5 +50'.split(' ');
const decimalSeparators = ', .'.split(' ');
function generateStep(key) {
let trigger;
if (numberChars.includes(key)) {
trigger = `.popup-numpad .number-char:contains("${key}")`;
} else if (modeButtons.includes(key)) {
trigger = `.popup-numpad .mode-button:contains("${key}")`;
} else if (key === 'Backspace') {
trigger = `.popup-numpad .numpad-backspace`;
} else if (decimalSeparators.includes(key)) {
trigger = `.popup-numpad .number-char.dot`;
}
return {
content: `'${key}' pressed in numpad`,
trigger,
};
}
return keys.split(' ').map(generateStep);
}
clickConfirm() {
return [
{
content: 'click confirm button',
trigger: '.popup-number .footer .confirm',
},
];
}
}
class Check {
isShown() {
return [
{
content: 'number popup is shown',
trigger: '.modal-dialog .popup-number',
run: () => {},
},
];
}
inputShownIs(val) {
return [
{
content: 'number input element check',
trigger: '.modal-dialog .popup-number .popup-input',
run: () => {},
},
{
content: `input shown is '${val}'`,
trigger: `.modal-dialog .popup-number .popup-input:contains("${val}")`,
run: () => {},
},
];
}
}
return createTourMethods('NumberPopup', Do, Check);
});

View file

@ -0,0 +1,47 @@
odoo.define('point_of_sale.tour.PartnerListScreenTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
clickPartner(name) {
return [
{
content: `click partner '${name}' from partner list screen`,
trigger: `.partnerlist-screen .partner-list-contents .partner-line td:contains("${name}")`,
},
];
}
clickPartnerDetailsButton(name) {
return [
{
content: `click partner details '${name}' from partner list screen`,
trigger: `.partner-line:contains('${name}') .edit-partner-button`,
}
]
}
clickBack() {
return [
{
trigger: ".partnerlist-screen .button.back",
},
];
}
}
class Check {
isShown() {
return [
{
content: 'partner list screen is shown',
trigger: '.pos-content .partnerlist-screen',
run: () => {},
},
];
}
}
class Execute {}
return createTourMethods('PartnerListScreen', Do, Check, Execute);
});

View file

@ -0,0 +1,251 @@
odoo.define('point_of_sale.tour.PaymentScreenTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
clickPaymentMethod(name) {
return [
{
content: `click '${name}' payment method`,
trigger: `.paymentmethods .button.paymentmethod:contains("${name}")`,
},
];
}
/**
* Delete the paymentline having the given payment method name and amount.
* @param {String} name payment method
* @param {String} amount
*/
clickPaymentlineDelButton(name, amount) {
return [
{
content: `delete ${name} paymentline with ${amount} amount`,
trigger: `.paymentlines .paymentline .payment-name:contains("${name}") ~ .delete-button`,
},
];
}
clickEmailButton() {
return [
{
content: `click email button`,
trigger: `.payment-buttons .js_email`,
},
];
}
clickInvoiceButton() {
return [{ content: 'click invoice button', trigger: '.payment-buttons .js_invoice' }];
}
clickValidate() {
return [
{
content: 'validate payment',
trigger: `.payment-screen .button.next.highlight`,
},
];
}
/**
* Press the numpad in sequence based on the given space-separated keys.
* Note: Maximum of 2 characters because NumberBuffer only allows 2 consecutive
* fast inputs. Fast inputs is the case in tours.
*
* @param {String} keys space-separated numpad keys
*/
pressNumpad(keys) {
const numberChars = '. +/- 0 1 2 3 4 5 6 7 8 9'.split(' ');
const modeButtons = '+10 +20 +50'.split(' ');
function generateStep(key) {
let trigger;
if (numberChars.includes(key)) {
trigger = `.payment-numpad .number-char:contains("${key}")`;
} else if (modeButtons.includes(key)) {
trigger = `.payment-numpad .mode-button:contains("${key}")`;
} else if (key === 'Backspace') {
trigger = `.payment-numpad .number-char img[alt="Backspace"]`;
}
return {
content: `'${key}' pressed in payment numpad`,
trigger,
};
}
return keys.split(' ').map(generateStep);
}
clickBack() {
return [
{
content: 'click back button',
trigger: '.payment-screen .button.back',
},
];
}
clickTipButton() {
return [
{
trigger: '.payment-screen .button.js_tip',
},
]
}
clickShipLaterButton() {
return [
{
content: 'click ship later button',
trigger: '.button:contains("Ship Later")',
},
]
}
}
class Check {
isShown() {
return [
{
content: 'payment screen is shown',
trigger: '.pos .payment-screen',
run: () => {},
},
];
}
/**
* Check if change is the provided amount.
* @param {String} amount
*/
changeIs(amount) {
return [
{
content: `change is ${amount}`,
trigger: `.payment-status-change .amount:contains("${amount}")`,
run: () => {},
},
];
}
/**
* Check if the remaining is the provided amount.
* @param {String} amount
*/
remainingIs(amount) {
return [
{
content: `remaining amount is ${amount}`,
trigger: `.payment-status-remaining .amount:contains("${amount}")`,
run: () => {},
},
];
}
/**
* Check if validate button is highlighted.
* @param {Boolean} isHighlighted
*/
validateButtonIsHighlighted(isHighlighted = true) {
return [
{
content: `validate button is ${
isHighlighted ? 'highlighted' : 'not highligted'
}`,
trigger: isHighlighted
? `.payment-screen .button.next.highlight`
: `.payment-screen .button.next:not(:has(.highlight))`,
run: () => {},
},
];
}
/**
* Check if the paymentlines are empty. Also provide the amount to pay.
* @param {String} amountToPay
*/
emptyPaymentlines(amountToPay) {
return [
{
content: `there are no paymentlines`,
trigger: `.paymentlines-empty`,
run: () => {},
},
{
content: `amount to pay is '${amountToPay}'`,
trigger: `.paymentlines-empty .total:contains("${amountToPay}")`,
run: () => {},
},
];
}
/**
* Check if the selected paymentline has the given payment method and amount.
* @param {String} paymentMethodName
* @param {String} amount
*/
selectedPaymentlineHas(paymentMethodName, amount) {
return [
{
content: `line paid via '${paymentMethodName}' is selected`,
trigger: `.paymentlines .paymentline.selected .payment-name:contains("${paymentMethodName}")`,
run: () => {},
},
{
content: `amount tendered in the line is '${amount}'`,
trigger: `.paymentlines .paymentline.selected .payment-amount:contains("${amount}")`,
run: () => {},
},
];
}
totalIs(amount) {
return [
{
content: `total is ${amount}`,
trigger: `.total:contains("${amount}")`,
run: () => {},
},
];
}
totalDueIs(amount) {
return [
{
content: `total due is ${amount}`,
trigger: `.payment-status-total-due:contains("${amount}")`,
run: () => {},
},
];
}
isInvoiceButtonChecked() {
return [
{
content: 'check invoice button is checked',
trigger: '.js_invoice.highlight',
run: () => {},
}
]
}
isInvoiceButtonNotChecked() {
return [
{
content: "check invoice button is checked",
trigger: ".js_invoice:not(.highlight)",
run: () => {},
},
];
}
}
class Execute {
pay(method, amount) {
const steps = [];
steps.push(...this._do.clickPaymentMethod(method));
for (let char of amount.split('')) {
steps.push(...this._do.pressNumpad(char));
}
steps.push(...this._check.validateButtonIsHighlighted());
steps.push(...this._do.clickValidate());
return steps;
}
}
return createTourMethods('PaymentScreen', Do, Check, Execute);
});

View file

@ -0,0 +1,91 @@
odoo.define('point_of_sale.tour.ProductConfiguratorTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
pickRadio(name) {
return [
{
content: `picking radio attribute with name ${name}`,
trigger: `.product-configurator-popup .attribute-name-cell label:contains('${name}')`,
},
];
}
pickSelect(name) {
return [
{
content: `picking select attribute with name ${name}`,
trigger: `.product-configurator-popup .configurator_select:has(option:contains('${name}'))`,
run: `text ${name}`,
},
];
}
pickColor(name) {
return [
{
content: `picking color attribute with name ${name}`,
trigger: `.product-configurator-popup .configurator_color[data-color='${name}']`,
},
];
}
fillCustomAttribute(value) {
return [
{
content: `filling custom attribute with value ${value}`,
trigger: `.product-configurator-popup .custom_value`,
run: `text ${value}`,
},
];
}
confirmAttributes() {
return [
{
content: `confirming product configuration`,
trigger: `.product-configurator-popup .button.confirm`,
},
];
}
cancelAttributes() {
return [
{
content: `canceling product configuration`,
trigger: `.product-configurator-popup .button.cancel`,
},
];
}
}
class Check {
isShown() {
return [
{
content: 'product configurator is shown',
trigger: '.product-configurator-popup:not(:has(.oe_hidden))',
run: () => {},
},
];
}
numberRadioOptions(number) {
return [
{
trigger: `.product-configurator-popup .attribute-name-cell`,
run: () => {
const radio_options = $('.product-configurator-popup .attribute-name-cell').length;
if (radio_options !== number) {
throw new Error(`Expected ${number} radio options, got ${radio_options}`);
}
}
},
];
}
}
return createTourMethods('ProductConfigurator', Do, Check);
});

View file

@ -0,0 +1,429 @@
odoo.define('point_of_sale.tour.ProductScreenTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
const { TextAreaPopup } = require('point_of_sale.tour.TextAreaPopupTourMethods');
class Do {
clickDisplayedProduct(name) {
return [
{
content: `click product '${name}'`,
trigger: `.product-list .product-name:contains("${name}")`,
},
];
}
clickOrderline(name, quantity) {
return [
{
content: `selecting orderline with product '${name}' and quantity '${quantity}'`,
trigger: `.order .orderline:not(:has(.selected)) .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`,
},
{
content: `orderline with product '${name}' and quantity '${quantity}' has been selected`,
trigger: `.order .orderline.selected .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`,
run: () => {},
},
];
}
clickSubcategory(name) {
return [
{
content: `selecting '${name}' subcategory`,
trigger: `.products-widget > .products-widget-control .category-simple-button:contains("${name}")`,
},
{
content: `'${name}' subcategory selected`,
trigger: `.breadcrumbs .breadcrumb-button:contains("${name}")`,
run: () => {},
},
];
}
clickHomeCategory() {
return [
{
content: `click Home subcategory`,
trigger: `.breadcrumbs .breadcrumb-home`,
},
];
}
/**
* Press the numpad in sequence based on the given space-separated keys.
* NOTE: Maximum of 2 characters because NumberBuffer only allows 2 consecutive
* fast inputs. Fast inputs is the case in tours.
*
* @param {String} keys space-separated numpad keys
*/
pressNumpad(keys) {
const numberChars = '. 0 1 2 3 4 5 6 7 8 9'.split(' ');
const modeButtons = 'Qty Price Disc'.split(' ');
function generateStep(key) {
let trigger;
if (numberChars.includes(key)) {
trigger = `.numpad .number-char:contains("${key}")`;
} else if (modeButtons.includes(key)) {
trigger = `.numpad .mode-button:contains("${key}")`;
} else if (key === 'Backspace') {
trigger = `.numpad .numpad-backspace`;
} else if (key === '+/-') {
trigger = `.numpad .numpad-minus`;
}
return {
content: `'${key}' pressed in product screen numpad`,
trigger,
};
}
return keys.split(' ').map(generateStep);
}
clickPayButton(shouldCheck = true) {
const steps = [{ content: 'click pay button', trigger: '.product-screen .actionpad .button.pay' }];
if (shouldCheck) {
steps.push({
content: 'now in payment screen',
trigger: '.pos-content .payment-screen',
run: () => {},
});
}
return steps;
}
clickPartnerButton() {
return [
{ content: 'click customer button', trigger: '.actionpad .button.set-partner' },
{
content: 'partner screen is shown',
trigger: '.pos-content .partnerlist-screen',
run: () => {},
},
];
}
clickCustomer(name) {
return [
{
content: `select customer '${name}'`,
trigger: `.partnerlist-screen .partner-line td:contains("${name}")`,
},
];
}
clickOrderlineCustomerNoteButton() {
return [
{
content: 'click customer note button',
trigger: '.control-buttons .control-button span:contains("Customer Note")',
}
]
}
clickRefund() {
return [
{
trigger: '.control-button:contains("Refund")',
},
];
}
confirmOpeningPopup() {
return [{ trigger: '.opening-cash-control .button:contains("Open session")' }];
}
clickPricelistButton() {
return [{ trigger: '.o_pricelist_button' }];
}
selectPriceList(name) {
return [
{
content: `select price list '${name}'`,
trigger: `.selection-item:contains("${name}")`,
},
];
}
enterOpeningAmount(amount) {
return [
{
content: 'enter opening amount',
trigger: '.cash-input-sub-section > .pos-input',
run: 'text ' + amount,
},
];
}
changeFiscalPosition(name) {
return [
{
content: 'click fiscal position button',
trigger: '.o_fiscal_position_button',
},
{
content: 'fiscal position screen is shown',
trigger: `.selection-item:contains("${name}")`,
},
];
}
scan_barcode(barcode) {
return [
{
content: `input barcode '${barcode}'`,
trigger: "input.ean",
run: `text ${barcode}`,
},
{
content: `button scan barcode '${barcode}'`,
trigger: "li.barcode",
run: 'click',
}
];
}
scan_ean13_barcode(barcode) {
return [
{
content: `input barcode '${barcode}'`,
trigger: "input.ean",
run: `text ${barcode}`,
},
{
content: `button scan EAN-13 barcode '${barcode}'`,
trigger: "li.custom_ean",
run: 'click',
}
];
}
clickLotIcon() {
return [
{
content: 'click lot icon',
trigger: '.line-lot-icon',
},
];
}
enterLotNumber(number) {
return [
{
content: 'enter lot number',
trigger: '.list-line-input:first()',
run: 'text ' + number,
},
{
content: 'click validate lot number',
trigger: '.popup .button.confirm',
}
];
}
}
class Check {
isShown() {
return [
{
content: 'product screen is shown',
trigger: '.product-screen',
run: () => {},
},
];
}
selectedOrderlineHas(name, quantity, price) {
const res = [
{
// check first if the order widget is there and has orderlines
content: 'order widget has orderlines',
trigger: '.order .orderlines',
run: () => {},
},
{
content: `'${name}' is selected`,
trigger: `.order .orderline.selected .product-name:contains("${name}")`,
run: function () {}, // it's a check
},
];
if (quantity) {
res.push({
content: `selected line has ${quantity} quantity`,
trigger: `.order .orderline.selected .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`,
run: function () {}, // it's a check
});
}
if (price) {
res.push({
content: `selected line has total price of ${price}`,
trigger: `.order .orderline.selected .product-name:contains("${name}") ~ .price:contains("${price}")`,
run: function () {}, // it's a check
});
}
return res;
}
orderIsEmpty() {
return [
{
content: `order is empty`,
trigger: `.order .order-empty`,
run: () => {},
},
];
}
productIsDisplayed(name) {
return [
{
content: `'${name}' should be displayed`,
trigger: `.product-list .product-name:contains("${name}")`,
run: () => {},
},
];
}
totalAmountIs(amount) {
return [
{
content: `order total amount is '${amount}'`,
trigger: `.order-container .order .summary .value:contains("${amount}")`,
run: () => {},
}
]
}
modeIsActive(mode) {
return [
{
content: `'${mode}' is active`,
trigger: `.numpad button.selected-mode:contains('${mode}')`,
run: function () {},
},
];
}
orderlineHasCustomerNote(name, quantity, note) {
return [
{
content: `line has ${quantity} quantity`,
trigger: `.order .orderline .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`,
run: function () {}, // it's a check
},
{
content: `line has '${note}' as customer note`,
trigger: `.order .orderline .info-list .orderline-note:contains("${note}")`,
run: function () {}, // it's a check
},
]
}
checkSecondCashClosingDetailsLineAmount(amount, sign) {
return [
{
content: 'Click close session button',
trigger: '.fa-sign-out',
},
{
content: 'Check closing details',
trigger: `.cash-overview tr:nth-child(2) td:contains("${amount}")`,
run: () => {}, // it's a check
},
{
content: 'Check closing details',
trigger: `.cash-overview tr:nth-child(2) .cash-sign:contains("${sign}")`,
run: () => {}, // it's a check
},
];
}
noDiscountApplied(originalPrice) {
return [
{
content: 'no discount is applied',
trigger: `.info:not(:contains(${originalPrice}))`,
},
];
}
discountOriginalPriceIs(original_price) {
return [
{
content: `discount original price is shown`,
trigger: `s:contains('${original_price}')`,
run: function () {},
},
];
}
checkFirstLotNumber(number) {
return [
{
content: 'Check lot number',
trigger: `.list-line-input:propValue('${number}')`,
run: () => {}, // it's a check
},
];
}
checkOrderlinesNumber(number) {
return [
{
content: `check orderlines number`,
trigger: `.order .orderlines .orderline`,
run: () => {
const orderline_amount = $('.order .orderlines .orderline').length;
if (orderline_amount !== number) {
throw new Error(`Expected ${number} orderlines, got ${orderline_amount}`);
}
},
},
];
}
checkTaxAmount(number) {
return [
{
content: `check order tax amount`,
trigger: `.subentry:contains("${number}")`,
},
];
}
}
class Execute {
/**
* Create an orderline for the given `productName` and `quantity`.
* - If `unitPrice` is provided, price of the product of the created line
* is changed to that value.
* - If `expectedTotal` is provided, the created orderline (which is the currently
* selected orderline) is checked if it contains the correct quantity and total
* price.
*
* @param {string} productName
* @param {string} quantity
* @param {string} unitPrice
* @param {string} expectedTotal
*/
addOrderline(productName, quantity, unitPrice = undefined, expectedTotal = undefined) {
const res = this._do.clickDisplayedProduct(productName);
if (unitPrice) {
res.push(...this._do.pressNumpad('Price'));
res.push(...this._check.modeIsActive('Price'));
res.push(...this._do.pressNumpad(unitPrice.toString().split('').join(' ')));
res.push(...this._do.pressNumpad('Qty'));
res.push(...this._check.modeIsActive('Qty'));
}
for (let char of (quantity.toString() == '1' ? '' : quantity.toString())) {
if ('.0123456789'.includes(char)) {
res.push(...this._do.pressNumpad(char));
} else if ('-'.includes(char)) {
res.push(...this._do.pressNumpad('+/-'));
}
}
if (expectedTotal) {
res.push(...this._check.selectedOrderlineHas(productName, quantity, expectedTotal));
} else {
res.push(...this._check.selectedOrderlineHas(productName, quantity));
}
return res;
}
addMultiOrderlines(...list) {
const steps = [];
for (let [product, qty, price] of list) {
steps.push(...this.addOrderline(product, qty, price));
}
return steps;
}
addCustomerNote(note) {
const res = [];
res.push(...this._do.clickOrderlineCustomerNoteButton());
res.push(...TextAreaPopup._do.inputText(note));
res.push(...TextAreaPopup._do.clickConfirm());
return res;
}
}
return createTourMethods('ProductScreen', Do, Check, Execute);
});

View file

@ -0,0 +1,127 @@
odoo.define('point_of_sale.tour.ReceiptScreenTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
clickNextOrder() {
return [
{
content: 'go to next screen',
trigger: '.receipt-screen .button.next.highlight',
},
];
}
setEmail(email) {
return [
{
trigger: '.receipt-screen .input-email input',
run: `text ${email}`,
},
];
}
clickSend(isHighlighted = true) {
return [
{
trigger: `.receipt-screen .input-email .send${isHighlighted ? '.highlight' : ''}`,
},
];
}
clickBack() {
return [
{
trigger: '.receipt-screen .button.back',
},
];
}
}
class Check {
isShown() {
return [
{
content: 'receipt screen is shown',
trigger: '.pos .receipt-screen',
run: () => {},
},
];
}
receiptIsThere() {
return [
{
content: 'there should be the receipt',
trigger: '.receipt-screen .pos-receipt',
run: () => {},
},
];
}
totalAmountContains(value) {
return [
{
trigger: `.receipt-screen .top-content h1:contains("${value}")`,
run: () => {},
},
];
}
emailIsSuccessful() {
return [
{
trigger: `.receipt-screen .notice .successful`,
run: () => {},
},
];
}
customerNoteIsThere(note) {
return [
{
trigger: `.receipt-screen .orderlines .pos-receipt-left-padding:contains("${note}")`
}
]
}
discountAmountIs(value) {
return [
{
trigger: `.pos-receipt>div:contains("Discounts")>span:contains("${value}")`,
run: () => {},
},
];
}
noDiscountAmount() {
return [
{
trigger: `.pos-receipt:not(:contains("Discounts"))`,
run: () => {},
},
];
}
noOrderlineContainsDiscount() {
return [
{
trigger: `.orderlines:not(:contains('->'))`,
run: () => { },
},
];
}
trackingMethodIsLot() {
return [
{
content: `tracking method is Lot`,
trigger: `li:contains("Lot Number")`,
run: () => {},
},
];
}
}
class Execute {
nextOrder() {
return [...this._check.isShown(), ...this._do.clickNextOrder()];
}
}
return createTourMethods('ReceiptScreen', Do, Check, Execute);
});

View file

@ -0,0 +1,39 @@
odoo.define('point_of_sale.tour.SelectionPopupTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
clickItem(name) {
return [
{
content: `click selection '${name}'`,
trigger: `.selection-item:contains("${name}")`,
},
];
}
}
class Check {
hasSelectionItem(name) {
return [
{
content: `selection popup has '${name}'`,
trigger: `.selection-item:contains("${name}")`,
run: () => {},
},
];
}
isShown() {
return [
{
content: 'selection popup is shown',
trigger: '.modal-dialog .popup-selection',
run: () => {},
},
];
}
}
return createTourMethods('SelectionPopup', Do, Check);
});

View file

@ -0,0 +1,39 @@
odoo.define('point_of_sale.tour.TextAreaPopupTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
inputText(val) {
return [
{
content: `input text '${val}'`,
trigger: `.modal-dialog .popup-textarea textarea`,
run: `text ${val}`,
},
];
}
clickConfirm() {
return [
{
content: 'confirm text input popup',
trigger: '.modal-dialog .confirm',
},
];
}
}
class Check {
isShown() {
return [
{
content: 'text input popup is shown',
trigger: '.modal-dialog .popup-textarea',
run: () => {},
},
];
}
}
return createTourMethods('TextAreaPopup', Do, Check);
});

View file

@ -0,0 +1,39 @@
odoo.define('point_of_sale.tour.TextInputPopupTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
inputText(val) {
return [
{
content: `input text '${val}'`,
trigger: `.modal-dialog .popup-textinput input`,
run: `text ${val}`,
},
];
}
clickConfirm() {
return [
{
content: 'confirm text input popup',
trigger: '.modal-dialog .confirm',
},
];
}
}
class Check {
isShown() {
return [
{
content: 'text input popup is shown',
trigger: '.modal-dialog .popup-textinput',
run: () => {},
},
];
}
}
return createTourMethods('TextInputPopup', Do, Check);
});

View file

@ -0,0 +1,195 @@
odoo.define('point_of_sale.tour.TicketScreenTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
clickNewTicket() {
return [{ trigger: '.ticket-screen .highlight' }];
}
clickDiscard() {
return [{ trigger: '.ticket-screen button.discard' }];
}
selectOrder(orderName) {
return [
{
trigger: `.ticket-screen .order-row > .col:nth-child(2):contains("${orderName}")`,
},
];
}
deleteOrder(orderName) {
return [
{
trigger: `.ticket-screen .orders > .order-row > .col:contains("${orderName}") ~ .col[name="delete"]`,
},
];
}
selectFilter(name) {
return [
{
trigger: `.pos-search-bar .filter`,
},
{
trigger: `.pos-search-bar .filter ul`,
run: () => {},
},
{
trigger: `.pos-search-bar .filter ul li:contains("${name}")`,
},
];
}
search(field, searchWord) {
return [
{
trigger: '.pos-search-bar input',
run: `text ${searchWord}`,
},
{
/**
* Manually trigger keyup event to show the search field list
* because the previous step do not trigger keyup event.
*/
trigger: '.pos-search-bar input',
run: function () {
document
.querySelector('.pos-search-bar input')
.dispatchEvent(new KeyboardEvent('keyup', { key: '' }));
},
},
{
trigger: `.pos-search-bar .search ul li:contains("${field}")`,
},
];
}
settleTips() {
return [
{
trigger: '.ticket-screen .buttons .settle-tips',
},
];
}
clickControlButton(name) {
return [
{
trigger: `.ticket-screen .control-button:contains("${name}")`,
},
];
}
clickOrderline(name) {
return [
{
trigger: `.ticket-screen .orderline:not(:has(.selected)) .product-name:contains("${name}")`,
},
{
trigger: `.ticket-screen .orderline.selected .product-name:contains("${name}")`,
run: () => {},
},
];
}
pressNumpad(key) {
let trigger;
if ('.0123456789'.includes(key)) {
trigger = `.numpad .number-char:contains("${key}")`;
} else if (key === 'Backspace') {
trigger = `.numpad .numpad-backspace`;
} else if (key === '+/-') {
trigger = `.numpad .numpad-minus`;
}
return [
{
trigger,
},
];
}
confirmRefund() {
return [
{
trigger: '.ticket-screen .button.pay',
},
];
}
}
class Check {
checkStatus(orderName, status) {
return [
{
trigger: `.ticket-screen .order-row > .col:nth-child(2):contains("${orderName}") ~ .col:nth-child(6):contains(${status})`,
run: () => {},
},
];
}
/**
* Check if the nth row contains the given string.
* Note that 1st row is the header-row.
*/
nthRowContains(n, string) {
return [
{
trigger: `.ticket-screen .orders > .order-row:nth-child(${n}):contains("${string}")`,
run: () => {},
},
];
}
noNewTicketButton() {
return [
{
trigger: '.ticket-screen .controls .buttons:nth-child(1):has(.discard)',
run: () => {},
},
];
}
orderWidgetIsNotEmpty() {
return [
{
trigger: '.ticket-screen:not(:has(.order-empty))',
run: () => {},
},
];
}
filterIs(name) {
return [
{
trigger: `.ticket-screen .pos-search-bar .filter span:contains("${name}")`,
run: () => {},
},
];
}
partnerIs(name) {
return [
{
trigger: `.ticket-screen .set-partner:contains("${name}")`,
run: () => {},
},
];
}
toRefundTextContains(text) {
return [
{
trigger: `.ticket-screen .to-refund-highlight:contains("${text}")`,
run: () => {},
},
];
}
refundedNoteContains(text) {
return [
{
trigger: `.ticket-screen .refund-note:contains("${text}")`,
run: () => {},
},
];
}
tipContains(amount) {
return [
{
trigger: `.ticket-screen .tip-cell:contains("${amount}")`,
run: () => {},
},
];
}
}
class Execute {}
return createTourMethods('TicketScreen', Do, Check, Execute);
});

View file

@ -0,0 +1,153 @@
odoo.define('point_of_sale.tour.utils', function (require) {
'use strict';
const config = require('web.config');
/**
* USAGE
* -----
*
* ```
* const { startSteps, getSteps, createTourMethods } = require('point_of_sale.utils');
* const { Other } = require('point_of_sale.tour.OtherMethods');
*
* // 1. Define classes Do, Check and Execute having methods that
* // each return array of tour steps.
* class Do {
* click() {
* return [{ content: 'click button', trigger: '.button' }];
* }
* }
* class Check {
* isHighligted() {
* return [{ content: 'button is highlighted', trigger: '.button.highlight', run: () => {} }];
* }
* }
* // Notice that Execute has access to methods defined in Do and Check classes
* // Also, we can compose steps from other module.
* class Execute {
* complexSteps() {
* return [...this._do.click(), ...this._check.isHighlighted(), ...Other._exec.complicatedSteps()];
* }
* }
*
* // 2. Instantiate these class definitions using `createTourMethods`.
* // The returned object gives access to the defined methods above
* // thru the do, check and exec properties.
* // - do gives access to the methods defined in Do class
* // - check gives access to the methods defined in Check class
* // - exec gives access to the methods defined in Execute class
* const Screen = createTourMethods('Screen', Do, Check, Execute);
*
* // 3. Call `startSteps` to start empty steps.
* startSteps();
*
* // 4. Call the tour methods to populate the steps created by `startSteps`.
* Screen.do.click(); // return of this method call is added to steps created by startSteps
* Screen.check.isHighlighted() // same as above
* Screen.exec.complexSteps() // same as above
*
* // 5. Call `getSteps` which returns the generated tour steps.
* const steps = getSteps();
* ```
*/
let steps = [];
function startSteps() {
// always start by waiting for loading to finish
steps = [
{
content: 'wait for loading to finish',
trigger: 'body:not(:has(.loader))',
run: function () {},
},
];
}
function getSteps() {
return steps;
}
// this is the method decorator
// when the method is called, the generated steps are added
// to steps
const methodProxyHandler = {
apply(target, thisArg, args) {
const res = target.call(thisArg, ...args);
if (config.isDebug()) {
// This step is added before the real steps.
// Very useful when debugging because we know which
// method call failed and what were the parameters.
const constructor = thisArg.constructor.name.split(' ')[1];
const methodName = target.name.split(' ')[1];
const argList = args
.map((a) => (typeof a === 'string' ? `'${a}'` : `${a}`))
.join(', ');
steps.push({
content: `DOING "${constructor}.${methodName}(${argList})"`,
trigger: '.pos',
run: () => {},
});
}
steps.push(...res);
return res;
},
};
// we proxy get of the method to decorate the method call
const proxyHandler = {
get(target, key) {
const method = target[key];
if (!method) {
throw new Error(`Tour method '${key}' is not available.`);
}
return new Proxy(method.bind(target), methodProxyHandler);
},
};
/**
* Creates an object with `do`, `check` and `exec` properties which are instances of
* the given `Do`, `Check` and `Execute` classes, respectively. Calling methods
* automatically adds the returned steps to the steps created by `startSteps`.
*
* There are however underscored version (_do, _check, _exec).
* Calling methods thru the underscored version does not automatically
* add the returned steps to the current steps array. Useful when composing
* steps from other methods.
*
* @param {String} name
* @param {Function} Do class containing methods which return array of tour steps
* @param {Function} Check similar to Do class but the steps are mainly for checking
* @param {Function} Execute class containing methods which return array of tour steps
* but has access to methods of Do and Check classes via .do and .check,
* respectively. Here, we define methods that return tour steps based
* on the combination of steps from Do and Check.
*/
function createTourMethods(name, Do, Check = class {}, Execute = class {}) {
Object.defineProperty(Do, 'name', { value: `${name}.do` });
Object.defineProperty(Check, 'name', { value: `${name}.check` });
Object.defineProperty(Execute, 'name', {
value: `${name}.exec`,
});
const methods = { do: new Do(), check: new Check(), exec: new Execute() };
// Allow Execute to have access to methods defined in Do and Check
// via do and exec, respectively.
methods.exec._do = methods.do;
methods.exec._check = methods.check;
return {
Do,
Check,
Execute,
[name]: {
do: new Proxy(methods.do, proxyHandler),
check: new Proxy(methods.check, proxyHandler),
exec: new Proxy(methods.exec, proxyHandler),
_do: methods.do,
_check: methods.check,
_exec: methods.exec,
},
};
}
return { startSteps, getSteps, createTourMethods };
});

View file

@ -0,0 +1,408 @@
/* global posmodel */
odoo.define('point_of_sale.tour.pricelist', function (require) {
"use strict";
var Tour = require('web_tour.tour');
var utils = require('web.utils');
var round_di = utils.round_decimals;
function assert (condition, message) {
if (! condition) {
throw message || "Assertion failed";
}
}
function assertProductPrice(product, pricelist_name, quantity, expected_price) {
return function () {
var pricelist = _.findWhere(posmodel.pricelists, {name: pricelist_name});
var frontend_price = product.get_price(pricelist, quantity);
frontend_price = round_di(frontend_price, posmodel.dp['Product Price']);
var diff = Math.abs( expected_price - frontend_price );
assert(diff < 0.001,
JSON.stringify({
product: product.id,
product_display_name: product.display_name,
pricelist_name: pricelist_name,
quantity: quantity
}) + ' DOESN\'T MATCH -> ' + expected_price + ' != ' + frontend_price);
return Promise.resolve();
};
}
// The global posmodel is only present when the posmodel is instanciated
// So, wait for everythiong to be loaded
var steps = [{ // Leave category displayed by default
content: 'waiting for loading to finish',
extra_trigger: 'body .pos:not(:has(.loader))', // Pos has finished loading
trigger: 'body:not(:has(.o_loading_indicator))', // WebClient has finished Loading
run: function () {
var product_wall_shelf = posmodel.db.search_product_in_category(0, 'Wall Shelf Unit')[0];
var product_small_shelf = posmodel.db.search_product_in_category(0, 'Small Shelf')[0];
var product_magnetic_board = posmodel.db.search_product_in_category(0, 'Magnetic Board')[0];
var product_monitor_stand = posmodel.db.search_product_in_category(0, 'Monitor Stand')[0];
var product_desk_pad = posmodel.db.search_product_in_category(0, 'Desk Pad')[0];
var product_letter_tray = posmodel.db.search_product_in_category(0, 'Letter Tray')[0];
var product_whiteboard = posmodel.db.search_product_in_category(0, 'Whiteboard')[0];
assertProductPrice(product_letter_tray, 'Public Pricelist', 0, 4.8)()
.then(assertProductPrice(product_letter_tray, 'Public Pricelist', 1, 4.8))
.then(assertProductPrice(product_letter_tray, 'Fixed', 1, 1))
.then(assertProductPrice(product_wall_shelf, 'Fixed', 1, 2))
.then(assertProductPrice(product_small_shelf, 'Fixed', 1, 13.95))
.then(assertProductPrice(product_wall_shelf, 'Percentage', 1, 0))
.then(assertProductPrice(product_small_shelf, 'Percentage', 1, 0.03))
.then(assertProductPrice(product_magnetic_board, 'Percentage', 1, 1.98))
.then(assertProductPrice(product_wall_shelf, 'Formula', 1, 6.86))
.then(assertProductPrice(product_small_shelf, 'Formula', 1, 2.99))
.then(assertProductPrice(product_magnetic_board, 'Formula', 1, 11.98))
.then(assertProductPrice(product_monitor_stand, 'Formula', 1, 8.19))
.then(assertProductPrice(product_desk_pad, 'Formula', 1, 6.98))
.then(assertProductPrice(product_wall_shelf, 'min_quantity ordering', 1, 2))
.then(assertProductPrice(product_wall_shelf, 'min_quantity ordering', 2, 1))
.then(assertProductPrice(product_letter_tray, 'Category vs no category', 1, 2))
.then(assertProductPrice(product_letter_tray, 'Category', 1, 2))
.then(assertProductPrice(product_wall_shelf, 'Product template', 1, 1))
.then(assertProductPrice(product_wall_shelf, 'Dates', 1, 2))
.then(assertProductPrice(product_small_shelf, 'Pricelist base rounding', 1, 13.95))
.then(assertProductPrice(product_whiteboard, 'Public Pricelist', 1, 3.2))
.then(function () {
$('.pos').addClass('done-testing');
});
},
}, {
trigger: '.opening-cash-control .button:contains("Open session")',
}];
steps = steps.concat([{
content: "wait for unit tests to finish",
trigger: ".pos.done-testing",
run: function () {}, // it's a check
}, {
content: "click category switch",
trigger: ".breadcrumb-home",
run: 'click',
}, {
content: "click pricelist button",
trigger: ".control-button.o_pricelist_button",
}, {
content: "verify default pricelist is set",
trigger: ".selection-item.selected:contains('Public Pricelist')",
run: function () {}, // it's a check
}, {
content: "select fixed pricelist",
trigger: ".selection-item:contains('Fixed')",
}, {
content: "open partner list",
trigger: "button.set-partner",
}, {
content: "select Deco Addict",
trigger: ".partner-line:contains('Deco Addict')",
}, {
content: "click pricelist button",
trigger: ".control-button.o_pricelist_button",
}, {
content: "verify pricelist changed",
trigger: ".selection-item.selected:contains('Public Pricelist')",
run: function () {}, // it's a check
}, {
content: "cancel pricelist dialog",
trigger: ".button.cancel:visible",
}, {
content: "open customer list",
trigger: "button.set-partner",
}, {
content: "select Lumber Inc",
trigger: ".partner-line:contains('Lumber Inc')",
}, {
content: "click pricelist button",
trigger: ".control-button.o_pricelist_button",
}, {
content: "verify pricelist remained public pricelist ('Not loaded' is not available)",
trigger: ".selection-item.selected:contains('Public Pricelist')",
run: function () {}, // it's a check
}, {
content: "cancel pricelist dialog",
trigger: ".button.cancel:visible",
}, {
content: "click pricelist button",
trigger: ".control-button.o_pricelist_button",
}, {
content: "select fixed pricelist",
trigger: ".selection-item:contains('min_quantity ordering')",
}, {
content: "order 1 kg shelf",
trigger: ".product:contains('Wall Shelf')",
}, {
content: "change qty to 2 kg",
trigger: ".numpad button.input-button:visible:contains('2')",
}, {
content: "qty of Wall Shelf line should be 2",
trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Wall Shelf')",
extra_trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Wall Shelf') ~ .info-list .info em:contains('2.0')",
run: function() {},
}, {
content: "verify that unit price of shelf changed to $1",
trigger: ".total > .value:contains('$ 2.00')",
run: function() {},
}, {
content: "order different shelf",
trigger: ".product:contains('Small Shelf')",
}, {
content: "Small Shelf line should be selected with quantity 1",
trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf')",
extra_trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf') ~ .info-list .info em:contains('1.0')",
run: function() {}
}, {
content: "change to price mode",
trigger: ".numpad button:contains('Price')",
}, {
content: "make sure price mode is activated",
trigger: ".numpad button.selected-mode:contains('Price')",
run: function() {},
}, {
content: "manually override the unit price of these shelf to $5",
trigger: ".numpad button.input-button:visible:contains('5')",
}, {
content: "Small Shelf line should be selected with unit price of 5",
trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf')",
extra_trigger: ".order-container .orderlines .orderline.selected .product-name:contains('Small Shelf') ~ .price:contains('5.0')",
}, {
content: "change back to qty mode",
trigger: ".numpad button:contains('Qty')",
}, {
content: "make sure qty mode is activated",
trigger: ".numpad button.selected-mode:contains('Qty')",
run: function() {},
}, {
content: "click pricelist button",
trigger: ".control-button.o_pricelist_button",
}, {
content: "select public pricelist",
trigger: ".selection-item:contains('Public Pricelist')",
}, {
content: "verify that the boni shelf have been recomputed and the shelf have not (their price was manually overridden)",
trigger: ".total > .value:contains('$ 8.96')",
}, {
content: "click pricelist button",
trigger: ".control-button.o_pricelist_button",
}, {
content: "select fixed pricelist",
trigger: ".selection-item:contains('min_quantity ordering')",
}, {
content: "close the Point of Sale frontend",
trigger: ".header-button",
}, {
content: "confirm closing the frontend",
trigger: ".header-button",
run: function() {}, //it's a check,
}]);
Tour.register('pos_pricelist', { test: true, url: '/pos/ui' }, steps);
});
odoo.define('point_of_sale.tour.acceptance', function (require) {
"use strict";
var Tour = require("web_tour.tour");
function add_product_to_order(product_name) {
return [{
content: 'buy ' + product_name,
trigger: '.product-list .product-name:contains("' + product_name + '")',
}, {
content: 'the ' + product_name + ' have been added to the order',
trigger: '.order .product-name:contains("' + product_name + '")',
run: function () {},
}];
}
function set_fiscal_position_on_order(fp_name) {
return [{
content: 'set fiscal position',
trigger: '.control-button.o_fiscal_position_button',
}, {
content: 'choose fiscal position ' + fp_name + ' to add to the order',
trigger: '.popups .popup .selection .selection-item:contains("' + fp_name + '")',
}, {
content: 'the fiscal position ' + fp_name + ' has been set to the order',
trigger: '.control-button.o_fiscal_position_button:contains("' + fp_name + '")',
run: function () {},
}];
}
function press_payment_numpad(val) {
return [{
content: `press ${val} on payment screen numpad`,
trigger: `.payment-numpad .input-button:contains("${val}"):visible`,
}]
}
function press_product_numpad(val) {
return [{
content: `press ${val} on product screen numpad`,
trigger: `.numpad .input-button:contains("${val}"):visible`,
}]
}
function selected_payment_has(name, val) {
return [{
content: `selected payment is ${name} and has ${val}`,
trigger: `.paymentlines .paymentline.selected .payment-name:contains("${name}")`,
extra_trigger: `.paymentlines .paymentline.selected .payment-name:contains("${name}") ~ .payment-amount:contains("${val}")`,
run: function () {},
}]
}
function selected_orderline_has({ product, price = null, quantity = null }) {
const result = [];
if (price !== null) {
result.push({
content: `Selected line has product '${product}' and price '${price}'`,
trigger: `.order-container .orderlines .orderline.selected .product-name:contains("${product}") ~ span.price:contains("${price}")`,
run: function () {},
});
}
if (quantity !== null) {
result.push({
content: `Selected line has product '${product}' and quantity '${quantity}'`,
trigger: `.order-container .orderlines .orderline.selected .product-name:contains('${product}') ~ .info-list .info em:contains('${quantity}')`,
run: function () {},
});
}
return result;
}
function verify_order_total(total_str) {
return [{
content: 'order total contains ' + total_str,
trigger: '.order .total .value:contains("' + total_str + '")',
run: function () {}, // it's a check
}];
}
function goto_payment_screen_and_select_payment_method() {
return [{
content: "go to payment screen",
trigger: '.button.pay',
}, {
content: "pay with cash",
trigger: '.paymentmethod:contains("Cash")',
}];
}
function finish_order() {
return [{
content: "validate the order",
trigger: '.payment-screen .button.next.highlight:visible',
}, {
content: "verify that the order has been successfully sent to the backend",
trigger: ".js_connected:visible",
run: function () {},
}, {
content: "click Next Order",
trigger: '.receipt-screen .button.next.highlight:visible',
}, {
content: "check if we left the receipt screen",
trigger: '.pos-content .screen:not(:has(.receipt-screen))',
run: function () {},
}];
}
var steps = [{
content: 'waiting for loading to finish',
trigger: 'body:not(:has(.loader))',
run: function () {},
}, { // Leave category displayed by default
content: "click category switch",
trigger: ".breadcrumb-home",
}];
steps = steps.concat(add_product_to_order('Desk Organizer'));
steps = steps.concat(verify_order_total('5.10'));
steps = steps.concat(add_product_to_order('Desk Organizer'));
steps = steps.concat(verify_order_total('10.20'));
steps = steps.concat(goto_payment_screen_and_select_payment_method());
/* add payment line of only 5.20
status:
order-total := 10.20
total-payment := 11.70
expect:
remaining := 0.00
change := 1.50
*/
steps = steps.concat(press_payment_numpad('5'));
steps = steps.concat(selected_payment_has('Cash', '5.0'));
steps = steps.concat([{
content: "verify remaining",
trigger: '.payment-status-remaining .amount:contains("5.20")',
run: function () {},
}, {
content: "verify change",
trigger: '.payment-status-change .amount:contains("0.00")',
run: function () {},
}]);
/* make additional payment line of 6.50
status:
order-total := 10.20
total-payment := 11.70
expect:
remaining := 0.00
change := 1.50
*/
steps = steps.concat([{
content: "pay with cash",
trigger: '.paymentmethod:contains("Cash")',
}]);
steps = steps.concat(selected_payment_has('Cash', '5.2'));
steps = steps.concat(press_payment_numpad('6'))
steps = steps.concat(selected_payment_has('Cash', '6.0'));
steps = steps.concat([{
content: "verify remaining",
trigger: '.payment-status-remaining .amount:contains("0.00")',
run: function () {},
}, {
content: "verify change",
trigger: '.payment-status-change .amount:contains("0.80")',
run: function () {},
}]);
steps = steps.concat(finish_order());
// test opw-672118 orderline subtotal rounding
steps = steps.concat(add_product_to_order('Desk Organizer'));
steps = steps.concat(selected_orderline_has({product: 'Desk Organizer', quantity: '1.0'}));
steps = steps.concat(press_product_numpad('.'))
steps = steps.concat(selected_orderline_has({product: 'Desk Organizer', quantity: '0.0', price: '0.0'}));
steps = steps.concat(press_product_numpad('9'))
steps = steps.concat(selected_orderline_has({product: 'Desk Organizer', quantity: '0.9', price: '4.59'}));
steps = steps.concat(press_product_numpad('9'))
steps = steps.concat(selected_orderline_has({product: 'Desk Organizer', quantity: '0.99', price: '5.05'}));
steps = steps.concat(goto_payment_screen_and_select_payment_method());
steps = steps.concat(selected_payment_has('Cash', '5.05'));
steps = steps.concat(finish_order());
// Test fiscal position one2many map (align with backend)
steps = steps.concat(add_product_to_order('Letter Tray'));
steps = steps.concat(selected_orderline_has({product: 'Letter Tray', quantity: '1.0'}));
steps = steps.concat(verify_order_total('5.28'));
steps = steps.concat(set_fiscal_position_on_order('FP-POS-2M'));
steps = steps.concat(verify_order_total('5.52'));
steps = steps.concat([{
content: "open closing the Point of Sale frontend popup",
trigger: ".header-button",
}, {
content: "close the Point of Sale frontend",
trigger: ".close-pos-popup .button:contains('Discard')",
run: function() {}, //it's a check,
}]);
Tour.register('pos_basic_order', { test: true, url: '/pos/ui' }, steps);
});

View file

@ -0,0 +1,34 @@
odoo.define('point_of_sale.test_env', async function (require) {
'use strict';
/**
* Many components in PoS are dependent on the PosGlobalState instance (pos).
* Therefore, for unit tests that require pos in the Components' env, we
* prepared here a test env maker (makePosTestEnv) based on
* makeTestEnvironment of web.
*/
const makeTestEnvironment = require('web.test_env');
const env = require('point_of_sale.env');
const { PosGlobalState } = require('point_of_sale.models');
const cleanup = require("@web/../tests/helpers/cleanup");
// We override this method in the pos unit tests to prevent the unnecessary error in the web tests.
cleanup.registerCleanup = () => {}
await env.session.is_bound;
const pos = PosGlobalState.create({ env });
await pos.load_server_data();
/**
* @param {Object} env default env
* @param {Function} providedRPC mock rpc
* @param {Function} providedDoAction mock do_action
*/
function makePosTestEnv(env = {}, providedRPC = null, providedDoAction = null) {
env = Object.assign(env, { pos });
return makeTestEnvironment(env, providedRPC);
}
return makePosTestEnv;
});

View file

@ -0,0 +1,414 @@
odoo.define('point_of_sale.tests.ComponentRegistry', function(require) {
'use strict';
const Registries = require('point_of_sale.Registries');
QUnit.module('unit tests for ComponentRegistry', {
before() {},
});
QUnit.test('basic extend', async function(assert) {
assert.expect(5);
class A {
constructor() {
assert.step('A');
}
}
Registries.Component.add(A);
let A1 = x =>
class extends x {
constructor() {
super();
assert.step('A1');
}
};
Registries.Component.extend(A, A1);
Registries.Component.freeze();
const RegA = Registries.Component.get(A);
let a = new RegA();
assert.verifySteps(['A', 'A1']);
assert.ok(a instanceof RegA);
assert.ok(RegA.name === 'A');
});
QUnit.test('addByExtending', async function(assert) {
assert.expect(8);
class A {
constructor() {
assert.step('A');
}
}
Registries.Component.add(A);
let B = x =>
class extends x {
constructor() {
super();
assert.step('B');
}
};
Registries.Component.addByExtending(B, A);
let A1 = x =>
class extends x {
constructor() {
super();
assert.step('A1');
}
};
Registries.Component.extend(A, A1);
let A2 = x =>
class extends x {
constructor() {
super();
assert.step('A2');
}
};
Registries.Component.extend(A, A2);
Registries.Component.freeze();
const RegA = Registries.Component.get(A);
const RegB = Registries.Component.get(B);
let b = new RegB();
assert.verifySteps(['A', 'A1', 'A2', 'B']);
assert.ok(b instanceof RegA);
assert.ok(b instanceof RegB);
assert.ok(RegB.name === 'B');
});
QUnit.test('extend the one that is added by extending', async function(assert) {
assert.expect(6);
class A {
constructor() {
assert.step('A');
}
}
Registries.Component.add(A);
let B = x =>
class extends x {
constructor() {
super();
assert.step('B');
}
};
Registries.Component.addByExtending(B, A);
let B1 = x =>
class extends x {
constructor() {
super();
assert.step('B1');
}
};
Registries.Component.extend(B, B1);
let B2 = x =>
class extends x {
constructor() {
super();
assert.step('B2');
}
};
Registries.Component.extend(B, B2);
let A1 = x =>
class extends x {
constructor() {
super();
assert.step('A1');
}
};
Registries.Component.extend(A, A1);
Registries.Component.freeze();
const RegB = Registries.Component.get(B);
new RegB();
assert.verifySteps(['A', 'A1', 'B', 'B1', 'B2']);
});
QUnit.test('addByExtending based on added by extending', async function(assert) {
assert.expect(10);
class A {
constructor() {
assert.step('A');
}
}
Registries.Component.add(A);
let B = x =>
class extends x {
constructor() {
super();
assert.step('B');
}
};
Registries.Component.addByExtending(B, A);
let A1 = x =>
class extends x {
constructor() {
super();
assert.step('A1');
}
};
Registries.Component.extend(A, A1);
let C = x =>
class extends x {
constructor() {
super();
assert.step('C');
}
};
Registries.Component.addByExtending(C, B);
let B7 = x =>
class extends x {
constructor() {
super();
assert.step('B7');
}
};
Registries.Component.extend(B, B7);
Registries.Component.freeze();
const RegA = Registries.Component.get(A);
const RegB = Registries.Component.get(B);
const RegC = Registries.Component.get(C);
let c = new RegC();
assert.verifySteps(['A', 'A1', 'B', 'B7', 'C']);
assert.ok(c instanceof RegA);
assert.ok(c instanceof RegB);
assert.ok(c instanceof RegC);
assert.ok(RegC.name === 'C');
});
QUnit.test('deeper inheritance', async function(assert) {
assert.expect(9);
class A {
constructor() {
assert.step('A');
}
}
Registries.Component.add(A);
let B = x =>
class extends x {
constructor() {
super();
assert.step('B');
}
};
Registries.Component.addByExtending(B, A);
let A1 = x =>
class extends x {
constructor() {
super();
assert.step('A1');
}
};
Registries.Component.extend(A, A1);
let C = x =>
class extends x {
constructor() {
super();
assert.step('C');
}
};
Registries.Component.addByExtending(C, B);
let B2 = x =>
class extends x {
constructor() {
super();
assert.step('B2');
}
};
Registries.Component.extend(B, B2);
let B3 = x =>
class extends x {
constructor() {
super();
assert.step('B3');
}
};
Registries.Component.extend(B, B3);
let A9 = x =>
class extends x {
constructor() {
super();
assert.step('A9');
}
};
Registries.Component.extend(A, A9);
let E = x =>
class extends x {
constructor() {
super();
assert.step('E');
}
};
Registries.Component.addByExtending(E, C);
Registries.Component.freeze();
// |A| => A9 -> A1 -> A
// |B| => B3 -> B2 -> B -> |A|
// |C| => C -> |B|
// |E| => E -> |C|
new (Registries.Component.get(E))();
assert.verifySteps(['A', 'A1', 'A9', 'B', 'B2', 'B3', 'C', 'E']);
});
QUnit.test('mixins?', async function(assert) {
assert.expect(12);
class A {
constructor() {
assert.step('A');
}
}
Registries.Component.add(A);
let Mixin = x =>
class extends x {
constructor() {
super();
assert.step('Mixin');
}
mixinMethod() {
return 'mixinMethod';
}
get mixinGetter() {
return 'mixinGetter';
}
};
// use the mixin when declaring B.
let B = x =>
class extends Mixin(x) {
constructor() {
super();
assert.step('B');
}
};
Registries.Component.addByExtending(B, A);
let A1 = x =>
class extends x {
constructor() {
super();
assert.step('A1');
}
};
Registries.Component.extend(A, A1);
Registries.Component.freeze();
B = Registries.Component.get(B);
const b = new B();
assert.verifySteps(['A', 'A1', 'Mixin', 'B']);
// instance of B should have the mixin properties
assert.strictEqual(b.mixinMethod(), 'mixinMethod');
assert.strictEqual(b.mixinGetter, 'mixinGetter');
// instance of A should not have the mixin properties
A = Registries.Component.get(A);
const a = new A();
assert.verifySteps(['A', 'A1']);
assert.notOk(a.mixinMethod);
assert.notOk(a.mixinGetter);
});
QUnit.test('extending methods', async function(assert) {
assert.expect(16);
class A {
foo() {
assert.step('A foo');
}
}
Registries.Component.add(A);
let B = x =>
class extends x {
bar() {
assert.step('B bar');
}
};
Registries.Component.addByExtending(B, A);
let A1 = x =>
class extends x {
bar() {
assert.step('A1 bar');
// should only be for A.
}
};
Registries.Component.extend(A, A1);
let B1 = x =>
class extends x {
foo() {
super.foo();
assert.step('B1 foo');
}
};
Registries.Component.extend(B, B1);
let C = x =>
class extends x {
foo() {
super.foo();
assert.step('C foo');
}
bar() {
super.bar();
assert.step('C bar');
}
};
Registries.Component.addByExtending(C, B);
Registries.Component.freeze();
A = Registries.Component.get(A);
B = Registries.Component.get(B);
C = Registries.Component.get(C);
const a = new A();
const b = new B();
const c = new C();
a.foo();
assert.verifySteps(['A foo']);
b.foo();
assert.verifySteps(['A foo', 'B1 foo']);
c.foo();
assert.verifySteps(['A foo', 'B1 foo', 'C foo']);
a.bar();
assert.verifySteps(['A1 bar']);
b.bar();
assert.verifySteps(['B bar']);
c.bar();
assert.verifySteps(['B bar', 'C bar']);
});
});

View file

@ -0,0 +1,69 @@
odoo.define('point_of_sale.tests.NumberBuffer', function(require) {
'use strict';
const NumberBuffer = require('point_of_sale.NumberBuffer');
const makeTestEnvironment = require('web.test_env');
const testUtils = require('web.test_utils');
const { mount } = require('@web/../tests/helpers/utils');
const { LegacyComponent } = require("@web/legacy/legacy_component");
const { useState, xml } = owl;
QUnit.module('unit tests for NumberBuffer', {
before() {},
});
QUnit.test('simple fast inputs with capture in between', async function(assert) {
assert.expect(3);
const target = testUtils.prepareTarget();
const env = makeTestEnvironment();
class Root extends LegacyComponent {
setup() {
this.state = useState({ buffer: '' });
NumberBuffer.activate();
NumberBuffer.use({
nonKeyboardInputEvent: 'numpad-click-input',
state: this.state,
});
}
resetBuffer() {
NumberBuffer.capture();
NumberBuffer.reset();
}
onClickOne() {
this.trigger('numpad-click-input', { key: '1' });
}
onClickTwo() {
this.trigger('numpad-click-input', { key: '2' });
}
}
Root.template = xml/* html */ `
<div>
<p><t t-esc="state.buffer" /></p>
<button class="one" t-on-click="onClickOne">1</button>
<button class="two" t-on-click="onClickTwo">2</button>
<button class="reset" t-on-click="resetBuffer">reset</button>
</div>
`;
await mount(Root, target, { env });
const oneButton = target.querySelector('button.one');
const twoButton = target.querySelector('button.two');
const resetButton = target.querySelector('button.reset');
const bufferEl = target.querySelector('p');
testUtils.dom.click(oneButton);
testUtils.dom.click(twoButton);
await testUtils.nextTick();
assert.strictEqual(bufferEl.textContent, '12');
testUtils.dom.click(resetButton);
await testUtils.nextTick();
assert.strictEqual(bufferEl.textContent, '');
testUtils.dom.click(twoButton);
testUtils.dom.click(oneButton);
await testUtils.nextTick();
assert.strictEqual(bufferEl.textContent, '21');
});
});

View file

@ -0,0 +1,165 @@
odoo.define('point_of_sale.tests.PosPopupController', function(require) {
'use strict';
const PosPopupController = require('point_of_sale.PosPopupController');
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
const PosComponent = require('point_of_sale.PosComponent');
const makeTestEnvironment = require('web.test_env');
const testUtils = require('web.test_utils');
const Registries = require('point_of_sale.Registries');
const { mount } = require('@web/../tests/helpers/utils');
const { EventBus, useSubEnv, xml } = owl;
QUnit.module('unit tests for PosPopupController', {
before() {
Registries.Component.freeze();
// Note that we are creating new popups here to decouple this test from the pos app.
class CustomPopup1 extends AbstractAwaitablePopup {}
CustomPopup1.template = xml/* html */`
<div class="popup custom-popup-1">
<footer>
<div class="confirm" t-on-click="confirm">
Yes
</div>
<div class="cancel" t-on-click="cancel">
No
</div>
</footer>
</div>
`;
class CustomPopup2 extends AbstractAwaitablePopup {}
CustomPopup2.template = xml/* html */`
<div class="popup custom-popup-2">
<footer>
<div class="confirm" t-on-click="confirm">
Okay
</div>
</footer>
</div>
`;
PosPopupController.components = { CustomPopup1, CustomPopup2 };
},
});
QUnit.test('allow multiple popups at the same time', async function(assert) {
assert.expect(12);
class Root extends PosComponent {
setup() {
super.setup();
useSubEnv({
isDebug: () => false,
posbus: new EventBus(),
});
}
}
Root.env = makeTestEnvironment();
Root.template = xml/* html */ `
<div>
<PosPopupController />
</div>
`;
const root = await mount(Root, testUtils.prepareTarget());
// Check 1 popup
let popup1Promise = root.showPopup('CustomPopup1', {});
await testUtils.nextTick();
assert.strictEqual(root.el.querySelectorAll('.popup').length, 1);
testUtils.dom.click(root.el.querySelector('.modal-dialog .custom-popup-1 .confirm'));
let result1 = await popup1Promise;
assert.strictEqual(result1.confirmed, true);
await testUtils.nextTick();
assert.strictEqual(root.el.querySelectorAll('.popup').length, 0);
// Check multiple popups
popup1Promise = root.showPopup('CustomPopup1', {});
await testUtils.nextTick();
// Check if the first popup is shown.
assert.strictEqual(root.el.querySelectorAll('.popup').length, 1);
let popup2Promise = root.showPopup('CustomPopup2', {});
await testUtils.nextTick();
// Check for the second popup.
assert.strictEqual(root.el.querySelectorAll('.popup').length, 2);
// popup 1 should be hidden
assert.strictEqual(root.el.querySelectorAll('.modal-dialog.oe_hidden').length, 1);
// click confirm on popup 2
testUtils.dom.click(root.el.querySelector('.modal-dialog .custom-popup-2 .confirm'));
await testUtils.nextTick();
// after confirming on popup 2, only 1 should remain.
assert.strictEqual(root.el.querySelectorAll('.popup').length, 1);
assert.strictEqual(root.el.querySelectorAll('.modal-dialog .custom-popup-2').length, 0);
// popup 1 should not be hidden
const CustomPopup1 = root.el.querySelector('.modal-dialog')
assert.strictEqual(![...CustomPopup1.classList].includes('oe_hidden'), true);
testUtils.dom.click(root.el.querySelector('.modal-dialog .custom-popup-1 .cancel'));
await testUtils.nextTick();
// after cancelling popup 1, no popup should remain.
assert.strictEqual(root.el.querySelectorAll('.popup').length, 0);
result1 = await popup1Promise;
let result2 = await popup2Promise;
assert.strictEqual(result1.confirmed, false); // false because it's cancelled.
assert.strictEqual(result2.confirmed, true); // true because it's confirmed.
});
QUnit.test('pressing cancel/confirm key should only close the top popup', async function(assert) {
assert.expect(6);
class Root extends PosComponent {
setup() {
super.setup();
useSubEnv({
isDebug: () => false,
posbus: new EventBus(),
});
}
}
Root.env = makeTestEnvironment();
Root.template = xml/* html */ `
<div>
<PosPopupController />
</div>
`;
const root = await mount(Root, testUtils.prepareTarget());
let popup1Promise = root.showPopup('CustomPopup1', { confirmKey: 'Enter', cancelKey: 'Escape' });
await testUtils.nextTick();
assert.strictEqual(root.el.querySelectorAll('.popup').length, 1);
let popup2Promise = root.showPopup('CustomPopup2', { confirmKey: 'Enter', cancelKey: 'Escape' });
await testUtils.nextTick();
assert.strictEqual(root.el.querySelectorAll('.popup').length, 2);
// Pressing 'Escape' should cancel the top popup which is the CustomPopup2.
testUtils.dom.triggerEvent(window, 'keyup', { key: 'Escape' });
await testUtils.nextTick();
// Therefore, the popup2Promise has now resolved with `confirmed` value = false.
const result2 = await popup2Promise;
assert.strictEqual(result2.confirmed, false);
assert.strictEqual(root.el.querySelectorAll('.popup').length, 1);
testUtils.dom.triggerEvent(window, 'keyup', { key: 'Enter' });
await testUtils.nextTick();
assert.strictEqual(root.el.querySelectorAll('.popup').length, 0);
const result1 = await popup1Promise;
assert.strictEqual(result1.confirmed, true);
});
});