diff --git a/addons/account_payment/i18n/es_MX.po b/addons/account_payment/i18n/es_MX.po index f7f051ca3da..519b2222c71 100644 --- a/addons/account_payment/i18n/es_MX.po +++ b/addons/account_payment/i18n/es_MX.po @@ -1,84 +1,67 @@ -# Translation of OpenERP Server. -# This file contains the translation of the following modules: -# * account_payment +# Spanish (Mexico) translation for openobject-addons +# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 +# This file is distributed under the same license as the openobject-addons package. +# FIRST AUTHOR , 2014. # msgid "" msgstr "" -"Project-Id-Version: OpenERP Server 6.0dev\n" -"Report-Msgid-Bugs-To: support@openerp.com\n" -"POT-Creation-Date: 2011-01-11 11:14+0000\n" -"PO-Revision-Date: 2011-01-12 13:13+0000\n" -"Last-Translator: Borja López Soilán (NeoPolus) \n" -"Language-Team: \n" +"Project-Id-Version: openobject-addons\n" +"Report-Msgid-Bugs-To: FULL NAME \n" +"POT-Creation-Date: 2012-12-21 17:05+0000\n" +"PO-Revision-Date: 2014-01-10 21:52+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Spanish (Mexico) \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-09-05 05:26+0000\n" -"X-Generator: Launchpad (build 13830)\n" +"X-Launchpad-Export-Date: 2014-01-11 05:45+0000\n" +"X-Generator: Launchpad (build 16890)\n" #. module: account_payment -#: field:payment.order,date_scheduled:0 -msgid "Scheduled date if fixed" -msgstr "Fecha planificada si es fija" +#: model:ir.actions.act_window,help:account_payment.action_payment_order_tree +msgid "" +"

\n" +" Click to create a payment order.\n" +"

\n" +" A payment order is a payment request from your company to " +"pay a\n" +" supplier invoice or a customer refund.\n" +"

\n" +" " +msgstr "" #. module: account_payment #: field:payment.line,currency:0 msgid "Partner Currency" -msgstr "Moneda de la empresa" +msgstr "" #. module: account_payment #: view:payment.order:0 msgid "Set to draft" -msgstr "Cambiar a borrador" +msgstr "" #. module: account_payment #: help:payment.order,mode:0 msgid "Select the Payment Mode to be applied." -msgstr "Seleccione el modo de pago a aplicar." +msgstr "" #. module: account_payment #: view:payment.mode:0 #: view:payment.order:0 msgid "Group By..." -msgstr "Agrupar por..." - -#. module: account_payment -#: model:ir.module.module,description:account_payment.module_meta_information -msgid "" -"\n" -"This module provides :\n" -"* a more efficient way to manage invoice payment.\n" -"* a basic mechanism to easily plug various automated payment.\n" -" " msgstr "" -"\n" -"Este módulo proporciona:\n" -"* Una forma más eficiente para gestionar el pago de las facturas.\n" -"* Un mecanismo básico para conectar fácilmente varios pagos automatizados.\n" -" " #. module: account_payment #: field:payment.order,line_ids:0 msgid "Payment lines" -msgstr "Líneas de pago" +msgstr "" #. module: account_payment #: view:payment.line:0 #: field:payment.line,info_owner:0 #: view:payment.order:0 msgid "Owner Account" -msgstr "Cuenta propietario" - -#. module: account_payment -#: help:payment.order,state:0 -msgid "" -"When an order is placed the state is 'Draft'.\n" -" Once the bank is confirmed the state is set to 'Confirmed'.\n" -" Then the order is paid the state is 'Done'." msgstr "" -"Cuando se hace una orden, el estado es 'Borrador'.\n" -" Una vez se confirma el banco, el estado es \"Confirmada\".\n" -" Cuando la orden se paga, el estado es 'Realizada'." #. module: account_payment #: help:account.invoice,amount_to_pay:0 @@ -86,108 +69,109 @@ msgid "" "The amount which should be paid at the current date\n" "minus the amount which is already in payment order" msgstr "" -"El importe que se debería haber pagado en la fecha actual\n" -"menos el importe que ya está en la orden de pago" #. module: account_payment +#: field:payment.line,company_id:0 #: field:payment.mode,company_id:0 +#: field:payment.order,company_id:0 msgid "Company" -msgstr "Compañía" +msgstr "" #. module: account_payment -#: field:payment.order,date_prefered:0 -msgid "Preferred date" -msgstr "Fecha preferida" +#: model:res.groups,name:account_payment.group_account_payment +msgid "Accounting / Payments" +msgstr "" #. module: account_payment #: selection:payment.line,state:0 msgid "Free" -msgstr "Libre" +msgstr "" #. module: account_payment +#: view:payment.order.create:0 #: field:payment.order.create,entries:0 msgid "Entries" -msgstr "Asientos" +msgstr "" #. module: account_payment #: report:payment.order:0 msgid "Used Account" -msgstr "Cuenta utilizada" +msgstr "" #. module: account_payment #: field:payment.line,ml_maturity_date:0 #: field:payment.order.create,duedate:0 msgid "Due Date" -msgstr "Fecha de vencimiento" - -#. module: account_payment -#: constraint:account.move.line:0 -msgid "You can not create move line on closed account." -msgstr "No puede crear una línea de movimiento en una cuenta cerrada." - -#. module: account_payment -#: view:account.move.line:0 -msgid "Account Entry Line" -msgstr "Línea del asiento contable" +msgstr "" #. module: account_payment #: view:payment.order.create:0 msgid "_Add to payment order" -msgstr "_Añadir a la orden de pago" +msgstr "" #. module: account_payment #: model:ir.actions.act_window,name:account_payment.action_account_payment_populate_statement #: model:ir.actions.act_window,name:account_payment.action_account_populate_statement_confirm msgid "Payment Populate statement" -msgstr "Extracto generar pago" +msgstr "" + +#. module: account_payment +#: code:addons/account_payment/account_invoice.py:43 +#, python-format +msgid "" +"You cannot cancel an invoice which has already been imported in a payment " +"order. Remove it from the following payment order : %s." +msgstr "" + +#. module: account_payment +#: code:addons/account_payment/account_invoice.py:43 +#: code:addons/account_payment/account_move_line.py:110 +#, python-format +msgid "Error!" +msgstr "" #. module: account_payment #: report:payment.order:0 #: view:payment.order:0 msgid "Amount" -msgstr "Importe" - -#. module: account_payment -#: sql_constraint:account.move.line:0 -msgid "Wrong credit or debit value in accounting entry !" -msgstr "¡Valor haber o debe erróneo en el asiento contable!" +msgstr "" #. module: account_payment #: view:payment.order:0 msgid "Total in Company Currency" -msgstr "Total en moneda de la compañía" +msgstr "" #. module: account_payment #: selection:payment.order,state:0 msgid "Cancelled" -msgstr "Cancelado" +msgstr "" #. module: account_payment #: model:ir.actions.act_window,name:account_payment.action_payment_order_tree_new msgid "New Payment Order" -msgstr "Nueva orden de pago" +msgstr "" #. module: account_payment #: report:payment.order:0 #: field:payment.order,reference:0 msgid "Reference" -msgstr "Referencia" +msgstr "" #. module: account_payment #: sql_constraint:payment.line:0 msgid "The payment line name must be unique!" -msgstr "¡El nombre de la línea de pago debe ser única!" +msgstr "" #. module: account_payment #: model:ir.actions.act_window,name:account_payment.action_payment_order_tree #: model:ir.ui.menu,name:account_payment.menu_action_payment_order_form msgid "Payment Orders" -msgstr "Órdenes de pago" +msgstr "" #. module: account_payment #: selection:payment.order,date_prefered:0 msgid "Directly" -msgstr "Directamente" +msgstr "" #. module: account_payment #: model:ir.actions.act_window,name:account_payment.action_payment_line_form @@ -195,45 +179,52 @@ msgstr "Directamente" #: view:payment.line:0 #: view:payment.order:0 msgid "Payment Line" -msgstr "Línea de pago" +msgstr "" #. module: account_payment #: view:payment.line:0 msgid "Amount Total" -msgstr "Importe total" +msgstr "" + +#. module: account_payment +#: help:payment.order,state:0 +msgid "" +"When an order is placed the status is 'Draft'.\n" +" Once the bank is confirmed the status is set to 'Confirmed'.\n" +" Then the order is paid the status is 'Done'." +msgstr "" #. module: account_payment #: view:payment.order:0 #: selection:payment.order,state:0 msgid "Confirmed" -msgstr "Confirmada" +msgstr "" #. module: account_payment #: help:payment.line,ml_date_created:0 msgid "Invoice Effective Date" -msgstr "Fecha vencimiento factura" +msgstr "" #. module: account_payment #: report:payment.order:0 msgid "Execution Type" -msgstr "Tipo ejecución" +msgstr "" #. module: account_payment #: selection:payment.line,state:0 msgid "Structured" -msgstr "Estructurado" +msgstr "" #. module: account_payment -#: view:payment.order:0 -#: field:payment.order,state:0 -msgid "State" -msgstr "Estado" +#: view:account.bank.statement:0 +msgid "Import Payment Lines" +msgstr "" #. module: account_payment #: view:payment.line:0 #: view:payment.order:0 msgid "Transaction Information" -msgstr "Información de transacción" +msgstr "" #. module: account_payment #: model:ir.actions.act_window,name:account_payment.action_payment_mode_form @@ -241,18 +232,19 @@ msgstr "Información de transacción" #: model:ir.ui.menu,name:account_payment.menu_action_payment_mode_form #: view:payment.mode:0 #: view:payment.order:0 +#: field:payment.order,mode:0 msgid "Payment Mode" -msgstr "Modo de pago" +msgstr "" #. module: account_payment #: field:payment.line,ml_date_created:0 msgid "Effective Date" -msgstr "Fecha vencimiento" +msgstr "" #. module: account_payment #: field:payment.line,ml_inv_ref:0 msgid "Invoice Ref." -msgstr "Ref. factura" +msgstr "" #. module: account_payment #: help:payment.order,date_prefered:0 @@ -261,119 +253,99 @@ msgid "" "by you.'Directly' stands for the direct execution.'Due date' stands for the " "scheduled date of execution." msgstr "" -"Seleccione una opción para la orden de pago: 'Fecha fija' para una fecha " -"especificada por usted. 'Directamente' para la ejecución directa. 'Fecha " -"vencimiento' para la fecha programada de ejecución." #. module: account_payment -#: code:addons/account_payment/account_move_line.py:110 -#, python-format -msgid "Error !" -msgstr "¡Error!" - -#. module: account_payment -#: view:account.move.line:0 -msgid "Total debit" -msgstr "Total debe" - -#. module: account_payment -#: field:payment.order,date_done:0 -msgid "Execution date" -msgstr "Fecha ejecución" +#: field:payment.order,date_created:0 +msgid "Creation Date" +msgstr "" #. module: account_payment #: help:payment.mode,journal:0 msgid "Bank or Cash Journal for the Payment Mode" -msgstr "Diario de banco o caja para el modo de pago." +msgstr "" #. module: account_payment #: selection:payment.order,date_prefered:0 msgid "Fixed date" -msgstr "Fecha fija" +msgstr "" #. module: account_payment #: field:payment.line,info_partner:0 #: view:payment.order:0 msgid "Destination Account" -msgstr "Cuenta de destino" +msgstr "" #. module: account_payment #: view:payment.line:0 msgid "Desitination Account" -msgstr "Cuenta de destino" +msgstr "" #. module: account_payment #: view:payment.order:0 msgid "Search Payment Orders" -msgstr "Buscar órdenes de pago" - -#. module: account_payment -#: constraint:account.move.line:0 -msgid "" -"You can not create move line on receivable/payable account without partner" msgstr "" -"No puede crear una línea de movimiento en una cuenta a cobrar/a pagar sin " -"una empresa." #. module: account_payment #: field:payment.line,create_date:0 msgid "Created" -msgstr "Creado" +msgstr "" #. module: account_payment #: view:payment.order:0 msgid "Select Invoices to Pay" -msgstr "Seleccionar facturas a pagar" +msgstr "" #. module: account_payment #: view:payment.line:0 msgid "Currency Amount Total" -msgstr "Importe total monetario" +msgstr "" #. module: account_payment #: view:payment.order:0 msgid "Make Payments" -msgstr "Realizar pagos" +msgstr "" #. module: account_payment #: field:payment.line,state:0 msgid "Communication Type" -msgstr "Tipo de comunicación" +msgstr "" #. module: account_payment -#: model:ir.module.module,shortdesc:account_payment.module_meta_information -msgid "Payment Management" -msgstr "Gestión de pagos" +#: field:payment.line,partner_id:0 +#: field:payment.mode,partner_id:0 +#: report:payment.order:0 +msgid "Partner" +msgstr "" #. module: account_payment #: field:payment.line,bank_statement_line_id:0 msgid "Bank statement line" -msgstr "Línea extracto bancario" +msgstr "" #. module: account_payment #: selection:payment.order,date_prefered:0 msgid "Due date" -msgstr "Fecha vencimiento" +msgstr "" #. module: account_payment #: field:account.invoice,amount_to_pay:0 msgid "Amount to be paid" -msgstr "Importe a pagar" +msgstr "" #. module: account_payment #: report:payment.order:0 msgid "Currency" -msgstr "Moneda" +msgstr "" #. module: account_payment #: view:account.payment.make.payment:0 msgid "Yes" -msgstr "Sí" +msgstr "" #. module: account_payment #: help:payment.line,info_owner:0 msgid "Address of the Main Partner" -msgstr "Dirección de la empresa principal" +msgstr "" #. module: account_payment #: help:payment.line,date:0 @@ -381,92 +353,79 @@ msgid "" "If no payment date is specified, the bank will treat this payment line " "directly" msgstr "" -"Si no se indica fecha de pago, el banco procesará esta línea de pago " -"directamente" #. module: account_payment #: model:ir.model,name:account_payment.model_account_payment_populate_statement msgid "Account Payment Populate Statement" -msgstr "Contabilidad extracto generar pago" +msgstr "" + +#. module: account_payment +#: code:addons/account_payment/account_move_line.py:110 +#, python-format +msgid "There is no partner defined on the entry line." +msgstr "" #. module: account_payment #: help:payment.mode,name:0 msgid "Mode of Payment" -msgstr "Modo de pago" +msgstr "" #. module: account_payment #: report:payment.order:0 msgid "Value Date" -msgstr "Fecha valor" +msgstr "" #. module: account_payment #: report:payment.order:0 msgid "Payment Type" -msgstr "Tipo de Pago" +msgstr "" #. module: account_payment #: help:payment.line,amount_currency:0 msgid "Payment amount in the partner currency" -msgstr "Importe pagado en la moneda de la empresa" +msgstr "" #. module: account_payment #: view:payment.order:0 #: selection:payment.order,state:0 msgid "Draft" -msgstr "Borrador" +msgstr "" + +#. module: account_payment +#: view:payment.order:0 +#: field:payment.order,state:0 +msgid "Status" +msgstr "" #. module: account_payment #: help:payment.line,communication2:0 msgid "The successor message of Communication." -msgstr "El mensaje de pago realizado a comunicar." - -#. module: account_payment -#: code:addons/account_payment/account_move_line.py:110 -#, python-format -msgid "No partner defined on entry line" -msgstr "No se ha definido la empresa en la línea de entrada" +msgstr "" #. module: account_payment #: help:payment.line,info_partner:0 msgid "Address of the Ordering Customer." -msgstr "Dirección del cliente que ordena." +msgstr "" #. module: account_payment #: view:account.payment.populate.statement:0 msgid "Populate Statement:" -msgstr "Generar extracto:" - -#. module: account_payment -#: view:account.move.line:0 -msgid "Total credit" -msgstr "Total haber" +msgstr "" #. module: account_payment #: help:payment.order,date_scheduled:0 msgid "Select a date if you have chosen Preferred Date to be fixed." msgstr "" -"Seleccione una fecha si ha seleccionado que la fecha preferida sea fija." - -#. module: account_payment -#: field:payment.order,user_id:0 -msgid "User" -msgstr "Usuario" #. module: account_payment #: field:account.payment.populate.statement,lines:0 -#: model:ir.actions.act_window,name:account_payment.act_account_invoice_2_payment_line msgid "Payment Lines" -msgstr "Líneas de pago" +msgstr "" #. module: account_payment #: model:ir.model,name:account_payment.model_account_move_line msgid "Journal Items" -msgstr "Apuntes contables" - -#. module: account_payment -#: constraint:account.move.line:0 -msgid "Company must be same for its related account and period." -msgstr "La compañía debe ser la misma para la cuenta y periodo relacionados." +msgstr "" #. module: account_payment #: help:payment.line,move_line_id:0 @@ -474,202 +433,192 @@ msgid "" "This Entry Line will be referred for the information of the ordering " "customer." msgstr "" -"Esta línea se usará como referencia para la información del cliente que " -"ordena." #. module: account_payment #: view:payment.order.create:0 msgid "Search" -msgstr "Buscar" +msgstr "" #. module: account_payment -#: model:ir.actions.report.xml,name:account_payment.payment_order1 -#: model:ir.model,name:account_payment.model_payment_order -msgid "Payment Order" -msgstr "Orden de pago" +#: field:payment.order,user_id:0 +msgid "Responsible" +msgstr "" #. module: account_payment #: field:payment.line,date:0 msgid "Payment Date" -msgstr "Fecha de pago" +msgstr "" #. module: account_payment #: report:payment.order:0 msgid "Total:" -msgstr "Total:" +msgstr "" #. module: account_payment -#: field:payment.order,date_created:0 -msgid "Creation date" -msgstr "Fecha de creación" +#: field:payment.order,date_done:0 +msgid "Execution Date" +msgstr "" #. module: account_payment #: view:account.payment.populate.statement:0 msgid "ADD" -msgstr "Añadir" +msgstr "" #. module: account_payment -#: view:account.bank.statement:0 -msgid "Import payment lines" -msgstr "Importar líneas de pago" +#: model:ir.actions.act_window,name:account_payment.action_create_payment_order +msgid "Populate Payment" +msgstr "" #. module: account_payment #: field:account.move.line,amount_to_pay:0 msgid "Amount to pay" -msgstr "Importe a pagar" +msgstr "" #. module: account_payment #: field:payment.line,amount:0 msgid "Amount in Company Currency" -msgstr "Importe en la moneda de la compañía" +msgstr "" #. module: account_payment #: help:payment.line,partner_id:0 msgid "The Ordering Customer" -msgstr "El cliente que ordena" +msgstr "" #. module: account_payment #: model:ir.model,name:account_payment.model_account_payment_make_payment msgid "Account make payment" -msgstr "Contabilidad realizar pago" +msgstr "" #. module: account_payment #: report:payment.order:0 msgid "Invoice Ref" -msgstr "Ref. factura" +msgstr "" #. module: account_payment #: field:payment.line,name:0 msgid "Your Reference" -msgstr "Su referencia" - -#. module: account_payment -#: field:payment.order,mode:0 -msgid "Payment mode" -msgstr "Modo de pago" +msgstr "" #. module: account_payment #: view:payment.order:0 msgid "Payment order" -msgstr "Órdenes de pago" +msgstr "" #. module: account_payment #: view:payment.line:0 #: view:payment.order:0 msgid "General Information" -msgstr "Información General" +msgstr "" #. module: account_payment #: view:payment.order:0 #: selection:payment.order,state:0 msgid "Done" -msgstr "Realizado" +msgstr "" #. module: account_payment #: model:ir.model,name:account_payment.model_account_invoice msgid "Invoice" -msgstr "Factura" +msgstr "" #. module: account_payment #: field:payment.line,communication:0 msgid "Communication" -msgstr "Comunicación" +msgstr "" #. module: account_payment #: view:account.payment.make.payment:0 #: view:account.payment.populate.statement:0 -#: view:payment.order:0 #: view:payment.order.create:0 msgid "Cancel" -msgstr "Cancelar" +msgstr "" + +#. module: account_payment +#: field:payment.line,bank_id:0 +msgid "Destination Bank Account" +msgstr "" #. module: account_payment #: view:payment.line:0 #: view:payment.order:0 msgid "Information" -msgstr "Información" +msgstr "" #. module: account_payment -#: model:ir.actions.act_window,help:account_payment.action_payment_order_tree -msgid "" -"A payment order is a payment request from your company to pay a supplier " -"invoice or a customer credit note. Here you can register all payment orders " -"that should be done, keep track of all payment orders and mention the " -"invoice reference and the partner the payment should be done for." +#: model:ir.actions.report.xml,name:account_payment.payment_order1 +#: model:ir.model,name:account_payment.model_payment_order +#: view:payment.order:0 +msgid "Payment Order" msgstr "" -"Una órden de pago es una petición de pago que realiza su compañía para pagar " -"una factura de proveedor o un apunte de crédito de un cliente. Aquí puede " -"registrar todas las órdenes de pago pendientes y hacer seguimiento de las " -"órdenes e indicar la referencia de factura y la entidad a la cual pagar." #. module: account_payment #: help:payment.line,amount:0 msgid "Payment amount in the company currency" -msgstr "Importe pagado en la moneda de la compañía" +msgstr "" #. module: account_payment #: view:payment.order.create:0 msgid "Search Payment lines" -msgstr "Buscar líneas de pago" +msgstr "" #. module: account_payment #: field:payment.line,amount_currency:0 msgid "Amount in Partner Currency" -msgstr "Importe en la moneda de la empresa" +msgstr "" #. module: account_payment #: field:payment.line,communication2:0 msgid "Communication 2" -msgstr "Comunicación 2" +msgstr "" #. module: account_payment -#: field:payment.line,bank_id:0 -msgid "Destination Bank account" -msgstr "Cuenta bancaria destino" +#: field:payment.order,date_scheduled:0 +msgid "Scheduled Date" +msgstr "" #. module: account_payment #: view:account.payment.make.payment:0 msgid "Are you sure you want to make payment?" -msgstr "¿Está seguro que quiere realizar el pago?" +msgstr "" #. module: account_payment #: view:payment.mode:0 #: field:payment.mode,journal:0 msgid "Journal" -msgstr "Diario" +msgstr "" #. module: account_payment #: field:payment.mode,bank_id:0 msgid "Bank account" -msgstr "Cuenta bancaria" +msgstr "" #. module: account_payment #: view:payment.order:0 msgid "Confirm Payments" -msgstr "Confirmar pagos" +msgstr "" #. module: account_payment #: field:payment.line,company_currency:0 #: report:payment.order:0 msgid "Company Currency" -msgstr "Moneda de la compañía" +msgstr "" #. module: account_payment #: model:ir.ui.menu,name:account_payment.menu_main_payment #: view:payment.line:0 #: view:payment.order:0 msgid "Payment" -msgstr "Pago" +msgstr "" #. module: account_payment #: report:payment.order:0 msgid "Payment Order / Payment" -msgstr "Orden de pago / Pago" +msgstr "" #. module: account_payment #: field:payment.line,move_line_id:0 msgid "Entry line" -msgstr "Línea del asiento" +msgstr "" #. module: account_payment #: help:payment.line,communication:0 @@ -677,134 +626,57 @@ msgid "" "Used as the message between ordering customer and current company. Depicts " "'What do you want to say to the recipient about this order ?'" msgstr "" -"Se utiliza como mensaje entre el cliente que hace el pedido y la compañía " -"actual. Describe '¿Qué quiere decir al receptor sobre este pedido?'" #. module: account_payment #: field:payment.mode,name:0 msgid "Name" -msgstr "Nombre" +msgstr "" #. module: account_payment #: report:payment.order:0 msgid "Bank Account" -msgstr "Cuenta bancaria" +msgstr "" #. module: account_payment #: view:payment.line:0 #: view:payment.order:0 msgid "Entry Information" -msgstr "Información del asiento" +msgstr "" #. module: account_payment #: model:ir.model,name:account_payment.model_payment_order_create msgid "payment.order.create" -msgstr "pago.orden.crear" +msgstr "" #. module: account_payment #: field:payment.line,order_id:0 msgid "Order" -msgstr "Orden" +msgstr "" #. module: account_payment #: field:payment.order,total:0 msgid "Total" -msgstr "Total" +msgstr "" #. module: account_payment #: view:account.payment.make.payment:0 #: model:ir.actions.act_window,name:account_payment.action_account_payment_make_payment msgid "Make Payment" -msgstr "Realizar pago" +msgstr "" #. module: account_payment -#: field:payment.line,partner_id:0 -#: report:payment.order:0 -msgid "Partner" -msgstr "Empresa" +#: field:payment.order,date_prefered:0 +msgid "Preferred Date" +msgstr "" #. module: account_payment -#: model:ir.actions.act_window,name:account_payment.action_create_payment_order -msgid "Populate Payment" -msgstr "Generar pago" +#: view:account.payment.make.payment:0 +#: view:account.payment.populate.statement:0 +#: view:payment.order.create:0 +msgid "or" +msgstr "" #. module: account_payment #: help:payment.mode,bank_id:0 msgid "Bank Account for the Payment Mode" -msgstr "Cuenta bancaria para el modo de pago" - -#. module: account_payment -#: constraint:account.move.line:0 -msgid "You can not create move line on view account." -msgstr "No puede crear una línea de movimiento en una cuenta de tipo vista." - -#~ msgid "Execution date:" -#~ msgstr "Fecha de ejecución:" - -#~ msgid "Suitable bank types" -#~ msgstr "Tipos de banco adecuados" - -#~ msgid "Invalid XML for View Architecture!" -#~ msgstr "¡XML inválido para la definición de la vista!" - -#~ msgid "_Cancel" -#~ msgstr "_Cancelar" - -#~ msgid "Date" -#~ msgstr "Fecha" - -#~ msgid "Reference:" -#~ msgstr "Referencia:" - -#~ msgid "Maturity Date" -#~ msgstr "Fecha vencimiento" - -#~ msgid "Specify the Code for Payment Type" -#~ msgstr "Indique el código para el tipo de pago" - -#~ msgid "Code" -#~ msgstr "Código" - -#~ msgid "" -#~ "The Object name must start with x_ and not contain any special character !" -#~ msgstr "" -#~ "¡El nombre del objeto debe empezar con x_ y no contener ningún carácter " -#~ "especial!" - -#~ msgid "Pay" -#~ msgstr "Pagar" - -#~ msgid "Draft Payment Order" -#~ msgstr "Orden de pago borrador" - -#~ msgid "Cash Journal for the Payment Mode" -#~ msgstr "Diario de caja para el modo de pago" - -#~ msgid "_Search" -#~ msgstr "_Buscar" - -#, python-format -#~ msgid "Partner '+ line.partner_id.name+ ' has no bank account defined" -#~ msgstr "" -#~ "Empresa '+ line.partner_id.name+ ' no tiene una cuenta bancaria definida" - -#~ msgid "Payment Orders to Validate" -#~ msgstr "Órdenes de pago a validar" - -#~ msgid "Payment type" -#~ msgstr "Tipo de pago" - -#~ msgid "_Add" -#~ msgstr "_Añadir" - -#~ msgid "Select the Payment Type for the Payment Mode." -#~ msgstr "Seleccione el tipo de pago para el modo de pago." - -#~ msgid "Invalid model name in the action definition." -#~ msgstr "Nombre de modelo no válido en la definición de acción." - -#~ msgid "Populate payment" -#~ msgstr "Rellenar la orden" - -#~ msgid "Populate Statement with Payment lines" -#~ msgstr "Llenar un extracto con líneas de pago" +msgstr "" diff --git a/addons/hw_escpos/__init__.py b/addons/hw_escpos/__init__.py new file mode 100644 index 00000000000..e26a47cc12b --- /dev/null +++ b/addons/hw_escpos/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import controllers +import escpos + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/addons/hw_escpos/__openerp__.py b/addons/hw_escpos/__openerp__.py new file mode 100644 index 00000000000..830b78300ea --- /dev/null +++ b/addons/hw_escpos/__openerp__.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + + +{ + 'name': 'ESC/POS Hardware Driver', + 'version': '1.0', + 'category': 'Hardware Drivers', + 'sequence': 6, + 'summary': 'Hardware Driver for ESC/POS Printers and Cashdrawers', + 'description': """ +ESC/POS Hardware Driver +======================= + +This module allows openerp to print with ESC/POS compatible printers and +to open ESC/POS controlled cashdrawers in the point of sale and other modules +that would need such functionality. + +""", + 'author': 'OpenERP SA', + 'depends': ['hw_proxy'], + 'test': [ + ], + 'installable': True, + 'auto_install': False, +} + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/hw_escpos/controllers/__init__.py b/addons/hw_escpos/controllers/__init__.py new file mode 100644 index 00000000000..b5f0bcc9ec6 --- /dev/null +++ b/addons/hw_escpos/controllers/__init__.py @@ -0,0 +1,3 @@ +import main +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/addons/hw_escpos/controllers/main.py b/addons/hw_escpos/controllers/main.py new file mode 100644 index 00000000000..78ed77a2318 --- /dev/null +++ b/addons/hw_escpos/controllers/main.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- +import logging +import simplejson +import os +import io +import base64 +import openerp +import time +import random +import math +import openerp.addons.hw_proxy.controllers.main as hw_proxy +import subprocess +import usb.core +from openerp.tools.translate import _ +from .. import escpos +from ..escpos import printer +#import escpos +#import escpos.printer +from PIL import Image + +from openerp import http +from openerp.http import request +from openerp.addons.web.controllers.main import manifest_list, module_boot, html_template + +_logger = logging.getLogger(__name__) + +class EscposDriver(hw_proxy.Proxy): + + supported_printers = [ + { 'vendor' : 0x04b8, 'product' : 0x0e03, 'name' : 'Epson TM-T20' } + ] + + def get_escpos_printer(self): + printers = self.connected_usb_devices(self.supported_printers) + if len(printers) > 0: + return escpos.printer.Usb(printers[0]['vendor'], printers[0]['product']) + else: + return None + + @http.route('/hw_proxy/open_cashbox', type='json', auth='admin') + def open_cashbox(self): + print 'ESC/POS: OPEN CASHBOX' + eprint = self.get_escpos_printer() + if eprint != None: + eprint.cashdraw(2) + eprint.cashdraw(5) + + @http.route('/hw_proxy/print_receipt', type='json', auth='admin') + def print_receipt(self, receipt): + print 'ESC/POS: PRINT RECEIPT' + eprint = self.get_escpos_printer() + if eprint != None: + self.print_receipt_body(eprint,receipt) + eprint.cut() + + def print_receipt_body(self,eprint,receipt): + + def check(string): + return string != True and bool(string) and string.strip() + + def price(amount): + return ("{0:."+str(receipt['precision']['price'])+"f}").format(amount) + + def money(amount): + return ("{0:."+str(receipt['precision']['money'])+"f}").format(amount) + + def quantity(amount): + if math.floor(amount) != amount: + return ("{0:."+str(receipt['precision']['quantity'])+"f}").format(amount) + else: + return str(amount) + + + def printline(left, right='', width=40, ratio=0.5, indent=0): + lwidth = int(width * ratio) + rwidth = width - lwidth + lwidth = lwidth - indent + + left = left[:lwidth] + if len(left) != lwidth: + left = left + ' ' * (lwidth - len(left)) + + right = right[-rwidth:] + if len(right) != rwidth: + right = ' ' * (rwidth - len(right)) + right + + return ' ' * indent + left + right + '\n' + + def print_taxes(): + taxes = receipt['tax_details'] + for tax in taxes: + eprint.text(printline(tax['tax']['name'],price(tax['amount']), width=40,ratio=0.6)) + + logo = None + + if receipt['company']['logo']: + img = receipt['company']['logo'] + img = img[img.find(',')+1:] + f = io.BytesIO('img') + f.write(base64.decodestring(img)) + f.seek(0) + logo_rgba = Image.open(f) + logo = Image.new('RGB', logo_rgba.size, (255,255,255)) + logo.paste(logo_rgba, mask=logo_rgba.split()[3]) + width = 300 + wfac = width/float(logo_rgba.size[0]) + height = int(logo_rgba.size[1]*wfac) + logo = logo.resize((width,height), Image.ANTIALIAS) + + # Receipt Header + if logo: + eprint._convert_image(logo) + eprint.text('\n') + else: + eprint.set(align='center',type='b',height=2,width=2) + eprint.text(receipt['company']['name'] + '\n') + + eprint.set(align='center',type='b') + if check(receipt['shop']['name']): + eprint.text(receipt['shop']['name'] + '\n') + if check(receipt['company']['contact_address']): + eprint.text(receipt['company']['contact address'] + '\n') + if check(receipt['company']['phone']): + eprint.text('Tel:' + receipt['company']['phone'] + '\n') + if check(receipt['company']['vat']): + eprint.text('VAT:' + receipt['company']['vat'] + '\n') + if check(receipt['company']['email']): + eprint.text(receipt['company']['email'] + '\n') + if check(receipt['company']['website']): + eprint.text(receipt['company']['website'] + '\n') + if check(receipt['header']): + eprint.text(receipt['header']+'\n') + if check(receipt['cashier']): + eprint.text('-'*32+'\n') + eprint.text('Served by '+receipt['cashier']+'\n') + + # Orderlines + eprint.text('\n\n') + eprint.set(align='center') + for line in receipt['orderlines']: + pricestr = price(line['price_display']) + if line['discount'] == 0 and line['unit_name'] == 'Unit(s)' and line['quantity'] == 1: + eprint.text(printline(line['product_name'],pricestr,ratio=0.6)) + else: + eprint.text(printline(line['product_name'],ratio=0.6)) + if line['discount'] != 0: + eprint.text(printline('Discount: '+str(line['discount'])+'%', ratio=0.6, indent=2)) + if line['unit_name'] == 'Unit(s)': + eprint.text( printline( quantity(line['quantity']) + ' x ' + price(line['price']), pricestr, ratio=0.6, indent=2)) + else: + eprint.text( printline( quantity(line['quantity']) + line['unit_name'] + ' x ' + price(line['price']), pricestr, ratio=0.6, indent=2)) + + # Subtotal if the taxes are not included + taxincluded = True + if money(receipt['subtotal']) != money(receipt['total_with_tax']): + eprint.text(printline('','-------')); + eprint.text(printline(_('Subtotal'),money(receipt['subtotal']),width=40, ratio=0.6)) + print_taxes() + #eprint.text(printline(_('Taxes'),money(receipt['total_tax']),width=40, ratio=0.6)) + taxincluded = False + + + # Total + eprint.text(printline('','-------')); + eprint.set(align='center',height=2) + eprint.text(printline(_(' TOTAL'),money(receipt['total_with_tax']),width=40, ratio=0.6)) + eprint.text('\n\n'); + + # Paymentlines + eprint.set(align='center') + for line in receipt['paymentlines']: + eprint.text(printline(line['journal'], money(line['amount']), ratio=0.6)) + + eprint.text('\n'); + eprint.set(align='center',height=2) + eprint.text(printline(_(' CHANGE'),money(receipt['change']),width=40, ratio=0.6)) + eprint.set(align='center') + eprint.text('\n'); + + # Extra Payment info + if receipt['total_discount'] != 0: + eprint.text(printline(_('Discounts'),money(receipt['total_discount']),width=40, ratio=0.6)) + if taxincluded: + print_taxes() + #eprint.text(printline(_('Taxes'),money(receipt['total_tax']),width=40, ratio=0.6)) + + # Footer + if check(receipt['footer']): + eprint.text('\n'+receipt['footer']+'\n\n') + eprint.text(receipt['name']+'\n') + eprint.text( str(receipt['date']['date']).zfill(2) + +'/'+ str(receipt['date']['month']+1).zfill(2) + +'/'+ str(receipt['date']['year']).zfill(4) + +' '+ str(receipt['date']['hour']).zfill(2) + +':'+ str(receipt['date']['minute']).zfill(2) ) + diff --git a/addons/hw_escpos/escpos/__init__.py b/addons/hw_escpos/escpos/__init__.py new file mode 100644 index 00000000000..22a5af61029 --- /dev/null +++ b/addons/hw_escpos/escpos/__init__.py @@ -0,0 +1 @@ +__all__ = ["constants","escpos","exceptions","printer"] diff --git a/addons/hw_escpos/escpos/constants.py b/addons/hw_escpos/escpos/constants.py new file mode 100644 index 00000000000..ebffc62729e --- /dev/null +++ b/addons/hw_escpos/escpos/constants.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- + +""" ESC/POS Commands (Constants) """ + +# Feed control sequences +CTL_LF = '\x0a' # Print and line feed +CTL_FF = '\x0c' # Form feed +CTL_CR = '\x0d' # Carriage return +CTL_HT = '\x09' # Horizontal tab +CTL_VT = '\x0b' # Vertical tab +# Printer hardware +HW_INIT = '\x1b\x40' # Clear data in buffer and reset modes +HW_SELECT = '\x1b\x3d\x01' # Printer select +HW_RESET = '\x1b\x3f\x0a\x00' # Reset printer hardware +# Cash Drawer +CD_KICK_2 = '\x1b\x70\x00' # Sends a pulse to pin 2 [] +CD_KICK_5 = '\x1b\x70\x01' # Sends a pulse to pin 5 [] +# Paper +PAPER_FULL_CUT = '\x1d\x56\x00' # Full cut paper +PAPER_PART_CUT = '\x1d\x56\x01' # Partial cut paper +# Text format +TXT_NORMAL = '\x1b\x21\x00' # Normal text +TXT_2HEIGHT = '\x1b\x21\x10' # Double height text +TXT_2WIDTH = '\x1b\x21\x20' # Double width text +TXT_UNDERL_OFF = '\x1b\x2d\x00' # Underline font OFF +TXT_UNDERL_ON = '\x1b\x2d\x01' # Underline font 1-dot ON +TXT_UNDERL2_ON = '\x1b\x2d\x02' # Underline font 2-dot ON +TXT_BOLD_OFF = '\x1b\x45\x00' # Bold font OFF +TXT_BOLD_ON = '\x1b\x45\x01' # Bold font ON +TXT_FONT_A = '\x1b\x4d\x00' # Font type A +TXT_FONT_B = '\x1b\x4d\x01' # Font type B +TXT_ALIGN_LT = '\x1b\x61\x00' # Left justification +TXT_ALIGN_CT = '\x1b\x61\x01' # Centering +TXT_ALIGN_RT = '\x1b\x61\x02' # Right justification + +# Text Encoding + +TXT_ENC_PC437 = '\x1b\x74\x00' # PC437 USA +TXT_ENC_KATAKANA= '\x1b\x74\x01' # KATAKANA (JAPAN) +TXT_ENC_PC850 = '\x1b\x74\x02' # PC850 Multilingual +TXT_ENC_PC860 = '\x1b\x74\x03' # PC860 Portuguese +TXT_ENC_PC863 = '\x1b\x74\x04' # PC863 Canadian-French +TXT_ENC_PC865 = '\x1b\x74\x05' # PC865 Nordic +TXT_ENC_KANJI6 = '\x1b\x74\x06' # One-pass Kanji, Hiragana +TXT_ENC_KANJI7 = '\x1b\x74\x07' # One-pass Kanji +TXT_ENC_KANJI8 = '\x1b\x74\x08' # One-pass Kanji +TXT_ENC_WPC1252 = '\x1b\x74\x10' # WPC1252 +TXT_ENC_PC866 = '\x1b\x74\x11' # PC866 Cyrillic #2 +TXT_ENC_PC852 = '\x1b\x74\x12' # PC852 Latin2 +TXT_ENC_PC858 = '\x1b\x74\x13' # PC858 Euro + +TXT_ENC_KATAKANA_MAP = { + # Maps UTF-8 Katakana symbols to KATAKANA Page Codes + + # Half-Width Katakanas + '\xef\xbd\xa1':'\xa1', # 。 + '\xef\xbd\xa2':'\xa2', # 「 + '\xef\xbd\xa3':'\xa3', # 」 + '\xef\xbd\xa4':'\xa4', # 、 + '\xef\xbd\xa5':'\xa5', # ・ + + '\xef\xbd\xa6':'\xa6', # ヲ + '\xef\xbd\xa7':'\xa7', # ァ + '\xef\xbd\xa8':'\xa8', # ィ + '\xef\xbd\xa9':'\xa9', # ゥ + '\xef\xbd\xaa':'\xaa', # ェ + '\xef\xbd\xab':'\xab', # ォ + '\xef\xbd\xac':'\xac', # ャ + '\xef\xbd\xad':'\xad', # ュ + '\xef\xbd\xae':'\xae', # ョ + '\xef\xbd\xaf':'\xaf', # ッ + '\xef\xbd\xb0':'\xb0', # ー + '\xef\xbd\xb1':'\xb1', # ア + '\xef\xbd\xb2':'\xb2', # イ + '\xef\xbd\xb3':'\xb3', # ウ + '\xef\xbd\xb4':'\xb4', # エ + '\xef\xbd\xb5':'\xb5', # オ + '\xef\xbd\xb6':'\xb6', # カ + '\xef\xbd\xb7':'\xb7', # キ + '\xef\xbd\xb8':'\xb8', # ク + '\xef\xbd\xb9':'\xb9', # ケ + '\xef\xbd\xba':'\xba', # コ + '\xef\xbd\xbb':'\xbb', # サ + '\xef\xbd\xbc':'\xbc', # シ + '\xef\xbd\xbd':'\xbd', # ス + '\xef\xbd\xbe':'\xbe', # セ + '\xef\xbd\xbf':'\xbf', # ソ + '\xef\xbe\x80':'\xc0', # タ + '\xef\xbe\x81':'\xc1', # チ + '\xef\xbe\x82':'\xc2', # ツ + '\xef\xbe\x83':'\xc3', # テ + '\xef\xbe\x84':'\xc4', # ト + '\xef\xbe\x85':'\xc5', # ナ + '\xef\xbe\x86':'\xc6', # ニ + '\xef\xbe\x87':'\xc7', # ヌ + '\xef\xbe\x88':'\xc8', # ネ + '\xef\xbe\x89':'\xc9', # ノ + '\xef\xbe\x8a':'\xca', # ハ + '\xef\xbe\x8b':'\xcb', # ヒ + '\xef\xbe\x8c':'\xcc', # フ + '\xef\xbe\x8d':'\xcd', # ヘ + '\xef\xbe\x8e':'\xce', # ホ + '\xef\xbe\x8f':'\xcf', # マ + '\xef\xbe\x90':'\xd0', # ミ + '\xef\xbe\x91':'\xd1', # ム + '\xef\xbe\x92':'\xd2', # メ + '\xef\xbe\x93':'\xd3', # モ + '\xef\xbe\x94':'\xd4', # ヤ + '\xef\xbe\x95':'\xd5', # ユ + '\xef\xbe\x96':'\xd6', # ヨ + '\xef\xbe\x97':'\xd7', # ラ + '\xef\xbe\x98':'\xd8', # リ + '\xef\xbe\x99':'\xd9', # ル + '\xef\xbe\x9a':'\xda', # レ + '\xef\xbe\x9b':'\xdb', # ロ + '\xef\xbe\x9c':'\xdc', # ワ + '\xef\xbe\x9d':'\xdd', # ン + + '\xef\xbe\x9e':'\xde', # ゙ + '\xef\xbe\x9f':'\xdf', # ゚ +} + +# Barcod format +BARCODE_TXT_OFF = '\x1d\x48\x00' # HRI barcode chars OFF +BARCODE_TXT_ABV = '\x1d\x48\x01' # HRI barcode chars above +BARCODE_TXT_BLW = '\x1d\x48\x02' # HRI barcode chars below +BARCODE_TXT_BTH = '\x1d\x48\x03' # HRI barcode chars both above and below +BARCODE_FONT_A = '\x1d\x66\x00' # Font type A for HRI barcode chars +BARCODE_FONT_B = '\x1d\x66\x01' # Font type B for HRI barcode chars +BARCODE_HEIGHT = '\x1d\x68\x64' # Barcode Height [1-255] +BARCODE_WIDTH = '\x1d\x77\x03' # Barcode Width [2-6] +BARCODE_UPC_A = '\x1d\x6b\x00' # Barcode type UPC-A +BARCODE_UPC_E = '\x1d\x6b\x01' # Barcode type UPC-E +BARCODE_EAN13 = '\x1d\x6b\x02' # Barcode type EAN13 +BARCODE_EAN8 = '\x1d\x6b\x03' # Barcode type EAN8 +BARCODE_CODE39 = '\x1d\x6b\x04' # Barcode type CODE39 +BARCODE_ITF = '\x1d\x6b\x05' # Barcode type ITF +BARCODE_NW7 = '\x1d\x6b\x06' # Barcode type NW7 +# Image format +S_RASTER_N = '\x1d\x76\x30\x00' # Set raster image normal size +S_RASTER_2W = '\x1d\x76\x30\x01' # Set raster image double width +S_RASTER_2H = '\x1d\x76\x30\x02' # Set raster image double height +S_RASTER_Q = '\x1d\x76\x30\x03' # Set raster image quadruple diff --git a/addons/hw_escpos/escpos/escpos.py b/addons/hw_escpos/escpos/escpos.py new file mode 100644 index 00000000000..9296568b4fc --- /dev/null +++ b/addons/hw_escpos/escpos/escpos.py @@ -0,0 +1,373 @@ +#!/usr/bin/python +''' +@author: Manuel F Martinez +@organization: Bashlinux +@copyright: Copyright (c) 2012 Bashlinux +@license: GPL +''' + +import Image +import qrcode +import time +import copy + +try: + import jcconv +except: + jcconv = None + print 'ESC/POS: please install jcconv for improved Japanese receipt printing:' + print ' # pip install jcconv' + +from constants import * +from exceptions import * + +class Escpos: + """ ESC/POS Printer object """ + device = None + encoding = None + + + def _check_image_size(self, size): + """ Check and fix the size of the image to 32 bits """ + if size % 32 == 0: + return (0, 0) + else: + image_border = 32 - (size % 32) + if (image_border % 2) == 0: + return (image_border / 2, image_border / 2) + else: + return (image_border / 2, (image_border / 2) + 1) + + + def _print_image(self, line, size): + """ Print formatted image """ + i = 0 + cont = 0 + buffer = "" + + self._raw(S_RASTER_N) + buffer = "%02X%02X%02X%02X" % (((size[0]/size[1])/8), 0, size[1], 0) + self._raw(buffer.decode('hex')) + buffer = "" + + while i < len(line): + hex_string = int(line[i:i+8],2) + buffer += "%02X" % hex_string + i += 8 + cont += 1 + if cont % 4 == 0: + self._raw(buffer.decode("hex")) + buffer = "" + cont = 0 + + + def _convert_image(self, im): + """ Parse image and prepare it to a printable format """ + pixels = [] + pix_line = "" + im_left = "" + im_right = "" + switch = 0 + img_size = [ 0, 0 ] + + + if im.size[0] > 512: + print "WARNING: Image is wider than 512 and could be truncated at print time " + if im.size[1] > 255: + raise ImageSizeError() + + im_border = self._check_image_size(im.size[0]) + for i in range(im_border[0]): + im_left += "0" + for i in range(im_border[1]): + im_right += "0" + + for y in range(im.size[1]): + img_size[1] += 1 + pix_line += im_left + img_size[0] += im_border[0] + for x in range(im.size[0]): + img_size[0] += 1 + RGB = im.getpixel((x, y)) + im_color = (RGB[0] + RGB[1] + RGB[2]) + im_pattern = "1X0" + pattern_len = len(im_pattern) + switch = (switch - 1 ) * (-1) + for x in range(pattern_len): + if im_color <= (255 * 3 / pattern_len * (x+1)): + if im_pattern[x] == "X": + pix_line += "%d" % switch + else: + pix_line += im_pattern[x] + break + elif im_color > (255 * 3 / pattern_len * pattern_len) and im_color <= (255 * 3): + pix_line += im_pattern[-1] + break + pix_line += im_right + img_size[0] += im_border[1] + + self._print_image(pix_line, img_size) + + + def image(self,path_img): + """ Open image file """ + im_open = Image.open(path_img) + im = im_open.convert("RGB") + # Convert the RGB image in printable image + self._convert_image(im) + + + def qr(self,text): + """ Print QR Code for the provided string """ + qr_code = qrcode.QRCode(version=4, box_size=4, border=1) + qr_code.add_data(text) + qr_code.make(fit=True) + qr_img = qr_code.make_image() + im = qr_img._img.convert("RGB") + # Convert the RGB image in printable image + self._convert_image(im) + + + def barcode(self, code, bc, width, height, pos, font): + """ Print Barcode """ + # Align Bar Code() + self._raw(TXT_ALIGN_CT) + # Height + if height >=2 or height <=6: + self._raw(BARCODE_HEIGHT) + else: + raise BarcodeSizeError() + # Width + if width >= 1 or width <=255: + self._raw(BARCODE_WIDTH) + else: + raise BarcodeSizeError() + # Font + if font.upper() == "B": + self._raw(BARCODE_FONT_B) + else: # DEFAULT FONT: A + self._raw(BARCODE_FONT_A) + # Position + if pos.upper() == "OFF": + self._raw(BARCODE_TXT_OFF) + elif pos.upper() == "BOTH": + self._raw(BARCODE_TXT_BTH) + elif pos.upper() == "ABOVE": + self._raw(BARCODE_TXT_ABV) + else: # DEFAULT POSITION: BELOW + self._raw(BARCODE_TXT_BLW) + # Type + if bc.upper() == "UPC-A": + self._raw(BARCODE_UPC_A) + elif bc.upper() == "UPC-E": + self._raw(BARCODE_UPC_E) + elif bc.upper() == "EAN13": + self._raw(BARCODE_EAN13) + elif bc.upper() == "EAN8": + self._raw(BARCODE_EAN8) + elif bc.upper() == "CODE39": + self._raw(BARCODE_CODE39) + elif bc.upper() == "ITF": + self._raw(BARCODE_ITF) + elif bc.upper() == "NW7": + self._raw(BARCODE_NW7) + else: + raise BarcodeTypeError() + # Print Code + if code: + self._raw(code) + else: + raise exception.BarcodeCodeError() + + def text(self,txt): + """ Print Utf8 encoded alpha-numeric text """ + if not txt: + return + try: + txt = txt.decode('utf-8') + except: + try: + txt = txt.decode('utf-16') + except: + pass + + self.extra_chars = 0 + + def encode_char(char): + """ + Encodes a single utf-8 character into a sequence of + esc-pos code page change instructions and character declarations + """ + char_utf8 = char.encode('utf-8') + encoded = '' + encoding = self.encoding # we reuse the last encoding to prevent code page switches at every character + encodings = { + 'cp437': TXT_ENC_PC437, + 'cp850': TXT_ENC_PC850, + 'cp852': TXT_ENC_PC852, + 'cp858': TXT_ENC_PC858, + 'cp865': TXT_ENC_PC860, + 'cp863': TXT_ENC_PC863, + 'cp865': TXT_ENC_PC865, + 'cp866': TXT_ENC_PC866, + 'katakana' : TXT_ENC_KATAKANA, + # TODO Support other encodings not natively supported by python + } + remaining = copy.copy(encodings) + + if not encoding : + encoding = 'cp437' + + while True: # Trying all encoding until one succeeds + try: + if encoding == 'katakana': # Japanese characters + if jcconv: + # try to convert japanese text to a half-katakanas + kata = jcconv.kata2half(jcconv.hira2kata(char_utf8)) + if kata != char_utf8: + self.extra_chars += len(kata.decode('utf-8')) - 1 + # the conversion may result in multiple characters + return encode_str(kata.decode('utf-8')) + else: + kata = char_utf8 + + if kata in TXT_ENC_KATAKANA_MAP: + encoded = TXT_ENC_KATAKANA_MAP[kata] + break + else: + raise ValueError() + else: + encoded = char.encode(encoding) + break + + except ValueError: #the encoding failed, select another one and retry + if encoding in remaining: + del remaining[encoding] + if len(remaining) >= 1: + encoding = remaining.items()[0][0] + else: + encoding = 'cp437' + encoded = '\xb1' # could not encode, output error character + break; + + if encoding != self.encoding: + # if the encoding changed, remember it and prefix the character with + # the esc-pos encoding change sequence + self.encoding = encoding + encoded = encodings[encoding] + encoded + + return encoded + + def encode_str(txt): + buffer = '' + for c in txt: + buffer += encode_char(c) + return buffer + + txt = encode_str(txt) + + # if the utf-8 -> codepage conversion inserted extra characters, + # remove double spaces to try to restore the original string length + # and prevent printing alignment issues + while self.extra_chars > 0: + dspace = txt.find(' ') + if dspace > 0: + txt = txt[:dspace] + txt[dspace+1:] + self.extra_chars -= 1 + else: + break + + self._raw(txt) + + def set(self, align='left', font='a', type='normal', width=1, height=1): + """ Set text properties """ + # Align + if align.upper() == "CENTER": + self._raw(TXT_ALIGN_CT) + elif align.upper() == "RIGHT": + self._raw(TXT_ALIGN_RT) + elif align.upper() == "LEFT": + self._raw(TXT_ALIGN_LT) + # Font + if font.upper() == "B": + self._raw(TXT_FONT_B) + else: # DEFAULT FONT: A + self._raw(TXT_FONT_A) + # Type + if type.upper() == "B": + self._raw(TXT_BOLD_ON) + self._raw(TXT_UNDERL_OFF) + elif type.upper() == "U": + self._raw(TXT_BOLD_OFF) + self._raw(TXT_UNDERL_ON) + elif type.upper() == "U2": + self._raw(TXT_BOLD_OFF) + self._raw(TXT_UNDERL2_ON) + elif type.upper() == "BU": + self._raw(TXT_BOLD_ON) + self._raw(TXT_UNDERL_ON) + elif type.upper() == "BU2": + self._raw(TXT_BOLD_ON) + self._raw(TXT_UNDERL2_ON) + elif type.upper == "NORMAL": + self._raw(TXT_BOLD_OFF) + self._raw(TXT_UNDERL_OFF) + # Width + if width == 2 and height != 2: + self._raw(TXT_NORMAL) + self._raw(TXT_2WIDTH) + elif height == 2 and width != 2: + self._raw(TXT_NORMAL) + self._raw(TXT_2HEIGHT) + elif height == 2 and width == 2: + self._raw(TXT_2WIDTH) + self._raw(TXT_2HEIGHT) + else: # DEFAULT SIZE: NORMAL + self._raw(TXT_NORMAL) + + + def cut(self, mode=''): + """ Cut paper """ + # Fix the size between last line and cut + # TODO: handle this with a line feed + self._raw("\n\n\n\n\n\n") + if mode.upper() == "PART": + self._raw(PAPER_PART_CUT) + else: # DEFAULT MODE: FULL CUT + self._raw(PAPER_FULL_CUT) + + + def cashdraw(self, pin): + """ Send pulse to kick the cash drawer """ + if pin == 2: + self._raw(CD_KICK_2) + elif pin == 5: + self._raw(CD_KICK_5) + else: + raise CashDrawerError() + + + def hw(self, hw): + """ Hardware operations """ + if hw.upper() == "INIT": + self._raw(HW_INIT) + elif hw.upper() == "SELECT": + self._raw(HW_SELECT) + elif hw.upper() == "RESET": + self._raw(HW_RESET) + else: # DEFAULT: DOES NOTHING + pass + + + def control(self, ctl): + """ Feed control sequences """ + if ctl.upper() == "LF": + self._raw(CTL_LF) + elif ctl.upper() == "FF": + self._raw(CTL_FF) + elif ctl.upper() == "CR": + self._raw(CTL_CR) + elif ctl.upper() == "HT": + self._raw(CTL_HT) + elif ctl.upper() == "VT": + self._raw(CTL_VT) diff --git a/addons/hw_escpos/escpos/exceptions.py b/addons/hw_escpos/escpos/exceptions.py new file mode 100644 index 00000000000..adbe6484f3e --- /dev/null +++ b/addons/hw_escpos/escpos/exceptions.py @@ -0,0 +1,80 @@ +""" ESC/POS Exceptions classes """ + +import os + +class Error(Exception): + """ Base class for ESC/POS errors """ + def __init__(self, msg, status=None): + Exception.__init__(self) + self.msg = msg + self.resultcode = 1 + if status is not None: + self.resultcode = status + + def __str__(self): + return self.msg + +# Result/Exit codes +# 0 = success +# 10 = No Barcode type defined +# 20 = Barcode size values are out of range +# 30 = Barcode text not supplied +# 40 = Image height is too large +# 50 = No string supplied to be printed +# 60 = Invalid pin to send Cash Drawer pulse + + +class BarcodeTypeError(Error): + def __init__(self, msg=""): + Error.__init__(self, msg) + self.msg = msg + self.resultcode = 10 + + def __str__(self): + return "No Barcode type is defined" + +class BarcodeSizeError(Error): + def __init__(self, msg=""): + Error.__init__(self, msg) + self.msg = msg + self.resultcode = 20 + + def __str__(self): + return "Barcode size is out of range" + +class BarcodeCodeError(Error): + def __init__(self, msg=""): + Error.__init__(self, msg) + self.msg = msg + self.resultcode = 30 + + def __str__(self): + return "Code was not supplied" + +class ImageSizeError(Error): + def __init__(self, msg=""): + Error.__init__(self, msg) + self.msg = msg + self.resultcode = 40 + + def __str__(self): + return "Image height is longer than 255px and can't be printed" + +class TextError(Error): + def __init__(self, msg=""): + Error.__init__(self, msg) + self.msg = msg + self.resultcode = 50 + + def __str__(self): + return "Text string must be supplied to the text() method" + + +class CashDrawerError(Error): + def __init__(self, msg=""): + Error.__init__(self, msg) + self.msg = msg + self.resultcode = 60 + + def __str__(self): + return "Valid pin must be set to send pulse" diff --git a/addons/hw_escpos/escpos/printer.py b/addons/hw_escpos/escpos/printer.py new file mode 100644 index 00000000000..dd5aa93e361 --- /dev/null +++ b/addons/hw_escpos/escpos/printer.py @@ -0,0 +1,136 @@ +#!/usr/bin/python +''' +@author: Manuel F Martinez +@organization: Bashlinux +@copyright: Copyright (c) 2012 Bashlinux +@license: GPL +''' + +import usb.core +import usb.util +import serial +import socket + +from escpos import * +from constants import * +from exceptions import * + +class Usb(Escpos): + """ Define USB printer """ + + def __init__(self, idVendor, idProduct, interface=0, in_ep=0x82, out_ep=0x01): + """ + @param idVendor : Vendor ID + @param idProduct : Product ID + @param interface : USB device interface + @param in_ep : Input end point + @param out_ep : Output end point + """ + self.idVendor = idVendor + self.idProduct = idProduct + self.interface = interface + self.in_ep = in_ep + self.out_ep = out_ep + self.open() + + + def open(self): + """ Search device on USB tree and set is as escpos device """ + self.device = usb.core.find(idVendor=self.idVendor, idProduct=self.idProduct) + if self.device is None: + print "Cable isn't plugged in" + + if self.device.is_kernel_driver_active(0): + try: + self.device.detach_kernel_driver(0) + except usb.core.USBError as e: + print "Could not detatch kernel driver: %s" % str(e) + + try: + self.device.set_configuration() + self.device.reset() + except usb.core.USBError as e: + print "Could not set configuration: %s" % str(e) + + + def _raw(self, msg): + """ Print any command sent in raw format """ + self.device.write(self.out_ep, msg, self.interface) + + + def __del__(self): + """ Release USB interface """ + if self.device: + usb.util.dispose_resources(self.device) + self.device = None + + + +class Serial(Escpos): + """ Define Serial printer """ + + def __init__(self, devfile="/dev/ttyS0", baudrate=9600, bytesize=8, timeout=1): + """ + @param devfile : Device file under dev filesystem + @param baudrate : Baud rate for serial transmission + @param bytesize : Serial buffer size + @param timeout : Read/Write timeout + """ + self.devfile = devfile + self.baudrate = baudrate + self.bytesize = bytesize + self.timeout = timeout + self.open() + + + def open(self): + """ Setup serial port and set is as escpos device """ + self.device = serial.Serial(port=self.devfile, baudrate=self.baudrate, bytesize=self.bytesize, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=self.timeout, dsrdtr=True) + + if self.device is not None: + print "Serial printer enabled" + else: + print "Unable to open serial printer on: %s" % self.devfile + + + def _raw(self, msg): + """ Print any command sent in raw format """ + self.device.write(msg) + + + def __del__(self): + """ Close Serial interface """ + if self.device is not None: + self.device.close() + + + +class Network(Escpos): + """ Define Network printer """ + + def __init__(self,host,port=9100): + """ + @param host : Printer's hostname or IP address + @param port : Port to write to + """ + self.host = host + self.port = port + self.open() + + + def open(self): + """ Open TCP socket and set it as escpos device """ + self.device = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.device.connect((self.host, self.port)) + + if self.device is None: + print "Could not open socket for %s" % self.host + + + def _raw(self, msg): + self.device.send(msg) + + + def __del__(self): + """ Close TCP connection """ + self.device.close() diff --git a/addons/hw_proxy/__init__.py b/addons/hw_proxy/__init__.py new file mode 100644 index 00000000000..a208bc1c551 --- /dev/null +++ b/addons/hw_proxy/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import controllers + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/addons/hw_proxy/__openerp__.py b/addons/hw_proxy/__openerp__.py new file mode 100644 index 00000000000..86e395d2522 --- /dev/null +++ b/addons/hw_proxy/__openerp__.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + + +{ + 'name': 'Hardware Proxy', + 'version': '1.0', + 'category': 'Point Of Sale', + 'sequence': 6, + 'summary': 'Connect the Web Client to Hardware Peripherals', + 'description': """ +Hardware Poxy +============= + +This module allows you to remotely use peripherals connected to this server. + +This modules only contains the enabling framework. The actual devices drivers +are found in other modules that must be installed separately. + +""", + 'author': 'OpenERP SA', + 'depends': [], + 'test': [ + ], + 'installable': True, + 'auto_install': False, +} + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/hw_proxy/controllers/__init__.py b/addons/hw_proxy/controllers/__init__.py new file mode 100644 index 00000000000..b5f0bcc9ec6 --- /dev/null +++ b/addons/hw_proxy/controllers/__init__.py @@ -0,0 +1,3 @@ +import main +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/addons/hw_proxy/controllers/main.py b/addons/hw_proxy/controllers/main.py new file mode 100644 index 00000000000..967323ae0d6 --- /dev/null +++ b/addons/hw_proxy/controllers/main.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +import logging +import simplejson +import os +import os.path +import openerp +import time +import random +import subprocess +import usb.core +import escpos +import escpos.printer +import werkzeug +import werkzeug.wrappers + +from openerp import http +from openerp.http import request +from openerp.addons.web.controllers.main import manifest_list, module_boot, html_template + +_logger = logging.getLogger(__name__) + +class Proxy(http.Controller): + def __init__(self): + self.scale = 'closed' + self.scale_weight = 0.0 + + def connected_usb_devices(self,devices): + connected = [] + for device in devices: + if usb.core.find(idVendor=device['vendor'], idProduct=device['product']) != None: + connected.append(device) + return connected + + @http.route('/hw_proxy/hello', type='http', auth='admin') + def helloajx(self): + return request.make_response('ping', { + 'Cache-Control': 'no-cache', + 'Content-Type': 'text/html; charset=utf-8', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET', + }) + + @http.route('/hw_proxy/handshake', type='json', auth='admin') + def handshake(self): + return True + + @http.route('/hw_proxy/scan_item_success', type='json', auth='admin') + def scan_item_success(self, ean): + """ + A product has been scanned with success + """ + print 'scan_item_success: ' + str(ean) + + @http.route('/hw_proxy/scan_item_error_unrecognized', type='json', auth='admin') + def scan_item_error_unrecognized(self, ean): + """ + A product has been scanned without success + """ + print 'scan_item_error_unrecognized: ' + str(ean) + + @http.route('/hw_proxy/help_needed', type='json', auth='admin') + def help_needed(self): + """ + The user wants an help (ex: light is on) + """ + print "help_needed" + + @http.route('/hw_proxy/help_canceled', type='json', auth='admin') + def help_canceled(self): + """ + The user stops the help request + """ + print "help_canceled" + + @http.route('/hw_proxy/weighting_start', type='json', auth='admin') + def weighting_start(self): + if self.scale == 'closed': + print "Opening (Fake) Connection to Scale..." + self.scale = 'open' + self.scale_weight = 0.0 + time.sleep(0.1) + print "... Scale Open." + else: + print "WARNING: Scale already Connected !!!" + + @http.route('/hw_proxy/weighting_read_kg', type='json', auth='admin') + def weighting_read_kg(self): + if self.scale == 'open': + print "Reading Scale..." + time.sleep(0.025) + self.scale_weight += 0.01 + print "... Done." + return self.scale_weight + else: + print "WARNING: Reading closed scale !!!" + return 0.0 + + @http.route('/hw_proxy/weighting_end', type='json', auth='admin') + def weighting_end(self): + if self.scale == 'open': + print "Closing Connection to Scale ..." + self.scale = 'closed' + self.scale_weight = 0.0 + time.sleep(0.1) + print "... Scale Closed." + else: + print "WARNING: Scale already Closed !!!" + + @http.route('/hw_proxy/payment_request', type='json', auth='admin') + def payment_request(self, price): + """ + The PoS will activate the method payment + """ + print "payment_request: price:"+str(price) + return 'ok' + + @http.route('/hw_proxy/payment_status', type='json', auth='admin') + def payment_status(self): + print "payment_status" + return { 'status':'waiting' } + + @http.route('/hw_proxy/payment_cancel', type='json', auth='admin') + def payment_cancel(self): + print "payment_cancel" + + @http.route('/hw_proxy/transaction_start', type='json', auth='admin') + def transaction_start(self): + print 'transaction_start' + + @http.route('/hw_proxy/transaction_end', type='json', auth='admin') + def transaction_end(self): + print 'transaction_end' + + @http.route('/hw_proxy/cashier_mode_activated', type='json', auth='admin') + def cashier_mode_activated(self): + print 'cashier_mode_activated' + + @http.route('/hw_proxy/cashier_mode_deactivated', type='json', auth='admin') + def cashier_mode_deactivated(self): + print 'cashier_mode_deactivated' + + @http.route('/hw_proxy/open_cashbox', type='json', auth='admin') + def open_cashbox(self): + print 'open_cashbox' + + @http.route('/hw_proxy/print_receipt', type='json', auth='admin') + def print_receipt(self, receipt): + print 'print_receipt' + str(receipt) + + @http.route('/hw_proxy/is_scanner_connected', type='json', auth='admin') + def print_receipt(self, receipt): + print 'is_scanner_connected?' + return False + + @http.route('/hw_proxy/scanner', type='json', auth='admin') + def print_receipt(self, receipt): + print 'scanner' + time.sleep(10) + return '' + + @http.route('/hw_proxy/log', type='json', auth='admin') + def log(self, arguments): + _logger.info(' '.join(str(v) for v in arguments)) + + @http.route('/hw_proxy/print_pdf_invoice', type='json', auth='admin') + def print_pdf_invoice(self, pdfinvoice): + print 'print_pdf_invoice' + str(pdfinvoice) + + diff --git a/addons/hw_scanner/__init__.py b/addons/hw_scanner/__init__.py new file mode 100644 index 00000000000..a208bc1c551 --- /dev/null +++ b/addons/hw_scanner/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import controllers + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/addons/hw_scanner/__openerp__.py b/addons/hw_scanner/__openerp__.py new file mode 100644 index 00000000000..4ef6149f253 --- /dev/null +++ b/addons/hw_scanner/__openerp__.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + + +{ + 'name': 'Barcode Scanner Hardware Driver', + 'version': '1.0', + 'category': 'Hardware Drivers', + 'sequence': 6, + 'summary': 'Hardware Driver for Barcode Scanners', + 'description': """ +Barcode Scanner Hardware Driver +================================ + +This module allows the web client to access a remotely installed barcode +scanner, and is used by the posbox to provide barcode scanner support to the +point of sale module. + +""", + 'author': 'OpenERP SA', + 'depends': ['hw_proxy'], + 'test': [ + ], + 'installable': True, + 'auto_install': False, +} + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/hw_scanner/controllers/__init__.py b/addons/hw_scanner/controllers/__init__.py new file mode 100644 index 00000000000..b5f0bcc9ec6 --- /dev/null +++ b/addons/hw_scanner/controllers/__init__.py @@ -0,0 +1,3 @@ +import main +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/addons/hw_scanner/controllers/main.py b/addons/hw_scanner/controllers/main.py new file mode 100644 index 00000000000..ec56a733671 --- /dev/null +++ b/addons/hw_scanner/controllers/main.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +import logging +import os +from os import listdir +from os.path import join +import openerp +import openerp.addons.hw_proxy.controllers.main as hw_proxy +from openerp.tools.translate import _ + +from openerp import http +from openerp.http import request + +_logger = logging.getLogger(__name__) + +from evdev import InputDevice, ecodes, categorize, list_devices +from select import select + +class ScannerDriver(hw_proxy.Proxy): + def __init__(self): + self.input_dir = '/dev/input/by-id/' + self.keymap = { + 2: ("1","!"), + 3: ("2","@"), + 4: ("3","#"), + 5: ("4","$"), + 6: ("5","%"), + 7: ("6","^"), + 8: ("7","&"), + 9: ("8","*"), + 10:("9","("), + 11:("0",")"), + 12:("-","_"), + 13:("=","+"), + # 14 BACKSPACE + # 15 TAB + 16:("q","Q"), + 17:("w","W"), + 18:("e","E"), + 19:("r","R"), + 20:("t","T"), + 21:("y","Y"), + 22:("u","U"), + 23:("i","I"), + 24:("o","O"), + 25:("p","P"), + 26:("[","{"), + 27:("]","}"), + # 28 ENTER + # 29 LEFT_CTRL + 30:("a","A"), + 31:("s","S"), + 32:("d","D"), + 33:("f","F"), + 34:("g","G"), + 35:("h","H"), + 36:("j","J"), + 37:("k","K"), + 38:("l","L"), + 39:(";",":"), + 40:("'","\""), + 41:("`","~"), + # 42 LEFT SHIFT + 43:("\\","|"), + 44:("z","Z"), + 45:("x","X"), + 46:("c","C"), + 47:("v","V"), + 48:("b","B"), + 49:("n","N"), + 50:("m","M"), + 51:(",","<"), + 52:(".",">"), + 53:("/","?"), + # 54 RIGHT SHIFT + 57:(" "," "), + } + + def get_device(self): + devices = [ device for device in listdir(self.input_dir)] + keyboards = [ device for device in devices if 'kbd' in device ] + scanners = [ device for device in devices if ('barcode' in device.lower()) or ('scanner' in device.lower()) ] + if len(scanners) > 0: + return InputDevice(join(self.input_dir,scanners[0])) + elif len(keyboards) > 0: + return InputDevice(join(self.input_dir,keyboards[0])) + else: + return None + + @http.route('/hw_proxy/is_scanner_connected', type='json', auth='admin') + def is_scanner_connected(self): + return self.get_device() != None + + @http.route('/hw_proxy/scanner', type='json', auth='admin') + def scanner(self): + device = self.get_device() + barcode = [] + shift = False + if not device: + return '' + else: + device.grab() + while True: + r,w,x = select([device],[],[],5) + if len(r) == 0: # timeout + device.ungrab() + return '' + for event in device.read(): + if event.type == ecodes.EV_KEY: + if event.value == 1: # keydown events + #print categorize(event) + if event.code in self.keymap: + if shift: + barcode.append(self.keymap[event.code][1]) + else: + barcode.append(self.keymap[event.code][0]) + elif event.code == 42 or event.code == 54: # SHIFT + shift = True + elif event.code == 28: # ENTER + device.ungrab() + barcode = ''.join(barcode) + return barcode + elif event.value == 0: #keyup events + if event.code == 42 or event.code == 54: # LEFT SHIFT + shift = False + + diff --git a/addons/point_of_sale/__openerp__.py b/addons/point_of_sale/__openerp__.py index dfdc2c57cb9..fa42a19e209 100644 --- a/addons/point_of_sale/__openerp__.py +++ b/addons/point_of_sale/__openerp__.py @@ -89,23 +89,17 @@ Main Features 'installable': True, 'application': True, 'js': [ - 'static/lib/mousewheel/jquery.mousewheel-3.0.6.js', + 'static/lib/fastclick.js', 'static/src/js/db.js', 'static/src/js/models.js', 'static/src/js/widget_base.js', 'static/src/js/widget_keyboard.js', - 'static/src/js/widget_scrollbar.js', 'static/src/js/widgets.js', 'static/src/js/devices.js', 'static/src/js/screens.js', 'static/src/js/tests.js', 'static/src/js/main.js', ], - 'css': [ - 'static/src/css/pos.css', # this is the default css with hover effects - #'static/src/css/pos_nohover.css', # this css has no hover effects (for resistive touchscreens) - 'static/src/css/keyboard.css' - ], 'qweb': ['static/src/xml/pos.xml'], 'auto_install': False, } diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py index 2461fe90b79..e6b82b7c74a 100644 --- a/addons/point_of_sale/controllers/main.py +++ b/addons/point_of_sale/controllers/main.py @@ -8,182 +8,78 @@ import random from openerp import http from openerp.http import request -from openerp.addons.web.controllers.main import manifest_list, module_boot, html_template +from openerp.addons.web.controllers.main import manifest_list, module_boot _logger = logging.getLogger(__name__) -class PointOfSaleController(http.Controller): - def __init__(self): - self.scale = 'closed' - self.scale_weight = 0.0 +html_template = """ + + + OpenERP POS - @http.route('/pos/app', type='http', auth='admin') - def app(self): - js = "\n ".join('' % i for i in manifest_list('js',db=request.db)) - css = "\n ".join('' % i for i in manifest_list('css',db=request.db)) + + - cookie = request.httprequest.cookies.get("instance0|session_id") - session_id = cookie.replace("%22","") - template = html_template.replace(' + + - r = template % { + + + + + + + + + + + + %(js)s + + + + + + +""" + +class PosController(http.Controller): + + @http.route('/pos/web', type='http', auth='none') + def a(self, debug=False, **k): + + + js_list = manifest_list('js',db=request.db, debug=debug) + css_list = manifest_list('css',db=request.db, debug=debug) + + print css_list + print js_list + + js = "\n".join('' % i for i in js_list) + #css = "\n".join('' % i for i in css_list) + r = html_template % { 'js': js, - 'css': css, - 'modules': simplejson.dumps(module_boot(request)), - 'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));' + # 'css': css, + 'modules': simplejson.dumps(module_boot(request.db)), + 'init': """ + var wc = new s.web.WebClient(); + wc.show_application = function(){ + wc.action_manager.do_action("pos.ui"); + }; + wc.show_login = function(){ + window.location.href = '/'; + } + wc.appendTo($(document.body)); + """ } return r - @http.route('/pos/manifest',type='http', auth='admin') - def manifest(self): - """ This generates a HTML5 cache manifest files that preloads the categories and products thumbnails - and other ressources necessary for the point of sale to work offline """ - - ml = ["CACHE MANIFEST"] - - # loading all the images in the static/src/img/* directories - def load_css_img(srcdir,dstdir): - for f in os.listdir(srcdir): - path = os.path.join(srcdir,f) - dstpath = os.path.join(dstdir,f) - if os.path.isdir(path) : - load_css_img(path,dstpath) - elif f.endswith(('.png','.PNG','.jpg','.JPG','.jpeg','.JPEG','.gif','.GIF')): - ml.append(dstpath) - - imgdir = openerp.modules.get_module_resource('point_of_sale','static/src/img'); - load_css_img(imgdir,'/point_of_sale/static/src/img') - - products = request.registry.get('product.product') - for p in products.search_read(request.cr, request.uid, [('pos_categ_id','!=',False)], ['name']): - product_id = p['id'] - url = "/web/binary/image?session_id=%s&model=product.product&field=image&id=%s" % (request.session_id, product_id) - ml.append(url) - - categories = request.registry.get('pos.category') - for c in categories.search_read(request.cr, request.uid, [], ['name']): - category_id = c['id'] - url = "/web/binary/image?session_id=%s&model=pos.category&field=image&id=%s" % (request.session_id, category_id) - ml.append(url) - - ml += ["NETWORK:","*"] - m = "\n".join(ml) - - return m - - @http.route('/pos/test_connection', type='json', auth='admin') - def test_connection(self): - _logger.info('Received Connection Test from the Point of Sale'); - - @http.route('/pos/scan_item_success', type='json', auth='admin') - def scan_item_success(self, ean): - """ - A product has been scanned with success - """ - print 'scan_item_success: ' + str(ean) - - @http.route('/pos/scan_item_error_unrecognized', type='json', auth='admin') - def scan_item_error_unrecognized(self, ean): - """ - A product has been scanned without success - """ - print 'scan_item_error_unrecognized: ' + str(ean) - - @http.route('/pos/help_needed', type='json', auth='admin') - def help_needed(self): - """ - The user wants an help (ex: light is on) - """ - print "help_needed" - - @http.route('/pos/help_canceled', type='json', auth='admin') - def help_canceled(self): - """ - The user stops the help request - """ - print "help_canceled" - - @http.route('/pos/weighting_start', type='json', auth='admin') - def weighting_start(self): - if self.scale == 'closed': - print "Opening (Fake) Connection to Scale..." - self.scale = 'open' - self.scale_weight = 0.0 - time.sleep(0.1) - print "... Scale Open." - else: - print "WARNING: Scale already Connected !!!" - - @http.route('/pos/weighting_read_kg', type='json', auth='admin') - def weighting_read_kg(self): - if self.scale == 'open': - print "Reading Scale..." - time.sleep(0.025) - self.scale_weight += 0.01 - print "... Done." - return self.scale_weight - else: - print "WARNING: Reading closed scale !!!" - return 0.0 - - @http.route('/pos/weighting_end', type='json', auth='admin') - def weighting_end(self): - if self.scale == 'open': - print "Closing Connection to Scale ..." - self.scale = 'closed' - self.scale_weight = 0.0 - time.sleep(0.1) - print "... Scale Closed." - else: - print "WARNING: Scale already Closed !!!" - - @http.route('/pos/payment_request', type='json', auth='admin') - def payment_request(self, price): - """ - The PoS will activate the method payment - """ - print "payment_request: price:"+str(price) - return 'ok' - - @http.route('/pos/payment_status', type='json', auth='admin') - def payment_status(self): - print "payment_status" - return { 'status':'waiting' } - - @http.route('/pos/payment_cancel', type='json', auth='admin') - def payment_cancel(self): - print "payment_cancel" - - @http.route('/pos/transaction_start', type='json', auth='admin') - def transaction_start(self): - print 'transaction_start' - - @http.route('/pos/transaction_end', type='json', auth='admin') - def transaction_end(self): - print 'transaction_end' - - @http.route('/pos/cashier_mode_activated', type='json', auth='admin') - def cashier_mode_activated(self): - print 'cashier_mode_activated' - - @http.route('/pos/cashier_mode_deactivated', type='json', auth='admin') - def cashier_mode_deactivated(self): - print 'cashier_mode_deactivated' - - @http.route('/pos/open_cashbox', type='json', auth='admin') - def open_cashbox(self): - print 'open_cashbox' - - @http.route('/pos/print_receipt', type='json', auth='admin') - def print_receipt(self, receipt): - print 'print_receipt' + str(receipt) - - @http.route('/pos/log', type='json', auth='admin') - def log(self, arguments): - _logger.info(' '.join(str(v) for v in arguments)) - - @http.route('/pos/print_pdf_invoice', type='json', auth='admin') - def print_pdf_invoice(self, pdfinvoice): - print 'print_pdf_invoice' + str(pdfinvoice) - - diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index aa6a798d6b9..e5047d8b5aa 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -69,12 +69,16 @@ class pos_config(osv.osv): 'currency_id' : fields.function(_get_currency, type="many2one", string="Currency", relation="res.currency"), 'iface_self_checkout' : fields.boolean('Self Checkout Mode', help="Check this if this point of sale should open by default in a self checkout mode. If unchecked, OpenERP uses the normal cashier mode by default."), - 'iface_cashdrawer' : fields.boolean('Cashdrawer Interface'), - 'iface_payment_terminal' : fields.boolean('Payment Terminal Interface'), - 'iface_electronic_scale' : fields.boolean('Electronic Scale Interface'), - 'iface_vkeyboard' : fields.boolean('Virtual KeyBoard Interface'), - 'iface_print_via_proxy' : fields.boolean('Print via Proxy'), + 'iface_cashdrawer' : fields.boolean('Cashdrawer',help="Automatically open the cashdrawer"), + 'iface_payment_terminal' : fields.boolean('Payment Terminal', help="Enables Payment Terminal integration"), + 'iface_electronic_scale' : fields.boolean('Electronic Scale', help="Enables Electronic Scale integration"), + 'iface_vkeyboard' : fields.boolean('Virtual KeyBoard', help="Enables an integrated Virtual Keyboard"), + 'iface_print_via_proxy' : fields.boolean('Print via Proxy', help="Bypass browser printing and prints via the hardware proxy"), 'iface_invoicing': fields.boolean('Invoicing',help='Enables invoice generation from the Point of Sale'), + 'iface_big_scrollbars': fields.boolean('Large Scrollbars',help='For imprecise industrial touchscreens'), + 'receipt_header': fields.text('Receipt Header',help="A short text that will be inserted as a header in the printed receipt"), + 'receipt_footer': fields.text('Receipt Footer',help="A short text that will be inserted as a footer in the printed receipt"), + 'proxy_ip': fields.char('Hardware Proxy IP Address', size=45), 'state' : fields.selection(POS_CONFIG_STATE, 'Status', required=True, readonly=True), 'sequence_id' : fields.many2one('ir.sequence', 'Order IDs Sequence', readonly=True, @@ -392,10 +396,9 @@ class pos_session(osv.osv): context.update(active_id=this_record.id) return { - 'type' : 'ir.actions.client', - 'name' : _('Start Point Of Sale'), - 'tag' : 'pos.ui', - 'context' : context, + 'type' : 'ir.actions.act_url', + 'url' : '/pos/web/', + 'target': 'self', } def wkf_action_open(self, cr, uid, ids, context=None): @@ -498,10 +501,9 @@ class pos_session(osv.osv): _("You cannot use the session of another users. This session is owned by %s. Please first close this one to use this point of sale." % session.user_id.name)) context.update({'active_id': ids[0]}) return { - 'type' : 'ir.actions.client', - 'name' : _('Start Point Of Sale'), - 'tag' : 'pos.ui', - 'context' : context, + 'type' : 'ir.actions.act_url', + 'target': 'self', + 'url': '/pos/web/', } class pos_order(osv.osv): @@ -634,10 +636,10 @@ class pos_order(osv.osv): 'warehouse_id': fields.related('session_id', 'config_id', 'warehouse_id', relation='stock.warehouse', type='many2one', string='Warehouse', store=True, readonly=True), 'date_order': fields.datetime('Order Date', readonly=True, select=True), 'user_id': fields.many2one('res.users', 'Salesman', help="Person who uses the the cash register. It can be a reliever, a student or an interim employee."), - 'amount_tax': fields.function(_amount_all, string='Taxes', digits_compute=dp.get_precision('Point Of Sale'), multi='all'), + 'amount_tax': fields.function(_amount_all, string='Taxes', digits_compute=dp.get_precision('Account'), multi='all'), 'amount_total': fields.function(_amount_all, string='Total', multi='all'), - 'amount_paid': fields.function(_amount_all, string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits_compute=dp.get_precision('Point Of Sale'), multi='all'), - 'amount_return': fields.function(_amount_all, 'Returned', digits_compute=dp.get_precision('Point Of Sale'), multi='all'), + 'amount_paid': fields.function(_amount_all, string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits_compute=dp.get_precision('Account'), multi='all'), + 'amount_return': fields.function(_amount_all, 'Returned', digits_compute=dp.get_precision('Account'), multi='all'), 'lines': fields.one2many('pos.order.line', 'order_id', 'Order Lines', states={'draft': [('readonly', False)]}, readonly=True), 'statement_ids': fields.one2many('account.bank.statement.line', 'pos_statement_id', 'Payments', states={'draft': [('readonly', False)]}, readonly=True), 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, states={'draft': [('readonly', False)]}, readonly=True), @@ -1240,11 +1242,11 @@ class pos_order_line(osv.osv): 'name': fields.char('Line No', size=32, required=True), 'notice': fields.char('Discount Notice', size=128), 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True, change_default=True), - 'price_unit': fields.float(string='Unit Price', digits=(16, 2)), - 'qty': fields.float('Quantity', digits=(16, 2)), + 'price_unit': fields.float(string='Unit Price', digits_compute=dp.get_precision('Account')), + 'qty': fields.float('Quantity', digits_compute=dp.get_precision('Product UoS')), 'price_subtotal': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal w/o Tax', store=True), 'price_subtotal_incl': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal', store=True), - 'discount': fields.float('Discount (%)', digits=(16, 2)), + 'discount': fields.float('Discount (%)', digits_compute=dp.get_precision('Account')), 'order_id': fields.many2one('pos.order', 'Order Ref', ondelete='cascade'), 'create_date': fields.datetime('Creation Date', readonly=True), } diff --git a/addons/point_of_sale/point_of_sale_view.xml b/addons/point_of_sale/point_of_sale_view.xml index 195b7ce6e0c..e6db84ac567 100644 --- a/addons/point_of_sale/point_of_sale_view.xml +++ b/addons/point_of_sale/point_of_sale_view.xml @@ -744,9 +744,10 @@ - + Start Point of Sale - pos.ui + /pos/web/ + self @@ -786,7 +787,7 @@ - + @@ -796,9 +797,15 @@ - + + + + + + + diff --git a/addons/point_of_sale/static/lib/fastclick.js b/addons/point_of_sale/static/lib/fastclick.js new file mode 100644 index 00000000000..a64edbe1a72 --- /dev/null +++ b/addons/point_of_sale/static/lib/fastclick.js @@ -0,0 +1,777 @@ +/** + * @preserve FastClick: polyfill to remove click delays on browsers with touch UIs. + * + * @version 0.6.11 + * @codingstandard ftlabs-jsv2 + * @copyright The Financial Times Limited [All Rights Reserved] + * @license MIT License (see LICENSE.txt) + */ + +/*jslint browser:true, node:true*/ +/*global define, Event, Node*/ + + +/** + * Instantiate fast-clicking listeners on the specificed layer. + * + * @constructor + * @param {Element} layer The layer to listen on + */ +function FastClick(layer) { + 'use strict'; + var oldOnClick, self = this; + + + /** + * Whether a click is currently being tracked. + * + * @type boolean + */ + this.trackingClick = false; + + + /** + * Timestamp for when when click tracking started. + * + * @type number + */ + this.trackingClickStart = 0; + + + /** + * The element being tracked for a click. + * + * @type EventTarget + */ + this.targetElement = null; + + + /** + * X-coordinate of touch start event. + * + * @type number + */ + this.touchStartX = 0; + + + /** + * Y-coordinate of touch start event. + * + * @type number + */ + this.touchStartY = 0; + + + /** + * ID of the last touch, retrieved from Touch.identifier. + * + * @type number + */ + this.lastTouchIdentifier = 0; + + + /** + * Touchmove boundary, beyond which a click will be cancelled. + * + * @type number + */ + this.touchBoundary = 10; + + + /** + * The FastClick layer. + * + * @type Element + */ + this.layer = layer; + + if (!layer || !layer.nodeType) { + throw new TypeError('Layer must be a document node'); + } + + /** @type function() */ + this.onClick = function() { return FastClick.prototype.onClick.apply(self, arguments); }; + + /** @type function() */ + this.onMouse = function() { return FastClick.prototype.onMouse.apply(self, arguments); }; + + /** @type function() */ + this.onTouchStart = function() { return FastClick.prototype.onTouchStart.apply(self, arguments); }; + + /** @type function() */ + this.onTouchMove = function() { return FastClick.prototype.onTouchMove.apply(self, arguments); }; + + /** @type function() */ + this.onTouchEnd = function() { return FastClick.prototype.onTouchEnd.apply(self, arguments); }; + + /** @type function() */ + this.onTouchCancel = function() { return FastClick.prototype.onTouchCancel.apply(self, arguments); }; + + if (FastClick.notNeeded(layer)) { + return; + } + + // Set up event handlers as required + if (this.deviceIsAndroid) { + layer.addEventListener('mouseover', this.onMouse, true); + layer.addEventListener('mousedown', this.onMouse, true); + layer.addEventListener('mouseup', this.onMouse, true); + } + + layer.addEventListener('click', this.onClick, true); + layer.addEventListener('touchstart', this.onTouchStart, false); + layer.addEventListener('touchmove', this.onTouchMove, false); + layer.addEventListener('touchend', this.onTouchEnd, false); + layer.addEventListener('touchcancel', this.onTouchCancel, false); + + // Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2) + // which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick + // layer when they are cancelled. + if (!Event.prototype.stopImmediatePropagation) { + layer.removeEventListener = function(type, callback, capture) { + var rmv = Node.prototype.removeEventListener; + if (type === 'click') { + rmv.call(layer, type, callback.hijacked || callback, capture); + } else { + rmv.call(layer, type, callback, capture); + } + }; + + layer.addEventListener = function(type, callback, capture) { + var adv = Node.prototype.addEventListener; + if (type === 'click') { + adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) { + if (!event.propagationStopped) { + callback(event); + } + }), capture); + } else { + adv.call(layer, type, callback, capture); + } + }; + } + + // If a handler is already declared in the element's onclick attribute, it will be fired before + // FastClick's onClick handler. Fix this by pulling out the user-defined handler function and + // adding it as listener. + if (typeof layer.onclick === 'function') { + + // Android browser on at least 3.2 requires a new reference to the function in layer.onclick + // - the old one won't work if passed to addEventListener directly. + oldOnClick = layer.onclick; + layer.addEventListener('click', function(event) { + oldOnClick(event); + }, false); + layer.onclick = null; + } +} + + +/** + * Android requires exceptions. + * + * @type boolean + */ +FastClick.prototype.deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0; + + +/** + * iOS requires exceptions. + * + * @type boolean + */ +FastClick.prototype.deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent); + + +/** + * iOS 4 requires an exception for select elements. + * + * @type boolean + */ +FastClick.prototype.deviceIsIOS4 = FastClick.prototype.deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent); + + +/** + * iOS 6.0(+?) requires the target element to be manually derived + * + * @type boolean + */ +FastClick.prototype.deviceIsIOSWithBadTarget = FastClick.prototype.deviceIsIOS && (/OS ([6-9]|\d{2})_\d/).test(navigator.userAgent); + + +/** + * Determine whether a given element requires a native click. + * + * @param {EventTarget|Element} target Target DOM element + * @returns {boolean} Returns true if the element needs a native click + */ +FastClick.prototype.needsClick = function(target) { + 'use strict'; + switch (target.nodeName.toLowerCase()) { + + // Don't send a synthetic click to disabled inputs (issue #62) + case 'button': + case 'select': + case 'textarea': + if (target.disabled) { + return true; + } + + break; + case 'input': + + // File inputs need real clicks on iOS 6 due to a browser bug (issue #68) + if ((this.deviceIsIOS && target.type === 'file') || target.disabled) { + return true; + } + + break; + case 'label': + case 'video': + return true; + } + + return (/\bneedsclick\b/).test(target.className); +}; + + +/** + * Determine whether a given element requires a call to focus to simulate click into element. + * + * @param {EventTarget|Element} target Target DOM element + * @returns {boolean} Returns true if the element requires a call to focus to simulate native click. + */ +FastClick.prototype.needsFocus = function(target) { + 'use strict'; + switch (target.nodeName.toLowerCase()) { + case 'textarea': + return true; + case 'select': + return !this.deviceIsAndroid; + case 'input': + switch (target.type) { + case 'button': + case 'checkbox': + case 'file': + case 'image': + case 'radio': + case 'submit': + return false; + } + + // No point in attempting to focus disabled inputs + return !target.disabled && !target.readOnly; + default: + return (/\bneedsfocus\b/).test(target.className); + } +}; + + +/** + * Send a click event to the specified element. + * + * @param {EventTarget|Element} targetElement + * @param {Event} event + */ +FastClick.prototype.sendClick = function(targetElement, event) { + 'use strict'; + var clickEvent, touch; + + // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24) + if (document.activeElement && document.activeElement !== targetElement) { + document.activeElement.blur(); + } + + touch = event.changedTouches[0]; + + // Synthesise a click event, with an extra attribute so it can be tracked + clickEvent = document.createEvent('MouseEvents'); + clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); + clickEvent.forwardedTouchEvent = true; + targetElement.dispatchEvent(clickEvent); +}; + +FastClick.prototype.determineEventType = function(targetElement) { + 'use strict;' + + //Issue #159: Android Chrome Select Box does not open with a synthetic click event + if (this.deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') { + return 'mousedown'; + } + + return 'click'; +} + + +/** + * @param {EventTarget|Element} targetElement + */ +FastClick.prototype.focus = function(targetElement) { + 'use strict'; + var length; + + // Issue #160: on iOS 7, some input elements (e.g. date datetime) throw a vague TypeError on setSelectionRange. These elements don't have an integer value for the selectionStart and selectionEnd properties, but unfortunately that can't be used for detection because accessing the properties also throws a TypeError. Just check the type instead. Filed as Apple bug #15122724. + if (this.deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time') { + length = targetElement.value.length; + targetElement.setSelectionRange(length, length); + } else { + targetElement.focus(); + } +}; + + +/** + * Check whether the given target element is a child of a scrollable layer and if so, set a flag on it. + * + * @param {EventTarget|Element} targetElement + */ +FastClick.prototype.updateScrollParent = function(targetElement) { + 'use strict'; + var scrollParent, parentElement; + + scrollParent = targetElement.fastClickScrollParent; + + // Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the + // target element was moved to another parent. + if (!scrollParent || !scrollParent.contains(targetElement)) { + parentElement = targetElement; + do { + if (parentElement.scrollHeight > parentElement.offsetHeight) { + scrollParent = parentElement; + targetElement.fastClickScrollParent = parentElement; + break; + } + + parentElement = parentElement.parentElement; + } while (parentElement); + } + + // Always update the scroll top tracker if possible. + if (scrollParent) { + scrollParent.fastClickLastScrollTop = scrollParent.scrollTop; + } +}; + + +/** + * @param {EventTarget} targetElement + * @returns {Element|EventTarget} + */ +FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) { + 'use strict'; + + // On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node. + if (eventTarget.nodeType === Node.TEXT_NODE) { + return eventTarget.parentNode; + } + + return eventTarget; +}; + + +/** + * On touch start, record the position and scroll offset. + * + * @param {Event} event + * @returns {boolean} + */ +FastClick.prototype.onTouchStart = function(event) { + 'use strict'; + var targetElement, touch, selection; + + // Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111). + if (event.targetTouches.length > 1) { + return true; + } + + targetElement = this.getTargetElementFromEventTarget(event.target); + touch = event.targetTouches[0]; + + if (this.deviceIsIOS) { + + // Only trusted events will deselect text on iOS (issue #49) + selection = window.getSelection(); + if (selection.rangeCount && !selection.isCollapsed) { + return true; + } + + if (!this.deviceIsIOS4) { + + // Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23): + // when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched + // with the same identifier as the touch event that previously triggered the click that triggered the alert. + // Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an + // immediately preceeding touch event (issue #52), so this fix is unavailable on that platform. + if (touch.identifier === this.lastTouchIdentifier) { + event.preventDefault(); + return false; + } + + this.lastTouchIdentifier = touch.identifier; + + // If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and: + // 1) the user does a fling scroll on the scrollable layer + // 2) the user stops the fling scroll with another tap + // then the event.target of the last 'touchend' event will be the element that was under the user's finger + // when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check + // is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42). + this.updateScrollParent(targetElement); + } + } + + this.trackingClick = true; + this.trackingClickStart = event.timeStamp; + this.targetElement = targetElement; + + this.touchStartX = touch.pageX; + this.touchStartY = touch.pageY; + + // Prevent phantom clicks on fast double-tap (issue #36) + if ((event.timeStamp - this.lastClickTime) < 200) { + event.preventDefault(); + } + + return true; +}; + + +/** + * Based on a touchmove event object, check whether the touch has moved past a boundary since it started. + * + * @param {Event} event + * @returns {boolean} + */ +FastClick.prototype.touchHasMoved = function(event) { + 'use strict'; + var touch = event.changedTouches[0], boundary = this.touchBoundary; + + if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) { + return true; + } + + return false; +}; + + +/** + * Update the last position. + * + * @param {Event} event + * @returns {boolean} + */ +FastClick.prototype.onTouchMove = function(event) { + 'use strict'; + if (!this.trackingClick) { + return true; + } + + // If the touch has moved, cancel the click tracking + if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) { + this.trackingClick = false; + this.targetElement = null; + } + + return true; +}; + + +/** + * Attempt to find the labelled control for the given label element. + * + * @param {EventTarget|HTMLLabelElement} labelElement + * @returns {Element|null} + */ +FastClick.prototype.findControl = function(labelElement) { + 'use strict'; + + // Fast path for newer browsers supporting the HTML5 control attribute + if (labelElement.control !== undefined) { + return labelElement.control; + } + + // All browsers under test that support touch events also support the HTML5 htmlFor attribute + if (labelElement.htmlFor) { + return document.getElementById(labelElement.htmlFor); + } + + // If no for attribute exists, attempt to retrieve the first labellable descendant element + // the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label + return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea'); +}; + + +/** + * On touch end, determine whether to send a click event at once. + * + * @param {Event} event + * @returns {boolean} + */ +FastClick.prototype.onTouchEnd = function(event) { + 'use strict'; + var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement; + + if (!this.trackingClick) { + return true; + } + + // Prevent phantom clicks on fast double-tap (issue #36) + if ((event.timeStamp - this.lastClickTime) < 200) { + this.cancelNextClick = true; + return true; + } + + // Reset to prevent wrong click cancel on input (issue #156). + this.cancelNextClick = false; + + this.lastClickTime = event.timeStamp; + + trackingClickStart = this.trackingClickStart; + this.trackingClick = false; + this.trackingClickStart = 0; + + // On some iOS devices, the targetElement supplied with the event is invalid if the layer + // is performing a transition or scroll, and has to be re-detected manually. Note that + // for this to function correctly, it must be called *after* the event target is checked! + // See issue #57; also filed as rdar://13048589 . + if (this.deviceIsIOSWithBadTarget) { + touch = event.changedTouches[0]; + + // In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null + targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement; + targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent; + } + + targetTagName = targetElement.tagName.toLowerCase(); + if (targetTagName === 'label') { + forElement = this.findControl(targetElement); + if (forElement) { + this.focus(targetElement); + if (this.deviceIsAndroid) { + return false; + } + + targetElement = forElement; + } + } else if (this.needsFocus(targetElement)) { + + // Case 1: If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through. + // Case 2: Without this exception for input elements tapped when the document is contained in an iframe, then any inputted text won't be visible even though the value attribute is updated as the user types (issue #37). + if ((event.timeStamp - trackingClickStart) > 100 || (this.deviceIsIOS && window.top !== window && targetTagName === 'input')) { + this.targetElement = null; + return false; + } + + this.focus(targetElement); + + // Select elements need the event to go through on iOS 4, otherwise the selector menu won't open. + if (!this.deviceIsIOS4 || targetTagName !== 'select') { + this.targetElement = null; + event.preventDefault(); + } + + return false; + } + + if (this.deviceIsIOS && !this.deviceIsIOS4) { + + // Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled + // and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42). + scrollParent = targetElement.fastClickScrollParent; + if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) { + return true; + } + } + + // Prevent the actual click from going though - unless the target node is marked as requiring + // real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted. + if (!this.needsClick(targetElement)) { + event.preventDefault(); + this.sendClick(targetElement, event); + } + + return false; +}; + + +/** + * On touch cancel, stop tracking the click. + * + * @returns {void} + */ +FastClick.prototype.onTouchCancel = function() { + 'use strict'; + this.trackingClick = false; + this.targetElement = null; +}; + + +/** + * Determine mouse events which should be permitted. + * + * @param {Event} event + * @returns {boolean} + */ +FastClick.prototype.onMouse = function(event) { + 'use strict'; + + // If a target element was never set (because a touch event was never fired) allow the event + if (!this.targetElement) { + return true; + } + + if (event.forwardedTouchEvent) { + return true; + } + + // Programmatically generated events targeting a specific element should be permitted + if (!event.cancelable) { + return true; + } + + // Derive and check the target element to see whether the mouse event needs to be permitted; + // unless explicitly enabled, prevent non-touch click events from triggering actions, + // to prevent ghost/doubleclicks. + if (!this.needsClick(this.targetElement) || this.cancelNextClick) { + + // Prevent any user-added listeners declared on FastClick element from being fired. + if (event.stopImmediatePropagation) { + event.stopImmediatePropagation(); + } else { + + // Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2) + event.propagationStopped = true; + } + + // Cancel the event + event.stopPropagation(); + event.preventDefault(); + + return false; + } + + // If the mouse event is permitted, return true for the action to go through. + return true; +}; + + +/** + * On actual clicks, determine whether this is a touch-generated click, a click action occurring + * naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or + * an actual click which should be permitted. + * + * @param {Event} event + * @returns {boolean} + */ +FastClick.prototype.onClick = function(event) { + 'use strict'; + var permitted; + + // It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early. + if (this.trackingClick) { + this.targetElement = null; + this.trackingClick = false; + return true; + } + + // Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target. + if (event.target.type === 'submit' && event.detail === 0) { + return true; + } + + permitted = this.onMouse(event); + + // Only unset targetElement if the click is not permitted. This will ensure that the check for !targetElement in onMouse fails and the browser's click doesn't go through. + if (!permitted) { + this.targetElement = null; + } + + // If clicks are permitted, return true for the action to go through. + return permitted; +}; + + +/** + * Remove all FastClick's event listeners. + * + * @returns {void} + */ +FastClick.prototype.destroy = function() { + 'use strict'; + var layer = this.layer; + + if (this.deviceIsAndroid) { + layer.removeEventListener('mouseover', this.onMouse, true); + layer.removeEventListener('mousedown', this.onMouse, true); + layer.removeEventListener('mouseup', this.onMouse, true); + } + + layer.removeEventListener('click', this.onClick, true); + layer.removeEventListener('touchstart', this.onTouchStart, false); + layer.removeEventListener('touchmove', this.onTouchMove, false); + layer.removeEventListener('touchend', this.onTouchEnd, false); + layer.removeEventListener('touchcancel', this.onTouchCancel, false); +}; + + +/** + * Check whether FastClick is needed. + * + * @param {Element} layer The layer to listen on + */ +FastClick.notNeeded = function(layer) { + 'use strict'; + var metaViewport; + + // Devices that don't support touch don't need FastClick + if (typeof window.ontouchstart === 'undefined') { + return true; + } + + if ((/Chrome\/[0-9]+/).test(navigator.userAgent)) { + + // Chrome on Android with user-scalable="no" doesn't need FastClick (issue #89) + if (FastClick.prototype.deviceIsAndroid) { + metaViewport = document.querySelector('meta[name=viewport]'); + if (metaViewport && metaViewport.content.indexOf('user-scalable=no') !== -1) { + return false; //WIP FIX + } + + // Chrome desktop doesn't need FastClick (issue #15) + } else { + return true; + } + } + + // IE10 with -ms-touch-action: none, which disables double-tap-to-zoom (issue #97) + if (layer.style.msTouchAction === 'none') { + return true; + } + + return false; +}; + + +/** + * Factory method for creating a FastClick object + * + * @param {Element} layer The layer to listen on + */ +FastClick.attach = function(layer) { + 'use strict'; + return new FastClick(layer); +}; + + +if (typeof define !== 'undefined' && define.amd) { + + // AMD. Register as an anonymous module. + define(function() { + 'use strict'; + return FastClick; + }); +} else if (typeof module !== 'undefined' && module.exports) { + module.exports = FastClick.attach; + module.exports.FastClick = FastClick; +} else { + window.FastClick = FastClick; +} diff --git a/addons/point_of_sale/static/lib/mousewheel/jquery.mousewheel-3.0.6.js b/addons/point_of_sale/static/lib/mousewheel/jquery.mousewheel-3.0.6.js deleted file mode 100644 index 38b60951b20..00000000000 --- a/addons/point_of_sale/static/lib/mousewheel/jquery.mousewheel-3.0.6.js +++ /dev/null @@ -1,84 +0,0 @@ -/*! Copyright (c) 2011 Brandon Aaron (http://brandonaaron.net) - * Licensed under the MIT License (LICENSE.txt). - * - * Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers. - * Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix. - * Thanks to: Seamus Leahy for adding deltaX and deltaY - * - * Version: 3.0.6 - * - * Requires: 1.2.2+ - */ - -(function($) { - -var types = ['DOMMouseScroll', 'mousewheel']; - -if ($.event.fixHooks) { - for ( var i=types.length; i; ) { - $.event.fixHooks[ types[--i] ] = $.event.mouseHooks; - } -} - -$.event.special.mousewheel = { - setup: function() { - if ( this.addEventListener ) { - for ( var i=types.length; i; ) { - this.addEventListener( types[--i], handler, false ); - } - } else { - this.onmousewheel = handler; - } - }, - - teardown: function() { - if ( this.removeEventListener ) { - for ( var i=types.length; i; ) { - this.removeEventListener( types[--i], handler, false ); - } - } else { - this.onmousewheel = null; - } - } -}; - -$.fn.extend({ - mousewheel: function(fn) { - return fn ? this.bind("mousewheel", fn) : this.trigger("mousewheel"); - }, - - unmousewheel: function(fn) { - return this.unbind("mousewheel", fn); - } -}); - - -function handler(event) { - var orgEvent = event || window.event, args = [].slice.call( arguments, 1 ), delta = 0, returnValue = true, deltaX = 0, deltaY = 0; - event = $.event.fix(orgEvent); - event.type = "mousewheel"; - - // Old school scrollwheel delta - if ( orgEvent.wheelDelta ) { delta = orgEvent.wheelDelta/120; } - if ( orgEvent.detail ) { delta = -orgEvent.detail/3; } - - // New school multidimensional scroll (touchpads) deltas - deltaY = delta; - - // Gecko - if ( orgEvent.axis !== undefined && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) { - deltaY = 0; - deltaX = -1*delta; - } - - // Webkit - if ( orgEvent.wheelDeltaY !== undefined ) { deltaY = orgEvent.wheelDeltaY/120; } - if ( orgEvent.wheelDeltaX !== undefined ) { deltaX = -1*orgEvent.wheelDeltaX/120; } - - // Add event and delta to the front of the arguments - args.unshift(event, delta, deltaX, deltaY); - - return ($.event.dispatch || $.event.handle).apply(this, args); -} - -})(jQuery); diff --git a/addons/point_of_sale/static/src/css/keyboard.css b/addons/point_of_sale/static/src/css/keyboard.css index d5ce468d7be..36cbea09368 100644 --- a/addons/point_of_sale/static/src/css/keyboard.css +++ b/addons/point_of_sale/static/src/css/keyboard.css @@ -1,6 +1,6 @@ /* Onscreen Keyboard http://net.tutsplus.com/tutorials/javascript-ajax/creating-a-keyboard-with-css-and-jquery/ */ -.point-of-sale .keyboard_frame{ +.pos .keyboard_frame{ display: none; position:absolute; left: 0; @@ -12,7 +12,6 @@ height: 0px; /* 235px, animated via jquery */ background-color: #BBB; overflow:hidden; - -webkit-box-shadow: 0px 0px 10px rgba(0,0,0, 0.3); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; @@ -22,35 +21,33 @@ z-index:10000; } -.point-of-sale .keyboard_frame .close_button{ +.pos .keyboard_frame .close_button{ height:40px; width:60px; text-align:center; background-color: #DDD; font-size: 12px; line-height:40px; - border: 1px solid #CCC; -webkit-border-radius: 5px; - -webkit-box-shadow: 0px 2px 5px rgba(0,0,0, 0.2); position:absolute; top:0; right:15px; cursor: pointer; } -.point-of-sale .keyboard li { +.pos .keyboard li { float: left; text-align: center; background-color: #fff; border: 1px solid #f0f0f0; top:0; + cursor: pointer; -moz-border-radius: 5px; -webkit-border-radius: 5px; - -webkit-box-shadow: 0px 2px 5px rgba(0,0,0, 0.2); -webkit-transition-property: top, background-color; -webkit-transition-duration: 0.2s; -webkit-transition-timing-function: linear; } -.point-of-sale .keyboard li:hover { +.pos .keyboard li:active{ position: relative; top: 2px; left: 0px; @@ -61,22 +58,22 @@ -webkit-transition-duration: 0.1s; -webkit-transition-timing-function: ease-out; } -.point-of-sale .uppercase { +.pos .uppercase { text-transform: uppercase; } -.point-of-sale .on { +.pos .on { display: none; } -.point-of-sale .firstitem{ +.pos .firstitem{ clear: left; } -.point-of-sale .keyboard .lastitem { +.pos .keyboard .lastitem { margin-right: 0 !important; } /* ---- full sized keyboard ---- */ -.point-of-sale .full_keyboard { +.pos .full_keyboard { list-style: none; font-size: 14px; width: 685px; @@ -84,35 +81,35 @@ margin-left: auto !important; margin-right: auto !important; } -.point-of-sale .full_keyboard li{ +.pos .full_keyboard li{ margin: 0 5px 5px 0 !important; width: 40px; height: 40px; line-height: 40px; } -.point-of-sale .full_keyboard .tab, .point-of-sale .full_keyboard .delete { +.pos .full_keyboard .tab, .pos .full_keyboard .delete { width: 70px; } -.point-of-sale .full_keyboard .capslock { +.pos .full_keyboard .capslock { width: 80px; } -.point-of-sale .full_keyboard .return { +.pos .full_keyboard .return { width: 77px; } -.point-of-sale .full_keyboard .left-shift { +.pos .full_keyboard .left-shift { width: 95px; } -.point-of-sale .full_keyboard .right-shift { +.pos .full_keyboard .right-shift { width: 109px; } -.point-of-sale .full_keyboard .space { +.pos .full_keyboard .space { clear: left; width: 673px; } /* ---- simplified keyboard ---- */ -.point-of-sale .simple_keyboard { +.pos .simple_keyboard { list-style: none; font-size: 16px; width: 555px; @@ -120,27 +117,27 @@ margin-left: auto !important; margin-right: auto !important; } -.point-of-sale .simple_keyboard li{ +.pos .simple_keyboard li{ margin: 0 5px 5px 0 !important; width: 49px; height: 49px; line-height: 49px; } -.point-of-sale .simple_keyboard .firstitem.row_asdf{ +.pos .simple_keyboard .firstitem.row_asdf{ margin-left:25px !important; } -.point-of-sale .simple_keyboard .firstitem.row_zxcv{ +.pos .simple_keyboard .firstitem.row_zxcv{ margin-left:55px !important; } -.point-of-sale .simple_keyboard .delete{ +.pos .simple_keyboard .delete{ width: 103px; } -.point-of-sale .simple_keyboard .return{ +.pos .simple_keyboard .return{ width: 103px; } -.point-of-sale .simple_keyboard .space{ +.pos .simple_keyboard .space{ width:273px; } -.point-of-sale .simple_keyboard .numlock{ +.pos .simple_keyboard .numlock{ width:103px; } diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 3a3a8f23231..a44f14ceb6a 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -1,9 +1,79 @@ +/* --- Fonts --- */ + @font-face{ font-family: 'Inconsolata'; src: url(../fonts/Inconsolata.otf); } -.point-of-sale { +/* --- Styling of OpenERP Elements --- */ +.ui-dialog{ + background: white; + padding: 10px; + border-radius: 3px; + font-family: sans-serif; + box-shadow: 0px 10px 40px rgba(0,0,0,0.4); +} +.ui-dialog button{ + padding: 8px; + min-width: 48px; +} +.ui-dialog .ui-icon-closethick{ + float: right; +} +/* --- Generic Restyling and Resets --- */ + +html { + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + font-family: sans-serif; +} +table { + border-spacing: 0px; + border-collapse: collapse; +} +td { + padding: 0px; +} + +.oe_hidden{ + display: none !important; +} +.clearfix:after { + content:" "; + display: block; + visibility: hidden; + line-height: 0; + height: 0; + clear: both; +} + + +.pos input::-webkit-outer-spin-button, +.pos input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.pos button{ + box-shadow: none; + outline: none; + border: none; + font-family: 'Lato'; +} +.pos button:hover{ + background: default; +} + +.pos .oe_hidden{ + display: none !important; +} + +.pos ul, .pos li { + margin: 0; + padding: 0; + list-style-type: none; +} + +.pos { padding: 0; margin: 0; background-color: #f0eeee; @@ -15,49 +85,16 @@ top: 0; width: 100%; height: 100%; - text-shadow: none; -webkit-user-select: none; -moz-user-select: none; user-select: none; -} - -.point-of-sale .oe_hidden{ - display: none !important; -} - -.point-of-sale ul, .point-of-sale li { - margin: 0; - padding: 0; - list-style-type: none; -} - -.point-of-sale .shadow-top{ - position: absolute; - top:0; - left:0; - right:0; - height:10px; - background: -webkit-linear-gradient(top,rgba(0,0,0,0.09),rgba(0,0,0,0)); - background: -moz-linear-gradient(top,rgba(0,0,0,0.09),rgba(0,0,0,0)); - background: -ms-linear-gradient(top,rgba(0,0,0,0.09),rgba(0,0,0,0)); - background: linear-gradient(top,rgba(0,0,0,0.09),rgba(0,0,0,0)); -} - -.point-of-sale .darker-shadow-top{ - position: absolute; - top:0; - left:0; - right:0; - height:10px; - background: -webkit-linear-gradient(top,rgba(0,0,0,0.15),rgba(0,0,0,0)); - background: -moz-linear-gradient(top,rgba(0,0,0,0.15),rgba(0,0,0,0)); - background: -ms-linear-gradient(top,rgba(0,0,0,0.15),rgba(0,0,0,0)); - background: linear-gradient(top,rgba(0,0,0,0.15),rgba(0,0,0,0)); + text-shadow: none; + overflow: hidden; } /* ********* The black loading screen ********* */ -.point-of-sale .loader{ +.pos .loader{ background-color: #222; position:absolute; left:0px; @@ -66,46 +103,144 @@ height:100%; z-index: 999; text-align: center; + font-family: Lato; + color: #555555; +} + +.pos .loader-feedback{ + width: 400px; + height: 160px; + margin: -60px -200px; + position: absolute; + left: 50%; top: 50%; + text-align: center; +} +.pos .loader-feedback h1{ + font-weight: 300; +} +.pos .loader-feedback .progressbar{ + background: rgb(73,73,73); + height: 1px; +} +.pos .loader-feedback .progressbar > .progress{ + height: 100%; + background: white; + width: 0%; + box-shadow: 0px 0px 5px rgba(255,255,255,0.35); +} +.pos .loader-feedback .button{ + display: inline-block; + margin: 25px auto; + line-height: 42px; + padding: 0px 16px; + font-size: 20px; + font-weight: 300; + border: solid 1px; + border-radius: 5px; + cursor: pointer; +} +.pos .loader-feedback .button:active{ + color: #222; + background: #555555; +} +/* ********* Generic Layout Constructs ********* */ + +.pos .window{ + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + display: table; + border: none; +} +.pos .window .subwindow{ + display: table-row; + width: 100%; + height: 100%; +} +.pos .window .subwindow.collapsed{ + height: 0px; +} +.pos .subwindow .subwindow-container{ + display: table-cell; + position: relative; +} +/* firefox seems to ignore the relative positionning of the subwindow-container + * putting this inside subwindow-container fixes it. + */ +.pos .subwindow .subwindow-container-fix{ + height: 100%; + position: relative; +} + +/* ---- Scrollers ----- */ + +.pos .scroller-container{ + position: absolute; + top: 0px; + left: 0px; + right: 0px; + bottom: 0px; +} +.pos .scroller{ + width: 100%; + height: 100%; + overflow: hidden; + overflow-y: auto; + -webkit-overflow-scrolling: touch; +} +.pos .scroller.horizontal{ + overflow-y: hidden; + overflow-x: auto; +} +.pos .scroller-content{ + -webkit-transform: translate3d(0,0,0); +} +.pos .scroller-container ::-webkit-scrollbar{ + width: 10px; + height: 10px; +} +.pos .scroller-container ::-webkit-scrollbar-track{ + background: rgb(224,224,224); + border-left: solid 1px rgb(200,200,200); + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} +.pos .scroller-container ::-webkit-scrollbar-thumb{ + background: rgb(168,168,168); + min-height: 30px; } /* ********* Generic element styling ********* */ -.point-of-sale a { +.pos a { text-decoration: none; color: #555555; } -.point-of-sale button, .point-of-sale a.button { +.pos button, .pos a.button { display: inline-block; cursor: pointer; padding: 4px 10px; font-size: 11px; border: 1px solid #cacaca; - border-radius: 4px; background: #e2e2e2; - background: -webkit-gradient(linear, left top, left bottom, from(#f0f0f0), to(#e2e2e2)); - background: -moz-linear-gradient(#f0f0f0, #e2e2e2); - background: -ms-linear-gradient(#f0f0f0, #e2e2e2); - background: linear-gradient(#f0f0f0, #e2e2e2); + border-radius: 3px; } -.point-of-sale ul, .point-of-sale ol { +.pos ul, .pos ol { padding: 0; margin: 0; } -.point-of-sale li { +.pos li { list-style-type: none; } -.point-of-sale .pos-right-align { +.pos .pos-right-align { text-align: right; } -.point-of-sale .pos-center-align { +.pos .pos-center-align { text-align: center; } -.point-of-sale .pos-right-align input { - text-align: right; - border: 1px solid #cecbcb; - border-radius: 4px; -} -.point-of-sale .pos-disc-font { +.pos .pos-disc-font { font-size: 12px; font-style:italic; color: #808080; @@ -114,7 +249,7 @@ /* ********* The black header bar ********* */ -.point-of-sale #topheader { +.pos .pos-topheader { position:absolute; left:0; top:0; @@ -124,14 +259,11 @@ padding:0; color: gray; background: #393939; - background: -moz-linear-gradient(#7b7979, #393939); - background: -webkit-gradient(linear, left top, left bottom, from(#7b7979), to(#393939)); - z-index: 2; /* Customize according bootstrap3 navbar */ } /* a) The left part of the top-bar */ -.point-of-sale #branding{ +.pos .pos-branding{ position: absolute; display: table-cell; left:0; @@ -145,13 +277,13 @@ line-height:100%; vertical-align: middle; } -.point-of-sale #branding img { +.pos .pos-logo { height: 32px; width: 116px; margin-left:5px; vertical-align:middle; } -.point-of-sale #branding .username{ +.pos .pos-branding .username{ float:right; color:#DDD; font-size:16px; @@ -162,7 +294,7 @@ /* b) The right part of the top-bar */ -.point-of-sale #rightheader { +.pos .pos-rightheader { position: absolute; left:440px; right:0; @@ -170,54 +302,45 @@ height:100%; } -.point-of-sale #rightheader button { +.pos .order-button{ color: #273072; height:27px; margin:3px; margin-right:0px; border: 1px solid #353A7E; background: #7f82ac; - background: -webkit-gradient(linear, left top, left bottom, from(#b2b3d7), to(#7f82ac)); - background: -moz-linear-gradient(#b2b3d7, #7f82ac); - background: -ms-linear-gradient(#b2b3d7, #7f82ac); - background: linear-gradient(#b2b3d7, #7f82ac); + text-shadow: none; + border-radius: 3px; +} +.pos .order-button.selected{ + font-weight: 900; + background: #666997; + color: rgb(236, 237, 255); } -.point-of-sale #rightheader button.square{ +.pos .order-button.square{ width: 32px; margin-left:4px; } -.point-of-sale div#order-selector { - display: inline; +.pos .order-selector { + display: inline-block; } -.point-of-sale ol#orders { - display: inline; +.pos .orders { + display: inline-block; + vertical-align: top; margin-left: 8px; } -.point-of-sale li.order-selector-button { - display: inline; -} -.point-of-sale li.selected-order button { - font-weight: 900; - background: #7174A8 !important; - color: rgb(236, 237, 255) !important; - text-shadow: 0px 1px rgba(0, 0, 0, 0.31); - -webkit-box-shadow: 0px 1px 2px rgb(63, 66, 139) inset; - -moz-box-shadow: 0px 1px 2px rgb(63, 66, 139) inset; - -ms-box-shadow: 0px 1px 2px rgb(63, 66, 139) inset; - box-shadow: 0px 1px 2px rgb(63, 66, 139) inset; -} /* c) The session buttons */ -.point-of-sale #rightheader .header-button{ +.pos .pos-rightheader .header-button{ float:right; height:32px; padding-left:10px; padding-right:10px; - border-right: 1px solid #3a3a3a; - border-left: 1px solid #3a3a3a; + border-right: 1px solid #292929; + border-left: 1px solid #292929; color:#DDD; line-height:32px; text-align:center; @@ -227,18 +350,17 @@ -webkit-transition-duration: 0.2s; -webkit-transition-timing-function: ease-out; } -.point-of-sale #rightheader .header-button:last-child{ +.pos .pos-rightheader .header-button:last-child{ border-left: 1px solid #3a3a3a; } -.point-of-sale #rightheader .header-button:active{ +.pos .pos-rightheader .header-button:active{ background: rgba(0,0,0,0.2); - text-shadow: #000 0px 0px 3px; color:#EEE; } /* c) The notifications indicator */ -.point-of-sale .oe_pos_synch-notification{ +.pos .oe_pos_synch-notification{ float:right; color: rgba(255,255,255,0.4); padding: 8px; @@ -248,17 +370,17 @@ font-style: italic; cursor:pointer; } -.point-of-sale .oe_pos_synch-notification.oe_inactive{ +.pos .oe_pos_synch-notification.oe_inactive{ cursor: default; } -.point-of-sale .oe_pos_synch-notification .oe_status_red{ +.pos .oe_pos_synch-notification .oe_status_red{ display:inline-block; cursor:pointer; width:16px; height:16px; background: url("../img/gtk-no.png") no-repeat ; } -.point-of-sale .oe_pos_synch-notification .oe_status_green{ +.pos .oe_pos_synch-notification .oe_status_green{ display:inline-block; width:16px; height:16px; background: url("../img/gtk-yes.png") no-repeat; @@ -266,7 +388,7 @@ /* ********* Contains everything below the bar ********* */ -.point-of-sale #content { +.pos .pos-content { width: 100%; position: absolute; top: 34px; @@ -276,7 +398,7 @@ /* ********* The leftpane contains the order, numpad and paypad ********* */ -.point-of-sale #leftpane { +.pos .pos-leftpane { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -ms-box-sizing: border-box; @@ -285,23 +407,17 @@ left:0; width:440px; top:0px; - /*bottom:105px;*/ bottom:0; border-right: solid 1px #CECBCB; - background-color: white; } -.point-of-sale #leftpane footer { - position: absolute; - bottom: 0; - left: 0; - width: 100%; +.pos .pos-leftpane .pads { background: #F0EEEE; white-space: nowrap; } /* ********* The paypad contains the payment buttons ********* */ -.point-of-sale #paypad { +.pos .paypad { padding: 8px 4px 8px 8px; display: inline-block; text-align: center; @@ -311,7 +427,7 @@ overflow-y: auto; overflow-x: hidden; } -.point-of-sale #paypad button { +.pos .paypad button { height: 39px; display: block; width: 100%; @@ -319,54 +435,42 @@ font-weight: bold; vertical-align: middle; color: #555555; - border-top: 1px solid #efefef; font-size: 14px; } -.point-of-sale #paypad button:active, -.point-of-sale #numpad button:active, -.point-of-sale #numpad .selected-mode, -.point-of-sale .popup button:active{ +.pos .paypad button:active, +.pos .numpad button:active, +.pos .numpad .selected-mode, +.pos .popup button:active{ border: none; color: white; background: #7f82ac; - background: -webkit-gradient(linear, left top, left bottom, from(#9d9fc5), to(#7f82ac)); - background: -moz-linear-gradient(#9d9fc5, #7f82ac); - background: -ms-linear-gradient(#9d9fc5, #7f82ac); - background: linear-gradient(#9d9fc5, #7f82ac); - text-shadow: 0px 0px 5px rgba(255,255,255,0.21); - -webkit-box-shadow: 0px 0px 2px rgba(0,0,0,0.3) inset, 0px 1px 2px rgba(0,0,0,0.1); - -moz-box-shadow: 0px 0px 2px rgba(0,0,0,0.3) inset, 0px 1px 2px rgba(0,0,0,0.1); - -ms-box-shadow: 0px 0px 2px rgba(0,0,0,0.3) inset, 0px 1px 2px rgba(0,0,0,0.1); - -o-box-shadow: 0px 0px 2px rgba(0,0,0,0.3) inset, 0px 1px 2px rgba(0,0,0,0.1); - box-shadow: 0px 0px 2px rgba(0,0,0,0.3) inset, 0px 1px 2px rgba(0,0,0,0.1); } /* ********* The Numpad ********* */ -.point-of-sale #numpad { +.pos .numpad { padding: 8px 8px 8px 4px; display: inline-block; text-align: center; } -.point-of-sale #numpad button { +.pos .numpad button { height: 50px; width: 50px; margin: 0px 0px 4px 0px; font-weight: bold; vertical-align: middle; color: #555555; - border-top: 1px solid #efefef; } -.point-of-sale .input-button { +.pos .input-button { font-size: 24px; } -.point-of-sale .mode-button, .point-of-sale #numpad-delete, .point-of-sale #numpad-minus { +.pos .mode-button { font-size: 14px; } /* ********* The right pane contains the screens and headers ********* */ -.point-of-sale #rightpane { +.pos .rightpane { position: absolute; top: 0; /*bottom: 105px;*/ @@ -374,24 +478,26 @@ left: 440px; right: 0; vertical-align: top; + -webkit-transform: translate3d(0,0,0); } -.point-of-sale #rightpane header { +.pos .rightpane-header { padding: 0; - height: 32px; + height: 48px; border-bottom: 1px solid #c7c7c7; background: #d3d3d3; - background: -moz-linear-gradient(white, #d3d3d3); - background: -webkit-gradient(linear, left top, left bottom, from(white), to(#d3d3d3)); + text-align: center; } /* ********* The product list ********* */ -.point-of-sale .product-list { - padding:10px !important; +.pos .product-list { + padding: 10px; + text-align: left; + -webkit-transform: translate3d(0,0,0); } -.point-of-sale .product-list-scroller{ +.pos .product-list-scroller{ -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -ms-box-sizing: border-box; @@ -399,8 +505,12 @@ width:100%; height:100%; overflow: hidden; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + -webkit-transform: translate3d(0,0,0); + } -.point-of-sale .product-list-container { +.pos .product-list-container { position:absolute; top:0px; bottom:0px; @@ -410,85 +520,89 @@ /* a) the product list navigation bar */ -.point-of-sale .breadcrumb li { - float: left; - line-height: 32px; - height: 32px; +.pos .breadcrumbs{ + display: inline-block; + text-align: left; + float:left; } -.point-of-sale .breadcrumb li:last-child { +.pos .breadcrumb{ + float: left; + display: inline-block; + line-height: 48px; + height: 48px; + min-width: 48px; +} +.pos .breadcrumb:last-child { padding-right: 3px; border-right: 1px solid #c5c5c5; } -.point-of-sale .breadcrumb a { +.pos .breadcrumb-button { display: inline-block; padding: 0 9px; vertical-align: top; - text-shadow: #f7f7f7 0 1px 1px; - color: #555555; - font-weight: bold; + color: #808080; + font-size: 14px; + cursor: pointer; } -.point-of-sale .bc-arrow { - height: 33px; +.pos .breadcrumb-arrow{ + width: 28px; } -.point-of-sale .homeimg { - width: 19px; - height: 19px; - margin: 6px 0; +.pos .breadcrumb-homeimg { + width: 27px; + margin: 12px 6px; } /* b) the search box */ -.point-of-sale .searchbox { +.pos .searchbox { position: absolute; right: 2px; } -.point-of-sale .searchbox input { - width: 130px; - border-radius: 11px; +.pos .searchbox input { + width: 100px; border: 1px solid #cecbcb; - padding: 3px 19px; + padding: 10px 20px; + padding-left: 38px; margin: 6px; - background: url("../img/search.png") no-repeat 5px; + background: url("../img/search.png") no-repeat 15px; background-color: white; + border-radius: 20px; + font-family: Lato; + font-size: 13px; } -.point-of-sale .search-clear { +.pos .searchbox input:focus { + outline: none; + box-shadow: 0px 0px 0px 2px rgb(153, 153, 255) inset; + color: rgb(153, 153, 255); +} +.pos .search-clear { position: absolute; + width: 30px; + height: 30px; top: 11px; - right: 11px; + left: 11px; cursor: pointer; - display: none; } /* c) the categories list */ -.point-of-sale #categories { - /*background:#f0f0f0;*/ +.pos .categories { position: relative; - background-image: url('../img/bg.png'); - border-bottom: 2px solid #e0e0e0; + border-bottom: 1px solid #cecece; } -.point-of-sale #categories .white-gradient{ - position: absolute; - top:50%; - left:0; - right:0; - bottom:0; - background: -webkit-linear-gradient(bottom,rgba(255,255,255,0.5),rgba(255,255,255,0)); - background: -moz-linear-gradient(bottom,rgba(255,255,255,0.5),rgba(255,255,255,0)); - background: -ms-linear-gradient(bottom,rgba(255,255,255,0.5),rgba(255,255,255,0)); - background: linear-gradient(bottom,rgba(255,255,255,0.5),rgba(255,255,255,0)); -} -.point-of-sale #categories h4 { +.pos .categories h4 { display: inline-block; margin: 9px 5px; } -.point-of-sale .category-list{ - padding:10px !important; +.pos .category-list{ + text-align: left; + padding: 10px; + background: rgb(229, 229, 229); } /* d) the category button */ -.point-of-sale .category-button { +.pos .category-button { position: relative; vertical-align: top; display: inline-block; @@ -497,42 +611,30 @@ width: 120px; height:120px; background:#fff; - border: 1px solid #fff; + border: 1px solid #d7d7d7; border-radius: 3px; - -webkit-box-shadow: 0px 2px 0px #E9E9E9, 0px 3px 8px rgba(0, 0, 0, 0.34); - -moz-box-shadow: 0px 2px 0px #E9E9E9, 0px 3px 8px rgba(0, 0, 0, 0.34); - box-shadow: 0px 2px 0px #E9E9E9, 0px 3px 8px rgba(0, 0, 0, 0.34); + border-bottom-width: 3px; cursor: pointer; } -.point-of-sale .category-simple-button{ +.pos .category-simple-button{ position: relative; display: inline-block; font-size: 14px; margin-right:10px; - padding:5px; + padding: 5px 12px; + line-height: 24px; + border-radius: 3px; cursor: pointer; border: 1px solid #cacaca; - border-radius: 4px; background: #e2e2e2; - background: -webkit-linear-gradient(#f0f0f0, #e2e2e2); - background: -moz-linear-gradient(#f0f0f0, #e2e2e2); - background: -ms-linear-gradient(#f0f0f0, #e2e2e2); - background: linear-gradient(#f0f0f0, #e2e2e2); - -webkit-box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); - -moz-box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); - box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); } -.point-of-sale .category-simple-button:active{ +.pos .category-simple-button:active{ color: white; background: #7f82ac; border: 1px solid #7f82ac; - background: -webkit-linear-gradient(#9d9fc5, #7f82ac); - background: -moz-linear-gradient(#9d9fc5, #7f82ac); - background: -ms-linear-gradient(#9d9fc5, #7f82ac); - background: linear-gradient(#9d9fc5, #7f82ac); -webkit-transition-property: background, border; -webkit-transition-duration: 0.2s; @@ -541,7 +643,7 @@ -.point-of-sale .category-button .category-img { +.pos .category-button .category-img { position: relative; width: 120px; height: 100px; @@ -549,12 +651,13 @@ cursor: pointer; } -.point-of-sale .category-button .category-img img { +.pos .category-button .category-img img { max-height: 100px; max-width: 120px; + vertical-align: middle; } -.point-of-sale .category-button .category-name { +.pos .category-button .category-name { position: absolute; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; @@ -564,9 +667,6 @@ top: auto; line-height: 14px; width: 100%; - background: -webkit-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); - background: -moz-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); - background: -ms-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); /* for some reason the -90deg orientation doesn't match the -webkit-linear-gradient. It should be 180deg here. * webkit also insists on rendering *both* gradients instead of only the native one. So it doesn't looks right. ugh. background: linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); */ @@ -578,7 +678,7 @@ /* e) the product */ -.point-of-sale .product { +.pos .product { position:relative; vertical-align: top; display: inline-block; @@ -588,15 +688,14 @@ width: 120px; height:120px; background:#fff; - border: 1px solid #fff; + border: 1px solid #e2e2e2; border-radius: 3px; + border-bottom-width: 3px; overflow: hidden; - -webkit-box-shadow: 0px 2px 0px #dad8e4, 0px 1px 8px #636480; - -moz-box-shadow: 0px 2px 0px #dad8e4, 0px 1px 8px #636480; - box-shadow: 0px 2px 0px #dad8e4, 0px 1px 8px #636480; + cursor: pointer; } -.point-of-sale .product .product-img { +.pos .product .product-img { position: relative; width: 120px; height: 100px; @@ -604,12 +703,13 @@ text-align: center; } -.point-of-sale .product .product-img img { +.pos .product .product-img img { max-height: 100px; max-width: 120px; + vertical-align: middle; } -.point-of-sale .product .price-tag { +.pos .product .price-tag { position: absolute; top: 2px; right: 2px; @@ -618,11 +718,10 @@ line-height: 13px; background: #7f82ac; padding: 2px 5px; - border-radius: 3px; - box-shadow: 0px 1px 0px #9A9CC5, 0px 3px 0px #7E86AC, 0px 3px 3px rgba(12, 14, 68, 0.67); + border-radius: 2px; } -.point-of-sale .product .product-name { +.pos .product .product-name { position: absolute; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; @@ -648,35 +747,30 @@ /* ********* The Screens ********* */ -.point-of-sale .screen { +.pos .screen { position:absolute; text-align: center; top:0px; bottom:0px; width:100%; - background-image: -webkit-radial-gradient(rgba(255, 255, 255, 0),rgba(51, 17, 124, 0.25)), url('../img/grid.png'); - box-shadow: 0px 36px 24px rgba(0, 0, 0, 0.14) inset; + overflow: auto; + -webkit-overflow-scrolling: touch; } -.point-of-sale .screen header h2 { +.pos .screen header h2 { margin-top: 0px; padding-top: 7px; - font-size: 1.5em /* Customize font according bootstrap3 */ } -.point-of-sale .screen p{ +.pos .screen p{ font-size: 18px; } -.point-of-sale .dialog{ +.pos .dialog{ width: 500px; margin-left: auto; margin-right: auto; margin-top: 50px; - padding: 40px; - background-color: #f8f8f8; - border-radius: 4px; text-align: center; - box-shadow: 0px 1px 0px white,0px -1px 0px white, 0px 4px 0px #DFDFDF, 0px 10px 30px rgba(0, 0, 0, 0.21); } -.point-of-sale .dialog p{ +.pos .dialog p{ font-size: 25px; margin-top: 10px; color: #5a5a5a; @@ -684,159 +778,192 @@ /* a) Layout for the Product Screen */ -.point-of-sale .screen .layout-table { +.pos .screen .layout-table { border:none; width:100%; height:100%; } -.point-of-sale .screen .header-row { +.pos .screen .header-row { border:none; width:100%; height:0px; } -.point-of-sale .screen .header-cell{ +.pos .screen .header-cell{ border:none; width:100%; height:0px; } -.point-of-sale .screen .content-row { +.pos .screen .content-row { width:100%; height:100%; } -.point-of-sale .screen .content-cell{ +.pos .screen .content-cell{ width:100%; } -.point-of-sale .screen .content-cell .content-container{ +.pos .screen .content-cell .content-container{ height:100%; position:relative; } /* b) The payment screen */ -.point-of-sale .pos-step-container { + +.pos .pos-payment-container { display: inline-block; - font-size: 1.5em; + font-size: 16px; + text-align: left; + width: 360px; } -.point-of-sale .greyed-out{ - color: #AAA; +.pos .payment-due-total { + text-align: center; + font-weight: bold; + font-size: 48px; + margin: 27px; + text-shadow: 0px 2px rgb(202, 202, 202); } -.point-of-sale .pos-step-container input{ - font-size: 1em; +.pos .paymentline{ + position: relative; + padding: 8px; + border-box: 3px; + box-sizing: border-box; + -moz-box-sizing: border-box; + border-radius: 3px; +} +.pos .paymentline-name{ + margin-bottom: 8px; +} +.pos .paymentline-input{ + font-size: 20px; + font-family: Lato; + display: block; + width: 100%; + box-sizing: border-box; + -moz-box-sizing: border-box; outline: none; border: none; - padding: 0px 8px; - padding-top: 8px; - margin-left: 16px; - border-radius: 5px; + padding: 6px 8px; background: white; - box-shadow: 0px -1px 0px #E2E2E2 inset,0px 1px 0px white inset, 0px 4px 0px #DDD inset, 0px 4px 8px rgba(0, 0, 0, 0.55) inset; - color: #4c4c4c; + color: #484848; + text-align: right; + border-radius: 3px; + box-shadow: 0px 2px rgba(143, 143, 143, 0.3) inset; +} + +.pos .paymentline-input:focus{ + color: rgb(130, 124, 255); + box-shadow: 0px 2px rgba(219, 219, 219, 0.3) inset; -webkit-animation: all 250ms linear; } -.point-of-sale .pos-step-container input:focus{ - box-shadow: 0px -1px 0px #C9CFFD inset,0px 1px 0px #B8C8FC inset, 0px 4px 0px #9FD5FF inset, 0px 4px 9px rgba(0, 31, 255, 0.55) inset; - color: #5d7ad6; - -webkit-animation: all 250ms linear; +.paymentline-delete { + width: 32px; + height: 32px; + padding: 5px; + text-align: center; + box-sizing: border-box; + -moz-box-sizing: border-box; + position: absolute; + bottom: 10px; + left: 8px; + cursor: pointer; } -.point-of-sale .pos-payment-container { - text-align: left; - min-width: 500px; - margin-top: 50px; - padding: 40px; - background-color: #f8f8f8; - border-radius: 4px; - box-shadow: 0px 1px 0px white,0px -1px 0px white, 0px 4px 0px #DFDFDF, 0px 10px 30px rgba(0, 0, 0, 0.21); -} -.point-of-sale .pos-payment-container .left-block{ +.pos .pos-payment-container .left-block{ display: inline-block; width:49%; margin:0; padding:0; text-align:left; } -.point-of-sale .pos-payment-container .header{ - margin-top: 0px; - margin-bottom:20px; - font-weight: bold; -} -.point-of-sale .pos-payment-container .infoline{ +.pos .pos-payment-container .infoline{ margin-top:5px; margin-bottom:5px; + padding: 0px 8px; + opacity: 0.5; } -.point-of-sale .pos-payment-container .right-block{ +.pos .pos-payment-container .infoline.bigger{ + opacity: 1; + font-size: 20px; +} +.pos .pos-payment-container .right-block{ display: inline-block; width:49%; margin:0; padding:0; text-align:right; } -.point-of-sale .pos-payment-container table { - width: 100%; - margin-bottom: 20px; -} -.point-of-sale .pos-payment-container td { - vertical-align: middle; -} -.point-of-sale .pos-payment-container .paymentline-type { - font-size: 1em; - font-weight: bold; - margin-right:10px; +.pos .paymentline.selected{ + background: rgb(220,220,220); } /* c) The receipt screen */ -.point-of-sale .pos-receipt-container { +.pos .pos-receipt-container { font-size: 0.75em; } -.point-of-sale .pos-sale-ticket { +.pos .pos-sale-ticket { text-align: left; width: 300px; background-color: white; margin: 20px; padding: 15px; + font-size: 14px; padding-bottom:30px; display: inline-block; font-family: "Inconsolata"; - border-radius: 2px; - box-shadow: 0px 1px 0px white, 0px 3px 0px #C1B9D6, 0px 8px 16px rgba(0, 0, 0, 0.3); + border: solid 1px rgb(220,220,220); + border-radius: 3px; + overflow: hidden; } -.point-of-sale .pos-sale-ticket .emph{ +.pos .pos-sale-ticket pre{ + font-family: "Inconsolata"; +} +.pos .pos-sale-ticket .emph{ font-size: 20px; margin:5px; } -.point-of-sale .pos-sale-ticket table { +.pos .pos-sale-ticket table { width: 100%; border: 0; table-layout: fixed; } -.point-of-sale .pos-sale-ticket table td { +.pos .pos-sale-ticket table td { border: 0; word-wrap: break-word; } @media print { - .point-of-sale{ - background: white !important; - } - .point-of-sale #topheader, .point-of-sale #leftpane { + .oe_leftbar, + .pos .pos-topheader, + .pos .pos-leftpane, + .pos .keyboard_frame { display: none !important; } - .point-of-sale #content { - top: 0px !important; + .pos, + .pos .pos-content, + .pos .rightpane, + .pos .screen, + .pos .window, + .pos .window .subwindow, + .pos .subwindow .subwindow-container{ + display: block; + position: static; + height: auto; } - .point-of-sale #rightpane { + .pos{ + background: white !important; + } + .pos .rightpane { left: 0px !important; background-color: white; } - #receipt-screen header { + .pos .receipt-screen header { display: none !important; } - #receipt-screen { + .pos .receipt-screen { text-align: left; } .pos-actionbar { @@ -845,12 +972,12 @@ .debug-widget{ display: none !important; } - .point-of-sale *{ + .pos *{ text-shadow: none !important; box-shadow: none !important; background: transparent !important; } - .point-of-sale .pos-sale-ticket{ + .pos .pos-sale-ticket{ margin: 0; margin-left: auto !important; margin-right: auto !important; @@ -862,20 +989,16 @@ /* d) The Scale screen */ -.point-of-sale .scale-screen .display{ +.pos .scale-screen .display{ position:relative; - width:600px; + width: 550px; height:190px; margin-top: 100px; margin-left: auto; margin-right: auto; - padding: 40px; - background: #f1f1f1; - box-shadow: 0px 1px 0px white, 0px -1px 0px white, 0px 4px 0px #dfdfdf, 0px 10px 30px rgba(0,0,0,0.21); - border-radius: 4px; } -.point-of-sale .scale-screen .product-picture { +.pos .scale-screen .product-picture { position: relative; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; @@ -892,17 +1015,17 @@ background:#fff; border: 1px solid #fff; border-radius: 3px; - box-shadow: 0px 2px 0px #e9e9e9, 0px 2px 8px rgba(0,0,0,0.2); + box-shadow: 0px 2px rgb(228, 228, 228); } -.point-of-sale .scale-screen .product-picture img{ - max-width: 178px; - max-height:178px; +.pos .scale-screen .product-picture img{ + max-width: 178px; + max-height: 178px; vertical-align: middle; cursor:pointer; } -.point-of-sale .scale-screen .product-picture .product-price{ +.pos .scale-screen .product-picture .product-price{ position: absolute; top:8px; right:8px; @@ -911,54 +1034,53 @@ line-height:1; color:white; background: #7f82ac; - padding: 2px 5px; + padding: 4px 5px; border-radius: 3px; cursor:pointer; } -.point-of-sale .scale-screen .product-name { +.pos .scale-screen .product-name { position: absolute; - left:40px; - top:50px; - height:50px; - font-size:36px; - line-height:50px; - text-align:right; - right:275px; + left: 0px; + right: 210px; + top: 10px; + height: 50px; + font-size: 36px; + line-height: 50px; + text-align: right; color: #8d8d8d; overflow: hidden; text-overflow: ellipsis; } -.point-of-sale .scale-screen .weight{ - position: absolute; - left: 40px; - height: 90px; - bottom: 55px; - right: 260px; - padding: 6px; - padding-right: 33px; - padding-top: 11px; - background: white; - border-radius: 3px; - box-shadow: 0px -1px #E2E2E2 inset,0px 1px white inset, 0px 4px #E6E4E4 inset, 0px 4px 8px rgba(0, 0, 0, 0.4) inset; - +.pos .scale-screen .weight{ + position: absolute; + left: 0; + top: 80px; + height: 90px; + bottom: 55px; + right: 210px; + padding: 6px; + padding-right: 33px; + padding-top: 11px; + background: white; + border-radius: 3px; + box-shadow: 0px 2px rgb(228, 228, 228) inset; } -.point-of-sale .scale-screen .weight p{ +.pos .scale-screen .weight p{ display: inline-block; text-align:right; line-height: 90px; - font-size: 70px; + font-size: 56px; width: 100%; height: 100%; margin: 0; margin-right: 18px; font-family: "Inconsolata"; color: #6c6c6c; - text-shadow: 0px 3px 3px rgba(0,0,0, 0.2); } /* e) The Welcome Screen */ -.point-of-sale .goodbye-message{ +.pos .goodbye-message{ position: absolute; left:50%; top:40%; @@ -973,34 +1095,31 @@ font-weight:bold; background-color: #F0EEEE; border: 1px solid #E0DDDD; - -webkit-box-shadow: 0px 10px 20px rgba(0,0,0, 0.3); - -moz-box-shadow: 0px 10px 20px rgba(0,0,0, 0.3); - -ms-box-shadow: 0px 10px 20px rgba(0,0,0, 0.3); + border-radius: 3px; + box-shadow: 0px 10px 20px rgba(0,0,0,0.4); z-index:1150; } /* ********* The OrderWidget ********* */ -.point-of-sale .order-container{ +.pos .order-container{ position: absolute; top: 0px; - bottom: 232px; width:100%; + height: 100%; background: #F0EEEE; } -.point-of-sale .order-scroller{ +.pos .order-scroller{ width:100%; height:100%; overflow:hidden; + overflow-y: auto; + -webkit-overflow-scrolling: touch; } -.point-of-sale .order{ +.pos .order{ background: white; - background: -webkit-linear-gradient(0deg,rgba(245,245,245,1),rgba(255,255,255,1), rgba(245,245,245,1)); - background: -moz-linear-gradient(0deg,rgba(245,245,245,1),rgba(255,255,255,1), rgba(245,245,245,1)); - background: -ms-linear-gradient(0deg,rgba(245,245,245,1),rgba(255,255,255,1), rgba(245,245,245,1)); - background: linear-gradient(0deg,rgba(245,245,245,1),rgba(255,255,255,1), rgba(245,245,245,1)); padding-bottom:15px; padding-top:15px; margin-left:16px; @@ -1008,14 +1127,12 @@ margin-top:16px; margin-bottom:16px; font-size:16px; - border-radius: 2px; - -webkit-box-shadow: 0px 1px 0px white, 0px 3px 0px #DFDFDF, 0px 7px 16px rgba(0, 0, 0, 0.20); - -moz-box-shadow: 0px 1px 0px white, 0px 3px 0px #DFDFDF, 0px 7px 16px rgba(0, 0, 0, 0.20); - box-shadow: 0px 1px 0px white, 0px 3px 0px #DFDFDF, 0px 7px 16px rgba(0, 0, 0, 0.20); - + border-radius: 3px; + border: solid 1px rgb(220,220,220); + -webkit-transform: translate3d(0,0,0); } -.point-of-sale .order .empty{ +.pos .order .empty{ text-align:center; margin-top: 15px; margin-bottom: 5px; @@ -1023,14 +1140,14 @@ font-weight: normal; } -.point-of-sale .order .summary{ +.pos .order .summary{ width:100%; text-align:right; font-weight: bold; margin-top:20px; margin-bottom:10px; } -.point-of-sale .order .summary .line{ +.pos .order .summary .line{ float: right; margin-right:15px; margin-left: 15px; @@ -1038,12 +1155,12 @@ border-top: solid 2px; border-color:#777; } -.point-of-sale .order .summary .line .subentry{ - font-size: 10px; +.pos .order .summary .line .subentry{ + font-size: 14px; font-weight: normal; text-align: center; } -.point-of-sale .order .summary .line.empty{ +.pos .order .summary .line.empty{ text-align: right; border-color:#BBB; color:#999; @@ -1051,7 +1168,7 @@ /* ********* The OrderLineWidget ********* */ -.point-of-sale .order .orderline{ +.pos .order .orderline{ width:100%; margin:0px; padding-top:3px; @@ -1067,25 +1184,25 @@ -moz-transition: background 250ms ease-in-out; transition: background 250ms ease-in-out; } -.point-of-sale .order .orderline:active{ +.pos .order .orderline:active{ background: rgba(140,143,183,0.05); -webkit-transition: background 50ms ease-in-out; -moz-transition: background 50ms ease-in-out; transition: background 50ms ease-in-out; } -.point-of-sale .order .orderline.empty:active{ +.pos .order .orderline.empty:active{ background: transparent; cursor: default; } -.point-of-sale .order .orderline.selected{ +.pos .order .orderline.selected{ background: rgba(140,143,183,0.2); -webkit-transition: background 250ms ease-in-out; -moz-transition: background 250ms ease-in-out; transition: background 250ms ease-in-out; cursor: default; } -.point-of-sale .order .orderline .product-name{ +.pos .order .orderline .product-name{ padding:0; display:inline-block; font-weight: bold; @@ -1093,16 +1210,16 @@ overflow:hidden; text-overflow: ellipsis; } -.point-of-sale .order .orderline .price{ +.pos .order .orderline .price{ padding:0; font-weight: bold; float:right; } -.point-of-sale .order .orderline .info-list{ +.pos .order .orderline .info-list{ color: #888; margin-left:10px; } -.point-of-sale .order .orderline .info-list em{ +.pos .order .orderline .info-list em{ color: #777; font-weight: bold; font-style:normal; @@ -1110,33 +1227,18 @@ /* ********* The ActionBarWidget ********* */ -.point-of-sale .pos-actionbar{ - position:absolute; - left: 0; - bottom: 0px; +.pos .pos-actionbar{ height: 105px; - width: 100%; - margin: 0; background: #f5f5f5; /*#ebebeb;*/ - border-top: solid 1px #afafb6; + border-top: solid 1px #cecece; z-index:900; } -.point-of-sale .pos-actionbar ul{ +.pos .pos-actionbar ul{ list-style: none; } -.point-of-sale .pos-actionbar-left-pane{ - height: 100%; - width: 434px; - margin: 0px; - padding-left:3px; - padding-right:3px; - border-right: solid 1px #dfdfdf; - float: left; -} - -.point-of-sale .pos-actionbar-button-list{ +.pos .pos-actionbar-button-list{ height: 100%; margin: 0px; padding-left:3px; @@ -1144,7 +1246,7 @@ overflow:hidden; } -.point-of-sale .pos-actionbar .button{ +.pos .pos-actionbar .button{ width: 90px; height: 90px; text-align:center; @@ -1158,62 +1260,43 @@ cursor: pointer; border: 1px solid #cacaca; - border-radius: 4px; + border-radius: 3px; background: #e2e2e2; - background: -webkit-linear-gradient(#f0f0f0, #e2e2e2); - background: -moz-linear-gradient(#f0f0f0, #e2e2e2); - background: -ms-linear-gradient(#f0f0f0, #e2e2e2); - background: linear-gradient(#f0f0f0, #e2e2e2); - -webkit-box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); - -moz-box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); - box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); } -.point-of-sale .pos-actionbar .button .label{ +.pos .pos-actionbar .button .label{ margin-top: 37px; } -.point-of-sale .pos-actionbar .button .icon{ +.pos .pos-actionbar .button .icon{ margin-top: 10px; } -.point-of-sale .pos-actionbar .button:active{ +.pos .pos-actionbar .button:active{ color: white; background: #7f82ac; border: 1px solid #7f82ac; - background: -webkit-linear-gradient(#9d9fc5, #7f82ac); - background: -moz-linear-gradient(#9d9fc5, #7f82ac); - background: -ms-linear-gradient(#9d9fc5, #7f82ac); - background: linear-gradient(#9d9fc5, #7f82ac); -webkit-transition-property: background, border; -webkit-transition-duration: 0.2s; -webkit-transition-timing-function: ease-out; } -.point-of-sale .pos-actionbar .button.disabled *{ +.pos .pos-actionbar .button.disabled{ opacity: 0.5; } -.point-of-sale .pos-actionbar .button.disabled:active{ +.pos .pos-actionbar .button.disabled:active{ border: 1px solid #cacaca; - border-radius: 4px; color: #555; cursor: default; background: #e2e2e2; - background: -webkit-linear-gradient(#f0f0f0, #e2e2e2); - background: -moz-linear-gradient(#f0f0f0, #e2e2e2); - background: -ms-linear-gradient(#f0f0f0, #e2e2e2); - background: linear-gradient(#f0f0f0, #e2e2e2); - -webkit-box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); - -moz-box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); - box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); } -.point-of-sale .pos-actionbar .button.rightalign{ +.pos .pos-actionbar .button.rightalign{ float:right; } /* ********* The Debug Widget ********* */ -.point-of-sale .debug-widget{ +.pos .debug-widget{ z-index:100000; position: absolute; right: 10px; @@ -1223,12 +1306,11 @@ background: rgba(0,0,0,0.82); color: white; - text-shadow: none; padding-bottom: 10px; - box-shadow: 0px 3px 20px rgba(0,0,0,0.3); - cursor:move; + cursor: move; + -webkit-transform: translate3d(0,0,0); } -.point-of-sale .debug-widget .toggle{ +.pos .debug-widget .toggle{ position: absolute; font-size: 16px; cursor:pointer; @@ -1237,17 +1319,17 @@ padding:10px; padding-right:15px; } -.point-of-sale .debug-widget .content{ +.pos .debug-widget .content{ overflow: hidden; } -.point-of-sale .debug-widget h1{ +.pos .debug-widget h1{ background:black; padding-top: 10px; padding-left: 10px; margin-top:0; margin-bottom:0; } -.point-of-sale .debug-widget .category{ +.pos .debug-widget .category{ background: black; padding-left: 10px; margin: 0px; @@ -1255,29 +1337,35 @@ padding-top:3px; padding-bottom:3px; } -.point-of-sale .debug-widget .button{ +.pos .debug-widget .button{ padding: 5px; padding-left: 15px; display: block; cursor:pointer; } -.point-of-sale .debug-widget .button:active{ +.pos .debug-widget .button:active{ background: rgba(96,21,177,0.45); } -.point-of-sale .debug-widget input{ +.pos .debug-widget input{ margin-left:10px; margin-top:7px; + padding: 4px; + width: 180px; + border: none; + box-sizing: border-box; + -moz-box-sizing: border-box; + border-radius: 3px; } -.point-of-sale .debug-widget .status{ +.pos .debug-widget .status{ padding: 5px; padding-left: 15px; display: block; cursor:default; } -.point-of-sale .debug-widget .status.on{ +.pos .debug-widget .status.on{ background-color: #6cd11d; } -.point-of-sale .debug-widget .event{ +.pos .debug-widget .event{ padding: 5px; padding-left: 15px; display: block; @@ -1287,7 +1375,7 @@ /* ********* The PopupWidgets ********* */ -.point-of-sale .modal-dialog{ +.pos .modal-dialog{ position: absolute; left: 0; top: 0; @@ -1296,7 +1384,7 @@ background-color: rgba(0,0,0,0.5); z-index:1000; } -.point-of-sale .modal-dialog .popup{ +.pos .modal-dialog .popup{ position: absolute; left:50%; top:50%; @@ -1310,11 +1398,11 @@ font-size:20px; font-weight:bold; background-color: #F0EEEE; - border-radius: 4px; - box-shadow: 0px -1px white, 0px 1px white, 0px 4px #949494, 0px 10px 20px rgba(0, 0, 0, 0.3); + border-radius: 3px; + box-shadow: 0px 10px 20px rgba(0,0,0,0.4); z-index:1200; } -.point-of-sale .popup .footer{ +.pos .popup .footer{ position:absolute; bottom:0; left:0; @@ -1322,7 +1410,7 @@ height:60px; border-top: 1px solid #E0DDDD; } -.point-of-sale .popup .button{ +.pos .popup .button{ float:right; width: 110px; height: 40px; @@ -1338,32 +1426,20 @@ cursor: pointer; border: 1px solid #cacaca; - border-radius: 4px; background: #e2e2e2; - background: -webkit-linear-gradient(#f0f0f0, #e2e2e2); - background: -moz-linear-gradient(#f0f0f0, #e2e2e2); - background: -ms-linear-gradient(#f0f0f0, #e2e2e2); - background: linear-gradient(#f0f0f0, #e2e2e2); - -webkit-box-shadow: 0px 2px 2px rgba(0,0,0, 0.3); - -moz-box-shadow: 0px 2px 2px rgba(0,0,0, 0.3); - box-shadow: 0px 2px 2px rgba(0,0,0, 0.3); } -.point-of-sale .popup .button:active{ +.pos .popup .button:active{ color: white; background: #7f82ac; border: 1px solid #7f82ac; - background: -webkit-linear-gradient(#9d9fc5, #7f82ac); - background: -moz-linear-gradient(#9d9fc5, #7f82ac); - background: -ms-linear-gradient(#9d9fc5, #7f82ac); - background: linear-gradient(#9d9fc5, #7f82ac); -webkit-transition-property: background, border; -webkit-transition-duration: 0.2s; -webkit-transition-timing-function: ease-out; } -.point-of-sale .popup .button.big-left{ +.pos .popup .button.big-left{ position:absolute; top: 120px; left:40px; @@ -1372,7 +1448,7 @@ line-height:180px; } -.point-of-sale .popup .button.big-right{ +.pos .popup .button.big-right{ position:absolute; top: 120px; right:40px; @@ -1381,78 +1457,58 @@ line-height:180px; } -/* ********* The ScrollBarWidget ********* */ +/* ********* The Webkit Scrollbar ********* */ -.point-of-sale .scrollbar{ - position:absolute; - top:7px; - right:7px; - width:48px; - bottom:7px; - background: rgba(0,0,0,0.1); +.pos *::-webkit-scrollbar{ + width: 10px; + height: 10px; +} +.pos *::-webkit-scrollbar-track{ + background: rgb(224,224,224); + border-left: solid 1px rgb(200,200,200); +} +.pos *::-webkit-scrollbar-thumb{ + background: rgb(168,168,168); + min-height: 30px; +} +.pos.big-scrollbars *::-webkit-scrollbar{ + width: 40px; + height: 40px; } -.point-of-sale .scrollbar .button{ - width:100%; - height: 48px; - line-height: 38px; - text-align: center; - font-size:48px; - border-radius: 4px; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - transition: all 250ms ease-in-out; +.pos.big-scrollbars *::-webkit-scrollbar-track{ + background: rgb(224,224,224); + border-left: none; } -.point-of-sale .scrollbar .button{ - color:white; - background: rgba(0,0,0,0.6); - -webkit-box-shadow: 0px 1px 4px rgba(0,0,0,0.01); - -moz-box-shadow: 0px 1px 4px rgba(0,0,0,0.01); - box-shadow: 0px 1px 4px rgba(0,0,0,0.01); - text-shadow: rgba(255,255,255,0.5) 0px 0px 10px; - -webkit-transition: all 250ms ease-in-out; - -moz-transition: all 250ms ease-in-out; - transition: all 250ms ease-in-out; +.pos.big-scrollbars *::-webkit-scrollbar-thumb{ + background: rgb(168,168,168); + min-height: 40px; + border-radius: 3px; } -.point-of-sale .scrollbar .button:active{ - text-shadow: rgba(255,255,255,0.8) 0px 0px 15px; +.pos.big-scrollbars *::-webkit-scrollbar-button{ + width: 40px; + height: 40px; + border-radius: 3px; + background: rgb(210,210,210); + background-size: cover; } -.point-of-sale .scrollbar .button.disabled{ - background: rgba(0,0,0,0.3); - color:rgba(255,255,255,0.5); - -webkit-transition: all 250ms ease-in-out; - -moz-transition: all 250ms ease-in-out; - transition: all 250ms ease-in-out; +.pos.big-scrollbars *::-webkit-scrollbar-button:decrement{ + background-image: url('../img/scroll-up.png'); } -.point-of-sale .scrollbar .down-button{ - position:absolute; - bottom:0px; -} -.point-of-sale .scrollbar .up-button{ - position:absolute; - top:0px; -} -.point-of-sale .scrollbar .scroller{ - position:absolute; - top:33%; - bottom:50%; - width:100%; - background: rgba(0,0,0,0.1); - border-radius: 4px; +.pos.big-scrollbars *::-webkit-scrollbar-button:increment{ + background-image: url('../img/scroll-down.png'); } + /* ********* Unsupported Browser Page ********* */ -.point-of-sale .not-supported-browser{ +.pos .not-supported-browser{ position: absolute; z-index: 100000; top: 0; bottom: 0; left: 0; right: 0; background: #2C2C2C; } -.point-of-sale .not-supported-browser .message{ +.pos .not-supported-browser .message{ width:600px; margin-top: 100px; margin-left: auto; @@ -1461,8 +1517,6 @@ color: #d3d3d3; font-size: 14px; } -.point-of-sale .not-supported-browser img{ +.pos .not-supported-browser img{ border-collapse: separate; - box-shadow: 0px 3px 38px rgba(0,0,0,0.3); - border-radius: 3px; } diff --git a/addons/point_of_sale/static/src/css/pos_nohover.css b/addons/point_of_sale/static/src/css/pos_nohover.css deleted file mode 100644 index 6f24abda53b..00000000000 --- a/addons/point_of_sale/static/src/css/pos_nohover.css +++ /dev/null @@ -1,1168 +0,0 @@ -@font-face{ - font-family: 'Inconsolata'; - src: url(../fonts/Inconsolata.otf); -} - -.point-of-sale { - padding: 0; - margin: 0; - background-color: #f0eeee; - font-family: "Lucida Grande", Helvetica, Verdana, Arial; - color: #555555; - font-size: 12px; - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} - -.point-of-sale .shadow-top{ - position: absolute; - top:0; - left:0; - right:0; - height:10px; - background: -webkit-linear-gradient(top,rgba(0,0,0,0.09),rgba(0,0,0,0)); - background: -moz-linear-gradient(top,rgba(0,0,0,0.09),rgba(0,0,0,0)); - background: -ms-linear-gradient(top,rgba(0,0,0,0.09),rgba(0,0,0,0)); - background: linear-gradient(top,rgba(0,0,0,0.09),rgba(0,0,0,0)); -} - -.point-of-sale .darker-shadow-top{ - position: absolute; - top:0; - left:0; - right:0; - height:10px; - background: -webkit-linear-gradient(top,rgba(0,0,0,0.15),rgba(0,0,0,0)); - background: -moz-linear-gradient(top,rgba(0,0,0,0.15),rgba(0,0,0,0)); - background: -ms-linear-gradient(top,rgba(0,0,0,0.15),rgba(0,0,0,0)); - background: linear-gradient(top,rgba(0,0,0,0.15),rgba(0,0,0,0)); -} - -/* ********* The black loading screen ********* */ - -.point-of-sale .loader{ - background-color: #222; - position:absolute; - left:0px; - top:0px; - width:100%; - height:100%; - z-index: 999; - text-align: center; -} -.point-of-sale .loader img{ - position:absolute; - top:50%; - left:50%; -} - -/* ********* Generic element styling ********* */ - -.point-of-sale a { - text-decoration: none; - color: #555555; -} -.point-of-sale button, .point-of-sale a.button { - display: inline-block; - cursor: pointer; - padding: 4px 10px; - font-size: 11px; - border: 1px solid #cacaca; - border-radius: 4px; - background: #e2e2e2; - background: -webkit-gradient(linear, left top, left bottom, from(#f0f0f0), to(#e2e2e2)); - background: -moz-linear-gradient(#f0f0f0, #e2e2e2); - background: -ms-linear-gradient(#f0f0f0, #e2e2e2); - background: linear-gradient(#f0f0f0, #e2e2e2); -} -.point-of-sale ul, .point-of-sale ol { - padding: 0; - margin: 0; -} -.point-of-sale li { - list-style-type: none; -} -.point-of-sale .pos-right-align { - text-align: right; -} -.point-of-sale .pos-right-align input { - text-align: right; - border: 1px solid #cecbcb; - border-radius: 4px; -} - -/* ********* The black header bar ********* */ - - -.point-of-sale #topheader { - position:absolute; - left:0; - top:0; - width: 100%; - height: 33px; - margin:0; - padding:0; - color: gray; - border-top: solid 1px #d3d3d3; - border-bottom: solid 1px black; - background: #393939; - background: -moz-linear-gradient(#7b7979, #393939); - background: -webkit-gradient(linear, left top, left bottom, from(#7b7979), to(#393939)); -} - -/* a) The left part of the top-bar */ - -.point-of-sale #branding{ - position: absolute; - display: table-cell; - left:0; - top:0; - width:439px; - height:100%; - margin:0; - padding:0; - border-right: 1px solid #373737; - text-align:left; - line-height:100%; - vertical-align: middle; -} -.point-of-sale #branding img { - height: 32px; - width: 116px; - margin-left:5px; - vertical-align:middle; -} -.point-of-sale #branding .username{ - float:right; - color:#DDD; - font-size:16px; - margin-right:32px; - margin-top:10px; - font-style:italic; -} - -/* b) The right part of the top-bar */ - -.point-of-sale #rightheader { - position: absolute; - left:440px; - right:0; - top:0; - height:100%; -} - -.point-of-sale #rightheader button { - color: #273072; - height:27px; - margin:3px; - margin-right:0px; - border: 1px solid #353A7E; - background: #7f82ac; - background: -webkit-gradient(linear, left top, left bottom, from(#b2b3d7), to(#7f82ac)); - background: -moz-linear-gradient(#b2b3d7, #7f82ac); - background: -ms-linear-gradient(#b2b3d7, #7f82ac); - background: linear-gradient(#b2b3d7, #7f82ac); -} - -.point-of-sale #rightheader button.neworder-button { - width: 32px; - margin-left:4px; - margin-right:4px; -} - -.point-of-sale div#order-selector { - display: inline; -} -.point-of-sale ol#orders { - display: inline; -} -.point-of-sale li.order-selector-button { - display: inline; -} -.point-of-sale li.selected-order button { - font-weight: 900; -} - -/* c) The session buttons */ - -.point-of-sale #rightheader .header-button{ - float:right; - height:32px; - padding-left:10px; - padding-right:10px; - border-right: 1px solid #3a3a3a; - border-left: 1px solid #3a3a3a; - color:#DDD; - line-height:32px; - text-align:center; - cursor: pointer; - - -webkit-transition-property: background; - -webkit-transition-duration: 0.2s; - -webkit-transition-timing-function: ease-out; -} -.point-of-sale #rightheader .header-button:last-child{ - border-left: 1px solid #3a3a3a; -} - -/* c) The notifications indicator */ - -.point-of-sale .oe_pos_synch-notification{ - float:right; - color: rgba(255,255,255,0.4); - padding: 8px; - line-height:16px; - font-size:16px; - vertical-align:middle; - font-style: italic; - cursor:pointer; -} - -.point-of-sale .oe_pos_synch-notification .oe_status_red{ - display:inline-block; - cursor:pointer; - width:16px; height:16px; - background: url("../img/gtk-no.png") no-repeat ; -} - -.point-of-sale .oe_pos_synch-notification .oe_status_green{ - display:inline-block; - width:16px; height:16px; - background: url("../img/gtk-yes.png") no-repeat; -} - -/* ********* Contains everything below the bar ********* */ - -.point-of-sale #content { - width: 100%; - position: absolute; - top: 35px; - bottom: 0; - background: #F0EEEE; -} - -/* ********* The leftpane contains the order, numpad and paypad ********* */ - -.point-of-sale #leftpane { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - -ms-box-sizing: border-box; - box-sizing: border-box; - position:absolute; - left:0; - width:440px; - top:0px; - /*bottom:105px;*/ - bottom:0; - border-right: solid 1px #CECBCB; - background-color: white; -} -.point-of-sale #leftpane footer { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - background: #F0EEEE; - white-space: nowrap; -} - -/* ********* The paypad contains the payment buttons ********* */ - -.point-of-sale #paypad { - padding: 8px 4px 8px 8px; - display: inline-block; - text-align: center; - vertical-align: top; -} -.point-of-sale #paypad button { - height: 54px; - width: 208px; - margin: 0 -3px; - font-weight: bold; - vertical-align: middle; - color: #555555; - border-top: 1px solid #efefef; - font-size: 14px; -} - -/* ********* The Numpad ********* */ - -.point-of-sale #numpad { - padding: 8px 8px 8px 4px; - display: inline-block; - text-align: center; -} -.point-of-sale #numpad button { - height: 54px; - width: 54px; - margin: 0 -3px; - font-weight: bold; - vertical-align: middle; - color: #555555; - border-top: 1px solid #efefef; -} -.point-of-sale #numpad .selected-mode { - color: white; - background: #7f82ac; - background: -webkit-gradient(linear, left top, left bottom, from(#9d9fc5), to(#7f82ac)); - background: -moz-linear-gradient(#9d9fc5, #7f82ac); - background: -ms-linear-gradient(#9d9fc5, #7f82ac); - background: linear-gradient(#9d9fc5, #7f82ac); -} -.point-of-sale .input-button { - font-size: 24px; -} -.point-of-sale .mode-button, .point-of-sale #numpad-delete, .point-of-sale #numpad-minus { - font-size: 14px; -} - -/* ********* The right pane contains the screens and headers ********* */ - -.point-of-sale #rightpane { - position: absolute; - top: 0; - /*bottom: 105px;*/ - bottom:0; - left: 440px; - right: 0; - vertical-align: top; -} - -.point-of-sale #rightpane header { - padding: 0; - height: 32px; - border-bottom: 1px solid #cecbcb; - background: #d3d3d3; - background: -moz-linear-gradient(white, #d3d3d3); - background: -webkit-gradient(linear, left top, left bottom, from(white), to(#d3d3d3)); -} - -/* ********* The product list ********* */ - -.point-of-sale .product-list { - padding:10px; -} - -.point-of-sale .product-list-scroller{ - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - -ms-box-sizing: border-box; - box-sizing: border-box; - width:100%; - height:100%; - overflow: hidden; -} -.point-of-sale .product-list-container { - position:absolute; - top:0px; - bottom:0px; - left:0px; - right:0px; - background: #eeedff; -} - -/* a) the product list navigation bar */ - -.point-of-sale .breadcrumb li { - float: left; - line-height: 32px; - height: 32px; -} -.point-of-sale .breadcrumb li:last-child { - padding-right: 3px; - border-right: 1px solid #c5c5c5; -} -.point-of-sale .breadcrumb a { - display: inline-block; - padding: 0 9px; - vertical-align: top; - text-shadow: #f7f7f7 0 1px 1px; - color: #555555; - font-weight: bold; -} -.point-of-sale .bc-arrow { - height: 33px; -} -.point-of-sale .homeimg { - width: 19px; - height: 19px; - margin: 6px 0; -} - -/* b) the search box */ - -.point-of-sale .searchbox { - position: absolute; - right: 2px; -} -.point-of-sale .searchbox input { - width: 130px; - border-radius: 11px; - border: 1px solid #cecbcb; - padding: 3px 19px; - margin: 6px; - background: url("../img/search.png") no-repeat 5px; - background-color: white; -} -.point-of-sale .search-clear { - position: absolute; - top: 11px; - right: 11px; - cursor: pointer; - display: none; -} - -/* c) the categories list */ - -.point-of-sale #categories { - /*background:#f0f0f0;*/ - position: relative; - background-image: url('../img/bg.png'); - border-bottom: 1px solid #cecbcb; -} -.point-of-sale #categories .white-gradient{ - position: absolute; - top:50%; - left:0; - right:0; - bottom:0; - background: -webkit-linear-gradient(bottom,rgba(255,255,255,0.5),rgba(255,255,255,0)); - background: -moz-linear-gradient(bottom,rgba(255,255,255,0.5),rgba(255,255,255,0)); - background: -ms-linear-gradient(bottom,rgba(255,255,255,0.5),rgba(255,255,255,0)); - background: linear-gradient(bottom,rgba(255,255,255,0.5),rgba(255,255,255,0)); -} -.point-of-sale #categories h4 { - display: inline-block; - margin: 9px 5px; -} - -.point-of-sale .category-list{ - padding:10px; -} -/* d) the category button */ - -.point-of-sale .category-button { - position: relative; - vertical-align: top; - display: inline-block; - font-size: 11px; - margin: 5px; - width: 120px; - height:120px; - background:#fff; - border: 1px solid #fff; - border-radius: 3px; - -webkit-box-shadow: 0px 1px 8px rgba(0,0,0,0.2); - -moz-box-shadow: 0px 1px 8px rgba(0,0,0,0.2); - box-shadow: 0px 1px 8px rgba(0,0,0,0.2); - cursor: pointer; -} - -.point-of-sale .category-simple-button{ - position: relative; - display: inline-block; - font-size: 14px; - margin-right:10px; - padding:5px; - - cursor: pointer; - border: 1px solid #cacaca; - border-radius: 4px; - - background: #e2e2e2; - background: -webkit-linear-gradient(#f0f0f0, #e2e2e2); - background: -moz-linear-gradient(#f0f0f0, #e2e2e2); - background: -ms-linear-gradient(#f0f0f0, #e2e2e2); - background: linear-gradient(#f0f0f0, #e2e2e2); - -webkit-box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); - -moz-box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); - box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); -} - -.point-of-sale .category-button .category-img { - position: relative; - width: 120px; - height: 100px; - text-align: center; - cursor: pointer; -} - -.point-of-sale .category-button .category-img img { - max-height: 100px; - max-width: 120px; -} - -.point-of-sale .category-button .category-name { - position: absolute; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - -ms-box-sizing: border-box; - box-sizing: border-box; - top:auto; - left: 2px; - right: 2px; - bottom: 2px; - background: #8a89ba; /*-webkit-linear-gradient(-90deg,rgba(138,137,186,0),rgba(138,137,186,1), rgba(138,137,186,1));*/ - padding: 3px; - /*color:#8a89ba;*/ - color: #FFF; - cursor: pointer; - border-radius: 3px; -} - -/* e) the product */ - -.point-of-sale .product { - position:relative; - vertical-align: top; - display: inline-block; - line-height: 100px; - font-size: 11px; - margin: 5px; - width: 120px; - height:120px; - background:#fff; - border: 1px solid #fff; - border-radius: 3px; - -webkit-box-shadow: 0px 1px 8px rgba(127,130,172,0.4); - -moz-box-shadow: 0px 1px 8px rgba(127,130,172,0.4); - box-shadow: 0px 1px 8px rgba(127,130,172,0.4); -} - -.point-of-sale .product .product-img { - position: relative; - width: 120px; - height: 100px; - background: white; - text-align: center; -} - -.point-of-sale .product .product-img img { - max-height: 100px; - max-width: 120px; -} - -.point-of-sale .product .price-tag { - position: absolute; - top: 2px; - right: 2px; - vertical-align: top; - color: white; - line-height: 14px; - background: #7f82ac; - padding: 2px 5px; - border-radius: 3px; -} - -.point-of-sale .product .product-name { - position: absolute; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - -ms-box-sizing: border-box; - box-sizing: border-box; - bottom:0; - top:auto; - line-height: 14px; - width:100%; - background: -webkit-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); - background: -moz-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); - background: -ms-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); - background: linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); - /*background:#FFF;*/ - padding: 3px; - padding-top:15px; -} - - -/* ********* The Screens ********* */ - -.point-of-sale .screen { - position:absolute; - text-align: center; - top:0px; - bottom:0px; - width:100%; -} -.point-of-sale .screen header h2 { - margin-top: 0px; - padding-top: 7px; -} -.point-of-sale .screen p{ - font-size: 18px; -} - -/* a) Layout for the Product Screen */ - -.point-of-sale .screen .layout-table { - border:none; - width:100%; - height:100%; -} - -.point-of-sale .screen .header-row { - border:none; - width:100%; - height:0px; -} - -.point-of-sale .screen .header-cell{ - border:none; - width:100%; - height:0px; -} -.point-of-sale .screen .content-row { - width:100%; - height:100%; -} -.point-of-sale .screen .content-cell{ - width:100%; -} -.point-of-sale .screen .content-cell .content-container{ - height:100%; - position:relative; -} - -/* b) The payment screen */ - -.point-of-sale .pos-step-container { - display: inline-block; - font-size: 1.5em; -} -.point-of-sale .greyed-out{ - color: #AAA; -} -.point-of-sale .pos-step-container input{ - font-size: 1em; -} - -.point-of-sale .pos-payment-container { - text-align: left; - min-width: 500px; -} -.point-of-sale .pos-payment-container .left-block{ - display: inline-block; - width:49%; - margin:0; - padding:0; - text-align:left; -} -.point-of-sale .pos-payment-container .header{ - margin-top: 50px; - margin-bottom:20px; - font-weight: bold; -} -.point-of-sale .pos-payment-container .infoline{ - margin-top:5px; - margin-bottom:5px; -} -.point-of-sale .pos-payment-container .right-block{ - display: inline-block; - width:49%; - margin:0; - padding:0; - text-align:right; -} -.point-of-sale .pos-payment-container table { - width: 100%; - margin-bottom: 20px; -} -.point-of-sale .pos-payment-container td { - vertical-align: middle; -} -.point-of-sale .pos-payment-container .paymentline-type { - font-size: 1em; - font-weight: bold; - margin-right:10px; -} - -/* c) The receipt screen */ - -.point-of-sale .pos-receipt-container { - font-size: 0.75em; -} - -.point-of-sale .pos-sale-ticket { - text-align: left; - width: 300px; - background-color: white; - margin: 20px; - padding: 15px; - padding-bottom:30px; - display: inline-block; - font-family: "Inconsolata"; - -webkit-box-shadow: 0px 5px 16px rgba(0,0,0, 0.3); - -moz-box-shadow: 0px 5px 16px rgba(0,0,0, 0.3); - box-shadow: 0px 5px 16px rgba(0,0,0, 0.3); -} -.point-of-sale .pos-sale-ticket .emph{ - font-size: 20px; - margin:5px; -} -.point-of-sale .pos-sale-ticket table { - width: 100%; - border: 0; -} -.point-of-sale .pos-sale-ticket table td { - border: 0; -} - -@media print { - .point-of-sale #topheader, .point-of-sale #leftpane { - display: none !important; - } - .point-of-sale #content { - top: 0px !important; - } - .point-of-sale #rightpane { - left: 0px !important; - background-color: white; - } - #receipt-screen header { - display: none !important; - } - #receipt-screen { - text-align: left; - } - .pos-sale-ticket { - margin: 0; - } -} - -/* d) The Scale screen */ - -.point-of-sale .scale-screen .display{ - position:relative; - width:600px; - height:190px; - margin-top: 100px; - margin-left: auto; - margin-right: auto; -} - -.point-of-sale .scale-screen .product-picture { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - -ms-box-sizing: border-box; - box-sizing: border-box; - display: float; - float: right; - margin: 5px; - width: 180px; - height:180px; - line-height:180px; - cursor:pointer; - - background:#fff; - border: 1px solid #fff; - border-radius: 3px; - -webkit-box-shadow: 0px 1px 8px rgba(0,0,0,0.2); - -moz-box-shadow: 0px 1px 8px rgba(0,0,0,0.2); - box-shadow: 0px 1px 8px rgba(0,0,0,0.2); -} - -.point-of-sale .scale-screen .product-picture img{ - vertical-align: middle; - cursor:pointer; -} - -.point-of-sale .scale-screen .product-picture .product-price{ - position: absolute; - top:8px; - right:8px; - width:auto; - height:auto; - line-height:1; - color:white; - background: #7f82ac; - padding: 2px 5px; - border-radius: 3px; - cursor:pointer; -} - -.point-of-sale .scale-screen .product-name { - position: absolute; - left:0; - top:20px; - height:50px; - font-size:40px; - line-height:50px; - text-align:right; - right:225px; -} -.point-of-sale .scale-screen .weight{ - position: absolute; - left:0; - height:90px; - bottom:15px; - right:220px; - padding:5px; -} -.point-of-sale .scale-screen .weight p{ - display: inline-block; - text-align:right; - line-height: 90px; - font-size: 80px; - width:100%; - height:100%; - margin:0; - font-family: "Inconsolata"; -} - -/* e) The Welcome Screen */ -.point-of-sale .goodbye-message{ - position: absolute; - left:50%; - top:30%; - width:500px; - height:400px; - margin-left: -250px; - margin-top: -200px; - padding:10px; - padding-top:20px; - text-align:center; - font-size:20px; - font-weight:bold; - background-color: #F0EEEE; - border: 1px solid #E0DDDD; - -webkit-box-shadow: 0px 10px 20px rgba(0,0,0, 0.3); - -moz-box-shadow: 0px 10px 20px rgba(0,0,0, 0.3); - -ms-box-shadow: 0px 10px 20px rgba(0,0,0, 0.3); - z-index:1150; -} - -/* ********* The OrderWidget ********* */ - -.point-of-sale .order-container{ - position: absolute; - top: 0px; - bottom: 232px; - width:100%; - background: #F0EEEE; -} - -.point-of-sale .order-scroller{ - width:100%; - height:100%; - overflow:hidden; -} - -.point-of-sale .order{ - background: #F00; - background: -webkit-linear-gradient(0deg,rgba(245,245,245,1),rgba(255,255,255,1), rgba(245,245,245,1)); - background: -moz-linear-gradient(0deg,rgba(245,245,245,1),rgba(255,255,255,1), rgba(245,245,245,1)); - background: -ms-linear-gradient(0deg,rgba(245,245,245,1),rgba(255,255,255,1), rgba(245,245,245,1)); - background: linear-gradient(0deg,rgba(245,245,245,1),rgba(255,255,255,1), rgba(245,245,245,1)); - padding-bottom:15px; - padding-top:15px; - margin-left:16px; - margin-right:16px; - margin-top:16px; - margin-bottom:16px; - font-size:16px; - -webkit-box-shadow: 0px 5px 16px rgba(0,0,0, 0.3); - -moz-box-shadow: 0px 5px 16px rgba(0,0,0, 0.3); - box-shadow: 0px 5px 16px rgba(0,0,0, 0.3); - -} - -.point-of-sale .order .empty{ - text-align:center; - margin-top: 15px; - margin-bottom: 5px; - color:#999; - font-weight: normal; -} - -.point-of-sale .order .summary{ - width:100%; - text-align:right; - font-weight: bold; - margin-top:20px; - margin-bottom:10px; -} -.point-of-sale .order .summary .line{ - margin-right:15px; - padding-top:5px; - border-top: solid 2px; - border-color:#777; -} -.point-of-sale .order .summary .line.empty{ - border-color:#BBB; - color:#999; -} - -/* ********* The OrderLineWidget ********* */ - -.point-of-sale .order .orderline{ - width:100%; - margin:0px; - padding-top:3px; - padding-bottom:10px; - padding-left:15px; - padding-right:15px; - cursor: pointer; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - -ms-box-sizing: border-box; - box-sizing: border-box; - -webkit-transition: background 250ms ease-in-out; - -moz-transition: background 250ms ease-in-out; - transition: background 250ms ease-in-out; -} - -.point-of-sale .order .orderline.selected{ - background: rgba(140,143,183,0.2); - -webkit-transition: background 250ms ease-in-out; - -moz-transition: background 250ms ease-in-out; - transition: background 250ms ease-in-out; - cursor: default; -} -.point-of-sale .order .orderline .product-name{ - padding:0; - display:inline-block; - font-weight: bold; - width:80%; - overflow:hidden; -} -.point-of-sale .order .orderline .price{ - padding:0; - font-weight: bold; - float:right; -} -.point-of-sale .order .orderline .info-list{ - color: #888; - margin-left:10px; -} -.point-of-sale .order .orderline .info-list em{ - color: #777; - font-weight: bold; - font-style:normal; -} - -/* ********* The ActionBarWidget ********* */ - -.point-of-sale .pos-actionbar{ - position:absolute; - left: 0; - bottom: 0px; - height: 105px; - width: 100%; - margin: 0; - background: #f5f5f5; /*#ebebeb;*/ - border-top: solid 1px #afafb6; - z-index:900; -} - -.point-of-sale .pos-actionbar ul{ - list-style: none; -} - -.point-of-sale .pos-actionbar-left-pane{ - height: 100%; - width: 434px; - margin: 0px; - padding-left:3px; - padding-right:3px; - border-right: solid 1px #dfdfdf; - float: left; -} - -.point-of-sale .pos-actionbar-button-list{ - height: 100%; - margin: 0px; - padding-left:3px; - padding-right:3px; - overflow:hidden; -} - -.point-of-sale .pos-actionbar .button{ - width: 90px; - height: 90px; - text-align:center; - margin:3px; - margin-top:6px; - float:left; - - font-size: 14px; - font-weight: bold; - - cursor: pointer; - - border: 1px solid #cacaca; - border-radius: 4px; - - background: #e2e2e2; - background: -webkit-linear-gradient(#f0f0f0, #e2e2e2); - background: -moz-linear-gradient(#f0f0f0, #e2e2e2); - background: -ms-linear-gradient(#f0f0f0, #e2e2e2); - background: linear-gradient(#f0f0f0, #e2e2e2); - -webkit-box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); - -moz-box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); - box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); -} -.point-of-sale .pos-actionbar .button .label{ - margin-top: 37px; -} -.point-of-sale .pos-actionbar .button .icon{ - margin-top: 10px; -} - -.point-of-sale .pos-actionbar .button.disabled *{ - opacity: 0.5; -} - -.point-of-sale .pos-actionbar .button.rightalign{ - float:right; -} - -/* ********* The PopupWidgets ********* */ - -.point-of-sale .modal-dialog{ - position: absolute; - left: 0; - top: 0; - width: 100%; - height:100%; - background-color: rgba(0,0,0,0.5); - z-index:1000; -} -.point-of-sale .modal-dialog .popup{ - position: absolute; - left:50%; - top:50%; - width:500px; - height:400px; - margin-left: -250px; - margin-top: -200px; - padding:10px; - padding-top:20px; - text-align:center; - font-size:20px; - font-weight:bold; - background-color: #F0EEEE; - border: 1px solid #E0DDDD; - -webkit-box-shadow: 0px 10px 20px rgba(0,0,0, 0.3); - -moz-box-shadow: 0px 10px 20px rgba(0,0,0, 0.3); - -ms-box-shadow: 0px 10px 20px rgba(0,0,0, 0.3); - z-index:1200; -} -.point-of-sale .popup .footer{ - position:absolute; - bottom:0; - left:0; - width:100%; - height:60px; - border-top: 1px solid #E0DDDD; -} -.point-of-sale .popup .button{ - float:right; - width: 110px; - height: 40px; - line-height:40px; - text-align:center; - margin:3px; - margin-top:10px; - margin-right:10px; - - font-size: 14px; - font-weight: bold; - - cursor: pointer; - - border: 1px solid #cacaca; - border-radius: 4px; - - background: #e2e2e2; - background: -webkit-linear-gradient(#f0f0f0, #e2e2e2); - background: -moz-linear-gradient(#f0f0f0, #e2e2e2); - background: -ms-linear-gradient(#f0f0f0, #e2e2e2); - background: linear-gradient(#f0f0f0, #e2e2e2); - -webkit-box-shadow: 0px 2px 2px rgba(0,0,0, 0.3); - -moz-box-shadow: 0px 2px 2px rgba(0,0,0, 0.3); - box-shadow: 0px 2px 2px rgba(0,0,0, 0.3); -} - -.point-of-sale .popup .button.big-left{ - position:absolute; - top: 120px; - left:40px; - width: 180px; - height: 180px; - line-height:180px; -} - -.point-of-sale .popup .button.big-right{ - position:absolute; - top: 120px; - right:40px; - width: 180px; - height: 180px; - line-height:180px; -} - -/* ********* The ScrollBarWidget ********* */ - -.point-of-sale .scrollbar{ - position:absolute; - top:7px; - right:7px; - width:48px; - bottom:7px; - background: rgba(0,0,0,0.1); - -} -.point-of-sale .scrollbar .button{ - width:100%; - height: 48px; - line-height: 38px; - text-align: center; - font-size:48px; - border-radius: 4px; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - transition: all 250ms ease-in-out; -} -.point-of-sale .scrollbar .button{ - color:white; - background: rgba(0,0,0,0.6); - -webkit-box-shadow: 0px 1px 4px rgba(0,0,0,0.01); - -moz-box-shadow: 0px 1px 4px rgba(0,0,0,0.01); - box-shadow: 0px 1px 4px rgba(0,0,0,0.01); - text-shadow: rgba(255,255,255,0.5) 0px 0px 10px; - -webkit-transition: all 250ms ease-in-out; - -moz-transition: all 250ms ease-in-out; - transition: all 250ms ease-in-out; -} -.point-of-sale .scrollbar .button.disabled{ - background: rgba(0,0,0,0.3); - color:rgba(255,255,255,0.5); - -webkit-transition: all 250ms ease-in-out; - -moz-transition: all 250ms ease-in-out; - transition: all 250ms ease-in-out; -} -.point-of-sale .scrollbar .down-button{ - position:absolute; - bottom:0px; -} -.point-of-sale .scrollbar .up-button{ - position:absolute; - top:0px; -} -.point-of-sale .scrollbar .scroller{ - position:absolute; - top:33%; - bottom:50%; - width:100%; - background: rgba(0,0,0,0.1); - border-radius: 4px; -} - diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-Bla-webfont.eot b/addons/point_of_sale/static/src/fonts/lato/Lato-Bla-webfont.eot new file mode 100644 index 00000000000..0c205b3fdc7 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-Bla-webfont.eot differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-Bla-webfont.svg b/addons/point_of_sale/static/src/fonts/lato/Lato-Bla-webfont.svg new file mode 100644 index 00000000000..2705e7fe72d --- /dev/null +++ b/addons/point_of_sale/static/src/fonts/lato/Lato-Bla-webfont.svg @@ -0,0 +1,311 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20102011 by tyPoland Lukasz Dziedzic with Reserved Font Name Lato Licensed under the SIL Open Font License Version 11 +Designer : Lukasz Dziedzic +Foundry : tyPoland Lukasz Dziedzic +Foundry URL : httpwwwtypolandcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-Bla-webfont.ttf b/addons/point_of_sale/static/src/fonts/lato/Lato-Bla-webfont.ttf new file mode 100644 index 00000000000..67b51336664 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-Bla-webfont.ttf differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-Bla-webfont.woff b/addons/point_of_sale/static/src/fonts/lato/Lato-Bla-webfont.woff new file mode 100644 index 00000000000..d5546a0749e Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-Bla-webfont.woff differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-BlaIta-webfont.eot b/addons/point_of_sale/static/src/fonts/lato/Lato-BlaIta-webfont.eot new file mode 100644 index 00000000000..5db81883225 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-BlaIta-webfont.eot differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-BlaIta-webfont.svg b/addons/point_of_sale/static/src/fonts/lato/Lato-BlaIta-webfont.svg new file mode 100644 index 00000000000..893f9a90bfa --- /dev/null +++ b/addons/point_of_sale/static/src/fonts/lato/Lato-BlaIta-webfont.svg @@ -0,0 +1,295 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20102011 by tyPoland Lukasz Dziedzic with Reserved Font Name Lato Licensed under the SIL Open Font License Version 11 +Designer : Lukasz Dziedzic +Foundry : tyPoland Lukasz Dziedzic +Foundry URL : httpwwwtypolandcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-BlaIta-webfont.ttf b/addons/point_of_sale/static/src/fonts/lato/Lato-BlaIta-webfont.ttf new file mode 100644 index 00000000000..544331ad512 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-BlaIta-webfont.ttf differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-BlaIta-webfont.woff b/addons/point_of_sale/static/src/fonts/lato/Lato-BlaIta-webfont.woff new file mode 100644 index 00000000000..5a2213be8ae Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-BlaIta-webfont.woff differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-Bol-webfont.eot b/addons/point_of_sale/static/src/fonts/lato/Lato-Bol-webfont.eot new file mode 100644 index 00000000000..ea37d45dd44 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-Bol-webfont.eot differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-Bol-webfont.svg b/addons/point_of_sale/static/src/fonts/lato/Lato-Bol-webfont.svg new file mode 100644 index 00000000000..2ed4d02700e --- /dev/null +++ b/addons/point_of_sale/static/src/fonts/lato/Lato-Bol-webfont.svg @@ -0,0 +1,311 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20102011 by tyPoland Lukasz Dziedzic with Reserved Font Name Lato Licensed under the SIL Open Font License Version 11 +Designer : Lukasz Dziedzic +Foundry : tyPoland Lukasz Dziedzic +Foundry URL : httpwwwtypolandcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-Bol-webfont.ttf b/addons/point_of_sale/static/src/fonts/lato/Lato-Bol-webfont.ttf new file mode 100644 index 00000000000..62cba72c41f Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-Bol-webfont.ttf differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-Bol-webfont.woff b/addons/point_of_sale/static/src/fonts/lato/Lato-Bol-webfont.woff new file mode 100644 index 00000000000..a880872fcfc Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-Bol-webfont.woff differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-BolIta-webfont.eot b/addons/point_of_sale/static/src/fonts/lato/Lato-BolIta-webfont.eot new file mode 100644 index 00000000000..8dd4a42b6b1 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-BolIta-webfont.eot differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-BolIta-webfont.svg b/addons/point_of_sale/static/src/fonts/lato/Lato-BolIta-webfont.svg new file mode 100644 index 00000000000..97e6bada8e6 --- /dev/null +++ b/addons/point_of_sale/static/src/fonts/lato/Lato-BolIta-webfont.svg @@ -0,0 +1,295 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20102011 by tyPoland Lukasz Dziedzic with Reserved Font Name Lato Licensed under the SIL Open Font License Version 11 +Designer : Lukasz Dziedzic +Foundry : tyPoland Lukasz Dziedzic +Foundry URL : httpwwwtypolandcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-BolIta-webfont.ttf b/addons/point_of_sale/static/src/fonts/lato/Lato-BolIta-webfont.ttf new file mode 100644 index 00000000000..00dd066046b Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-BolIta-webfont.ttf differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-BolIta-webfont.woff b/addons/point_of_sale/static/src/fonts/lato/Lato-BolIta-webfont.woff new file mode 100644 index 00000000000..0aa2cd8949f Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-BolIta-webfont.woff differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-Hai-webfont.eot b/addons/point_of_sale/static/src/fonts/lato/Lato-Hai-webfont.eot new file mode 100644 index 00000000000..cd61b776933 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-Hai-webfont.eot differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-Hai-webfont.svg b/addons/point_of_sale/static/src/fonts/lato/Lato-Hai-webfont.svg new file mode 100644 index 00000000000..b1d3d71c01b --- /dev/null +++ b/addons/point_of_sale/static/src/fonts/lato/Lato-Hai-webfont.svg @@ -0,0 +1,311 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20102011 by tyPoland Lukasz Dziedzic with Reserved Font Name Lato Licensed under the SIL Open Font License Version 11 +Designer : Lukasz Dziedzic +Foundry : Lukasz Dziedzic +Foundry URL : httpwwwtypolandcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-Hai-webfont.ttf b/addons/point_of_sale/static/src/fonts/lato/Lato-Hai-webfont.ttf new file mode 100644 index 00000000000..bc44a908252 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-Hai-webfont.ttf differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-Hai-webfont.woff b/addons/point_of_sale/static/src/fonts/lato/Lato-Hai-webfont.woff new file mode 100644 index 00000000000..2e65f282ef0 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-Hai-webfont.woff differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-HaiIta-webfont.eot b/addons/point_of_sale/static/src/fonts/lato/Lato-HaiIta-webfont.eot new file mode 100644 index 00000000000..8b9484ef876 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-HaiIta-webfont.eot differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-HaiIta-webfont.svg b/addons/point_of_sale/static/src/fonts/lato/Lato-HaiIta-webfont.svg new file mode 100644 index 00000000000..745fec0b339 --- /dev/null +++ b/addons/point_of_sale/static/src/fonts/lato/Lato-HaiIta-webfont.svg @@ -0,0 +1,295 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20102011 by tyPoland Lukasz Dziedzic with Reserved Font Name Lato Licensed under the SIL Open Font License Version 11 +Designer : Lukasz Dziedzic +Foundry : Lukasz Dziedzic +Foundry URL : httpwwwtypolandcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-HaiIta-webfont.ttf b/addons/point_of_sale/static/src/fonts/lato/Lato-HaiIta-webfont.ttf new file mode 100644 index 00000000000..dbc2c496998 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-HaiIta-webfont.ttf differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-HaiIta-webfont.woff b/addons/point_of_sale/static/src/fonts/lato/Lato-HaiIta-webfont.woff new file mode 100644 index 00000000000..a9ef2fb7356 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-HaiIta-webfont.woff differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-Lig-webfont.eot b/addons/point_of_sale/static/src/fonts/lato/Lato-Lig-webfont.eot new file mode 100644 index 00000000000..bbe4688c5b6 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-Lig-webfont.eot differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-Lig-webfont.svg b/addons/point_of_sale/static/src/fonts/lato/Lato-Lig-webfont.svg new file mode 100644 index 00000000000..878c7292cbc --- /dev/null +++ b/addons/point_of_sale/static/src/fonts/lato/Lato-Lig-webfont.svg @@ -0,0 +1,311 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20102011 by tyPoland Lukasz Dziedzic with Reserved Font Name Lato Licensed under the SIL Open Font License Version 11 +Designer : Lukasz Dziedzic +Foundry : Lukasz Dziedzic +Foundry URL : httpwwwtypolandcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-Lig-webfont.ttf b/addons/point_of_sale/static/src/fonts/lato/Lato-Lig-webfont.ttf new file mode 100644 index 00000000000..86c688752f6 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-Lig-webfont.ttf differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-Lig-webfont.woff b/addons/point_of_sale/static/src/fonts/lato/Lato-Lig-webfont.woff new file mode 100644 index 00000000000..717f30633e4 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-Lig-webfont.woff differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-LigIta-webfont.eot b/addons/point_of_sale/static/src/fonts/lato/Lato-LigIta-webfont.eot new file mode 100644 index 00000000000..006a3ad8689 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-LigIta-webfont.eot differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-LigIta-webfont.svg b/addons/point_of_sale/static/src/fonts/lato/Lato-LigIta-webfont.svg new file mode 100644 index 00000000000..cd6f696dbca --- /dev/null +++ b/addons/point_of_sale/static/src/fonts/lato/Lato-LigIta-webfont.svg @@ -0,0 +1,295 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20102011 by tyPoland Lukasz Dziedzic with Reserved Font Name Lato Licensed under the SIL Open Font License Version 11 +Designer : Lukasz Dziedzic +Foundry : Lukasz Dziedzic +Foundry URL : httpwwwtypolandcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-LigIta-webfont.ttf b/addons/point_of_sale/static/src/fonts/lato/Lato-LigIta-webfont.ttf new file mode 100644 index 00000000000..c4ed3e26241 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-LigIta-webfont.ttf differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-LigIta-webfont.woff b/addons/point_of_sale/static/src/fonts/lato/Lato-LigIta-webfont.woff new file mode 100644 index 00000000000..de2ca966f27 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-LigIta-webfont.woff differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-Reg-webfont.eot b/addons/point_of_sale/static/src/fonts/lato/Lato-Reg-webfont.eot new file mode 100644 index 00000000000..ca9183b6dc7 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-Reg-webfont.eot differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-Reg-webfont.svg b/addons/point_of_sale/static/src/fonts/lato/Lato-Reg-webfont.svg new file mode 100644 index 00000000000..9b560a89d57 --- /dev/null +++ b/addons/point_of_sale/static/src/fonts/lato/Lato-Reg-webfont.svg @@ -0,0 +1,311 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20102011 by tyPoland Lukasz Dziedzic with Reserved Font Name Lato Licensed under the SIL Open Font License Version 11 +Designer : Lukasz Dziedzic +Foundry : tyPoland Lukasz Dziedzic +Foundry URL : httpwwwtypolandcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-Reg-webfont.ttf b/addons/point_of_sale/static/src/fonts/lato/Lato-Reg-webfont.ttf new file mode 100644 index 00000000000..243f7c19498 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-Reg-webfont.ttf differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-Reg-webfont.woff b/addons/point_of_sale/static/src/fonts/lato/Lato-Reg-webfont.woff new file mode 100644 index 00000000000..4db1c61d264 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-Reg-webfont.woff differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-RegIta-webfont.eot b/addons/point_of_sale/static/src/fonts/lato/Lato-RegIta-webfont.eot new file mode 100644 index 00000000000..752df925adf Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-RegIta-webfont.eot differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-RegIta-webfont.svg b/addons/point_of_sale/static/src/fonts/lato/Lato-RegIta-webfont.svg new file mode 100644 index 00000000000..660b5e402cb --- /dev/null +++ b/addons/point_of_sale/static/src/fonts/lato/Lato-RegIta-webfont.svg @@ -0,0 +1,295 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20102011 by tyPoland Lukasz Dziedzic with Reserved Font Name Lato Licensed under the SIL Open Font License Version 11 +Designer : Lukasz Dziedzic +Foundry : tyPoland Lukasz Dziedzic +Foundry URL : httpwwwtypolandcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-RegIta-webfont.ttf b/addons/point_of_sale/static/src/fonts/lato/Lato-RegIta-webfont.ttf new file mode 100644 index 00000000000..d8c1e8a9484 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-RegIta-webfont.ttf differ diff --git a/addons/point_of_sale/static/src/fonts/lato/Lato-RegIta-webfont.woff b/addons/point_of_sale/static/src/fonts/lato/Lato-RegIta-webfont.woff new file mode 100644 index 00000000000..24d6d9565b9 Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/lato/Lato-RegIta-webfont.woff differ diff --git a/addons/point_of_sale/static/src/fonts/lato/SIL Open Font License 1.1.txt b/addons/point_of_sale/static/src/fonts/lato/SIL Open Font License 1.1.txt new file mode 100644 index 00000000000..e4b0c4ff5e6 --- /dev/null +++ b/addons/point_of_sale/static/src/fonts/lato/SIL Open Font License 1.1.txt @@ -0,0 +1,91 @@ +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/addons/point_of_sale/static/src/fonts/lato/stylesheet.css b/addons/point_of_sale/static/src/fonts/lato/stylesheet.css new file mode 100644 index 00000000000..14e03ec3007 --- /dev/null +++ b/addons/point_of_sale/static/src/fonts/lato/stylesheet.css @@ -0,0 +1,124 @@ +/* Generated by Font Squirrel (http://www.fontsquirrel.com) on May 17, 2013 09:03:53 AM America/New_York */ + + + +@font-face { + font-family: 'Lato'; + font-weight: 100; + font-style: normal; + src: url('Lato-Hai-webfont.eot'); + src: url('Lato-Hai-webfont.eot?#iefix') format('embedded-opentype'), + url('Lato-Hai-webfont.woff') format('woff'), + url('Lato-Hai-webfont.ttf') format('truetype'), + url('Lato-Hai-webfont.svg#LatoHairline') format('svg'); + +} + +@font-face { + font-family: 'Lato'; + font-weight: 100; + font-style: italic; + src: url('Lato-HaiIta-webfont.eot'); + src: url('Lato-HaiIta-webfont.eot?#iefix') format('embedded-opentype'), + url('Lato-HaiIta-webfont.woff') format('woff'), + url('Lato-HaiIta-webfont.ttf') format('truetype'), + url('Lato-HaiIta-webfont.svg#LatoHairlineItalic') format('svg'); + +} + +@font-face { + font-family: 'Lato'; + font-weight: 300; + font-style: normal; + src: url('Lato-Lig-webfont.eot'); + src: url('Lato-Lig-webfont.eot?#iefix') format('embedded-opentype'), + url('Lato-Lig-webfont.woff') format('woff'), + url('Lato-Lig-webfont.ttf') format('truetype'), + url('Lato-Lig-webfont.svg#LatoLight') format('svg'); + +} + +@font-face { + font-family: 'Lato'; + font-weight: 300; + font-style: italic; + src: url('Lato-LigIta-webfont.eot'); + src: url('Lato-LigIta-webfont.eot?#iefix') format('embedded-opentype'), + url('Lato-LigIta-webfont.woff') format('woff'), + url('Lato-LigIta-webfont.ttf') format('truetype'), + url('Lato-LigIta-webfont.svg#LatoLightItalic') format('svg'); + +} + +@font-face { + font-family: 'Lato'; + font-weight: 400; + font-style: normal; + src: url('Lato-Reg-webfont.eot'); + src: url('Lato-Reg-webfont.eot?#iefix') format('embedded-opentype'), + url('Lato-Reg-webfont.woff') format('woff'), + url('Lato-Reg-webfont.ttf') format('truetype'), + url('Lato-Reg-webfont.svg#LatoRegular') format('svg'); + +} + +@font-face { + font-family: 'Lato'; + font-weight: 400; + font-style: italic; + src: url('Lato-RegIta-webfont.eot'); + src: url('Lato-RegIta-webfont.eot?#iefix') format('embedded-opentype'), + url('Lato-RegIta-webfont.woff') format('woff'), + url('Lato-RegIta-webfont.ttf') format('truetype'), + url('Lato-RegIta-webfont.svg#LatoItalic') format('svg'); + +} + +@font-face { + font-family: 'Lato'; + font-weight: 700; + font-style: normal; + src: url('Lato-Bol-webfont.eot'); + src: url('Lato-Bol-webfont.eot?#iefix') format('embedded-opentype'), + url('Lato-Bol-webfont.woff') format('woff'), + url('Lato-Bol-webfont.ttf') format('truetype'), + url('Lato-Bol-webfont.svg#LatoBold') format('svg'); + +} + +@font-face { + font-family: 'Lato'; + font-weight: 700; + font-style: italic; + src: url('Lato-BolIta-webfont.eot'); + src: url('Lato-BolIta-webfont.eot?#iefix') format('embedded-opentype'), + url('Lato-BolIta-webfont.woff') format('woff'), + url('Lato-BolIta-webfont.ttf') format('truetype'), + url('Lato-BolIta-webfont.svg#LatoBoldItalic') format('svg'); + +} + +@font-face { + font-family: 'Lato'; + font-weight: 900; + font-style: normal; + src: url('Lato-Bla-webfont.eot'); + src: url('Lato-Bla-webfont.eot?#iefix') format('embedded-opentype'), + url('Lato-Bla-webfont.woff') format('woff'), + url('Lato-Bla-webfont.ttf') format('truetype'), + url('Lato-Bla-webfont.svg#LatoBlack') format('svg'); + +} + +@font-face { + font-family: 'Lato'; + font-weight: 900; + font-style: italic; + src: url('Lato-BlaIta-webfont.eot'); + src: url('Lato-BlaIta-webfont.eot?#iefix') format('embedded-opentype'), + url('Lato-BlaIta-webfont.woff') format('woff'), + url('Lato-BlaIta-webfont.ttf') format('truetype'), + url('Lato-BlaIta-webfont.svg#LatoBlackItalic') format('svg'); + +} + diff --git a/addons/point_of_sale/static/src/img/bc-arrow-big.png b/addons/point_of_sale/static/src/img/bc-arrow-big.png new file mode 100644 index 00000000000..f845fe6837d Binary files /dev/null and b/addons/point_of_sale/static/src/img/bc-arrow-big.png differ diff --git a/addons/point_of_sale/static/src/img/scroll-down.png b/addons/point_of_sale/static/src/img/scroll-down.png new file mode 100644 index 00000000000..5fd07589157 Binary files /dev/null and b/addons/point_of_sale/static/src/img/scroll-down.png differ diff --git a/addons/point_of_sale/static/src/img/scroll-up.png b/addons/point_of_sale/static/src/img/scroll-up.png new file mode 100644 index 00000000000..b34a90014a2 Binary files /dev/null and b/addons/point_of_sale/static/src/img/scroll-up.png differ diff --git a/addons/point_of_sale/static/src/img/touch-icon-128.png b/addons/point_of_sale/static/src/img/touch-icon-128.png new file mode 100644 index 00000000000..5bbf31d6457 Binary files /dev/null and b/addons/point_of_sale/static/src/img/touch-icon-128.png differ diff --git a/addons/point_of_sale/static/src/img/touch-icon-196.png b/addons/point_of_sale/static/src/img/touch-icon-196.png new file mode 100644 index 00000000000..dbde2d99447 Binary files /dev/null and b/addons/point_of_sale/static/src/img/touch-icon-196.png differ diff --git a/addons/point_of_sale/static/src/img/touch-icon-ipad-retina.png b/addons/point_of_sale/static/src/img/touch-icon-ipad-retina.png new file mode 100644 index 00000000000..4f1e1db5c98 Binary files /dev/null and b/addons/point_of_sale/static/src/img/touch-icon-ipad-retina.png differ diff --git a/addons/point_of_sale/static/src/img/touch-icon-ipad.png b/addons/point_of_sale/static/src/img/touch-icon-ipad.png new file mode 100644 index 00000000000..8b8c111406f Binary files /dev/null and b/addons/point_of_sale/static/src/img/touch-icon-ipad.png differ diff --git a/addons/point_of_sale/static/src/img/touch-icon-iphone-retina.png b/addons/point_of_sale/static/src/img/touch-icon-iphone-retina.png new file mode 100644 index 00000000000..593c1506061 Binary files /dev/null and b/addons/point_of_sale/static/src/img/touch-icon-iphone-retina.png differ diff --git a/addons/point_of_sale/static/src/img/touch-icon-iphone.png b/addons/point_of_sale/static/src/img/touch-icon-iphone.png new file mode 100644 index 00000000000..4122e61f9c0 Binary files /dev/null and b/addons/point_of_sale/static/src/img/touch-icon-iphone.png differ diff --git a/addons/point_of_sale/static/src/img/touch-icon.svg b/addons/point_of_sale/static/src/img/touch-icon.svg new file mode 100644 index 00000000000..8ce5f30e13e --- /dev/null +++ b/addons/point_of_sale/static/src/img/touch-icon.svg @@ -0,0 +1,199 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/point_of_sale/static/src/js/TODO.txt b/addons/point_of_sale/static/src/js/TODO.txt deleted file mode 100644 index 1696f0b117d..00000000000 --- a/addons/point_of_sale/static/src/js/TODO.txt +++ /dev/null @@ -1,63 +0,0 @@ -v Affichage Catégories doivent respecter le poids -v Affichage Catégories par poids doivent avoir une hiérarchie plate -v Le Onscreen keyboard ne fonctionne plus -v Scrolling dans la sélection produit et dans - la liste de courses -v Réductions -v Redesign liste de courses -v bugs scans produits par prix par poids -v si pas photo photo par defaut -v WebSQL -v Redesign header, bouton retour aux backend en mode caissière -v bouton exit en mode caissière doit retourner au backend. -v posting orders -v bug ajout ligne payment à zero + curseur dedans + suppression + design plus grand -v si case print via proxy cochée, alors on skip l'ecran receipt -v bouton exit self checkout -v demarrage en mode self-checkout si self-checkout -v produits poid code barre non reconnus -v activer popup client aux écrans self-checkout -v discount pas plus grand que 100 -v numpad dans l'écran payment pour pouvoir entrer le montant -v différence de total. Methode d'arrondis - -- login alternatif -- user preferences : désactiver la balance etc. -- numpad state parfois sans state -- numpad state en mode pesée ?? le cacher ? -- Redesign du receipt -- retirer le receipt screen -- le numpad en mode paymenet marche pas -- l'écran de payement est hyper laid -- l'écran de pesée est hyper laid -x générer les données de printing -x popups d'erreur certainement buggés - -TODO AUG 20 ------------ - -* GREEN STATUS - - make it green ! - -* TRUNK - - Connection status tooltip - v Remove receipt screen - - Finish the receipt JSON generation - - Modifie le widget de liste course - v supprimer l'écran - v bloquer sur l'impression - v L'impression est foireuse - - description_sales - -* CLIENT - v create a new branch - - Self-checkout welcome screen - ~ removal of products for the root category - - Terminal de payement - - Code à barres - - Vidanges - - Design - - Backport - - CSS pour écran résistif - - Test sur matos - diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js index 66791bb3c8b..e4dc633e0f6 100644 --- a/addons/point_of_sale/static/src/js/db.js +++ b/addons/point_of_sale/static/src/js/db.js @@ -1,36 +1,12 @@ function openerp_pos_db(instance, module){ - /* The db module was intended to be used to store all the data needed to run the Point - * of Sale in offline mode. (Products, Categories, Orders, ...) It would also use WebSQL - * or IndexedDB to make the searching and sorting products faster. It turned out not to be - * a so good idea after all. - * - * First it is difficult to make the Point of Sale truly independant of the server. A lot - * of functionality cannot realistically run offline, like generating invoices. - * - * IndexedDB turned out to be complicated and slow as hell, and loading all the data at the - * start made the point of sale take forever to load over small connections. - * - * LocalStorage has a hard 5.0MB on chrome. For those kind of sizes, it is just better - * to put the data in memory and it's not too big to download each time you launch the PoS. - * - * So at this point we are dropping the support for offline mode, and this module doesn't really - * make sense anymore. But if at some point you want to store millions of products and if at - * that point indexedDB has improved to the point it is usable, you can just implement this API. - * - * You would also need to change the way the models are loaded at the start to not reload all your - * product data. - */ - - /* PosLS is a localstorage based implementation of the point of sale database. - * FIXME: The Products definitions and categories are stored on the locastorage even tough they're - * always reloaded at launch. This could induce a slowdown because the data needs to be reparsed from - * JSON before each operation. If you have a huge amount of products (around 25000) it can also - * blow the 5.0MB localstorage limit. + /* The PosDB holds reference to data that is either + * - static: does not change between pos reloads + * - persistent : must stay between reloads ( orders ) */ - module.PosLS = instance.web.Class.extend({ - name: 'openerp_pos_ls', //the prefix of the localstorage data + module.PosDB = instance.web.Class.extend({ + name: 'openerp_pos_db', //the prefix of the localstorage data limit: 100, // the maximum number of results returned by a search init: function(options){ options = options || {}; @@ -132,7 +108,7 @@ function openerp_pos_db(instance, module){ return this.cache[store]; } var data = localStorage[this.name + '_' + store]; - if(data !== undefined){ + if(data !== undefined && data !== ""){ data = JSON.parse(data); this.cache[store] = data; return data; diff --git a/addons/point_of_sale/static/src/js/devices.js b/addons/point_of_sale/static/src/js/devices.js index 7071a862413..24957486dbd 100644 --- a/addons/point_of_sale/static/src/js/devices.js +++ b/addons/point_of_sale/static/src/js/devices.js @@ -104,33 +104,138 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal this.notifications = {}; this.bypass_proxy = false; - this.connection = new instance.web.Session(undefined,url); - this.connection.session_id = _.uniqueId('posproxy'); - this.test_connection(); + this.connection = null; + this.connected = new $.Deferred(); + this.status = 'disconnected'; + window.proxy = this; - }, close: function(){ - this.connection.destroy(); + if(this.status !== 'disconnected'){ + this.connection.destroy(); + this.status = 'disconnected'; + } + }, + connect : function(url){ + var self = this; + this.connection = new instance.web.Session(undefined,url); + this.connection.session_id = _.uniqueId('posproxy'); + this.status = 'connecting'; + + return this.message('handshake').then(function(){ + self.status = 'connected'; + localStorage['hw_proxy_url'] = url; + self.connected.resolve(); + },function(){ + self.status = 'disconnected'; + self.connected.reject(); + console.error('Could not connect to the Proxy'); + }); }, message : function(name,params){ var callbacks = this.notifications[name] || []; for(var i = 0; i < callbacks.length; i++){ callbacks[i](params); } - if(this.connected){ - return this.connection.rpc('/pos/' + name, params || {}); + if(this.status !== 'disconnected'){ + return this.connection.rpc('/hw_proxy/' + name, params || {}); }else{ return (new $.Deferred()).reject(); } }, - test_connection: function(){ - var self = this; - this.connected = true; - return this.message('test_connection').fail(function(){ - self.connected = false; - console.error('Could not connect to the Proxy'); - }); + + test_connection: function(host,timeout){ + return $.ajax({ + url: host + '/hw_proxy/hello', + method: 'GET', + timeout: timeout || 10000, + }); + }, + + find_proxy: function(options){ + options = options || {}; + var self = this; + var port = ':8069'; + var urls = []; + var found = false; + var proxies = []; + var done = new $.Deferred(); + var parallel = 8; + var threads = []; + var progress = 0; + + if(options.force_ip){ + urls.push(options.force_ip); + }else{ + if(localStorage['hw_proxy_url']){ + urls.push(localStorage['hw_proxy_url']); + } + + urls.push('http://localhost'+port); + + for(var i = 0; i < 256; i++){ + urls.push('http://192.168.0.'+i+port); + urls.push('http://192.168.1.'+i+port); + urls.push('http://192.168.2.'+i+port); + urls.push('http://10.0.0.'+i+port); + } + } + + var prog_inc = 1/urls.length; + + function update_progress(){ + progress = found ? 1 : progress + prog_inc; + if(options.progress){ + options.progress(progress); + } + } + + function thread(url,done){ + if(!url){ + done.resolve(); + } + var c = self.test_connection(url, 100) + .done(function(){ + found = true; + update_progress(); + proxies.push(url); + done.resolve(url); + }) + .fail(function(){ + update_progress(); + var next_url = urls.shift(); + if(found ||! self.searching_for_proxy || !next_url){ + done.resolve(); + }else{ + thread(next_url,done); + } + }); + return done; + } + + this.searching_for_proxy = true; + + for(var i = 0; i < Math.min(parallel,urls.length); i++){ + threads.push(thread(urls.shift(),new $.Deferred())); + } + + var done = new $.Deferred(); + + $.when.apply($,threads).then(function(){ + var urls = []; + for(var i = 0; i < arguments.length; i++){ + if(arguments[i]){ + urls.push(arguments[i]); + } + } + done.resolve(urls); + }); + + return done; + }, + + stop_searching: function(){ + this.searching_for_proxy = false; }, // this allows the client to be notified when a proxy call is made. The notification @@ -360,6 +465,7 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal init: function(attributes){ this.pos = attributes.pos; this.action_callback = {}; + this.proxy = attributes.proxy; this.action_callback_stack = []; @@ -516,16 +622,20 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal return parse_result; }, - scan: function(type,code){ - if (type === 'ean13'){ + scan: function(code){ + if(code.length < 3){ + return; + }else if(code.length === 13 && /^\d+$/.test(code)){ var parse_result = this.parse_ean(code); - }else if(type === 'reference'){ + }else if(this.pos.db.get_product_by_reference(code)){ var parse_result = { encoding: 'reference', type: 'unit', code: code, prefix: '', }; + }else{ + return; } if (parse_result.type === 'error') { //most likely a checksum error, raise warning @@ -541,12 +651,6 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal } }, - on_reference: function(code){ - if(this.action_callback['reference']){ - this.action_callback['reference'](code); - } - }, - // starts catching keyboard events and tries to interpret codebar // calling the callbacks when needed. connect: function(){ @@ -583,11 +687,7 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal // Internal Ref 5449 vs EAN13 5449000... timeout = setTimeout(function(){ - if(code.length === 13 && onlynumbers){ - self.scan('ean13',code); - }else if(code.length >= 3 && self.pos.db.get_product_by_reference(code)){ - self.scan('reference',code); - } + self.scan(code); code = ""; onlynumbers = true; },100); @@ -599,6 +699,27 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal // stops catching keyboard events disconnect: function(){ $('body').off('keypress', this.handler) + this.remote_scanning = false; + }, + connect_to_proxy: function(){ + var self = this; + this.remote_scanning = true; + + this.proxy.connected.then(function waitforbarcode(){ + if(!self.remote_scanning){ + return; + } + return self.proxy.connection.rpc('/hw_proxy/scanner',{}).then(function(barcode){ + if(!self.remote_scanning){ + return; + } + self.scan(barcode); + waitforbarcode(); + }, + function(){ + setTimeout(waitforbarcode,5000); + }); + }); }, }); diff --git a/addons/point_of_sale/static/src/js/main.js b/addons/point_of_sale/static/src/js/main.js index 55f0bbd1f61..90fa3567cda 100644 --- a/addons/point_of_sale/static/src/js/main.js +++ b/addons/point_of_sale/static/src/js/main.js @@ -13,8 +13,6 @@ openerp.point_of_sale = function(instance) { openerp_pos_keyboard(instance,module); // import pos_keyboard_widget.js - openerp_pos_scrollbar(instance,module); // import pos_scrollbar_widget.js - openerp_pos_screens(instance,module); // import pos_screens.js openerp_pos_devices(instance,module); // import pos_devices.js diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index e47101ce27d..a626b20b77e 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1,5 +1,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sale var QWeb = instance.web.qweb; + var _t = instance.web._t; var round_di = instance.web.round_decimals; var round_pr = instance.web.round_precision @@ -19,41 +20,41 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal Backbone.Model.prototype.initialize.call(this, attributes); var self = this; this.session = session; - this.ready = $.Deferred(); // used to notify the GUI that the PosModel has loaded all resources this.flush_mutex = new $.Mutex(); // used to make sure the orders are sent to the server once at time + this.pos_widget = attributes.pos_widget; - this.barcode_reader = new module.BarcodeReader({'pos': this}); // used to read barcodes this.proxy = new module.ProxyDevice(); // used to communicate to the hardware devices via a local proxy - this.proxy_queue = new module.JobQueue(); // used to prevent parallels communications to the proxy - this.db = new module.PosLS(); // a database used to store the products and categories - this.db.clear('products','categories'); + this.barcode_reader = new module.BarcodeReader({'pos': this, proxy:this.proxy}); // used to read barcodes + this.barcode_reader.connect_to_proxy(); + this.proxy_queue = new module.JobQueue(); // used to prevent parallels communications to the proxy + this.db = new module.PosDB(); // a local database used to search trough products and categories & store pending orders this.debug = jQuery.deparam(jQuery.param.querystring()).debug !== undefined; //debug mode + + // Business data; loaded from the server at launch + this.accounting_precision = 2; //TODO + this.company_logo = null; + this.company_logo_base64 = ''; + this.currency = null; + this.shop = null; + this.company = null; + this.user = null; + this.users = []; + this.partners = []; + this.cashier = null; + this.cashregisters = []; + this.bankstatements = []; + this.taxes = []; + this.pos_session = null; + this.config = null; + this.units = []; + this.units_by_id = {}; + this.pricelist = null; + window.posmodel = this; - // default attributes values. If null, it will be loaded below. + // these dynamic attributes can be watched for change by other models or widgets this.set({ 'nbr_pending_operations': 0, - - 'currency': {symbol: '$', position: 'after'}, - 'shop': null, - 'company': null, - 'user': null, // the user that loaded the pos - 'user_list': null, // list of all users - 'partner_list': null, // list of all partners with an ean - 'cashier': null, // the logged cashier, if different from user - 'orders': new module.OrderCollection(), - //this is the product list as seen by the product list widgets, it will change based on the category filters - 'products': new module.ProductCollection(), - 'cashRegisters': null, - - 'bank_statements': null, - 'taxes': null, - 'pos_session': null, - 'pos_config': null, - 'units': null, - 'units_by_id': null, - 'pricelist': null, - 'selectedOrder': null, }); @@ -64,15 +65,11 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal // We fetch the backend data on the server asynchronously. this is done only when the pos user interface is launched, // Any change on this data made on the server is thus not reflected on the point of sale until it is relaunched. // when all the data has loaded, we compute some stuff, and declare the Pos ready to be used. - $.when(this.load_server_data()) - .done(function(){ - //self.log_loaded_data(); //Uncomment if you want to log the data to the console for easier debugging - self.ready.resolve(); - }).fail(function(){ - //we failed to load some backend data, or the backend was badly configured. - //the error messages will be displayed in PosWidget - self.ready.reject(); + this.ready = this.load_server_data() + .then(function(){ + return self.connect_to_posbox(); }); + }, // releases ressources holds by the model at the end of life of the posmodel @@ -83,8 +80,30 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal this.barcode_reader.disconnect(); }, + connect_to_posbox: function(){ + var self = this; + this.pos_widget.loading_message(_t('Connecting to the PosBox'),0); + + this.pos_widget.loading_skip(function(){ + self.proxy.stop_searching(); + }); + + return this.proxy.find_proxy({ + force_ip: self.config.proxy_ip || undefined, + progress: function(prog){ + self.pos_widget.loading_progress(prog); + }, + }).then(function(proxies){ + if(proxies.length > 0){ + self.proxy.connect(proxies[0]); + } + }); + }, + // helper function to load data from the server fetch: function(model, fields, domain, ctx){ + this._load_progress = (this._load_progress || 0) + 0.05; + this.pos_widget.loading_message(_t('Loading')+' '+model,this._load_progress); return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all() }, // loads all the needed data on the sever. returns a deferred indicating when all the data has loaded. @@ -93,7 +112,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal var loaded = self.fetch('res.users',['name','company_id'],[['id','=',this.session.uid]]) .then(function(users){ - self.set('user',users[0]); + self.user = users[0]; return self.fetch('res.company', [ @@ -108,75 +127,72 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal ], [['id','=',users[0].company_id[0]]]); }).then(function(companies){ - self.set('company',companies[0]); + self.company = companies[0]; return self.fetch('res.partner',['contact_address'],[['id','=',companies[0].partner_id[0]]]); }).then(function(company_partners){ - self.get('company').contact_address = company_partners[0].contact_address; + self.company.contact_address = company_partners[0].contact_address; return self.fetch('product.uom', null, null); }).then(function(units){ - self.set('units',units); + self.units = units; var units_by_id = {}; for(var i = 0, len = units.length; i < len; i++){ units_by_id[units[i].id] = units[i]; } - self.set('units_by_id',units_by_id); - - return self.fetch('product.packaging', null, null); - }).then(function(packagings){ - self.set('product.packaging',packagings); + self.units_by_id = units_by_id; return self.fetch('res.users', ['name','ean13'], [['ean13', '!=', false]]); }).then(function(users){ - self.set('user_list',users); + self.users = users; return self.fetch('res.partner', ['name','ean13'], [['ean13', '!=', false]]); }).then(function(partners){ - self.set('partner_list',partners); + self.partners = partners; - return self.fetch('account.tax', ['amount', 'price_include', 'type']); + return self.fetch('account.tax', ['name','amount', 'price_include', 'type']); }).then(function(taxes){ - self.set('taxes', taxes); + self.taxes = taxes; return self.fetch( 'pos.session', ['id', 'journal_ids','name','user_id','config_id','start_at','stop_at'], [['state', '=', 'opened'], ['user_id', '=', self.session.uid]] ); - }).then(function(sessions){ - self.set('pos_session', sessions[0]); + }).then(function(pos_sessions){ + self.pos_session = pos_sessions[0]; return self.fetch( 'pos.config', ['name','journal_ids','warehouse_id','journal_id','pricelist_id', 'iface_self_checkout', 'iface_led', 'iface_cashdrawer', 'iface_payment_terminal', 'iface_electronic_scale', 'iface_barscan', 'iface_vkeyboard', - 'iface_print_via_proxy','iface_cashdrawer','iface_invoicing','state','sequence_id','session_ids'], - [['id','=', self.get('pos_session').config_id[0]]] + 'iface_print_via_proxy','iface_cashdrawer','iface_invoicing','iface_big_scrollbars', + 'receipt_header','receipt_footer','proxy_ip', + 'state','sequence_id','session_ids'], + [['id','=', self.pos_session.config_id[0]]] ); }).then(function(configs){ - var pos_config = configs[0]; - self.set('pos_config', pos_config); - self.iface_electronic_scale = !!pos_config.iface_electronic_scale; - self.iface_print_via_proxy = !!pos_config.iface_print_via_proxy; - self.iface_vkeyboard = !!pos_config.iface_vkeyboard; - self.iface_self_checkout = !!pos_config.iface_self_checkout; - self.iface_cashdrawer = !!pos_config.iface_cashdrawer; - self.iface_invoicing = !!pos_config.iface_invoicing; + self.config = configs[0]; - return self.fetch('stock.warehouse',[],[['id','=',pos_config.warehouse_id[0]]]); + return self.fetch('stock.warehouse',[],[['id','=', self.config.warehouse_id[0]]]); }).then(function(shops){ - self.set('shop',shops[0]); + self.shop = shops[0]; - return self.fetch('product.pricelist',['currency_id'],[['id','=',self.get('pos_config').pricelist_id[0]]]); + return self.fetch('product.pricelist',['currency_id'],[['id','=',self.config.pricelist_id[0]]]); }).then(function(pricelists){ - self.set('pricelist',pricelists[0]); + self.pricelist = pricelists[0]; - return self.fetch('res.currency',['symbol','position','rounding','accuracy'],[['id','=',self.get('pricelist').currency_id[0]]]); + return self.fetch('res.currency',['symbol','position','rounding','accuracy'],[['id','=',self.pricelist.currency_id[0]]]); }).then(function(currencies){ - self.set('currency',currencies[0]); + self.currency = currencies[0]; + /* + return (new instance.web.Model('decimal.precision')).call('get_precision',[['Account']]); + }).then(function(precision){ + self.accounting_precision = precision; + console.log("PRECISION",precision); +*/ return self.fetch('product.packaging',['ean','product_id']); }).then(function(packagings){ self.db.add_packagings(packagings); @@ -190,7 +206,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal ['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13', 'default_code', 'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description'], [['sale_ok','=',true],['available_in_pos','=',true]], - {pricelist: self.get('pricelist').id} // context for price + {pricelist: self.pricelist.id} // context for price ); }).then(function(products){ self.db.add_products(products); @@ -198,53 +214,55 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal return self.fetch( 'account.bank.statement', ['account_id','currency','journal_id','state','name','user_id','pos_session_id'], - [['state','=','open'],['pos_session_id', '=', self.get('pos_session').id]] + [['state','=','open'],['pos_session_id', '=', self.pos_session.id]] ); - }).then(function(bank_statements){ - var journals = new Array(); - _.each(bank_statements,function(statement) { + }).then(function(bankstatements){ + var journals = []; + _.each(bankstatements,function(statement) { journals.push(statement.journal_id[0]) }); - self.set('bank_statements', bank_statements); + self.bankstatements = bankstatements; return self.fetch('account.journal', undefined, [['id','in', journals]]); }).then(function(journals){ - self.set('journals',journals); + self.journals = journals; // associate the bank statements with their journals. - var bank_statements = self.get('bank_statements'); - for(var i = 0, ilen = bank_statements.length; i < ilen; i++){ + var bankstatements = self.bankstatements + for(var i = 0, ilen = bankstatements.length; i < ilen; i++){ for(var j = 0, jlen = journals.length; j < jlen; j++){ - if(bank_statements[i].journal_id[0] === journals[j].id){ - bank_statements[i].journal = journals[j]; - bank_statements[i].self_checkout_payment_method = journals[j].self_checkout_payment_method; + if(bankstatements[i].journal_id[0] === journals[j].id){ + bankstatements[i].journal = journals[j]; + bankstatements[i].self_checkout_payment_method = journals[j].self_checkout_payment_method; } } } - self.set({'cashRegisters' : new module.CashRegisterCollection(self.get('bank_statements'))}); + self.cashregisters = bankstatements; + + // Load the company Logo + + self.company_logo = new Image(); + self.company_logo.crossOrigin = 'anonymous'; + var logo_loaded = new $.Deferred(); + self.company_logo.onload = function(){ + var c = document.createElement('canvas'); + c.width = self.company_logo.width; + c.height = self.company_logo.height; + var ctx = c.getContext('2d'); + ctx.drawImage(self.company_logo,0,0); + self.company_logo_base64 = c.toDataURL(); + logo_loaded.resolve(); + }; + self.company_logo.onerror = function(){ + logo_loaded.reject(); + }; + self.company_logo.src = window.location.origin + '/web/binary/company_logo'; + + return logo_loaded; }); return loaded; }, - // logs the usefull posmodel data to the console for debug purposes - log_loaded_data: function(){ - console.log('PosModel data has been loaded:'); - console.log('PosModel: units:',this.get('units')); - console.log('PosModel: bank_statements:',this.get('bank_statements')); - console.log('PosModel: journals:',this.get('journals')); - console.log('PosModel: taxes:',this.get('taxes')); - console.log('PosModel: pos_session:',this.get('pos_session')); - console.log('PosModel: pos_config:',this.get('pos_config')); - console.log('PosModel: cashRegisters:',this.get('cashRegisters')); - console.log('PosModel: shop:',this.get('shop')); - console.log('PosModel: company:',this.get('company')); - console.log('PosModel: currency:',this.get('currency')); - console.log('PosModel: user_list:',this.get('user_list')); - console.log('PosModel: user:',this.get('user')); - console.log('PosModel.session:',this.session); - console.log('PosModel end of data log.'); - }, - // this is called when an order is removed from the order collection. It ensures that there is always an existing // order and a valid selected order on_removed_order: function(removed_order,index,reason){ @@ -376,7 +394,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal _flush_order: function(order_id, options){ var self = this; options = options || {}; - timeout = typeof options.timeout === 'number' ? options.timeout : 5000; + timeout = typeof options.timeout === 'number' ? options.timeout : 7500; var order = this.db.get_order(order_id); order.to_invoice = options.to_invoice || false; @@ -442,33 +460,16 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal } if(parsed_code.type === 'price'){ - selectedOrder.addProduct(new module.Product(product), {price:parsed_code.value}); + selectedOrder.addProduct(product, {price:parsed_code.value}); }else if(parsed_code.type === 'weight'){ - selectedOrder.addProduct(new module.Product(product), {quantity:parsed_code.value, merge:false}); + selectedOrder.addProduct(product, {quantity:parsed_code.value, merge:false}); }else{ - selectedOrder.addProduct(new module.Product(product)); + selectedOrder.addProduct(product); } return true; }, }); - module.CashRegister = Backbone.Model.extend({ - }); - - module.CashRegisterCollection = Backbone.Collection.extend({ - model: module.CashRegister, - }); - - module.Product = Backbone.Model.extend({ - get_image_url: function(){ - return instance.session.url('/web/binary/image', {model: 'product.product', field: 'image_medium', id: this.get('id')}); - }, - }); - - module.ProductCollection = Backbone.Collection.extend({ - model: module.Product, - }); - // An orderline represent one element of the content of a client's shopping cart. // An orderline contains a product, its quantity, its price, discount. etc. // An Order contains zero or more Orderlines. @@ -477,7 +478,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal this.pos = options.pos; this.order = options.order; this.product = options.product; - this.price = options.product.get('price'); + this.price = options.product.price; this.quantity = 1; this.quantityStr = '1'; this.discount = 0; @@ -490,7 +491,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal var disc = Math.min(Math.max(parseFloat(discount) || 0, 0),100); this.discount = disc; this.discountStr = '' + disc; - this.trigger('change'); + this.trigger('change',this); }, // returns the discount [0,100]% get_discount: function(){ @@ -520,7 +521,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal this.quantityStr = '' + this.quantity; } } - this.trigger('change'); + this.trigger('change',this); }, // return the quantity of product get_quantity: function(){ @@ -539,7 +540,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal }, // return the unit of measure of the product get_unit: function(){ - var unit_id = (this.product.get('uos_id') || this.product.get('uom_id')); + var unit_id = (this.product.uos_id || this.product.uom_id); if(!unit_id){ return undefined; } @@ -547,7 +548,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal if(!this.pos){ return undefined; } - return this.pos.get('units_by_id')[unit_id]; + return this.pos.units_by_id[unit_id]; }, // return the product of this orderline get_product: function(){ @@ -556,7 +557,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal // selects or deselects this orderline set_selected: function(selected){ this.selected = selected; - this.trigger('change'); + this.trigger('change',this); }, // returns true if this orderline is selected is_selected: function(){ @@ -565,7 +566,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal // when we add an new orderline we want to merge it with the last line to see reduce the number of items // in the orderline. This returns true if it makes sense to merge the two can_be_merged_with: function(orderline){ - if( this.get_product().get('id') !== orderline.get_product().get('id')){ //only orderline of the same product can be merged + if( this.get_product().id !== orderline.get_product().id){ //only orderline of the same product can be merged return false; }else if(this.get_product_type() !== orderline.get_product_type()){ return false; @@ -585,7 +586,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal qty: this.get_quantity(), price_unit: this.get_unit_price(), discount: this.get_discount(), - product_id: this.get_product().get('id'), + product_id: this.get_product().id, }; }, //used to create a json of the ticket, to be sent to the printer @@ -595,26 +596,26 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal unit_name: this.get_unit().name, price: this.get_unit_price(), discount: this.get_discount(), - product_name: this.get_product().get('name'), + product_name: this.get_product().name, price_display : this.get_display_price(), price_with_tax : this.get_price_with_tax(), price_without_tax: this.get_price_without_tax(), tax: this.get_tax(), - product_description: this.get_product().get('description'), - product_description_sale: this.get_product().get('description_sale'), + product_description: this.get_product().description, + product_description_sale: this.get_product().description_sale, }; }, // changes the base price of the product for this orderline set_unit_price: function(price){ this.price = round_di(parseFloat(price) || 0, 2); - this.trigger('change'); + this.trigger('change',this); }, get_unit_price: function(){ - var rounding = this.pos.get('currency').rounding; + var rounding = this.pos.currency.rounding; return round_pr(this.price,rounding); }, get_display_price: function(){ - var rounding = this.pos.get('currency').rounding; + var rounding = this.pos.currency.rounding; return round_pr(round_pr(this.get_unit_price() * this.get_quantity(),rounding) * (1- this.get_discount()/100.0),rounding); }, get_price_without_tax: function(){ @@ -626,18 +627,21 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal get_tax: function(){ return this.get_all_prices().tax; }, + get_tax_details: function(){ + return this.get_all_prices().taxDetails; + }, get_all_prices: function(){ var self = this; - var currency_rounding = this.pos.get('currency').rounding; + var currency_rounding = this.pos.currency.rounding; var base = round_pr(this.get_quantity() * this.get_unit_price() * (1.0 - (this.get_discount() / 100.0)), currency_rounding); var totalTax = base; var totalNoTax = base; - var product_list = this.pos.get('product_list'); var product = this.get_product(); - var taxes_ids = product.get('taxes_id');; - var taxes = self.pos.get('taxes'); + var taxes_ids = product.taxes_id; + var taxes = self.pos.taxes; var taxtotal = 0; + var taxdetail = {}; _.each(taxes_ids, function(el) { var tax = _.detect(taxes, function(t) {return t.id === el;}); if (tax.price_include) { @@ -652,6 +656,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal tmp = round_pr(tmp,currency_rounding); taxtotal += tmp; totalNoTax -= tmp; + taxdetail[tax.id] = tmp; } else { var tmp; if (tax.type === "percent") { @@ -664,12 +669,14 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal tmp = round_pr(tmp,currency_rounding); taxtotal += tmp; totalTax += tmp; + taxdetail[tax.id] = tmp; } }); return { "priceWithTax": totalTax, "priceWithoutTax": totalNoTax, "tax": taxtotal, + "taxDetails": taxdetail, }; }, }); @@ -678,32 +685,37 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal model: module.Orderline, }); - // Every PaymentLine contains a cashregister and an amount of money. + // Every Paymentline contains a cashregister and an amount of money. module.Paymentline = Backbone.Model.extend({ initialize: function(attributes, options) { this.amount = 0; - this.cashregister = options.cashRegister; + this.cashregister = options.cashregister; + this.name = this.cashregister.journal_id[1]; + this.selected = false; }, //sets the amount of money on this payment line set_amount: function(value){ - this.amount = parseFloat(value) || 0; - this.trigger('change'); + this.amount = round_di(parseFloat(value) || 0, 2); + this.trigger('change:amount',this); }, // returns the amount of money on this paymentline get_amount: function(){ return this.amount; }, - // returns the associated cashRegister - get_cashregister: function(){ - return this.cashregister; + set_selected: function(selected){ + if(this.selected !== selected){ + this.selected = selected; + this.trigger('change:selected',this); + } }, + // returns the associated cashregister //exports as JSON for server communication export_as_JSON: function(){ return { name: instance.web.datetime_to_str(new Date()), - statement_id: this.cashregister.get('id'), - account_id: (this.cashregister.get('account_id'))[0], - journal_id: (this.cashregister.get('journal_id'))[0], + statement_id: this.cashregister.id, + account_id: this.cashregister.account_id[0], + journal_id: this.cashregister.journal_id[0], amount: this.get_amount() }; }, @@ -711,7 +723,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal export_for_printing: function(){ return { amount: this.get_amount(), - journal: this.cashregister.get('journal_id')[1], + journal: this.cashregister.journal_id[1], }; }, }); @@ -722,7 +734,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal // An order more or less represents the content of a client's shopping cart (the OrderLines) - // plus the associated payment information (the PaymentLines) + // plus the associated payment information (the Paymentlines) // there is always an active ('selected') order in the Pos, a new one is created // automaticaly once an order is completed and sent to the server. module.Order = Backbone.Model.extend({ @@ -736,8 +748,9 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal name: "Order " + this.uid, client: null, }); - this.pos = attributes.pos; - this.selected_orderline = undefined; + this.pos = attributes.pos; + this.selected_orderline = undefined; + this.selected_paymentline = undefined; this.screen_data = {}; // see ScreenSelector this.receipt_type = 'receipt'; // 'receipt' || 'invoice' return this; @@ -747,7 +760,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal }, addProduct: function(product, options){ options = options || {}; - var attr = product.toJSON(); + var attr = JSON.parse(JSON.stringify(product)); attr.pos = this.pos; attr.order = this; var line = new module.Orderline({}, {pos: this.pos, order: this, product: product}); @@ -774,13 +787,21 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal getLastOrderline: function(){ return this.get('orderLines').at(this.get('orderLines').length -1); }, - addPaymentLine: function(cashRegister) { + addPaymentline: function(cashregister) { var paymentLines = this.get('paymentLines'); - var newPaymentline = new module.Paymentline({},{cashRegister:cashRegister}); - if(cashRegister.get('journal').type !== 'cash'){ - newPaymentline.set_amount( this.getDueLeft() ); + var newPaymentline = new module.Paymentline({},{cashregister:cashregister}); + if(cashregister.journal.type !== 'cash'){ + newPaymentline.set_amount( Math.max(this.getDueLeft(),0) ); } paymentLines.add(newPaymentline); + this.selectPaymentline(newPaymentline); + + }, + removePaymentline: function(line){ + if(this.selected_paymentline === line){ + this.selectPaymentline(undefined); + } + this.get('paymentLines').remove(line); }, getName: function() { return this.get('name'); @@ -810,6 +831,32 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal return sum + orderLine.get_tax(); }), 0); }, + getTaxDetails: function(){ + var details = {}; + var fulldetails = []; + var taxes_by_id = {}; + + for(var i = 0; i < this.pos.taxes.length; i++){ + taxes_by_id[this.pos.taxes[i].id] = this.pos.taxes[i]; + } + + this.get('orderLines').each(function(line){ + var ldetails = line.get_tax_details(); + for(var id in ldetails){ + if(ldetails.hasOwnProperty(id)){ + details[id] = (details[id] || 0) + ldetails[id]; + } + } + }); + + for(var id in details){ + if(details.hasOwnProperty(id)){ + fulldetails.push({amount: details[id], tax: taxes_by_id[id]}); + } + } + + return fulldetails; + }, getPaidTotal: function() { return (this.get('paymentLines')).reduce((function(sum, paymentLine) { return sum + paymentLine.get_amount(); @@ -867,9 +914,9 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal paymentlines.push(paymentline.export_for_printing()); }); var client = this.get('client'); - var cashier = this.pos.get('cashier') || this.pos.get('user'); - var company = this.pos.get('company'); - var shop = this.pos.get('shop'); + var cashier = this.pos.cashier || this.pos.user; + var company = this.pos.company; + var shop = this.pos.shop; var date = new Date(); return { @@ -881,18 +928,27 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal total_tax: this.getTax(), total_paid: this.getPaidTotal(), total_discount: this.getDiscountTotal(), + tax_details: this.getTaxDetails(), change: this.getChange(), name : this.getName(), client: client ? client.name : null , invoice_id: null, //TODO cashier: cashier ? cashier.name : null, + header: this.pos.config.receipt_header || '', + footer: this.pos.config.receipt_footer || '', + precision: { + price: 2, + money: 2, + quantity: 3, + }, date: { year: date.getFullYear(), month: date.getMonth(), date: date.getDate(), // day of the month day: date.getDay(), // day of the week hour: date.getHours(), - minute: date.getMinutes() + minute: date.getMinutes() , + isostring: date.toISOString(), }, company:{ email: company.email, @@ -902,11 +958,12 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal vat: company.vat, name: company.name, phone: company.phone, + logo: this.pos.company_logo_base64, }, shop:{ name: shop.name, }, - currency: this.pos.get('currency'), + currency: this.pos.currency, }; }, export_as_JSON: function() { @@ -927,9 +984,9 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal amount_return: this.getChange(), lines: orderLines, statement_ids: paymentLines, - pos_session_id: this.pos.get('pos_session').id, + pos_session_id: this.pos.pos_session.id, partner_id: this.get_client() ? this.get_client().id : false, - user_id: this.pos.get('cashier') ? this.pos.get('cashier').id : this.pos.get('user').id, + user_id: this.pos.cashier ? this.pos.cashier.id : this.pos.user.id, uid: this.uid, }; }, @@ -949,6 +1006,24 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal this.selected_orderline = undefined; } }, + deselectLine: function(){ + if(this.selected_orderline){ + this.selected_orderline.set_selected(false); + this.selected_orderline = undefined; + } + }, + selectPaymentline: function(line){ + if(line !== this.selected_paymentline){ + if(this.selected_paymentline){ + this.selected_paymentline.set_selected(false); + } + this.selected_paymentline = line; + if(this.selected_paymentline){ + this.selected_paymentline.set_selected(true); + } + this.trigger('change:selected_paymentline',this.selected_paymentline); + } + }, }); module.OrderCollection = Backbone.Collection.extend({ diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 01ccd538d8c..7860de8b328 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -157,6 +157,8 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa barcode_product_screen: 'products', //if defined, this screen will be loaded when a product is scanned barcode_product_error_popup: 'error-product', //if defined, this popup will be loaded when there's an error in the popup + hotkeys_handlers: {}, + // what happens when a product is scanned : // it will add the product to the order and go to barcode_product_screen. Or show barcode_product_error_popup if // there's an error. @@ -180,10 +182,10 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa // - if there's a user with a matching ean, put it as the active 'cashier', go to cashier mode, and return true // - else : do nothing and return false. You probably want to extend this to show and appropriate error popup... barcode_cashier_action: function(code){ - var users = this.pos.get('user_list'); + var users = this.pos.users; for(var i = 0, len = users.length; i < len; i++){ if(users[i].ean13 === code.code){ - this.pos.set('cashier',users[i]); + this.pos.cashier = users[i]; this.pos_widget.username.refresh(); this.pos.proxy.cashier_mode_activated(); this.pos_widget.screen_selector.set_user_mode('cashier'); @@ -199,7 +201,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa // - if there's a user with a matching ean, put it as the active 'client' and return true // - else : return false. barcode_client_action: function(code){ - var partners = this.pos.get('partner_list'); + var partners = this.pos.partners; for(var i = 0, len = partners.length; i < len; i++){ if(partners[i].ean13 === code.code){ this.pos.get('selectedOrder').set_client(partners[i]); @@ -227,13 +229,11 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa // with add_action_button() show_action_bar: function(){ this.pos_widget.action_bar.show(); - this.$el.css({'bottom':'105px'}); }, // hides the action bar. The actionbar is automatically hidden when it is empty hide_action_bar: function(){ this.pos_widget.action_bar.hide(); - this.$el.css({'bottom':'0px'}); }, // adds a new button to the action bar. The button definition takes three parameters, all optional : @@ -278,7 +278,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.pos_widget.set_left_action_bar_visible(this.show_leftpane && !this.cashier_mode); this.pos_widget.set_cashier_controls_visible(this.cashier_mode); - if(this.cashier_mode && this.pos.iface_self_checkout){ + if(this.cashier_mode && this.pos.config.iface_self_checkout){ this.pos_widget.client_button.show(); }else{ this.pos_widget.client_button.hide(); @@ -497,6 +497,17 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.set_weight(0); this.renderElement(); + this.hotkey_handler = function(event){ + if(event.which === 13){ + self.order_product(); + self.pos_widget.screen_selector.set_current_screen(self.next_screen); + }else if(event.which === 27){ + self.pos_widget.screen_selector.set_current_screen(self.previous_screen); + } + }; + + $('body').on('keyup',this.hotkey_handler); + this.add_action_button({ label: _t('Back'), icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png', @@ -546,11 +557,11 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }, get_product_name: function(){ var product = this.get_product(); - return (product ? product.get('name') : undefined) || 'Unnamed Product'; + return (product ? product.name : undefined) || 'Unnamed Product'; }, get_product_price: function(){ var product = this.get_product(); - return (product ? product.get('price') : 0) || 0; + return (product ? product.price : 0) || 0; }, set_weight: function(weight){ this.weight = weight; @@ -559,9 +570,18 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa get_product_weight_string: function(){ return (this.weight || 0).toFixed(3) + ' Kg'; }, + get_product_image_url: function(){ + var product = this.get_product(); + if(product){ + return window.location.origin + '/web/binary/image?model=product.product&field=image_medium&id='+product.id; + }else{ + return ""; + } + }, close: function(){ var self = this; this._super(); + $('body').off('keyup',this.hotkey_handler); this.pos.proxy_queue.clear(); this.pos.proxy_queue.schedule(function(){ @@ -616,15 +636,15 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa //we get the first cashregister marked as self-checkout var selfCheckoutRegisters = []; - for(var i = 0; i < self.pos.get('cashRegisters').models.length; i++){ - var cashregister = self.pos.get('cashRegisters').models[i]; + for(var i = 0; i < self.pos.cashregisters.length; i++){ + var cashregister = self.pos.cashregisters[i]; if(cashregister.self_checkout_payment_method){ selfCheckoutRegisters.push(cashregister); } } - var cashregister = selfCheckoutRegisters[0] || self.pos.get('cashRegisters').models[0]; - currentOrder.addPaymentLine(cashregister); + var cashregister = selfCheckoutRegisters[0] || self.pos.cashregisters[0]; + currentOrder.addPaymentline(cashregister); self.pos.push_order(currentOrder) currentOrder.destroy(); self.pos.proxy.transaction_end(); @@ -664,6 +684,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png', click: function(){ self.queue.schedule(self.cancel); + self.pos_widget.screen_selector.set_current_screen(self.previous_screen); } }); }, @@ -683,6 +704,13 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa show_numpad: false, show_leftpane: false, + start: function(){ + this._super(); + $('.goodbye-message').click(function(){ + $(this).addClass('oe_hidden'); + }); + }, + barcode_product_action: function(code){ this.pos.proxy.transaction_start(); this._super(code); @@ -727,19 +755,23 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa start: function(){ //FIXME this should work as renderElement... but then the categories aren't properly set. explore why var self = this; - this.product_categories_widget = new module.ProductCategoriesWidget(this,{}); - this.product_categories_widget.replace($('.placeholder-ProductCategoriesWidget')); this.product_list_widget = new module.ProductListWidget(this,{ click_product_action: function(product){ - if(product.get('to_weight') && self.pos.iface_electronic_scale){ + if(product.to_weight && self.pos.config.iface_electronic_scale){ self.pos_widget.screen_selector.set_current_screen( self.cashier_mode ? self.scale_screen : self.client_scale_screen, {product: product}); }else{ self.pos.get('selectedOrder').addProduct(product); } }, + product_list: this.pos.db.get_product_by_category(0) }); this.product_list_widget.replace($('.placeholder-ProductListWidget')); + + this.product_categories_widget = new module.ProductCategoriesWidget(this,{ + product_list_widget: this.product_list_widget, + }); + this.product_categories_widget.replace($('.placeholder-ProductCategoriesWidget')); }, show: function(){ @@ -748,7 +780,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.product_categories_widget.reset_category(); - this.pos_widget.order_widget.set_numpad_state(this.pos_widget.numpad.state); + this.pos_widget.order_widget.set_editable(true); if(this.pos_widget.screen_selector.current_mode === 'client'){ this.add_action_button({ @@ -763,13 +795,13 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa close: function(){ this._super(); - this.pos_widget.order_widget.set_numpad_state(null); - this.pos_widget.payment_screen.set_numpad_state(null); - if(this.pos.iface_vkeyboard && this.pos_widget.onscreen_keyboard){ + + this.pos_widget.order_widget.set_editable(false); + + if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){ this.pos_widget.onscreen_keyboard.hide(); } }, - }); module.ReceiptScreenWidget = module.ScreenWidget.extend({ @@ -778,18 +810,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa show_numpad: true, show_leftpane: true, - init: function(parent, options) { - this._super(parent,options); - this.model = options.model; - this.user = this.pos.get('user'); - this.company = this.pos.get('company'); - this.shop_obj = this.pos.get('shop'); - }, - renderElement: function() { - this._super(); - this.pos.bind('change:selectedOrder', this.change_selected_order, this); - this.change_selected_order(); - }, show: function(){ this._super(); var self = this; @@ -806,9 +826,9 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa click: function() { self.finishOrder(); }, }); + this.refresh(); this.print(); - // THIS IS THE HACK OF THE CENTURY // // The problem is that in chrome the print() is asynchronous and doesn't // execute until all rpc are finished. So it conflicts with the rpc used @@ -837,23 +857,18 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa finishOrder: function() { this.pos.get('selectedOrder').destroy(); }, - change_selected_order: function() { - if (this.currentOrderLines) - this.currentOrderLines.unbind(); - this.currentOrderLines = (this.pos.get('selectedOrder')).get('orderLines'); - this.currentOrderLines.bind('add', this.refresh, this); - this.currentOrderLines.bind('change', this.refresh, this); - this.currentOrderLines.bind('remove', this.refresh, this); - if (this.currentPaymentLines) - this.currentPaymentLines.unbind(); - this.currentPaymentLines = (this.pos.get('selectedOrder')).get('paymentLines'); - this.currentPaymentLines.bind('all', this.refresh, this); - this.refresh(); - }, refresh: function() { - this.currentOrder = this.pos.get('selectedOrder'); - $('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{widget:this})); + var order = this.pos.get('selectedOrder'); + $('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{ + widget:this, + order: order, + orderlines: order.get('orderLines').models, + paymentlines: order.get('paymentLines').models, + })); }, + close: function(){ + this._super(); + } }); module.PaymentScreenWidget = module.ScreenWidget.extend({ @@ -861,34 +876,79 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa back_screen: 'products', next_screen: 'receipt', init: function(parent, options) { + var self = this; this._super(parent,options); - this.model = options.model; - this.pos.bind('change:selectedOrder', this.change_selected_order, this); - this.bindPaymentLineEvents(); - this.bind_orderline_events(); - this.paymentlinewidgets = []; - this.focusedLine = null; + + this.pos.bind('change:selectedOrder',function(){ + this.bind_events(); + this.renderElement(); + },this); + + this.bind_events(); + + this.line_delete_handler = function(event){ + var node = this; + while(node && !node.classList.contains('paymentline')){ + node = node.parentNode; + } + if(node){ + self.pos.get('selectedOrder').removePaymentline(node.line) + } + event.stopPropagation(); + }; + + this.line_change_handler = function(event){ + var node = this; + while(node && !node.classList.contains('paymentline')){ + node = node.parentNode; + } + if(node){ + node.line.set_amount(this.value); + } + + }; + + this.line_click_handler = function(event){ + var node = this; + while(node && !node.classList.contains('paymentline')){ + node = node.parentNode; + } + if(node){ + self.pos.get('selectedOrder').selectPaymentline(node.line); + } + }; + + this.hotkey_handler = function(event){ + if(event.which === 13){ + self.validate_order(); + }else if(event.which === 27){ + self.back(); + } + }; + }, show: function(){ this._super(); var self = this; - - if( this.pos.iface_cashdrawer && this.pos.get('selectedOrder').get('paymentLines').find( function(pl){ return pl.cashregister.get('journal').type === 'cash'; })){ + + this.enable_numpad(); + this.focus_selected_line(); + + document.body.addEventListener('keyup', this.hotkey_handler); + + if( this.pos.config.iface_cashdrawer + && this.pos.get('selectedOrder').get('paymentLines').find( function(pl){ + return pl.cashregister.journal.type === 'cash'; + })){ this.pos.proxy.open_cashbox(); } - this.set_numpad_state(this.pos_widget.numpad.state); - + this.add_action_button({ label: _t('Back'), icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png', click: function(){ - _.each(self.paymentlinewidgets, function(widget){ - if( widget.payment_line.get_amount() === 0 ){ - widget.payment_line.destroy(); - } - }); - self.pos_widget.screen_selector.set_current_screen(self.back_screen); + self.back(); }, }); @@ -897,22 +957,22 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa name: 'validation', icon: '/point_of_sale/static/src/img/icons/png48/validate.png', click: function(){ - self.validateCurrentOrder(); + self.validate_order(); }, }); - if(this.pos.iface_invoicing){ + if( this.pos.config.iface_invoicing ){ this.add_action_button({ label: 'Invoice', name: 'invoice', icon: '/point_of_sale/static/src/img/icons/png48/invoice.png', click: function(){ - self.validateCurrentOrder({invoice: true}); + self.validate_order({invoice: true}); }, }); } - if( this.pos.iface_cashdrawer ){ + if( this.pos.config.iface_cashdrawer ){ this.add_action_button({ label: _t('Cash'), name: 'cashbox', @@ -923,23 +983,167 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }); } - this.updatePaymentSummary(); - this.line_refocus(); + this.update_payment_summary(); + }, close: function(){ this._super(); - this.pos_widget.order_widget.set_numpad_state(null); - this.pos_widget.payment_screen.set_numpad_state(null); + this.disable_numpad(); + document.body.removeEventListener('keyup',this.hotkey_handler); + }, + remove_empty_lines: function(){ + var order = this.pos.get('selectedOrder'); + var lines = order.get('paymentLines').models.slice(0); + for(var i = 0; i < lines.length; i++){ + var line = lines[i]; + if(line.get_amount() === 0){ + order.removePaymentline(line); + } + } }, back: function() { - this.pos_widget.screen_selector.set_current_screen(self.back_screen); + this.remove_empty_lines(); + this.pos_widget.screen_selector.set_current_screen(this.back_screen); }, - validateCurrentOrder: function(options) { + bind_events: function() { + if(this.old_order){ + this.old_order.unbind(null,null,this); + } + var order = this.pos.get('selectedOrder'); + order.bind('change:selected_paymentline',this.focus_selected_line,this); + + this.old_order = order; + + if(this.old_paymentlines){ + this.old_paymentlines.unbind(null,null,this); + } + var paymentlines = order.get('paymentLines'); + paymentlines.bind('add', this.add_paymentline, this); + paymentlines.bind('change:selected', this.rerender_paymentline, this); + paymentlines.bind('change:amount', function(line){ + if(!line.selected && line.node){ + line.node.value = line.amount.toFixed(2); + } + this.update_payment_summary(); + },this); + paymentlines.bind('remove', this.remove_paymentline, this); + paymentlines.bind('all', this.update_payment_summary, this); + + this.old_paymentlines = paymentlines; + + if(this.old_orderlines){ + this.old_orderlines.unbind(null,null,this); + } + var orderlines = order.get('orderLines'); + orderlines.bind('all', this.update_payment_summary, this); + + this.old_orderlines = orderlines; + }, + focus_selected_line: function(){ + var line = this.pos.get('selectedOrder').selected_paymentline; + if(line){ + var input = line.node.querySelector('input'); + if(!input){ + return; + } + var value = input.value; + input.focus(); + + if(this.numpad_state){ + this.numpad_state.reset(); + } + + if(Number(value) === 0){ + input.value = ''; + }else{ + input.value = value; + input.select(); + } + } + }, + add_paymentline: function(line) { + var list_container = this.el.querySelector('.payment-lines'); + list_container.appendChild(this.render_paymentline(line)); + + if(this.numpad_state){ + this.numpad_state.reset(); + } + }, + render_paymentline: function(line){ + var el_html = openerp.qweb.render('Paymentline',{widget: this, line: line}); + el_html = _.str.trim(el_html); + + var el_node = document.createElement('tbody'); + el_node.innerHTML = el_html; + el_node = el_node.childNodes[0]; + el_node.line = line; + el_node.querySelector('.paymentline-delete') + .addEventListener('click', this.line_delete_handler); + el_node.addEventListener('click', this.line_click_handler); + el_node.querySelector('input') + .addEventListener('keyup', this.line_change_handler); + + line.node = el_node; + + return el_node; + }, + rerender_paymentline: function(line){ + var old_node = line.node; + var new_node = this.render_paymentline(line); + + old_node.parentNode.replaceChild(new_node,old_node); + }, + remove_paymentline: function(line){ + line.node.parentNode.removeChild(line.node); + line.node = undefined; + }, + renderElement: function(){ + this._super(); + + var paymentlines = this.pos.get('selectedOrder').get('paymentLines').models; + var list_container = this.el.querySelector('.payment-lines'); + + for(var i = 0; i < paymentlines.length; i++){ + list_container.appendChild(this.render_paymentline(paymentlines[i])); + } + + this.update_payment_summary(); + }, + update_payment_summary: function() { + var currentOrder = this.pos.get('selectedOrder'); + var paidTotal = currentOrder.getPaidTotal(); + var dueTotal = currentOrder.getTotalTaxIncluded(); + var remaining = dueTotal > paidTotal ? dueTotal - paidTotal : 0; + var change = paidTotal > dueTotal ? paidTotal - dueTotal : 0; + + this.$('.payment-due-total').html(this.format_currency(dueTotal)); + this.$('.payment-paid-total').html(this.format_currency(paidTotal)); + this.$('.payment-remaining').html(this.format_currency(remaining)); + this.$('.payment-change').html(this.format_currency(change)); + if(currentOrder.selected_orderline === undefined){ + remaining = 1; // What is this ? + } + + if(this.pos_widget.action_bar){ + this.pos_widget.action_bar.set_button_disabled('validation', !this.is_paid()); + this.pos_widget.action_bar.set_button_disabled('invoice', !this.is_paid()); + } + }, + is_paid: function(){ + var currentOrder = this.pos.get('selectedOrder'); + return (currentOrder.getTotalTaxIncluded() >= 0.000001 + && currentOrder.getPaidTotal() + 0.000001 >= currentOrder.getTotalTaxIncluded()); + + }, + validate_order: function(options) { var self = this; options = options || {}; var currentOrder = this.pos.get('selectedOrder'); + if(!this.is_paid()){ + return; + } if(options.invoice){ // deactivate the validation button while we try to send the order @@ -966,111 +1170,46 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }else{ this.pos.push_order(currentOrder) - if(this.pos.iface_print_via_proxy){ + if(this.pos.config.iface_print_via_proxy){ this.pos.proxy.print_receipt(currentOrder.export_for_printing()); this.pos.get('selectedOrder').destroy(); //finish order and go back to scan screen }else{ this.pos_widget.screen_selector.set_current_screen(this.next_screen); } } - }, - bindPaymentLineEvents: function() { - this.currentPaymentLines = (this.pos.get('selectedOrder')).get('paymentLines'); - this.currentPaymentLines.bind('add', this.addPaymentLine, this); - this.currentPaymentLines.bind('remove', this.renderElement, this); - this.currentPaymentLines.bind('all', this.updatePaymentSummary, this); - }, - bind_orderline_events: function() { - this.currentOrderLines = (this.pos.get('selectedOrder')).get('orderLines'); - this.currentOrderLines.bind('all', this.updatePaymentSummary, this); - }, - change_selected_order: function() { - this.currentPaymentLines.unbind(); - this.bindPaymentLineEvents(); - this.currentOrderLines.unbind(); - this.bind_orderline_events(); - this.renderElement(); - }, - line_refocus: function(lineWidget){ - if(lineWidget){ - if(this.focusedLine !== lineWidget){ - this.focusedLine = lineWidget; - } - } - if(this.focusedLine){ - this.focusedLine.focus(); - } - }, - addPaymentLine: function(newPaymentLine) { - var self = this; - var l = new module.PaymentlineWidget(this, { - payment_line: newPaymentLine, - }); - l.on('delete_payment_line', self, function(r) { - self.deleteLine(r); - }); - l.appendTo(this.$('#paymentlines')); - this.paymentlinewidgets.push(l); - if(this.numpadState){ - this.numpadState.resetValue(); - } - this.line_refocus(l); - }, - renderElement: function() { - this._super(); - this.$('#paymentlines').empty(); - for(var i = 0, len = this.paymentlinewidgets.length; i < len; i++){ - this.paymentlinewidgets[i].destroy(); - } - this.paymentlinewidgets = []; - - this.currentPaymentLines.each(_.bind( function(paymentLine) { - this.addPaymentLine(paymentLine); - }, this)); - this.updatePaymentSummary(); - }, - deleteLine: function(lineWidget) { - this.currentPaymentLines.remove([lineWidget.payment_line]); - lineWidget.destroy(); - }, - updatePaymentSummary: function() { - var currentOrder = this.pos.get('selectedOrder'); - var paidTotal = currentOrder.getPaidTotal(); - var dueTotal = currentOrder.getTotalTaxIncluded(); - var remaining = dueTotal > paidTotal ? dueTotal - paidTotal : 0; - var change = paidTotal > dueTotal ? paidTotal - dueTotal : 0; - this.$('#payment-due-total').html(this.format_currency(dueTotal)); - this.$('#payment-paid-total').html(this.format_currency(paidTotal)); - this.$('#payment-remaining').html(this.format_currency(remaining)); - this.$('#payment-change').html(this.format_currency(change)); - if(currentOrder.selected_orderline === undefined){ - remaining = 1; // What is this ? + // hide onscreen (iOS) keyboard + setTimeout(function(){ + document.activeElement.blur(); + $("input").blur(); + },250); + }, + enable_numpad: function(){ + this.disable_numpad(); //ensure we don't register the callbacks twice + this.numpad_state = this.pos_widget.numpad.state; + if(this.numpad_state){ + this.numpad_state.reset(); + this.numpad_state.changeMode('payment'); + this.numpad_state.bind('set_value', this.set_value, this); + this.numpad_state.bind('change:mode', this.set_mode_back_to_payment, this); } - - if(this.pos_widget.action_bar){ - this.pos_widget.action_bar.set_button_disabled('validation', remaining > 0.000001); - this.pos_widget.action_bar.set_button_disabled('invoice', remaining > 0.000001); + + }, + disable_numpad: function(){ + if(this.numpad_state){ + this.numpad_state.unbind('set_value', this.set_value); + this.numpad_state.unbind('change:mode',this.set_mode_back_to_payment); } }, - set_numpad_state: function(numpadState) { - if (this.numpadState) { - this.numpadState.unbind('set_value', this.set_value); - this.numpadState.unbind('change:mode', this.setNumpadMode); - } - this.numpadState = numpadState; - if (this.numpadState) { - this.numpadState.bind('set_value', this.set_value, this); - this.numpadState.bind('change:mode', this.setNumpadMode, this); - this.numpadState.reset(); - this.setNumpadMode(); - } - }, - setNumpadMode: function() { - this.numpadState.set({mode: 'payment'}); + set_mode_back_to_payment: function() { + this.numpad_state.set({mode: 'payment'}); }, set_value: function(val) { - this.currentPaymentLines.last().set_amount(val); + var selected_line =this.pos.get('selectedOrder').selected_paymentline; + if(selected_line){ + selected_line.set_amount(val); + selected_line.node.querySelector('input').value = selected_line.amount.toFixed(2); + } }, }); } diff --git a/addons/point_of_sale/static/src/js/tests.js b/addons/point_of_sale/static/src/js/tests.js index 3b1dbc14503..018b343de58 100644 --- a/addons/point_of_sale/static/src/js/tests.js +++ b/addons/point_of_sale/static/src/js/tests.js @@ -13,8 +13,8 @@ function openerp_pos_tests(instance, module){ //module is instance.point_of_sale // randomly switch product categories this.category_switch = function(interval){ queue.schedule(function(){ - var breadcrumbs = $('.breadcrumb a'); - var categories = $('li.category-button'); + var breadcrumbs = $('.breadcrumb-button'); + var categories = $('.category-button'); if(categories.length > 0){ var rnd = Math.floor(Math.random()*categories.length); categories.eq(rnd).click(); @@ -35,12 +35,12 @@ function openerp_pos_tests(instance, module){ //module is instance.point_of_sale while(order_size--){ order_queue.schedule(function(){ - var products = $('.product a'); + var products = $('.product'); if(products.length > 0){ var rnd = Math.floor(Math.random()*products.length); products.eq(rnd).click(); } - },{duration:250}); + },{duration:20}); } order_queue.finished().then(function(){ $('.deleteorder-button').click(); @@ -60,19 +60,20 @@ function openerp_pos_tests(instance, module){ //module is instance.point_of_sale while(order_size--){ order_queue.schedule(function(){ - var products = $('.product a'); + var products = $('.product'); if(products.length > 0){ var rnd = Math.floor(Math.random()*products.length); products.eq(rnd).click(); } - },{duration:250}); + },{duration:50}); } order_queue.schedule(function(){ $('.paypad-button:first').click(); },{duration:250}); order_queue.schedule(function(){ - $('.paymentline-amount input:first').val(10000); - $('.paymentline-amount input:first').keyup(); + $('.paymentline-input:first').val(10000); + $('.paymentline-input:first').keydown(); + $('.paymentline-input:first').keyup(); },{duration:250}); order_queue.schedule(function(){ $('.pos-actionbar-button-list .button:eq(2)').click(); diff --git a/addons/point_of_sale/static/src/js/widget_base.js b/addons/point_of_sale/static/src/js/widget_base.js index 30d85f2696a..648ab725f9e 100644 --- a/addons/point_of_sale/static/src/js/widget_base.js +++ b/addons/point_of_sale/static/src/js/widget_base.js @@ -19,8 +19,8 @@ function openerp_pos_basewidget(instance, module){ //module is instance.point_of }, build_currency_template: function(){ - if(this.pos && this.pos.get('currency')){ - this.currency = this.pos.get('currency'); + if(this.pos && this.pos.currency){ + this.currency = this.pos.currency; }else{ this.currency = {symbol: '$', position: 'after', rounding: 0.01}; } diff --git a/addons/point_of_sale/static/src/js/widget_keyboard.js b/addons/point_of_sale/static/src/js/widget_keyboard.js index 22245b31227..a174c713a2e 100644 --- a/addons/point_of_sale/static/src/js/widget_keyboard.js +++ b/addons/point_of_sale/static/src/js/widget_keyboard.js @@ -35,56 +35,68 @@ function openerp_pos_keyboard(instance, module){ //module is instance.point_of_s this.numlock = false; }, - connect : function($target){ + connect : function(target){ var self = this; - this.$target = $target; - $target.focus(function(){self.show();}); + this.$target = $(target); + this.$target.focus(function(){self.show();}); + }, + generateEvent: function(type,key){ + var event = document.createEvent("KeyboardEvent"); + var initMethod = event.initKeyboardEvent ? 'initKeyboardEvent' : 'initKeyEvent'; + event[initMethod]( type, + true, //bubbles + true, //cancelable + window, //viewArg + false, //ctrl + false, //alt + false, //shift + false, //meta + ((typeof key.code === 'undefined') ? key.char.charCodeAt(0) : key.code), + ((typeof key.char === 'undefined') ? String.fromCharCode(key.code) : key.char) + ); + return event; + }, // Write a character to the input zone writeCharacter: function(character){ - var $input = this.$target; - if(character === '\n'){ - $input.trigger($.Event('keydown',{which:13})); - $input.trigger($.Event('keyup',{which:13})); - }else{ - $input[0].value += character; - $input.keydown(); - $input.keyup(); + var input = this.$target[0]; + input.dispatchEvent(this.generateEvent('keydown',{char: character})); + if(character !== '\n'){ + input.value += character; } - }, - - // Sends a 'return' character to the input zone. TODO - sendReturn: function(){ + input.dispatchEvent(this.generateEvent('keyup',{char: character})); }, // Removes the last character from the input zone. deleteCharacter: function(){ - var $input = this.$target; - var input_value = $input[0].value; - $input[0].value = input_value.substr(0, input_value.length - 1); - $input.keydown(); - $input.keyup(); + var input = this.$target[0]; + input.dispatchEvent(this.generateEvent('keydown',{code: 8})); + input.value = input.value.substr(0, input.value.length -1); + input.dispatchEvent(this.generateEvent('keyup',{code: 8})); }, // Clears the content of the input zone. deleteAllCharacters: function(){ - var $input = this.$target; - $input[0].value = ""; - $input.keydown(); - $input.keyup(); + var input = this.$target[0]; + if(input.value){ + input.dispatchEvent(this.generateEvent('keydown',{code: 8})); + input.value = ""; + input.dispatchEvent(this.generateEvent('keyup',{code: 8})); + } }, // Makes the keyboard show and slide from the bottom of the screen. show: function(){ - $('.keyboard_frame').show().animate({'height':'235px'}, 500, 'swing'); + $('.keyboard_frame').show().css({'height':'235px'}); }, // Makes the keyboard hide by sliding to the bottom of the screen. hide: function(){ - var self = this; - var frame = $('.keyboard_frame'); - frame.animate({'height':'0'}, 500, 'swing', function(){ frame.hide(); self.reset(); }); + $('.keyboard_frame') + .css({'height':'0'}) + .hide(); + this.reset(); }, //What happens when the shift key is pressed : toggle case, remove capslock diff --git a/addons/point_of_sale/static/src/js/widget_scrollbar.js b/addons/point_of_sale/static/src/js/widget_scrollbar.js deleted file mode 100644 index 001e96d0bb6..00000000000 --- a/addons/point_of_sale/static/src/js/widget_scrollbar.js +++ /dev/null @@ -1,265 +0,0 @@ -/* - * This Widget provides a javascript scrollbar that is suitable to use with resistive - * tactile screens. - * - * Options: - * target_widget : the widget that will be scrolled. - * target_selector : if you don't want to scroll the root element of the the widget, you can provide a - * jquery selector string that will match on the widget's dom element. If there is no widget provided, - * it will match on the document - * step: on each click, the target will be scrolled by it's deplayed size multiplied by this value. - * duration: this is the duration of the scrolling animation - * wheel_step: the target will be scrolled by wheel_step pixels on each mouse scroll. - * track_bottom: the target will be kept on bottom when it's on the bottom and the size has changed - * on_show: this function will be called with the scrollbar as sole argument when the scrollbar is shown - * on_hide: this function will be called with the scrollbar as sole argument when the scrollbar is hidden - */ -function openerp_pos_scrollbar(instance, module){ //module is instance.point_of_sale - - module.ScrollbarWidget = instance.web.Widget.extend({ - template:'ScrollbarWidget', - - init: function(parent,options){ - var self = this; - options = options || {}; - this._super(parent,options); - this.target_widget = options.target_widget; - this.target_selector = options.target_selector; - this.scroll_target = this.target().scrollTop(); - this.scroll_step = options.step || 0.8; - this.scroll_duration = options.duration || 250; - this.wheel_step = options.wheel_step || 80; - this.name = options.name || 'unnamed'; - this.bottom = false; // true if the scroller cannot be scrolled further - this.track_bottom = options.track_bottom || false; - this.on_show = options.on_show || function(){}; - this.on_hide = options.on_hide || function(){}; - - // these handlers are declared once for the object's lifetime so that we can bind and unbind them. - this.resize_handler = function(){ - setTimeout(function(){ - if(self.bottom && self.track_bottom){ - self.set_position(Number.MAX_VALUE); - } - self.update_scroller_dimensions(); - self.update_button_status(); - self.auto_hide(false); - },0); - }; - this.target_mousewheel_handler = function(event,delta){ - self.scroll(delta*self.wheel_step); - } - }, - - renderElement: function(){ - this._super(); - var self = this; - this.$('.up-button').off('click').click(function(){ - self.page_up(); - }); - this.$('.down-button').off('click').click(function(){ - self.page_down(); - }); - this.update_scroller_dimensions(false); - this.update_button_status(); - this.auto_hide(false); - this.$el.bind('mousewheel',function(event,delta){ - self.scroll(delta*self.wheel_step); - return false; - }); - this.$el.bind('click',function(event){ - var vpos = event.pageY - self.$el.offset().top; - var spos = self.scroller_dimensions(); - if(vpos > spos.bar_pos && vpos < spos.pos){ - self.page_up(); - }else if( (vpos < spos.bar_pos + spos.bar_height) && - (vpos > spos.pos + spos.height) ){ - self.page_down(); - } - }); - // FIXME: use the event bus to handle window resize events - $(window).unbind('resize',this.resize_handler); - $(window).bind('resize',this.resize_handler); - - this.target().unbind('mousewheel',this.target_mousewheel_handler); - this.target().bind('mousewheel',this.target_mousewheel_handler); - - // because the rendering is asynchronous we must wait for the next javascript update - // for good dimensions values - setTimeout(function(){ - self.update_scroller_dimensions(false); - self.update_button_status(); - self.auto_hide(false); - },0); - }, - - destroy: function(){ - $(window).unbind('resize',this.resize_handler); - this.target().unbind('mousewheel',this.target_mousewheel_handler); - this._super(); - }, - - // shows the scrollbar. if animated is true, it will do it in an animated fashion - show: function(animated){ //FIXME: animated show and hide don't work ... ? - if(animated){ - this.$el.removeClass('oe_hidden').animate({'width':'48px'}, 500, 'swing'); - }else{ - this.$el.removeClass('oe_hidden').css('width','48px'); - } - this.on_show(this); - }, - - // hides the scrollbar. if animated is true, it will do it in a animated fashion - hide: function(animated){ - var self = this; - if(animated){ - this.$el.animate({'width':'0px'}, 500, 'swing', function(){ self.$el.addClass('oe_hidden');}); - }else{ - this.$el.addClass('oe_hidden').css('width','0px'); - } - this.on_hide(this); - }, - - // returns the scroller position and other information as a dictionnary with the following fields: - // pos: the position in pixels of the top of the scroller starting from the top of the scrollbar - // height: the height of the scroller in pixels - // bar_pos: the position of the top of the scrollbar's inner region, starting from the top - // bar_height: the height of the scrollbar's inner region - scroller_dimensions: function(){ - var target = this.target()[0]; - var scroller_height = target.clientHeight / target.scrollHeight || 0; - var scroller_pos = this.scroll_target / target.scrollHeight || 0; - var button_up_height = this.$('.up-button')[0].offsetHeight || 48; - var button_down_height = this.$('.down-button')[0].offsetHeight || 48; - - var bar_height = this.$el[0].offsetHeight || 96; - var scrollbar_height = bar_height - button_up_height - button_down_height; - - scroller_pos = scroller_pos * scrollbar_height + button_up_height; - scroller_height = scroller_height * scrollbar_height; - - return { pos: Math.round(scroller_pos), - height: Math.round(scroller_height), - bar_pos: button_up_height, - bar_height: scrollbar_height }; - }, - - - //checks if it should show or hide the scrollbar based on the target content and then show or hide it - // if animated is true, then the scrollbar will be shown or hidden with an animation - auto_hide: function(animated){ - var target = this.target()[0]; - if(target.clientHeight && (target.clientHeight === target.scrollHeight)){ - this.hide(animated); - }else{ - this.show(animated); - } - }, - - //returns the pageup/down scrolling distance in pixels - get_scroll_step: function(){ - var target = this.target()[0]; - var step = target.clientHeight * this.scroll_step; - var c = target.scrollHeight / step; - var c = Math.max(1,Math.ceil(c)); - return target.scrollHeight / c; - }, - - //sets the scroller to the correct size and position based on the target scrolling status - //if animated is true, the scroller will move smoothly to its destination - update_scroller_dimensions: function(animated){ - var dim = this.scroller_dimensions(); - var target = this.target()[0]; - if(animated){ - this.$('.scroller').animate({'top':dim.pos+'px', 'height': dim.height+'px'},this.scroll_duration); - }else{ - this.$('.scroller').css({'top':dim.pos+'px', 'height': dim.height+'px'}); - } - if(this.scroll_target + target.clientHeight >= target.scrollHeight){ - this.bottom = true; - }else{ - this.bottom = false; - } - }, - - //disable or enable the up/down buttons according to the scrolled position - update_button_status: function(){ - var target = this.target()[0]; - this.$('.up-button').removeClass('disabled'); - this.$('.down-button').removeClass('disabled'); - if(this.scroll_target === 0){ - this.$('.up-button').addClass('disabled'); - } - if(this.scroll_target + target.clientHeight >= target.scrollHeight){ - this.$('.down-button').addClass('disabled'); - } - }, - - //returns the jquery object of the scrolling target - target: function(){ - if(this.target_widget){ - if(this.target_selector){ - return this.target_widget.$(this.target_selector); - }else{ - return this.target_widget.$el; - } - }else if(this.target_selector){ - return $(this.target_selector); - }else{ - return undefined; - } - }, - - //scroll one page up - page_up: function(){ - return this.set_position(this.scroll_target - this.get_scroll_step(), true); - }, - - //scroll one page down - page_down: function(){ - return this.set_position(this.scroll_target + this.get_scroll_step(), true); - }, - - //scrolls up or down by pixels - scroll: function(pixels){ - return this.set_position(this.scroll_target - pixels, false); - }, - - //scroll to a specific position (in pixels). - //if animated is true, it will do this in an animated fashion with a duration equal to scroll_duration - set_position: function(position,animated){ - var self = this; - var target = this.target()[0]; - var bottom = target.scrollHeight-target.clientHeight; - this.scroll_target = Math.max(0,Math.min(bottom,position)); - if(this.scroll_target === 0){ - this.position = 'top'; - }else if(this.scroll_target === 'bottom'){ - this.position = 'bottom'; - }else{ - this.position = 'center'; - } - if(animated){ - this.target().animate({'scrollTop':this.scroll_target},this.scroll_duration); - this.update_button_status(); - this.update_scroller_dimensions(true); - }else{ - this.target().scrollTop(this.scroll_target); - this.update_scroller_dimensions(false); - this.update_button_status(); - } - return this.scroll_target; - }, - - //returns the current position of the scrollbar - get_position: function(){ - return this.scroll_target; - }, - - //returns true if it cannot be scrolled further down - is_at_bottom: function(){ - return this.bottom; - } - - }); -} diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index 866e3c7ba72..9118ba15ea6 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -1,55 +1,47 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sale - var QWeb = instance.web.qweb, - _t = instance.web._t; + var QWeb = instance.web.qweb; + var _t = instance.web._t; - // The ImageCache is used to hide the latency of the application cache on-disk access in chrome - // that causes annoying flickering on product pictures. Why the hell a simple access to - // the application cache involves such latency is beyond me, hopefully one day this can be - // removed. - module.ImageCache = instance.web.Class.extend({ + module.DomCache = instance.web.Class.extend({ init: function(options){ options = options || {}; - this.max_size = options.max_size || 500; + this.max_size = options.max_size || 2000; this.cache = {}; this.access_time = {}; this.size = 0; }, - get_image_uncached: function(url){ - var img = new Image(); - img.src = url; - return img; - }, - // returns a DOM Image object from an url, and cache the last 500 (by default) results - get_image: function(url){ - var cached = this.cache[url]; - if(cached){ - this.access_time[url] = (new Date()).getTime(); - return cached; - }else{ - var img = new Image(); - img.src = url; + cache_node: function(key,node){ + var cached = this.cache[key]; + this.cache[key] = node; + this.access_time[key] = new Date().getTime(); + if(!cached){ + this.size++; while(this.size >= this.max_size){ - var oldestUrl = null; - var oldestTime = (new Date()).getTime(); - for(var url in this.cache){ - var time = this.access_time[url]; - if(time <= oldestTime){ - oldestTime = time; - oldestUrl = url; + var oldest_key = null; + var oldest_time = new Date().getTime(); + for(var key in this.cache){ + var time = this.access_time[key]; + if(time <= oldest_time){ + oldest_time = time; + oldest_key = key; } } - if(oldestUrl){ - delete this.cache[oldestUrl]; - delete this.access_time[oldestUrl]; + if(oldestKey){ + delete this.cache[oldest_key]; + delete this.access_time[oldest_key]; } this.size--; } - this.cache[url] = img; - this.access_time[url] = (new Date()).getTime(); - this.size++; - return img; } + return node; + }, + get_node: function(key){ + var cached = this.cache[key]; + if(cached){ + this.access_time[key] = new Date().getTime(); + } + return cached; }, }); @@ -58,6 +50,8 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa init: function(parent, options) { this._super(parent); this.state = new module.NumpadState(); + window.numpadstate = this.state; + var self = this; }, start: function() { this.state.bind('change:mode', this.changedMode, this); @@ -89,7 +83,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, }); - // The paypad allows to select the payment method (cashRegisters) + // The paypad allows to select the payment method (cashregisters) // used to pay the order. module.PaypadWidget = module.PosBaseWidget.extend({ template: 'PaypadWidget', @@ -97,11 +91,11 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa var self = this; this._super(); - this.pos.get('cashRegisters').each(function(cashRegister) { + _.each(this.pos.cashregisters,function(cashregister) { var button = new module.PaypadButtonWidget(self,{ pos: self.pos, pos_widget : self.pos_widget, - cashRegister: cashRegister, + cashregister: cashregister, }); button.appendTo(self.$el); }); @@ -112,7 +106,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa template: 'PaypadButtonWidget', init: function(parent, options){ this._super(parent, options); - this.cashRegister = options.cashRegister; + this.cashregister = options.cashregister; }, renderElement: function() { var self = this; @@ -123,67 +117,56 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa console.warn('TODO should not get there...?'); return; } - self.pos.get('selectedOrder').addPaymentLine(self.cashRegister); + self.pos.get('selectedOrder').addPaymentline(self.cashregister); self.pos_widget.screen_selector.set_current_screen('payment'); }); }, }); - module.OrderlineWidget = module.PosBaseWidget.extend({ - template: 'OrderlineWidget', - init: function(parent, options) { - this._super(parent,options); - - this.model = options.model; - this.order = options.order; - - this.model.bind('change', this.refresh, this); - }, - renderElement: function() { - var self = this; - this._super(); - this.$el.click(function(){ - self.order.selectLine(self.model); - self.trigger('order_line_selected'); - }); - if(this.model.is_selected()){ - this.$el.addClass('selected'); - } - }, - refresh: function(){ - this.renderElement(); - this.trigger('order_line_refreshed'); - }, - destroy: function(){ - this.model.unbind('change',this.refresh,this); - this._super(); - }, - }); - module.OrderWidget = module.PosBaseWidget.extend({ template:'OrderWidget', init: function(parent, options) { + var self = this; this._super(parent,options); - this.display_mode = options.display_mode || 'numpad'; // 'maximized' | 'actionbar' | 'numpad' - this.set_numpad_state(options.numpadState); + this.editable = false; this.pos.bind('change:selectedOrder', this.change_selected_order, this); this.bind_orderline_events(); - this.orderlinewidgets = []; + this.line_click_handler = function(event){ + if(!self.editable){ + return; + } + self.pos.get('selectedOrder').selectLine(this.orderline); + self.pos_widget.numpad.state.reset(); + }; }, - set_numpad_state: function(numpadState) { - if (this.numpadState) { - this.numpadState.unbind('set_value', this.set_value); - } - this.numpadState = numpadState; - if (this.numpadState) { - this.numpadState.bind('set_value', this.set_value, this); - this.numpadState.reset(); - } + enable_numpad: function(){ + this.disable_numpad(); //ensure we don't register the callbacks twice + this.numpad_state = this.pos_widget.numpad.state; + if(this.numpad_state){ + this.numpad_state.reset(); + this.numpad_state.bind('set_value', this.set_value, this); + } + + }, + disable_numpad: function(){ + if(this.numpad_state){ + this.numpad_state.unbind('set_value', this.set_value); + this.numpad_state.reset(); + } + }, + set_editable: function(editable){ + this.editable = editable; + if(editable){ + this.enable_numpad(); + }else{ + this.disable_numpad(); + this.pos.get('selectedOrder').deselectLine(); + } }, set_value: function(val) { var order = this.pos.get('selectedOrder'); - if (order.get('orderLines').length !== 0) { - var mode = this.numpadState.get('mode'); + if (this.editable && order.getSelectedLine()) { + var mode = this.numpad_state.get('mode'); if( mode === 'quantity'){ order.getSelectedLine().set_quantity(val); }else if( mode === 'discount'){ @@ -194,141 +177,89 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa } }, change_selected_order: function() { - this.currentOrderLines.unbind(); this.bind_orderline_events(); this.renderElement(); }, bind_orderline_events: function() { - this.currentOrderLines = (this.pos.get('selectedOrder')).get('orderLines'); - this.currentOrderLines.bind('add', this.renderElement, this); - this.currentOrderLines.bind('remove', this.renderElement, this); + var lines = this.pos.get('selectedOrder').get('orderLines'); + lines.unbind(); + lines.bind('add', function(){ + this.numpad_state.reset(); + this.renderElement(true); + },this); + lines.bind('remove', function(line){ + this.remove_orderline(line); + this.numpad_state.reset(); + this.update_summary(); + },this); + lines.bind('change', function(line){ + this.rerender_orderline(line); + this.update_summary(); + },this); }, - update_numpad: function() { - this.selected_line = this.pos.get('selectedOrder').getSelectedLine(); - if (this.numpadState) - this.numpadState.reset(); + render_orderline: function(orderline){ + var el_str = openerp.qweb.render('Orderline',{widget:this, line:orderline}); + var el_node = document.createElement('div'); + el_node.innerHTML = _.str.trim(el_str); + el_node = el_node.childNodes[0]; + el_node.orderline = orderline; + el_node.addEventListener('click',this.line_click_handler); + + orderline.node = el_node; + return el_node; }, - renderElement: function() { - var self = this; - this._super(); - - // freeing subwidgets - - if(this.scrollbar){ - this.scrollbar.destroy(); + remove_orderline: function(order_line){ + if(this.pos.get('selectedOrder').get('orderLines').length === 0){ + this.renderElement(); + }else{ + order_line.node.parentNode.removeChild(order_line.node); } - for(var i = 0, len = this.orderlinewidgets.length; i < len; i++){ - this.orderlinewidgets[i].destroy(); - } - this.orderlinewidgets = []; + }, + rerender_orderline: function(order_line){ + var node = order_line.node; + var replacement_line = this.render_orderline(order_line); + node.parentNode.replaceChild(replacement_line,node); + }, + // overriding the openerp framework replace method for performance reasons + replace: function($target){ + this.renderElement(); + var target = $target[0]; + target.parentNode.replaceChild(this.el,target); + }, + renderElement: function(scrollbottom){ + this.pos_widget.numpad.state.reset(); - if(this.display_mode === 'maximized'){ - $('.point-of-sale .order-container').css({'bottom':'0px'}); - }else if(this.display_mode === 'actionbar'){ - $('.point-of-sale .order-container').css({'bottom':'105px'}); - }else if(this.display_mode !== 'numpad'){ - console.error('ERROR: OrderWidget renderElement(): wrong display_mode:',this.display_mode); + var order = this.pos.get('selectedOrder'); + var orderlines = order.get('orderLines').models; + + var el_str = openerp.qweb.render('OrderWidget',{widget:this, order:order, orderlines:orderlines}); + var el_node = document.createElement('div'); + el_node.innerHTML = _.str.trim(el_str); + el_node = el_node.childNodes[0]; + + var list_container = el_node.querySelector('.orderlines'); + for(var i = 0, len = orderlines.length; i < len; i++){ + var orderline = this.render_orderline(orderlines[i]); + list_container.appendChild(orderline); } - var $content = this.$('.orderlines'); - this.currentOrderLines.each(_.bind( function(orderLine) { - var line = new module.OrderlineWidget(this, { - model: orderLine, - order: this.pos.get('selectedOrder'), - }); - line.on('order_line_selected', self, self.update_numpad); - line.on('order_line_refreshed', self, self.update_summary); - line.appendTo($content); - self.orderlinewidgets.push(line); - }, this)); - this.update_numpad(); + if(this.el && this.el.parentNode){ + this.el.parentNode.replaceChild(el_node,this.el); + } + this.el = el_node; this.update_summary(); - var position = this.scrollbar ? this.scrollbar.get_position() : 0; - var at_bottom = this.scrollbar ? this.scrollbar.is_at_bottom() : false; - - this.scrollbar = new module.ScrollbarWidget(this,{ - target_widget: this, - target_selector: '.order-scroller', - name: 'order', - track_bottom: true, - on_show: function(){ - self.$('.order-scroller').css({'width':'89%'},100); - }, - on_hide: function(){ - self.$('.order-scroller').css({'width':'100%'},100); - }, - }); - - this.scrollbar.replace(this.$('.placeholder-ScrollbarWidget')); - this.scrollbar.set_position(position); - - if( at_bottom ){ - this.scrollbar.set_position(Number.MAX_VALUE, false); + if(scrollbottom){ + this.el.querySelector('.order-scroller').scrollTop = 100 * orderlines.length; } - }, update_summary: function(){ var order = this.pos.get('selectedOrder'); var total = order ? order.getTotalTaxIncluded() : 0; var taxes = order ? total - order.getTotalTaxExcluded() : 0; - this.$('.summary .total > .value').html(this.format_currency(total)); - this.$('.summary .total .subentry .value').html(this.format_currency(taxes)); - }, - set_display_mode: function(mode){ - if(this.display_mode !== mode){ - this.display_mode = mode; - this.renderElement(); - } - }, - }); - - module.PaymentlineWidget = module.PosBaseWidget.extend({ - template: 'PaymentlineWidget', - init: function(parent, options) { - this._super(parent,options); - this.payment_line = options.payment_line; - this.payment_line.bind('change', this.changedAmount, this); - }, - changeAmount: function(event) { - var newAmount = event.currentTarget.value; - var amount = parseFloat(newAmount); - if(!isNaN(amount)){ - this.amount = amount; - this.payment_line.set_amount(amount); - } - }, - checkAmount: function(e){ - if (e.which !== 0 && e.charCode !== 0) { - if(isNaN(String.fromCharCode(e.charCode))){ - return (String.fromCharCode(e.charCode) === "." && e.currentTarget.value.toString().split(".").length < 2)?true:false; - } - } - return true - }, - changedAmount: function() { - if (this.amount !== this.payment_line.get_amount()){ - this.renderElement(); - } - }, - renderElement: function() { - var self = this; - this.name = this.payment_line.get_cashregister().get('journal_id')[1]; - this._super(); - this.$('input').keypress(_.bind(this.checkAmount, this)) - .keyup(function(event){ - self.changeAmount(event); - }); - this.$('.delete-payment-line').click(function() { - self.trigger('delete_payment_line', self); - }); - }, - focus: function(){ - var val = this.$('input')[0].value; - this.$('input')[0].focus(); - this.$('input')[0].value = val; - this.$('input')[0].select(); + this.el.querySelector('.summary .total > .value').innerText = this.format_currency(total); + this.el.querySelector('.summary .total .subentry .value').innerText = this.format_currency(taxes); }, }); @@ -350,7 +281,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa self.selectOrder(); }); if( this.order === this.pos.get('selectedOrder') ){ - this.$el.addClass('selected-order'); + this.$el.addClass('selected'); } }, selectOrder: function(event) { @@ -450,8 +381,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, }); - module.CategoryButton = module.PosBaseWidget.extend({ - }); module.ProductCategoriesWidget = module.PosBaseWidget.extend({ template: 'ProductCategoriesWidget', init: function(parent, options){ @@ -462,7 +391,29 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.category = this.pos.root_category; this.breadcrumb = []; this.subcategories = []; + this.product_list_widget = options.product_list_widget || null; + this.category_cache = new module.DomCache(); this.set_category(); + + this.switch_category_handler = function(event){ + self.set_category(self.pos.db.get_category_by_id(Number(this.dataset['categoryId']))); + self.renderElement(); + }; + + this.clear_search_handler = function(event){ + self.clear_search(); + }; + + var search_timeout = null; + this.search_handler = function(event){ + clearTimeout(search_timeout); + + var query = this.value; + + search_timeout = setTimeout(function(){ + self.perform_search(self.category, query, event.which === 13); + },70); + }; }, // changes the category. if undefined, sets to root category @@ -485,59 +436,89 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, get_image_url: function(category){ - return instance.session.url('/web/binary/image', {model: 'pos.category', field: 'image_medium', id: category.id}); + return window.location.origin + '/web/binary/image?model=pos.category&field=image_medium&id='+category.id; + }, + + render_category: function( category, with_image ){ + var cached = this.category_cache.get_node(category.id); + if(!cached){ + if(with_image){ + var image_url = this.get_image_url(category); + var category_html = QWeb.render('CategoryButton',{ + widget: this, + category: category, + image_url: this.get_image_url(category), + }); + category_html = _.str.trim(category_html); + var category_node = document.createElement('div'); + category_node.innerHTML = category_html; + category_node = category_node.childNodes[0]; + }else{ + var category_html = QWeb.render('CategorySimpleButton',{ + widget: this, + category: category, + }); + category_html = _.str.trim(category_html); + var category_node = document.createElement('div'); + category_node.innerHTML = category_html; + category_node = category_node.childNodes[0]; + } + this.category_cache.cache_node(category.id,category_node); + return category_node; + } + return cached; + }, + + replace: function($target){ + this.renderElement(); + var target = $target[0]; + target.parentNode.replaceChild(this.el,target); }, renderElement: function(){ var self = this; - this._super(); + + var el_str = openerp.qweb.render(this.template, {widget: this}); + var el_node = document.createElement('div'); + el_node.innerHTML = el_str; + el_node = el_node.childNodes[1]; + + if(this.el && this.el.parentNode){ + this.el.parentNode.replaceChild(el_node,this.el); + } + + this.el = el_node; var hasimages = false; //if none of the subcategories have images, we don't display buttons with icons - _.each(this.subcategories, function(category){ - if(category.image){ + for(var i = 0; i < this.subcategories.length; i++){ + if(this.subcategories[i].image){ hasimages = true; + break; } - }); + } - _.each(this.subcategories, function(category){ - if(hasimages){ - var button = QWeb.render('CategoryButton',{category:category}); - var button = _.str.trim(button); - var button = $(button); - button.find('img').replaceWith(self.pos_widget.image_cache.get_image(self.get_image_url(category))); - }else{ - var button = QWeb.render('CategorySimpleButton',{category:category}); - button = _.str.trim(button); // we remove whitespace between buttons to fix spacing - var button = $(button); - } + var list_container = el_node.querySelector('.category-list'); + for(var i = 0, len = this.subcategories.length; i < len; i++){ + list_container.appendChild(this.render_category(this.subcategories[i],hasimages)); + }; - button.appendTo(this.$('.category-list')).click(function(event){ - var id = category.id; - var cat = self.pos.db.get_category_by_id(id); - self.set_category(cat); - self.renderElement(); - }); - }); - // breadcrumb click actions - this.$(".oe-pos-categories-list a").click(function(event){ - var id = $(event.target).data("category-id"); - var category = self.pos.db.get_category_by_id(id); - self.set_category(category); - self.renderElement(); - }); + var buttons = el_node.querySelectorAll('.js-category-switch'); + for(var i = 0; i < buttons.length; i++){ + buttons[i].addEventListener('click',this.switch_category_handler); + } - this.search_and_categories(); + var products = this.pos.db.get_product_by_category(this.category.id); + this.product_list_widget.set_product_list(products); - if(this.pos.iface_vkeyboard && this.pos_widget.onscreen_keyboard){ - this.pos_widget.onscreen_keyboard.connect(this.$('.searchbox input')); + this.el.querySelector('.searchbox input').addEventListener('keyup',this.search_handler); + + this.el.querySelector('.search-clear').addEventListener('click',this.clear_search_handler); + + if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){ + this.pos_widget.onscreen_keyboard.connect($(this.el.querySelector('.searchbox input'))); } }, - set_product_type: function(type){ // 'all' | 'weightable' - this.product_type = type; - this.reset_category(); - }, - // resets the current category to the root category reset_category: function(){ this.set_category(); @@ -547,55 +528,29 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa // empties the content of the search box clear_search: function(){ var products = this.pos.db.get_product_by_category(this.category.id); - this.pos.get('products').reset(products); - this.$('.searchbox input').val('').focus(); - this.$('.search-clear').fadeOut(); + this.product_list_widget.set_product_list(products); + var input = this.el.querySelector('.searchbox input'); + input.value = ''; + input.focus(); + }, + perform_search: function(category, query, buy_result){ + if(query){ + var products = this.pos.db.search_product_in_category(category.id,query) + if(buy_result && products.length === 1){ + this.pos.get('selectedOrder').addProduct(products[0]); + this.clear_search(); + }else{ + this.product_list_widget.set_product_list(products); + } + }else{ + var products = this.pos.db.get_product_by_category(this.category.id); + this.product_list_widget.set_product_list(products); + } }, - // filters the products, and sets up the search callbacks - search_and_categories: function(category){ - var self = this; - - // find all products belonging to the current category - var products = this.pos.db.get_product_by_category(this.category.id); - self.pos.get('products').reset(products); - - - var searchtimeout = null; - // filter the products according to the search string - this.$('.searchbox input').keyup(function(event){ - clearTimeout(searchtimeout); - - var query = $(this).val().toLowerCase(); - - searchtimeout = setTimeout(function(){ - if(query){ - if(event.which === 13){ - if( self.pos.get('products').size() === 1 ){ - self.pos.get('selectedOrder').addProduct(self.pos.get('products').at(0)); - self.clear_search(); - } - }else{ - var products = self.pos.db.search_product_in_category(self.category.id, query); - self.pos.get('products').reset(products); - self.$('.search-clear').fadeIn(); - } - }else{ - var products = self.pos.db.get_product_by_category(self.category.id); - self.pos.get('products').reset(products); - self.$('.search-clear').fadeOut(); - } - },200); - }); - - //reset the search when clicking on reset - this.$('.search-clear').click(function(){ - self.clear_search(); - }); - }, }); - module.ProductListWidget = module.ScreenWidget.extend({ + module.ProductListWidget = module.PosBaseWidget.extend({ template:'ProductListWidget', init: function(parent, options) { var self = this; @@ -605,41 +560,66 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.weight = options.weight || 0; this.show_scale = options.show_scale || false; this.next_screen = options.next_screen || false; - this.click_product_action = options.click_product_action; - this.pos.get('products').bind('reset', function(){ - self.renderElement(); - }); + this.click_product_handler = function(event){ + var product = self.pos.db.get_product_by_id(this.dataset['productId']); + options.click_product_action(product); + }; + + this.product_list = options.product_list || []; + this.product_cache = new module.DomCache(); }, + set_product_list: function(product_list){ + this.product_list = product_list; + this.renderElement(); + }, + get_product_image_url: function(product){ + return window.location.origin + '/web/binary/image?model=product.product&field=image_medium&id='+product.id; + }, + replace: function($target){ + this.renderElement(); + var target = $target[0]; + target.parentNode.replaceChild(this.el,target); + }, + + render_product: function(product){ + var cached = this.product_cache.get_node(product.id); + if(!cached){ + var image_url = this.get_product_image_url(product); + var product_html = QWeb.render('Product',{ + widget: this, + product: product, + image_url: this.get_product_image_url(product), + }); + var product_node = document.createElement('div'); + product_node.innerHTML = product_html; + product_node = product_node.childNodes[1]; + this.product_cache.cache_node(product.id,product_node); + return product_node; + } + return cached; + }, + renderElement: function() { var self = this; - this._super(); - - if(this.scrollbar){ - this.scrollbar.destroy(); + + // this._super() + var el_str = openerp.qweb.render(this.template, {widget: this}); + var el_node = document.createElement('div'); + el_node.innerHTML = el_str; + el_node = el_node.childNodes[1]; + + if(this.el && this.el.parentNode){ + this.el.parentNode.replaceChild(el_node,this.el); } - var products = this.pos.get('products').models || []; - - _.each(products,function(product,i){ - var $product = $(QWeb.render('Product',{ widget:self, product: products[i] })); - $product.find('img').replaceWith(self.pos_widget.image_cache.get_image(products[i].get_image_url())); - $product.find('a').click(function(){ self.click_product_action(product); }); - $product.appendTo(self.$('.product-list')); - }); - - this.scrollbar = new module.ScrollbarWidget(this,{ - target_widget: this, - target_selector: '.product-list-scroller', - on_show: function(){ - self.$('.product-list-scroller').css({'padding-right':'62px'},100); - }, - on_hide: function(){ - self.$('.product-list-scroller').css({'padding-right':'0px'},100); - }, - }); - - this.scrollbar.replace(this.$('.placeholder-ScrollbarWidget')); + this.el = el_node; + var list_container = el_node.querySelector('.product-list'); + for(var i = 0, len = this.product_list.length; i < len; i++){ + var product_node = this.render_product(this.product_list[i]); + product_node.addEventListener('click',this.click_product_handler); + list_container.appendChild(product_node); + }; }, }); @@ -660,9 +640,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa get_name: function(){ var user; if(this.mode === 'cashier'){ - user = this.pos.get('cashier') || this.pos.get('user'); + user = this.pos.cashier || this.pos.user; }else{ - user = this.pos.get('selectedOrder').get_client() || this.pos.get('user'); + user = this.pos.get('selectedOrder').get_client() || this.pos.user; } if(user){ return user.name; @@ -717,10 +697,62 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa 'payment_status', ], minimized: false, + init: function(parent,options){ + this._super(parent,options); + var self = this; + + this.minimized = false; + + // for dragging the debug widget around + this.dragging = false; + this.dragpos = {x:0, y:0}; + + function eventpos(event){ + if(event.touches && event.touches[0]){ + return {x: event.touches[0].screenX, y: event.touches[0].screenY}; + }else{ + return {x: event.screenX, y: event.screenY}; + } + } + + this.dragend_handler = function(event){ + self.dragging = false; + }; + this.dragstart_handler = function(event){ + self.dragging = true; + self.dragpos = eventpos(event); + }; + this.dragmove_handler = function(event){ + if(self.dragging){ + var top = this.offsetTop; + var left = this.offsetLeft; + var pos = eventpos(event); + var dx = pos.x - self.dragpos.x; + var dy = pos.y - self.dragpos.y; + + self.dragpos = pos; + + this.style.right = 'auto'; + this.style.bottom = 'auto'; + this.style.left = left + dx + 'px'; + this.style.top = top + dy + 'px'; + } + event.preventDefault(); + event.stopPropagation(); + }; + }, start: function(){ var self = this; - this.$el.draggable(); + this.el.addEventListener('mouseleave', this.dragend_handler); + this.el.addEventListener('mouseup', this.dragend_handler); + this.el.addEventListener('touchend', this.dragend_handler); + this.el.addEventListener('touchcancel',this.dragend_handler); + this.el.addEventListener('mousedown', this.dragstart_handler); + this.el.addEventListener('touchstart', this.dragstart_handler); + this.el.addEventListener('mousemove', this.dragmove_handler); + this.el.addEventListener('touchmove', this.dragmove_handler); + this.$('.toggle').click(function(){ var content = self.$('.content'); var bg = self.$el; @@ -831,10 +863,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa init: function() { this._super(arguments[0],{}); - instance.web.blockUI(); - - this.pos = new module.PosModel(this.session); - this.pos.pos_widget = this; + this.pos = new module.PosModel(this.session,{pos_widget:this}); this.pos_widget = this; //So that pos_widget's childs have pos_widget set automatically this.numpad_visible = true; @@ -842,14 +871,39 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.leftpane_visible = true; this.leftpane_width = '440px'; this.cashier_controls_visible = true; - this.image_cache = new module.ImageCache(); // for faster products image display + + FastClick.attach(document.body); }, - + + disable_rubberbanding: function(){ + // prevent the pos body from being scrollable. + document.body.addEventListener('touchmove',function(event){ + var node = event.target; + while(node){ + if(node.classList && node.classList.contains('touch-scrollable')){ + return; + } + node = node.parentNode; + } + event.preventDefault(); + }); + }, + start: function() { var self = this; return self.pos.ready.done(function() { $('.oe_tooltip').remove(); // remove tooltip from the start session button + + // remove default webclient handlers that induce click delay + $(document).off(); + $(window).off(); + $('html').off(); + $('body').off(); + $(self.$el).parent().off(); + $('document').off(); + $('.oe_web_client').off(); + $('.openerp_webclient_container').off(); self.build_currency_template(); self.renderElement(); @@ -868,7 +922,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa order: new_order, pos: self.pos }); - new_order_button.appendTo($('#orders')); + new_order_button.appendTo(this.$('.orders')); new_order_button.selectOrder(); }, self); @@ -876,26 +930,27 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa self.build_widgets(); - self.screen_selector.set_default_screen(); + if(self.pos.config.iface_big_scrollbars){ + self.$el.addClass('big-scrollbars'); + } + self.screen_selector.set_default_screen(); self.pos.barcode_reader.connect(); instance.webclient.set_content_full_screen(true); - if (!self.pos.get('pos_session')) { + if (!self.pos.session) { self.screen_selector.show_popup('error', 'Sorry, we could not create a user session'); - }else if(!self.pos.get('pos_config')){ + }else if(!self.pos.config){ self.screen_selector.show_popup('error', 'Sorry, we could not find any PoS Configuration for this session'); } - instance.web.unblockUI(); self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').addClass('oe_hidden');}); self.pos.flush(); }).fail(function(){ // error when loading models data from the backend - instance.web.unblockUI(); return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_session_opening']], ['res_id']) .pipe( _.bind(function(res){ return instance.session.rpc('/web/action/load', {'action_id': res[0]['res_id']}) @@ -906,7 +961,27 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, self)); }); }, - + loading_progress: function(fac){ + this.$('.loader .loader-feedback').removeClass('oe_hidden'); + this.$('.loader .progress').css({'width': ''+Math.floor(fac*100)+'%'}); + }, + loading_message: function(msg,progress){ + this.$('.loader .loader-feedback').removeClass('oe_hidden'); + this.$('.loader .message').text(msg); + if(typeof progress !== 'undefined'){ + this.loading_progress(progress); + } + }, + loading_skip: function(callback){ + if(callback){ + this.$('.loader .loader-feedback').removeClass('oe_hidden'); + this.$('.loader .button.skip').removeClass('oe_hidden'); + this.$('.loader .button.skip').off('click'); + this.$('.loader .button.skip').click(callback); + }else{ + this.$('.loader .button.skip').addClass('oe_hidden'); + } + }, // This method instantiates all the screens, widgets, etc. If you want to add new screens change the // startup screen, etc, override this method. build_widgets: function() { @@ -915,91 +990,91 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa // -------- Screens --------- this.product_screen = new module.ProductScreenWidget(this,{}); - this.product_screen.appendTo($('#rightpane')); + this.product_screen.appendTo(this.$('.screens')); this.receipt_screen = new module.ReceiptScreenWidget(this, {}); - this.receipt_screen.appendTo($('#rightpane')); + this.receipt_screen.appendTo(this.$('.screens')); this.payment_screen = new module.PaymentScreenWidget(this, {}); - this.payment_screen.appendTo($('#rightpane')); + this.payment_screen.appendTo(this.$('.screens')); this.welcome_screen = new module.WelcomeScreenWidget(this,{}); - this.welcome_screen.appendTo($('#rightpane')); + this.welcome_screen.appendTo(this.$('.screens')); this.client_payment_screen = new module.ClientPaymentScreenWidget(this, {}); - this.client_payment_screen.appendTo($('#rightpane')); + this.client_payment_screen.appendTo(this.$('.screens')); this.scale_invite_screen = new module.ScaleInviteScreenWidget(this, {}); - this.scale_invite_screen.appendTo($('#rightpane')); + this.scale_invite_screen.appendTo(this.$('.screens')); this.scale_screen = new module.ScaleScreenWidget(this,{}); - this.scale_screen.appendTo($('#rightpane')); + this.scale_screen.appendTo(this.$('.screens')); // -------- Popups --------- this.help_popup = new module.HelpPopupWidget(this, {}); - this.help_popup.appendTo($('.point-of-sale')); + this.help_popup.appendTo(this.$el); this.error_popup = new module.ErrorPopupWidget(this, {}); - this.error_popup.appendTo($('.point-of-sale')); + this.error_popup.appendTo(this.$el); this.error_product_popup = new module.ProductErrorPopupWidget(this, {}); - this.error_product_popup.appendTo($('.point-of-sale')); + this.error_product_popup.appendTo(this.$el); this.error_session_popup = new module.ErrorSessionPopupWidget(this, {}); - this.error_session_popup.appendTo($('.point-of-sale')); + this.error_session_popup.appendTo(this.$el); this.choose_receipt_popup = new module.ChooseReceiptPopupWidget(this, {}); - this.choose_receipt_popup.appendTo($('.point-of-sale')); + this.choose_receipt_popup.appendTo(this.$el); this.error_negative_price_popup = new module.ErrorNegativePricePopupWidget(this, {}); - this.error_negative_price_popup.appendTo($('.point-of-sale')); + this.error_negative_price_popup.appendTo(this.$el); this.error_no_client_popup = new module.ErrorNoClientPopupWidget(this, {}); - this.error_no_client_popup.appendTo($('.point-of-sale')); + this.error_no_client_popup.appendTo(this.$el); this.error_invoice_transfer_popup = new module.ErrorInvoiceTransferPopupWidget(this, {}); - this.error_invoice_transfer_popup.appendTo($('.point-of-sale')); + this.error_invoice_transfer_popup.appendTo(this.$el); // -------- Misc --------- this.notification = new module.SynchNotificationWidget(this,{}); - this.notification.appendTo(this.$('#rightheader')); + this.notification.appendTo(this.$('.pos-rightheader')); this.username = new module.UsernameWidget(this,{}); this.username.replace(this.$('.placeholder-UsernameWidget')); this.action_bar = new module.ActionBarWidget(this); - this.action_bar.appendTo($(".point-of-sale #rightpane")); + this.action_bar.replace(this.$(".placeholder-RightActionBar")); this.left_action_bar = new module.ActionBarWidget(this); - this.left_action_bar.appendTo($(".point-of-sale #leftpane")); + this.left_action_bar.replace(this.$('.placeholder-LeftActionBar')); this.paypad = new module.PaypadWidget(this, {}); - this.paypad.replace($('#placeholder-PaypadWidget')); + this.paypad.replace(this.$('.placeholder-PaypadWidget')); this.numpad = new module.NumpadWidget(this); - this.numpad.replace($('#placeholder-NumpadWidget')); + this.numpad.replace(this.$('.placeholder-NumpadWidget')); this.order_widget = new module.OrderWidget(this, {}); - this.order_widget.replace($('#placeholder-OrderWidget')); + this.order_widget.replace(this.$('.placeholder-OrderWidget')); this.onscreen_keyboard = new module.OnscreenKeyboardWidget(this, { 'keyboard_model': 'simple' }); - this.onscreen_keyboard.appendTo($(".point-of-sale #content")); + this.onscreen_keyboard.replace(this.$('.placeholder-OnscreenKeyboardWidget')); this.close_button = new module.HeaderButtonWidget(this,{ label: _t('Close'), action: function(){ self.close(); }, }); - this.close_button.appendTo(this.$('#rightheader')); + this.close_button.appendTo(this.$('.pos-rightheader')); this.client_button = new module.HeaderButtonWidget(this,{ label: _t('Self-Checkout'), action: function(){ self.screen_selector.set_user_mode('client'); }, }); - this.client_button.appendTo(this.$('#rightheader')); + this.client_button.appendTo(this.$('.pos-rightheader')); // -------- Screen Selector --------- @@ -1027,13 +1102,16 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, default_client_screen: 'welcome', default_cashier_screen: 'products', - default_mode: this.pos.iface_self_checkout ? 'client' : 'cashier', + default_mode: this.pos.config.iface_self_checkout ? 'client' : 'cashier', }); if(this.pos.debug){ this.debug_widget = new module.DebugWidget(this); - this.debug_widget.appendTo(this.$('#content')); + this.debug_widget.appendTo(this.$('.pos-content')); } + + this.disable_rubberbanding(); + }, changed_pending_operations: function () { @@ -1048,13 +1126,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.set_left_action_bar_visible(false); this.numpad.show(); this.paypad.show(); - this.order_widget.set_display_mode('numpad'); }else{ this.numpad.hide(); this.paypad.hide(); - if(this.order_widget.display_mode === 'numpad'){ - this.order_widget.set_display_mode('maximized'); - } } } }, @@ -1064,27 +1138,22 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa if(visible){ this.set_numpad_visible(false); this.left_action_bar.show(); - this.order_widget.set_display_mode('actionbar'); }else{ this.left_action_bar.hide(); - if(this.order_widget.display_mode === 'actionbar'){ - this.order_widget.set_display_mode('maximized'); - } } } }, - //shows or hide the leftpane (contains the list of orderlines, the numpad, the paypad, etc.) set_leftpane_visible: function(visible){ if(visible !== this.leftpane_visible){ this.leftpane_visible = visible; if(visible){ - $('#leftpane').removeClass('oe_hidden').animate({'width':this.leftpane_width},500,'swing'); - $('#rightpane').animate({'left':this.leftpane_width},500,'swing'); + this.$('.pos-leftpane').removeClass('oe_hidden').animate({'width':this.leftpane_width},500,'swing'); + this.$('.pos-rightpane').animate({'left':this.leftpane_width},500,'swing'); }else{ - var leftpane = $('#leftpane'); - $('#leftpane').animate({'width':'0px'},500,'swing', function(){ leftpane.addClass('oe_hidden'); }); - $('#rightpane').animate({'left':'0px'},500,'swing'); + var leftpane = this.$('.pos-leftpane'); + leftpane.animate({'width':'0px'},500,'swing', function(){ leftpane.addClass('oe_hidden'); }); + this.$('.pos-rightpane').animate({'left':'0px'},500,'swing'); } } }, @@ -1093,11 +1162,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa if(visible !== this.cashier_controls_visible){ this.cashier_controls_visible = visible; if(visible){ - $('#loggedas').removeClass('oe_hidden'); - $('#rightheader').removeClass('oe_hidden'); + this.$('.pos-rightheader').removeClass('oe_hidden'); }else{ - $('#loggedas').addClass('oe_hidden'); - $('#rightheader').addClass('oe_hidden'); + this.$('.pos-rightheader').addClass('oe_hidden'); } } }, @@ -1105,15 +1172,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa var self = this; function close(){ - return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_client_pos_menu']], ['res_id']).pipe( - _.bind(function(res) { - return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) { - var action = result; - action.context = _.extend(action.context || {}, {'cancel_action': {type: 'ir.actions.client', tag: 'reload'}}); - //self.destroy(); - this.do_action(action); - }, this)); - }, self)); + return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_client_pos_menu']], ['res_id']).pipe(function(res) { + window.location = '/web#action=' + res[0]['res_id']; + }); } var draft_order = _.find( self.pos.get('orders').models, function(order){ diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 742c823702e..a6b53a8fdbe 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -4,35 +4,87 @@ -
-
-
- +
+
+
+
-
-
- - -
    +
    +
    + + +
    -
    -
    -
    -
    -
    -
    -
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + + +
    -
    + +
    +
    +
    +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +

    Loading

    +
    +
    +
    + +
    + <!--[if IE]>
    @@ -73,12 +125,12 @@ -
    +
    -
    +
    @@ -105,67 +157,66 @@ -
  1. +
    - +
    -
  2. +
    -
  3. + -
  4. +
    -
    - +
    -
    -
    -
      -
    -
    +
    +
    +
    +
    -
    -
      -
    +
    +
    +
    -
    -
    +
    @@ -188,7 +239,6 @@
    -

    Product Weighting

    @@ -205,7 +255,7 @@ - +

    @@ -213,47 +263,36 @@
    -
    -

    Payment

    -
    -
    -
    -
    - - Total: +
    +
    +
    +
    +
    +
    + + Paid: - +
    -
    -
    -
    -
    + -
    -

    Receipt

    +
    @@ -263,7 +302,7 @@
    -

    Welcome

    +

    Welcome

    Please scan an item or your member card

    @@ -276,15 +315,15 @@ -
    -

    Please scan an item

    +
    +

    Please scan an item

    -

    Payment

    +

    Payment

    Please insert your card in the reader and follow the instructions to complete @@ -295,18 +334,11 @@

    -

    Please put your product on the scale

    +

    Please put your product on the scale

    - -
    - - -
    -
    -