Merge pull request #1983 from odoo-dev/master-pos-new-payment-screen-fva

Point of Sale : New Payment Screen & Receipt Screens
This commit is contained in:
Frédéric Van der Essen
2014-08-25 17:36:12 +02:00
35 changed files with 2515 additions and 1041 deletions
+3 -1
View File
@@ -72,6 +72,7 @@ class pos_config(osv.osv):
'iface_scan_via_proxy' : fields.boolean('Scan via Proxy', help="Enable barcode scanning with a remotely connected barcode scanner"),
'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'),
'iface_fullscreen': fields.boolean('Fullscreen', help='Display the Point of Sale in full screen mode'),
'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('IP Address', help='The hostname or ip address of the hardware proxy, Will be autodetected if left empty', size=45),
@@ -394,7 +395,7 @@ class pos_session(osv.osv):
for obj in self.browse(cr, uid, ids, context=context):
for statement in obj.statement_ids:
statement.unlink(context=context)
return True
return super(pos_session, self).unlink(cr, uid, ids, context=context)
def open_cb(self, cr, uid, ids, context=None):
@@ -553,6 +554,7 @@ class pos_order(osv.osv):
orders_to_save = [o for o in orders if o['data']['name'] not in existing_references]
order_ids = []
for tmp_order in orders_to_save:
to_invoice = tmp_order['to_invoice']
order = tmp_order['data']
+72 -49
View File
@@ -628,57 +628,80 @@
</div>
</header>
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name"/>
</h1>
<group col="4">
<field name="picking_type_id" widget="selection" groups="stock.group_locations"
on_change="onchange_picking_type_id(picking_type_id)"/>
<field name="stock_location_id" groups="stock.group_locations"/>
<field name="pricelist_id" groups="product.group_sale_pricelist"/>
<field name="currency_id" invisible="1"/>
<field name="journal_id" widget="selection"/>
<field name="group_by" groups="account.group_account_user"/>
<field name="sequence_id" readonly="1" groups="base.group_no_one"/>
</group>
<separator string="Available Payment Methods" colspan="4"/>
<field name="journal_ids" colspan="4" nolabel="1">
<tree string="Journals">
<field name="code" />
<field name="name" />
<field name="type" />
<field name="cash_control" />
</tree>
</field>
<group string="Features" >
<group>
<field name="iface_vkeyboard" />
<field name="iface_invoicing" />
<sheet>
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name"/>
</h1>
<group col="4">
<field name="picking_type_id" widget="selection" groups="stock.group_locations"
on_change="onchange_picking_type_id(picking_type_id)"/>
<field name="stock_location_id" groups="stock.group_locations"/>
<field name="pricelist_id" groups="product.group_sale_pricelist"/>
<field name="currency_id" invisible="1"/>
<field name="journal_id" widget="selection"/>
<field name="group_by" groups="account.group_account_user"/>
<field name="sequence_id" readonly="1" groups="base.group_no_one"/>
</group>
<group>
<field name="iface_big_scrollbars" />
<separator string="Available Payment Methods" colspan="4"/>
<field name="journal_ids" colspan="4" nolabel="1">
<tree string="Journals">
<field name="code" />
<field name="name" />
<field name="type" />
<field name="cash_control" />
</tree>
</field>
<group string="Features" >
<group>
<field name="iface_vkeyboard" />
<field name="iface_invoicing" />
</group>
<group>
<field name="iface_fullscreen" />
<field name="iface_big_scrollbars" />
</group>
</group>
</group>
<group string="Hardware Proxy" >
<field name="proxy_ip" />
<field name="iface_print_via_proxy" />
<field name="iface_scan_via_proxy" />
<field name="iface_electronic_scale" />
<field name="iface_cashdrawer" />
</group>
<group string="Receipt" >
<field name="receipt_header" placeholder="A custom receipt header message"/>
<field name="receipt_footer" placeholder="A custom receipt header footage"/>
</group>
<group string="Barcode Types" col="4">
<field name="barcode_product" />
<field name="barcode_cashier" />
<field name="barcode_customer" />
<field name="barcode_weight" />
<field name="barcode_discount" />
<field name="barcode_price" />
</group>
<group string="Hardware Proxy" >
<field name="proxy_ip" />
<field name="iface_print_via_proxy" />
<field name="iface_scan_via_proxy" />
<field name="iface_electronic_scale" />
<field name="iface_cashdrawer" />
</group>
<group string="Receipt" >
<field name="receipt_header" placeholder="A custom receipt header message"/>
<field name="receipt_footer" placeholder="A custom receipt header footage"/>
</group>
<group string="Barcode Types" col="1">
<p>
Barcode Patterns allow to match barcodes to actions or to embed information such as price and quantity in the barcode.
Barcode Patterns only work with EAN13 barcodes.
</p>
<p>
Each type of barcode accepts a list of patterns seprated by commas. A scanned
barcode will be attributed to a type if it matches one of its patterns.
The patterns take the form of EAN13 barcodes. Numbers in the pattern must match
the number in the scanned barcode. A 'x' or a '*' in a pattern will match
any one number. If the patterns are shorter than EAN13 barcodes, they are assumed
to be prefixes and match at the beginning. Weight, Price and Discount patterns also
tell how the weight, price or discount is encoded in the barcode. 'N' indicate the
positions where the integer part is en encoded, and 'D' where the decimals are encoded.
If multiple pattern match one barcode, the longest pattern with the less 'x' or '*' is
considered the matching one. If a barcode matches no pattern it will not be found in
the POS.
</p>
<group col="4">
<field name="barcode_product" />
<field name="barcode_cashier" />
<field name="barcode_customer" />
<field name="barcode_weight" />
<field name="barcode_discount" />
<field name="barcode_price" />
</group>
</group>
</sheet>
</form>
</field>
+176 -94
View File
@@ -20,6 +20,19 @@
.ui-dialog .ui-icon-closethick{
float: right;
}
div.modal.in {
position: absolute;
background: white;
padding: 20px;
box-shadow: 0px 10px 20px black;
border-radius: 3px;
max-width: 600px;
max-height: 400px;
margin-top: -200px;
margin-left: -300px;
top: 50%;
left: 50%;
}
/* --- Generic Restyling and Resets --- */
html {
@@ -483,10 +496,11 @@ td {
margin-right: 4px;
}
.pos .control-button.highlight{
background: #6EC89B;
border: solid 1px #6EC89B;
color: white;
.pos .control-button.highlight,
.pos .button.highlight {
background: #6EC89B !important;
border: solid 1px #6EC89B !important;
color: white !important;
}
.pos .control-button:active {
background: #7F82AC;
@@ -502,20 +516,20 @@ td {
color: inherit;
}
/* ********* The paypad contains the payment buttons ********* */
/* ********* The actionpad (payment, set customer) ********* */
.pos .paypad {
padding: 8px 4px 8px 8px;
.pos .actionpad{
padding: 8px 3px 8px 19px;
display: inline-block;
text-align: center;
vertical-align: top;
width: 205px;
width: 183px;
max-height: 350px;
overflow-y: auto;
overflow-x: hidden;
}
.pos .paypad button {
height: 39px;
.pos .actionpad .button {
height: 50px;
display: block;
width: 100%;
margin: 0px -6px 4px -2px;
@@ -524,13 +538,20 @@ td {
color: #555555;
font-size: 14px;
}
.pos .paypad button:active,
.pos .numpad button:active,
.pos .numpad .selected-mode,
.pos .popup button:active{
border: none;
.pos .actionpad .button.pay {
height: 158px;
}
.pos .actionpad .button.pay .fa {
display: block;
font-size: 32px;
line-height: 54px;
padding-top: 6px;
background: rgb(86, 86, 86);
color: white;
background: #7f82ac;
width: 60px;
margin: auto;
border-radius: 30px;
margin-bottom: 10px;
}
/* ********* The Numpad ********* */
@@ -543,7 +564,7 @@ td {
.pos .numpad button {
height: 50px;
width: 50px;
margin: 0px 0px 4px 0px;
margin: 0px 3px 4px 0px;
font-weight: bold;
vertical-align: middle;
color: #555555;
@@ -913,6 +934,9 @@ td {
overflow-y: auto;
border-right: dashed 1px rgb(215,215,215);
}
.screen .left-content.pc40{
right: 66%;
}
.screen .right-content{
position: absolute;
right:0px; top: 64px; bottom: 0px;
@@ -920,6 +944,9 @@ td {
overflow-x: hidden;
overflow-y: auto;
}
.screen .right-content.pc60{
left:34%
}
.screen .centered-content{
position: absolute;
right:25%; top: 64px; bottom: 0px;
@@ -966,101 +993,125 @@ td {
position:relative;
}
/* b) The payment screen */
.pos .pos-payment-container {
.pos .payment-numpad {
display: inline-block;
font-size: 16px;
text-align: left;
width: 360px;
}
.pos .payment-due-total {
width: 50%;
box-sizing: border-box;
padding: 8px;
text-align: center;
font-weight: bold;
font-size: 48px;
margin: 27px;
text-shadow: 0px 2px rgb(202, 202, 202);
float: left;
}
.pos .payment-numpad .numpad button {
width: 66px;
height: 66px;
}
.pos .paymentlines-container {
padding: 16px;
padding-top: 0;
border-bottom: dashed 1px gainsboro;
min-height: 154px;
}
.pos .paymentlines {
width: 100%;
}
.pos .paymentlines .controls {
width: 40px;
}
.pos .paymentlines .label > * {
font-size: 16px;
padding: 8px;
}
.pos .paymentlines tbody{
background: white;
border-radius: 3px;
}
.pos .paymentline{
position: relative;
padding: 8px;
border-box: 3px;
box-sizing: border-box;
-moz-box-sizing: border-box;
border-radius: 3px;
font-size: 22px;
}
.pos .paymentline-name{
margin-bottom: 8px;
.pos .paymentline.selected {
font-size: 22px;
background: #6EC89B;
color: white;
}
.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: 6px 8px;
.pos .paymentline.selected .edit {
background: white;
color: #484848;
text-align: right;
color: #6EC89B;
outline: 3px blue;
box-shadow: 0px 0px 0px 3px #6EC89B;
position: relative;
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;
.pos .paymentline > *{
padding: 8px 12px;
}
.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;
.pos .paymentline .col-due,
.pos .paymentline .col-tendered,
.pos .paymentline .col-change {
min-width: 90px;
}
.pos .paymentline .col-change.highlight {
background: rgb(184, 152, 204);
}
.pos .paymentline .col-name {
font-size: 16px;
}
.pos .paymentline .delete-button{
cursor: pointer;
text-align: center;
}
.pos .payment-buttons {
display: inline-block;
width: 50%;
box-sizing: border-box;
padding: 16px;
float: right;
}
.pos .pos-payment-container .left-block{
display: inline-block;
width:49%;
margin:0;
padding:0;
text-align:left;
}
.pos .pos-payment-container .infoline{
margin-top:5px;
margin-bottom:5px;
.payment-screen .payment-buttons .button {
background: rgb(221, 221, 221);
line-height: 48px;
margin-bottom: 4px;
border-radius: 3px;
font-size: 16px;
padding: 0px 8px;
opacity: 0.5;
border: solid 1px rgb(202, 202, 202);
cursor: pointer;
text-align: center;
}
.pos .pos-payment-container .infoline.bigger{
opacity: 1;
font-size: 20px;
.payment-screen .paymentlines-empty .total {
text-align: center;
padding: 24px 0px 18px;
font-size: 64px;
color: #43996E;
text-shadow: 0px 2px white, 0px 2px 2px rgba(0, 0, 0, 0.27);
}
.pos .pos-payment-container .right-block{
display: inline-block;
width:49%;
margin:0;
padding:0;
text-align:right;
}
.pos .paymentline.selected{
background: rgb(220,220,220);
.payment-screen .paymentlines-empty .message {
text-align: center;
}
/* c) The receipt screen */
.pos .receipt-screen .centered-content .button {
line-height: 40px;
padding: 3px 13px;
font-size: 20px;
text-align: center;
background: rgb(230, 230, 230);
margin: 16px;
margin-bottom: 0px;
border-radius: 3px;
border: solid 1px rgb(209, 209, 209);
cursor: pointer;
}
.pos .pos-receipt-container {
font-size: 0.75em;
text-align: center;
}
.pos .pos-sale-ticket {
@@ -1095,10 +1146,16 @@ td {
}
@media print {
body {
margin: 0;
}
.oe_leftbar,
.pos .pos-topheader,
.pos .pos-leftpane,
.pos .keyboard_frame {
.pos .keyboard_frame,
.pos .receipt-screen header,
.pos .receipt-screen .top-content,
.pos .receipt-screen .centered-content .button {
display: none !important;
}
.pos,
@@ -1119,12 +1176,17 @@ td {
left: 0px !important;
background-color: white;
}
.pos .receipt-screen header {
display: none !important;
}
.pos .receipt-screen {
text-align: left;
}
.pos .receipt-screen .centered-content{
position: static;
border: none;
margin: none;
}
.pos .pos-receipt-container {
text-align: left;
}
.pos-actionbar {
display: none !important;
}
@@ -1469,10 +1531,12 @@ td {
font-size: 24px;
vertical-align: top;
}
.splitbill-screen .paymentmethods {
.splitbill-screen .paymentmethods,
.payment-screen .paymentmethods {
margin: 16px;
}
.splitbill-screen .paymentmethod {
.splitbill-screen .paymentmethod,
.payment-screen .paymentmethod {
background: rgb(221, 221, 221);
line-height: 40px;
margin-bottom: 4px;
@@ -1482,6 +1546,14 @@ td {
cursor: pointer;
text-align: center;
}
.splitbill-screen .paymentmethod.active,
.payment-screen .paymentmethod.active {
background: #6EC89B;
color: white;
border-color: #6EC89B;
}
/* ********* The ActionBarWidget ********* */
@@ -1663,7 +1735,17 @@ td {
.pos .popup .comment{
font-weight: normal;
font-size: 18px;
margin: 0px 16px;
margin: 0px 16px 16px 16px;
}
.pos .popup .comment.traceback {
height: 264px;
overflow: auto;
font-size: 14px;
text-align: left;
font-family: 'Inconsolata';
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
}
.pos .popup .footer{
position:absolute;
Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

+3
View File
@@ -345,6 +345,9 @@ function openerp_pos_db(instance, module){
});
this.save('orders',orders);
},
remove_all_orders: function(){
this.save('orders',[]);
},
get_orders: function(){
return this.load('orders',[]);
},
@@ -19,8 +19,6 @@ openerp.point_of_sale = function(instance) {
openerp_pos_widgets(instance,module); // import pos_widgets.js
openerp_pos_tests(instance,module); // import pos_tests.js
instance.web.client_actions.add('pos.ui', 'instance.point_of_sale.PosWidget');
};
+319 -215
View File
@@ -114,180 +114,250 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
return done;
},
// helper function to load data from the server
// helper function to load data from the server. Obsolete use the models loader below.
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()
},
// Server side model loaders. This is the list of the models that need to be loaded from
// the server. The models are loaded one by one by this list's order. The 'loaded' callback
// is used to store the data in the appropriate place once it has been loaded. This callback
// can return a deferred that will pause the loading of the next module.
// a shared temporary dictionary is available for loaders to communicate private variables
// used during loading such as object ids, etc.
models: [
{
model: 'res.users',
fields: ['name','company_id'],
domain: function(self){ return [['id','=',self.session.uid]]; },
loaded: function(self,users){ self.user = users[0]; },
},{
model: 'res.company',
fields: [ 'currency_id', 'email', 'website', 'company_registry', 'vat', 'name', 'phone', 'partner_id' ],
domain: function(self){ return [['id','=',self.user.company_id[0]]]; },
loaded: function(self,companies){ self.company = companies[0]; },
},{
model: 'product.uom',
fields: [],
domain: null,
loaded: function(self,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];
units[i].groupable = ( units[i].category_id[0] === 1 );
units[i].is_unit = ( units[i].id === 1 );
}
self.units_by_id = units_by_id;
}
},{
model: 'res.users',
fields: ['name','ean13'],
domain: null,
loaded: function(self,users){ self.users = users; },
},{
model: 'res.partner',
fields: ['name','street','city','country_id','phone','zip','mobile','email','ean13'],
domain: null,
loaded: function(self,partners){
self.partners = partners;
self.db.add_partners(partners);
},
},{
model: 'account.tax',
fields: ['name','amount', 'price_include', 'type'],
domain: null,
loaded: function(self,taxes){ self.taxes = taxes; },
},{
model: 'pos.session',
fields: ['id', 'journal_ids','name','user_id','config_id','start_at','stop_at','sequence_number'],
domain: function(self){ return [['state','=','opened'],['user_id','=',self.session.uid]]; },
loaded: function(self,pos_sessions){ self.pos_session = pos_sessions[0]; },
},{
model: 'pos.config',
fields: [],
domain: function(self){ return [['id','=', self.pos_session.config_id[0]]]; },
loaded: function(self,configs){
self.config = configs[0];
self.config.use_proxy = self.config.iface_payment_terminal ||
self.config.iface_electronic_scale ||
self.config.iface_print_via_proxy ||
self.config.iface_scan_via_proxy ||
self.config.iface_cashdrawer;
self.barcode_reader.add_barcode_patterns({
'product': self.config.barcode_product,
'cashier': self.config.barcode_cashier,
'client': self.config.barcode_customer,
'weight': self.config.barcode_weight,
'discount': self.config.barcode_discount,
'price': self.config.barcode_price,
});
},
},{
model: 'stock.location',
fields: [],
domain: function(self){ return [['id','=', self.config.stock_location_id[0]]]; },
loaded: function(self, locations){ self.shop = locations[0]; },
},{
model: 'product.pricelist',
fields: ['currency_id'],
domain: function(self){ return [['id','=',self.config.pricelist_id[0]]]; },
loaded: function(self, pricelists){ self.pricelist = pricelists[0]; },
},{
model: 'res.currency',
fields: ['symbol','position','rounding','accuracy'],
domain: function(self){ return [['id','=',self.pricelist.currency_id[0]]]; },
loaded: function(self, currencies){
self.currency = currencies[0];
},
},{
model: 'product.packaging',
fields: ['ean','product_tmpl_id'],
domain: null,
loaded: function(self, packagings){
self.db.add_packagings(packagings);
},
},{
model: 'pos.category',
fields: ['id','name','parent_id','child_id','image'],
domain: null,
loaded: function(self, categories){
self.db.add_categories(categories);
},
},{
model: 'product.product',
fields: ['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13', 'default_code', 'variants',
'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description',
'product_tmpl_id'],
domain: function(self){ return [['sale_ok','=',true],['available_in_pos','=',true]]; },
context: function(self){ return { pricelist: self.pricelist.id }; },
loaded: function(self, products){
self.db.add_products(products);
},
},{
model: 'account.bank.statement',
fields: ['account_id','currency','journal_id','state','name','user_id','pos_session_id'],
domain: function(self){ return [['state', '=', 'open'],['pos_session_id', '=', self.pos_session.id]]; },
loaded: function(self, bankstatements, tmp){
self.bankstatements = bankstatements;
tmp.journals = [];
_.each(bankstatements,function(statement){
tmp.journals.push(statement.journal_id[0]);
});
},
},{
model: 'account.journal',
fields: [],
domain: function(self,tmp){ return [['id','in',tmp.journals]]; },
loaded: function(self, journals){
self.journals = journals;
// associate the bank statements with their journals.
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(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.cashregisters = bankstatements;
},
},{
loaded: function(self){
self.company_logo = new Image();
self.company_logo.crossOrigin = 'anonymous';
var logo_loaded = new $.Deferred();
self.company_logo.onload = function(){
var img = self.company_logo;
var ratio = 1;
var targetwidth = 300;
var maxheight = 150;
if( img.width !== targetwidth ){
ratio = targetwidth / img.width;
}
if( img.height * ratio > maxheight ){
ratio = maxheight / img.height;
}
var width = Math.floor(img.width * ratio);
var height = Math.floor(img.height * ratio);
var c = document.createElement('canvas');
c.width = width;
c.height = height
var ctx = c.getContext('2d');
ctx.drawImage(self.company_logo,0,0, width, height);
self.company_logo_base64 = c.toDataURL();
window.logo64 = self.company_logo_base64;
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;
},
},
],
// loads all the needed data on the sever. returns a deferred indicating when all the data has loaded.
load_server_data: function(){
var self = this;
var loaded = new $.Deferred();
var progress = 0;
var progress_step = 1.0 / self.models.length;
var tmp = {}; // this is used to share a temporary state between models loaders
var loaded = self.fetch('res.users',['name','company_id'],[['id','=',this.session.uid]])
.then(function(users){
self.user = users[0];
return self.fetch('res.company',
[
'currency_id',
'email',
'website',
'company_registry',
'vat',
'name',
'phone',
'partner_id',
],
[['id','=',users[0].company_id[0]]],
{show_address_only: true});
}).then(function(companies){
self.company = companies[0];
return self.fetch('product.uom', null, null);
}).then(function(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];
units[i].groupable = ( units[i].category_id[0] === 1 );
units[i].is_unit = ( units[i].id === 1 );
}
self.units_by_id = units_by_id;
function load_model(index){
if(index >= self.models.length){
loaded.resolve();
}else{
var model = self.models[index];
self.pos_widget.loading_message(_t('Loading')+' '+(model.model || ''), progress);
var fields = typeof model.fields === 'function' ? model.fields(self,tmp) : model.fields;
var domain = typeof model.domain === 'function' ? model.domain(self,tmp) : model.domain;
var context = typeof model.context === 'function' ? model.context(self,tmp) : model.context;
progress += progress_step;
return self.fetch('res.users', ['name','ean13'], [['ean13', '!=', false]]);
}).then(function(users){
self.users = users;
return self.fetch('res.partner', ['name','street','city','country_id','phone','zip','mobile','email','ean13']);
}).then(function(partners){
self.partners = partners;
self.db.add_partners(partners);
return self.fetch('account.tax', ['name','amount', 'price_include', 'type']);
}).then(function(taxes){
self.taxes = taxes;
return self.fetch(
'pos.session',
['id', 'journal_ids','name','user_id','config_id','start_at','stop_at','sequence_number'],
[['state', '=', 'opened'], ['user_id', '=', self.session.uid]]
);
}).then(function(pos_sessions){
self.pos_session = pos_sessions[0];
return self.fetch('pos.config',[],[['id','=', self.pos_session.config_id[0]]]);
}).then(function(configs){
self.config = configs[0];
self.config.use_proxy = self.config.iface_payment_terminal ||
self.config.iface_electronic_scale ||
self.config.iface_print_via_proxy ||
self.config.iface_scan_via_proxy ||
self.config.iface_cashdrawer;
self.barcode_reader.add_barcode_patterns({
'product': self.config.barcode_product,
'cashier': self.config.barcode_cashier,
'client': self.config.barcode_customer,
'weight': self.config.barcode_weight,
'discount': self.config.barcode_discount,
'price': self.config.barcode_price,
});
return self.fetch('stock.location',[],[['id','=', self.config.stock_location_id[0]]]);
}).then(function(shops){
self.shop = shops[0];
return self.fetch('product.pricelist',['currency_id'],[['id','=',self.config.pricelist_id[0]]]);
}).then(function(pricelists){
self.pricelist = pricelists[0];
return self.fetch('res.currency',['symbol','position','rounding','accuracy'],[['id','=',self.pricelist.currency_id[0]]]);
}).then(function(currencies){
self.currency = currencies[0];
return self.fetch('product.packaging',['ean','product_tmpl_id']);
}).then(function(packagings){
self.db.add_packagings(packagings);
return self.fetch('pos.category', ['id','name','parent_id','child_id','image']);
}).then(function(categories){
self.db.add_categories(categories);
return self.fetch(
'product.product',
['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13', 'default_code', 'variants',
'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description',
'product_tmpl_id'],
[['sale_ok','=',true],['available_in_pos','=',true]],
{pricelist: self.pricelist.id} // context for price
);
}).then(function(products){
self.db.add_products(products);
return self.fetch(
'account.bank.statement',
['account_id','currency','journal_id','state','name','user_id','pos_session_id'],
[['state','=','open'],['pos_session_id', '=', self.pos_session.id]]
);
}).then(function(bankstatements){
var journals = [];
_.each(bankstatements,function(statement) {
journals.push(statement.journal_id[0]);
});
self.bankstatements = bankstatements;
return self.fetch('account.journal', undefined, [['id','in', journals]]);
}).then(function(journals){
self.journals = journals;
// associate the bank statements with their journals.
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(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;
}
if( model.model ){
new instance.web.Model(model.model).query(fields).filter(domain).context(context).all()
.then(function(result){
try{ // catching exceptions in model.loaded(...)
$.when(model.loaded(self,result,tmp))
.then(function(){ load_model(index + 1); },
function(err){ loaded.reject(err); });
}catch(err){
loaded.reject(err);
}
},function(err){
loaded.reject(err);
});
}else if( model.loaded ){
try{ // catching exceptions in model.loaded(...)
$.when(model.loaded(self,tmp))
.then( function(){ load_model(index +1); },
function(err){ loaded.reject(err); });
}catch(err){
loaded.reject(err);
}
}else{
load_model(index + 1);
}
self.cashregisters = bankstatements;
}
}
// Load the company Logo
try{
load_model(0);
}catch(err){
loaded.reject(err);
}
self.company_logo = new Image();
self.company_logo.crossOrigin = 'anonymous';
var logo_loaded = new $.Deferred();
self.company_logo.onload = function(){
var img = self.company_logo;
var ratio = 1;
var targetwidth = 300;
var maxheight = 150;
if( img.width !== targetwidth ){
ratio = targetwidth / img.width;
}
if( img.height * ratio > maxheight ){
ratio = maxheight / img.height;
}
var width = Math.floor(img.width * ratio);
var height = Math.floor(img.height * ratio);
var c = document.createElement('canvas');
c.width = width;
c.height = height
var ctx = c.getContext('2d');
ctx.drawImage(self.company_logo,0,0, width, height);
self.company_logo_base64 = c.toDataURL();
window.logo64 = self.company_logo_base64;
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;
},
@@ -324,20 +394,20 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
// it returns a deferred that succeeds after having tried to send the order and all the other pending orders.
push_order: function(order) {
var self = this;
this.proxy.log('push_order',order.export_as_JSON());
var order_id = this.db.add_order(order.export_as_JSON());
if(order){
this.proxy.log('push_order',order.export_as_JSON());
this.db.add_order(order.export_as_JSON());
}
var pushed = new $.Deferred();
this.set('synch',{state:'connecting', pending:self.db.get_orders().length});
this.flush_mutex.exec(function(){
var flushed = self._flush_all_orders();
var flushed = self._flush_orders(self.db.get_orders());
flushed.always(function(){
flushed.always(function(ids){
pushed.resolve();
});
return flushed;
});
return pushed;
},
@@ -361,8 +431,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
var order_id = this.db.add_order(order.export_as_JSON());
this.set('synch',{state:'connecting', pending:self.db.get_orders().length});
this.flush_mutex.exec(function(){
var done = new $.Deferred(); // holds the mutex
@@ -373,7 +441,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
// things will happen as a duplicate will be sent next time
// so we must make sure the server detects and ignores duplicated orders
var transfer = self._flush_order(order_id, {timeout:30000, to_invoice:true});
var transfer = self._flush_orders([self.db.get_order(order_id)], {timeout:30000, to_invoice:true});
transfer.fail(function(){
invoiced.reject('error-transfer');
@@ -382,10 +450,12 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
// on success, get the order id generated by the server
transfer.pipe(function(order_server_id){
// generate the pdf and download it
self.pos_widget.do_action('point_of_sale.pos_invoice_report',{additional_context:{
active_ids:order_server_id,
}});
invoiced.resolve();
done.resolve();
});
@@ -397,62 +467,33 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
return invoiced;
},
// attemps to send all pending orders ( stored in the pos_db ) to the server,
// and remove the successfully sent ones from the db once
// it has been confirmed that they have been sent correctly.
flush: function() {
// wrapper around the _save_to_server that updates the synch status widget
_flush_orders: function(orders, options) {
var self = this;
var flushed = new $.Deferred();
this.flush_mutex.exec(function(){
var done = new $.Deferred();
this.set('synch',{ state: 'connecting', pending: orders.length});
self._flush_all_orders()
.done( function(){ flushed.resolve();})
.fail( function(){ flushed.reject(); })
.always(function(){ done.resolve(); });
return done;
});
return flushed;
},
// attempts to send the locally stored order of id 'order_id'
// the sending is asynchronous and can take some time to decide if it is successful or not
// it is therefore important to only call this method from inside a mutex
// this method returns a deferred indicating wether the sending was successful or not
// there is a timeout parameter which is set to 2 seconds by default.
_flush_order: function( order_id, options) {
return this._flush_all_orders([this.db.get_order(order_id)], options);
},
// attempts to send all the locally stored orders. As with _flush_order, it should only be
// called from within a mutex.
// this method returns a deferred that always succeeds when all orders have been tried to be sent,
// even if none of them could actually be sent.
_flush_all_orders: function () {
var self = this;
self.set('synch', {
state: 'connecting',
pending: self.get('synch').pending
});
return self._save_to_server(self.db.get_orders()).done(function () {
return self._save_to_server(orders, options).done(function (server_ids) {
var pending = self.db.get_orders().length;
self.set('synch', {
state: pending ? 'connecting' : 'connected',
pending: pending
});
return server_ids;
});
},
// send an array of orders to the server
// available options:
// - timeout: timeout for the rpc call in ms
// returns a deferred that resolves with the list of
// server generated ids for the sent orders
_save_to_server: function (orders, options) {
if (!orders || !orders.length) {
var result = $.Deferred();
result.resolve();
result.resolve([]);
return result;
}
@@ -474,11 +515,18 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
shadow: !options.to_invoice,
timeout: timeout
}
).then(function () {
).then(function (server_ids) {
_.each(orders, function (order) {
self.db.remove_order(order.id);
});
}).fail(function (unused, event){
return server_ids;
}).fail(function (error, event){
if(error.code === 200 ){ // Business Logic Error, not a connection problem
self.pos_widget.screen_selector.show_popup('error-traceback',{
message: error.data.message,
comment: error.data.debug
});
}
// prevent an error popup creation by the rpc failure
// we want the failure to be silent as we send the orders in the background
event.preventDefault();
@@ -816,6 +864,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
this.receipt_type = 'receipt'; // 'receipt' || 'invoice'
this.temporary = attributes.temporary || false;
this.sequence_number = this.pos.pos_session.sequence_number++;
this.to_invoice = false;
return this;
},
is_empty: function(){
@@ -948,11 +997,47 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
return sum + paymentLine.get_amount();
}), 0);
},
getChange: function() {
return this.getPaidTotal() - this.getTotalTaxIncluded();
getChange: function(paymentline) {
if (!paymentline) {
var change = this.getPaidTotal() - this.getTotalTaxIncluded();
} else {
var change = -this.getTotalTaxIncluded();
var lines = this.get('paymentLines').models;
for (var i = 0; i < lines.length; i++) {
change += lines[i].get_amount();
if (lines[i] === paymentline) {
break;
}
}
}
return round_pr(Math.max(0,change), this.pos.currency.rounding);
},
getDueLeft: function() {
return this.getTotalTaxIncluded() - this.getPaidTotal();
getDueLeft: function(paymentline) {
if (!paymentline) {
var due = this.getTotalTaxIncluded() - this.getPaidTotal();
} else {
var due = this.getTotalTaxIncluded();
var lines = this.get('paymentLines').models;
for (var i = 0; i < lines.length; i++) {
if (lines[i] === paymentline) {
break;
} else {
due -= lines[i].get_amount();
}
}
}
return round_pr(Math.max(0,due), this.pos.currency.rounding);
},
isPaid: function(){
return this.getDueLeft() === 0;
},
isPaidWithCash: function(){
return !!this.get('paymentLines').find( function(pl){
return pl.cashregister.journal.type === 'cash';
});
},
finalize: function(){
this.destroy();
},
// sets the type of receipt 'receipt'(default) or 'invoice'
set_receipt_type: function(type){
@@ -984,6 +1069,25 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
}
}
},
set_to_invoice: function(to_invoice) {
this.to_invoice = to_invoice;
},
is_to_invoice: function(){
return this.to_invoice;
},
// remove all the paymentlines with zero money in it
clean_empty_paymentlines: function() {
var lines = this.get('paymentLines').models;
var empty = [];
for ( var i = 0; i < lines.length; i++) {
if (!lines[i].get_amount()) {
empty.push(lines[i]);
}
}
for ( var i = 0; i < empty.length; i++) {
this.removePaymentline(empty[i]);
}
},
//see set_screen_data
get_screen_data: function(key){
return this.screen_data[key];
+342 -361
View File
@@ -73,8 +73,9 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
load_saved_screen: function(){
this.close_popup();
var selectedOrder = this.pos.get('selectedOrder');
// this.set_current_screen(selectedOrder.get_screen_data('screen') || this.default_screen,null,'refresh');
this.set_current_screen(this.default_screen,null,'refresh');
// FIXME : this changing screen behaviour is sometimes confusing ...
this.set_current_screen(selectedOrder.get_screen_data('screen') || this.default_screen,null,'refresh');
//this.set_current_screen(this.default_screen,null,'refresh');
},
set_user_mode: function(user_mode){
@@ -210,26 +211,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
barcode_error_action: function(code){
this.pos_widget.screen_selector.show_popup('error-barcode',code.code);
},
// shows an action bar on the screen. The actionbar is automatically shown when you add a button
// with add_action_button()
show_action_bar: function(){
this.pos_widget.action_bar.show();
},
// hides the action bar. The actionbar is automatically hidden when it is empty
hide_action_bar: function(){
this.pos_widget.action_bar.hide();
},
// adds a new button to the action bar. The button definition takes three parameters, all optional :
// - label: the text below the button
// - icon: a small icon that will be shown
// - click: a callback that will be executed when the button is clicked.
// the method returns a reference to the button widget, and automatically show the actionbar.
add_action_button: function(button_def){
this.show_action_bar();
return this.pos_widget.action_bar.add_new_button(button_def);
},
// this method shows the screen and sets up all the widget related to this screen. Extend this method
// if you want to alter the behavior of the screen.
@@ -241,12 +222,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.$el.removeClass('oe_hidden');
}
if(this.pos_widget.action_bar.get_button_count() > 0){
this.show_action_bar();
}else{
this.hide_action_bar();
}
var self = this;
this.pos_widget.set_numpad_visible(this.show_numpad);
@@ -269,7 +244,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
if(this.pos.barcode_reader){
this.pos.barcode_reader.reset_action_callbacks();
}
this.pos_widget.action_bar.destroy_buttons();
},
// this methods hides the screen. It's not a good place to put your cleanup stuff as it is called on the
@@ -314,6 +288,26 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
});
module.FullscreenPopup = module.PopUpWidget.extend({
template:'FullscreenPopupWidget',
show: function(){
var self = this;
this._super();
this.renderElement();
this.$('.button.fullscreen').off('click').click(function(){
window.document.body.webkitRequestFullscreen();
self.pos_widget.screen_selector.close_popup();
});
this.$('.button.cancel').off('click').click(function(){
self.pos_widget.screen_selector.close_popup();
});
},
ismobile: function(){
return typeof window.orientation !== 'undefined';
}
});
module.ChooseReceiptPopupWidget = module.PopUpWidget.extend({
template:'ChooseReceiptPopupWidget',
show: function(){
@@ -348,6 +342,8 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
var self = this;
this._super();
$('body').append('<audio src="/point_of_sale/static/src/sounds/error.wav" autoplay="true"></audio>');
if( text && (text.message || text.comment) ){
this.$('.message').text(text.message);
this.$('.comment').text(text.comment);
@@ -365,6 +361,9 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
},
});
module.ErrorTracebackPopupWidget = module.ErrorPopupWidget.extend({
template:'ErrorTracebackPopupWidget',
});
module.ErrorSessionPopupWidget = module.ErrorPopupWidget.extend({
template:'ErrorSessionPopupWidget',
@@ -375,6 +374,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
show: function(barcode){
this._super();
this.$('.barcode').text(barcode);
},
});
@@ -404,14 +404,22 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
},
});
module.ErrorNoClientPopupWidget = module.ErrorPopupWidget.extend({
template: 'ErrorNoClientPopupWidget',
});
module.ErrorInvoiceTransferPopupWidget = module.ErrorPopupWidget.extend({
template: 'ErrorInvoiceTransferPopupWidget',
});
module.UnsentOrdersPopupWidget = module.PopUpWidget.extend({
template: 'UnsentOrdersPopupWidget',
show: function(options){
var self = this;
this._super(options);
this.renderElement();
this.$('.button.confirm').click(function(){
self.pos_widget.screen_selector.close_popup();
});
},
});
module.ScaleScreenWidget = module.ScreenWidget.extend({
template:'ScaleScreenWidget',
@@ -557,6 +565,11 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
module.ClientListScreenWidget = module.ScreenWidget.extend({
template: 'ClientListScreenWidget',
init: function(parent, options){
this._super(parent, options);
this.partner_cache = new module.DomCache();
},
show_leftpane: false,
auto_back: true,
@@ -587,7 +600,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
}
this.$('.client-list-contents').delegate('.client-line','click',function(event){
console.log('uh');
self.line_select(event,$(this),parseInt($(this).data('id')));
});
@@ -628,14 +640,24 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.$('.searchbox input').focus();
},
render_list: function(partners){
var contents = this.$('.client-list-contents');
contents.empty();
var contents = this.$el[0].querySelector('.client-list-contents');
contents.innerHtml = "";
for(var i = 0, len = partners.length; i < len; i++){
var clientline = $(QWeb.render('ClientLine',{partner:partners[i]}));
if( partners[i] === this.new_client ){
clientline.addClass('highlight');
var partner = partners[i];
var clientline = this.partner_cache.get_node(partner.id);
if(!clientline){
var clientline_html = QWeb.render('ClientLine',{partner:partners[i]});
var clientline = document.createElement('tbody');
clientline.innerHTML = clientline_html;
clientline = clientline.childNodes[1];
this.partner_cache.cache_node(partner.id,clientline);
}
contents.append(clientline);
if( partners === this.new_client ){
clientline.classList.add('highlight');
}else{
clientline.classList.remove('highlight');
}
contents.appendChild(clientline);
}
},
save_changes: function(){
@@ -730,30 +752,16 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
module.ReceiptScreenWidget = module.ScreenWidget.extend({
template: 'ReceiptScreenWidget',
show_numpad: true,
show_leftpane: true,
show_numpad: false,
show_leftpane: false,
show: function(){
this._super();
var self = this;
var print_button = this.add_action_button({
label: _t('Print'),
icon: '/point_of_sale/static/src/img/icons/png48/printer.png',
click: function(){ self.print(); },
});
var finish_button = this.add_action_button({
label: _t('Next Order'),
icon: '/point_of_sale/static/src/img/icons/png48/go-next.png',
click: function() { self.finishOrder(); },
});
this.refresh();
this.print();
//
// 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
// to send the orders to the backend, and the user is able to go to the next
@@ -770,372 +778,345 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
// 2 seconds is the same as the default timeout for sending orders and so the dialog
// should have appeared before the timeout... so yeah that's not ultra reliable.
finish_button.set_disabled(true);
this.lock_screen(true);
setTimeout(function(){
finish_button.set_disabled(false);
self.lock_screen(false);
}, 2000);
},
lock_screen: function(locked) {
this._locked = locked;
if (locked) {
this.$('.next').removeClass('highlight');
} else {
this.$('.next').addClass('highlight');
}
},
print: function() {
window.print();
},
finishOrder: function() {
this.pos.get('selectedOrder').destroy();
finish_order: function() {
if (!this._locked) {
this.pos.get_order().finalize();
}
},
renderElement: function() {
var self = this;
this._super();
this.$('.next').click(function(){
self.finish_order();
});
this.$('.button.print').click(function(){
self.print();
});
},
refresh: function() {
var order = this.pos.get('selectedOrder');
$('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{
var order = this.pos.get_order();
this.$('.pos-receipt-container').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({
template: 'PaymentScreenWidget',
back_screen: 'products',
next_screen: 'receipt',
template: 'PaymentScreenWidget',
back_screen: 'product',
next_screen: 'receipt',
show_leftpane: false,
show_numpad: false,
init: function(parent, options) {
var self = this;
this._super(parent,options);
this._super(parent, options);
this.pos.bind('change:selectedOrder',function(){
this.bind_events();
this.renderElement();
this.watch_order_changes();
},this);
this.watch_order_changes();
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){
this.inputbuffer = "";
this.firstinput = true;
this.keyboard_handler = function(event){
var key = '';
if ( event.keyCode === 13 ) { // Enter
self.validate_order();
}else if(event.which === 27){
self.back();
} else if ( event.keyCode === 190 ) { // Dot
key = '.';
} else if ( event.keyCode === 46 ) { // Delete
key = 'CLEAR';
} else if ( event.keyCode === 8 ) { // Backspace
key = 'BACKSPACE';
event.preventDefault(); // Prevents history back nav
} else if ( event.keyCode >= 48 && event.keyCode <= 57 ){ // Numbers
key = '' + (event.keyCode - 48);
} else if ( event.keyCode >= 96 && event.keyCode <= 105 ){ // Numpad Numbers
key = '' + (event.keyCode - 96);
} else if ( event.keyCode === 189 || event.keyCode === 109 ) { // Minus
key = '-';
} else if ( event.keyCode === 107 ) { // Plus
key = '+';
}
self.payment_input(key);
};
},
// resets the current input buffer
reset_input: function(){
var line = this.pos.get_order().selected_paymentline;
this.firstinput = true;
if (line) {
this.inputbuffer = this.format_currency_no_symbol(line.get_amount());
} else {
this.inputbuffer = "";
}
},
// handle both keyboard and numpad input. Accepts
// a string that represents the key pressed.
payment_input: function(input) {
var oldbuf = this.inputbuffer.slice(0);
if (input === '.') {
if (this.firstinput) {
this.inputbuffer = "0.";
}else if (!this.inputbuffer.length || this.inputbuffer === '-') {
this.inputbuffer += "0.";
} else if (this.inputbuffer.indexOf('.') < 0){
this.inputbuffer = this.inputbuffer + '.';
}
} else if (input === 'CLEAR') {
this.inputbuffer = "";
} else if (input === 'BACKSPACE') {
this.inputbuffer = this.inputbuffer.substring(0,this.inputbuffer.length - 1);
} else if (input === '+') {
if ( this.inputbuffer[0] === '-' ) {
this.inputbuffer = this.inputbuffer.substring(1,this.inputbuffer.length);
}
} else if (input === '-') {
if ( this.inputbuffer[0] === '-' ) {
this.inputbuffer = this.inputbuffer.substring(1,this.inputbuffer.length);
} else {
this.inputbuffer = '-' + this.inputbuffer;
}
} else if (input[0] === '+' && !isNaN(parseFloat(input))) {
this.inputbuffer = '' + ((parseFloat(this.inputbuffer) || 0) + parseFloat(input));
} else if (!isNaN(parseInt(input))) {
if (this.firstinput) {
this.inputbuffer = '' + input;
} else {
this.inputbuffer += input;
}
}
this.firstinput = false;
if (this.inputbuffer !== oldbuf) {
var order = this.pos.get_order();
if (order.selected_paymentline) {
order.selected_paymentline.set_amount(parseFloat(this.inputbuffer));
this.order_changes();
this.render_paymentlines();
this.$('.paymentline.selected .edit').text(this.inputbuffer);
}
}
},
click_numpad: function(button) {
this.payment_input(button.data('action'));
},
render_numpad: function() {
var self = this;
var numpad = $(QWeb.render('PaymentScreen-Numpad', { widget:this }));
numpad.on('click','button',function(){
self.click_numpad($(this));
});
return numpad;
},
click_delete_paymentline: function(cid){
var lines = this.pos.get_order().get('paymentLines').models;
for ( var i = 0; i < lines.length; i++ ) {
if (lines[i].cid === cid) {
this.pos.get_order().removePaymentline(lines[i]);
this.reset_input();
this.render_paymentlines();
return;
}
}
},
click_paymentline: function(cid){
var lines = this.pos.get_order().get('paymentLines').models;
for ( var i = 0; i < lines.length; i++ ) {
if (lines[i].cid === cid) {
this.pos.get_order().selectPaymentline(lines[i]);
this.reset_input();
this.render_paymentlines();
return;
}
}
},
render_paymentlines: function() {
var self = this;
var order = this.pos.get_order();
var lines = order.get('paymentLines').models;
this.$('.paymentlines-container').empty();
var lines = $(QWeb.render('PaymentScreen-Paymentlines', {
widget: this,
order: order,
paymentlines: lines,
}));
lines.on('click','.delete-button',function(){
self.click_delete_paymentline($(this).data('cid'));
});
lines.on('click','.paymentline',function(){
self.click_paymentline($(this).data('cid'));
});
lines.appendTo(this.$('.paymentlines-container'));
},
click_paymentmethods: function(id) {
var cashregister = null;
for ( var i = 0; i < this.pos.cashregisters.length; i++ ) {
if ( this.pos.cashregisters[i].journal_id[0] === id ){
cashregister = this.pos.cashregisters[i];
break;
}
}
this.pos.get_order().addPaymentline( cashregister );
this.reset_input();
this.render_paymentlines();
},
render_paymentmethods: function() {
var self = this;
var methods = $(QWeb.render('PaymentScreen-Paymentmethods', { widget:this }));
methods.on('click','.paymentmethod',function(){
self.click_paymentmethods($(this).data('id'));
});
return methods;
},
click_invoice: function(){
var order = this.pos.get_order();
order.set_to_invoice(!order.is_to_invoice());
if (order.is_to_invoice()) {
this.$('.js_invoice').addClass('highlight');
} else {
this.$('.js_invoice').removeClass('highlight');
}
},
renderElement: function() {
var self = this;
this._super();
var numpad = this.render_numpad();
numpad.appendTo(this.$('.payment-numpad'));
var methods = this.render_paymentmethods();
methods.appendTo(this.$('.paymentmethods-container'));
this.render_paymentlines();
this.$('.back').click(function(){
self.pos_widget.screen_selector.back();
});
this.$('.next').click(function(){
self.validate_order();
});
this.$('.js_invoice').click(function(){
self.click_invoice();
});
},
show: function(){
this.pos.get_order().clean_empty_paymentlines();
this.reset_input();
this.render_paymentlines();
this.order_changes();
window.document.body.addEventListener('keydown',this.keyboard_handler);
this._super();
},
hide: function(){
window.document.body.removeEventListener('keydown',this.keyboard_handler);
this._super();
},
// sets up listeners to watch for order changes
watch_order_changes: function() {
var self = this;
this.enable_numpad();
this.focus_selected_line();
document.body.addEventListener('keyup', this.hotkey_handler);
this.add_action_button({
label: _t('Back'),
icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
click: function(){
self.back();
},
});
this.add_action_button({
label: _t('Validate'),
name: 'validation',
icon: '/point_of_sale/static/src/img/icons/png48/validate.png',
click: function(){
self.validate_order();
},
});
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.validate_order({invoice: true});
},
});
}
if( this.pos.config.iface_cashdrawer ){
this.add_action_button({
label: _t('Cash'),
name: 'cashbox',
icon: '/point_of_sale/static/src/img/open-cashbox.png',
click: function(){
self.pos.proxy.open_cashbox();
},
});
}
this.update_payment_summary();
},
close: function(){
this._super();
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.remove_empty_lines();
this.pos_widget.screen_selector.set_current_screen(this.back_screen);
},
bind_events: function() {
var order = this.pos.get_order();
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);
order.bind('all',function(){
self.order_changes();
});
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) {
// called when the order is changed, used to show if
// the order is paid or not
order_changes: function(){
var self = this;
var order = this.pos.get_order();
if (order.isPaid()) {
self.$('.next').addClass('highlight');
}else{
self.$('.next').removeClass('highlight');
}
},
// Check if the order is paid, then sends it to the backend,
// and complete the sale process
validate_order: function() {
var self = this;
options = options || {};
var currentOrder = this.pos.get('selectedOrder');
var order = this.pos.get_order();
if(!this.is_paid()){
if (!order.isPaid() || this.invoicing) {
return;
}
if( this.pos.config.iface_cashdrawer
&& this.pos.get('selectedOrder').get('paymentLines').find( function(pl){
return pl.cashregister.journal.type === 'cash';
})){
if (order.isPaidWithCash() && this.pos.config.iface_cashdrawer) {
this.pos.proxy.open_cashbox();
}
if(options.invoice){
// deactivate the validation button while we try to send the order
this.pos_widget.action_bar.set_button_disabled('validation',true);
this.pos_widget.action_bar.set_button_disabled('invoice',true);
var invoiced = this.pos.push_and_invoice_order(currentOrder);
if (order.is_to_invoice()) {
var invoiced = this.pos.push_and_invoice_order(order);
this.invoicing = true;
invoiced.fail(function(error){
if(error === 'error-no-client'){
self.pos_widget.screen_selector.show_popup('error-no-client');
}else{
self.invoicing = false;
if (error === 'error-no-client') {
self.pos_widget.screen_selector.show_popup('confirm',{
message: _t('Please select the Customer'),
comment: _t('You need to select the customer before you can invoice an order.'),
confirm: function(){
self.pos_widget.screen_selector.set_current_screen('clientlist');
},
});
} else {
self.pos_widget.screen_selector.show_popup('error-invoice-transfer');
}
self.pos_widget.action_bar.set_button_disabled('validation',false);
self.pos_widget.action_bar.set_button_disabled('invoice',false);
});
invoiced.done(function(){
self.pos_widget.action_bar.set_button_disabled('validation',false);
self.pos_widget.action_bar.set_button_disabled('invoice',false);
self.pos.get('selectedOrder').destroy();
self.invoicing = false;
order.finalize();
});
}else{
this.pos.push_order(currentOrder)
if(this.pos.config.iface_print_via_proxy){
} else {
this.pos.push_order(order)
if (this.pos.config.iface_print_via_proxy) {
var receipt = currentOrder.export_for_printing();
this.pos.proxy.print_receipt(QWeb.render('XmlReceipt',{
receipt: receipt, widget: self,
}));
this.pos.get('selectedOrder').destroy(); //finish order and go back to scan screen
}else{
order.finalize(); //finish order and go back to scan screen
} else {
this.pos_widget.screen_selector.set_current_screen(this.next_screen);
}
}
// 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);
}
},
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_mode_back_to_payment: function() {
this.numpad_state.set({mode: 'payment'});
},
set_value: function(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);
}
},
});
}
+86 -90
View File
@@ -1,93 +1,89 @@
function openerp_pos_tests(instance, module){ //module is instance.point_of_sale
(function() {
'use strict';
// Various UI Tests to measure performance and memory leaks.
module.UiTester = function(){
var running = false;
var queue = new module.JobQueue();
openerp.Tour.register({
id: 'pos_basic_order',
name: 'Complete a basic order trough the Front-End',
path: '/web#model=pos.session.opening&action=point_of_sale.action_pos_session_opening',
mode: 'test',
steps: [
{
title: 'Wait fot the bloody screen to be ready',
wait: 200,
},
{
title: 'Load the Session',
waitNot: '.oe_loading:visible',
element: 'span:contains("Resume Session"),span:contains("Start Session")',
},
{
title: 'Loading the Loading Screen',
waitFor: '.loader'
},
{
title: 'Waiting for the end of loading...',
waitFor: '.loader:hidden',
},
{
title: 'Loading The Point of Sale',
waitFor: '.pos',
},
{
title: 'On va manger des CHIPS!',
element: '.product-list .product-name:contains("250g Lays Pickels")',
},
{
title: 'The chips have been added to the Order',
waitFor: '.order .product-name:contains("250g Lays Pickels")',
},
{
title: 'The order total has been updated to the correct value',
wait: 2000,
waitFor: '.order .total .value:contains("1.48 €")',
},
{
title: "Let's buy more chips",
element: '.product-list .product-name:contains("250g Lays Pickels")',
},
{
title: "Let's veryify we pay the correct price for two bags of chips",
waitFor: '.order .total .value:contains("2.96 €")',
},
{
title: "Let's pay with a debit card",
element: ".paypad-button:contains('Bank')",
},
{
title: "Let's accept the payment",
onload: function(){
// The test cannot validate or cancel the print() ... so we replace it by a noop !.
window._print = window.print;
window.print = function(){ console.log('Print!') };
},
element: ".button .iconlabel:contains('Validate'):visible",
},
{
title: "Let's finish the order",
element: ".button:not(.disabled) .iconlabel:contains('Next'):visible",
},
{
onload: function(){
window.print = window._print;
window._print = undefined;
},
title: "Let's wait for the order posting",
waitFor: ".oe_status.js_synch .js_connected:visible",
},
{
title: "Let's close the Point of Sale",
element: ".header-button:contains('Close')",
},
{
title: "Wait for the backend to ready itself",
element: 'span:contains("Resume Session"),span:contains("Start Session")',
},
],
});
// stop the currently running test
this.stop = function(){
queue.clear();
};
})();
// randomly switch product categories
this.category_switch = function(interval){
queue.schedule(function(){
var breadcrumbs = $('.breadcrumb-button');
var categories = $('.category-button');
if(categories.length > 0){
var rnd = Math.floor(Math.random()*categories.length);
categories.eq(rnd).click();
}else{
var rnd = Math.floor(Math.random()*breadcrumbs.length);
breadcrumbs.eq(rnd).click();
}
},{repeat:true, duration:interval});
};
// randomly order products then resets the order
this.order_products = function(interval){
queue.schedule(function(){
var def = new $.Deferred();
var order_queue = new module.JobQueue();
var order_size = 1 + Math.floor(Math.random()*10);
while(order_size--){
order_queue.schedule(function(){
var products = $('.product');
if(products.length > 0){
var rnd = Math.floor(Math.random()*products.length);
products.eq(rnd).click();
}
},{duration:20});
}
order_queue.finished().then(function(){
$('.deleteorder-button').click();
def.resolve();
});
return def;
},{repeat:true, duration: interval});
};
// makes a complete product order cycle ( print via proxy must be activated, and scale deactivated )
this.full_order_cycle = function(interval){
queue.schedule(function(){
var def = new $.Deferred();
var order_queue = new module.JobQueue();
var order_size = 1 + Math.floor(Math.random()*50);
while(order_size--){
order_queue.schedule(function(){
var products = $('.product');
if(products.length > 0){
var rnd = Math.floor(Math.random()*products.length);
products.eq(rnd).click();
}
},{duration:50});
}
order_queue.schedule(function(){
$('.paypad-button:first').click();
},{duration:250});
order_queue.schedule(function(){
$('.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();
},{duration:250});
order_queue.schedule(function(){
def.resolve();
});
return def;
},{repeat: true, duration: interval});
};
};
if(jQuery.deparam(jQuery.param.querystring()).debug !== undefined){
window.pos_test_ui = new module.UiTester();
}
}
@@ -1,5 +1,7 @@
function openerp_pos_basewidget(instance, module){ //module is instance.point_of_sale
var round_pr = instance.web.round_precision
// This is a base class for all Widgets in the POS. It exposes relevant data to the
// templates :
// - widget.currency : { symbol: '$' | '€' | ..., position: 'before' | 'after }
@@ -27,6 +29,12 @@ function openerp_pos_basewidget(instance, module){ //module is instance.point_of
var decimals = Math.max(0,Math.ceil(Math.log(1.0 / this.currency.rounding) / Math.log(10)));
this.format_currency_no_symbol = function(amount){
amount = round_pr(amount,this.currency.rounding);
amount = amount.toFixed(decimals);
return amount;
};
this.format_currency = function(amount){
if(typeof amount === 'number'){
amount = Math.round(amount*100)/100;
@@ -37,7 +45,7 @@ function openerp_pos_basewidget(instance, module){ //module is instance.point_of
}else{
return this.currency.symbol + ' ' + amount;
}
}
};
},
show: function(){
+75 -140
View File
@@ -83,46 +83,21 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
},
});
// The paypad allows to select the payment method (cashregisters)
// used to pay the order.
module.PaypadWidget = module.PosBaseWidget.extend({
template: 'PaypadWidget',
// The action pads contains the payment button and the customer selection button.
module.ActionpadWidget = module.PosBaseWidget.extend({
template: 'ActionpadWidget',
renderElement: function() {
var self = this;
this._super();
_.each(this.pos.cashregisters,function(cashregister) {
var button = new module.PaypadButtonWidget(self,{
pos: self.pos,
pos_widget : self.pos_widget,
cashregister: cashregister,
});
button.appendTo(self.$el);
this.$('.pay').click(function(){
self.pos.pos_widget.screen_selector.set_current_screen('payment');
});
this.$('.set-customer').click(function(){
self.pos.pos_widget.screen_selector.set_current_screen('clientlist');
});
}
});
module.PaypadButtonWidget = module.PosBaseWidget.extend({
template: 'PaypadButtonWidget',
init: function(parent, options){
this._super(parent, options);
this.cashregister = options.cashregister;
},
renderElement: function() {
var self = this;
this._super();
this.$el.click(function(){
if (self.pos.get('selectedOrder').get('screen') === 'receipt'){ //TODO Why ?
console.warn('TODO should not get there...?');
return;
}
self.pos.get('selectedOrder').addPaymentline(self.cashregister);
self.pos_widget.screen_selector.set_current_screen('payment');
});
},
});
module.OrderWidget = module.PosBaseWidget.extend({
template:'OrderWidget',
init: function(parent, options) {
@@ -318,90 +293,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
},
});
module.ActionButtonWidget = instance.web.Widget.extend({
template:'ActionButtonWidget',
icon_template:'ActionButtonWidgetWithIcon',
init: function(parent, options){
this._super(parent, options);
this.label = options.label || 'button';
this.rightalign = options.rightalign || false;
this.click_action = options.click;
this.disabled = options.disabled || false;
if(options.icon){
this.icon = options.icon;
this.template = this.icon_template;
}
},
set_disabled: function(disabled){
if(this.disabled != disabled){
this.disabled = !!disabled;
this.renderElement();
}
},
renderElement: function(){
this._super();
if(this.click_action && !this.disabled){
this.$el.click(_.bind(this.click_action, this));
}
},
});
module.ActionBarWidget = instance.web.Widget.extend({
template:'ActionBarWidget',
init: function(parent, options){
this._super(parent,options);
this.button_list = [];
this.buttons = {};
this.visibility = {};
},
set_element_visible: function(element, visible, action){
if(visible != this.visibility[element]){
this.visibility[element] = !!visible;
if(visible){
this.$('.'+element).removeClass('oe_hidden');
}else{
this.$('.'+element).addClass('oe_hidden');
}
}
if(visible && action){
this.action[element] = action;
this.$('.'+element).off('click').click(action);
}
},
set_button_disabled: function(name, disabled){
var b = this.buttons[name];
if(b){
b.set_disabled(disabled);
}
},
destroy_buttons:function(){
for(var i = 0; i < this.button_list.length; i++){
this.button_list[i].destroy();
}
this.button_list = [];
this.buttons = {};
return this;
},
get_button_count: function(){
return this.button_list.length;
},
add_new_button: function(button_options){
var button = new module.ActionButtonWidget(this,button_options);
this.button_list.push(button);
if(button_options.name){
this.buttons[button_options.name] = button;
}
button.appendTo(this.$('.pos-actionbar-button-list'));
return button;
},
show:function(){
this.$el.removeClass('oe_hidden');
},
hide:function(){
this.$el.addClass('oe_hidden');
},
});
module.ProductCategoriesWidget = module.PosBaseWidget.extend({
template: 'ProductCategoriesWidget',
init: function(parent, options){
@@ -797,6 +688,19 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.$('.button.reference').click(function(){
self.pos.barcode_reader.scan(self.$('input.ean').val());
});
this.$('.button.show_orders').click(function(){
self.pos.pos_widget.screen_selector.show_popup('unsent-orders');
});
this.$('.button.delete_orders').click(function(){
self.pos.pos_widget.screen_selector.show_popup('confirm',{
message: _t('Delete Unsent Orders ?'),
comment: _t('This operation will permanently destroy all unsent orders from the local storage. You will lose all the data. This operation cannot be undone.'),
confirm: function(){
self.pos.db.remove_all_orders();
self.pos.set({synch: { state:'connected', pending: 0 }});
},
});
});
_.each(this.eans, function(ean, name){
self.$('.button.'+name).click(function(){
self.$('input.ean').val(ean);
@@ -840,7 +744,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
self.set_status(synch.state, synch.pending);
});
this.$el.click(function(){
self.pos.flush();
self.pos.push_order();
});
},
});
@@ -903,7 +807,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
// - a header, containing the list of orders
// - a leftpane, containing the list of bought products (orderlines)
// - a rightpane, containing the screens (see pos_screens.js)
// - an actionbar on the bottom, containing various action buttons
// - popups
// - an onscreen keyboard
// a screen_selector which controls the switching between screens and the showing/closing of popups
@@ -1001,23 +904,50 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
self.screen_selector.show_popup('error', 'Sorry, we could not create a user session');
}else if(!self.pos.config){
self.screen_selector.show_popup('error', 'Sorry, we could not find any PoS Configuration for this session');
}else if(self.pos.config.iface_fullscreen && document.body.webkitRequestFullscreen && (
window.screen.availWidth > window.innerWidth ||
window.screen.availHeight > window.innerHeight )){
self.screen_selector.show_popup('fullscreen');
}
self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').addClass('oe_hidden');});
self.pos.flush();
self.pos.push_order();
}).fail(function(){ // error when loading models data from the backend
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']})
.pipe(_.bind(function(result){
var action = result.result;
this.do_action(action);
}, this));
}, self));
}).fail(function(err){ // error when loading models data from the backend
self.loading_error(err);
});
},
loading_error: function(err){
var self = this;
var message = err.message;
var comment = err.stack;
if(err.message === 'XmlHttpRequestError '){
message = 'Network Failure (XmlHttpRequestError)';
comment = 'The Point of Sale could not be loaded due to a network problem.\n Please check your internet connection.';
}else if(err.message === 'OpenERP Server Error'){
message = err.data.message;
comment = err.data.debug;
}
if( typeof comment !== 'string' ){
comment = 'Traceback not available.';
}
var popup = $(QWeb.render('ErrorTracebackPopupWidget',{
widget: { message: message, comment: comment },
}));
popup.find('.button').click(function(){
self.close();
});
popup.css({ zindex: 9001 });
popup.appendTo(this.$el);
},
loading_progress: function(fac){
this.$('.loader .loader-feedback').removeClass('oe_hidden');
this.$('.loader .progress').css({'width': ''+Math.floor(fac*100)+'%'});
@@ -1076,15 +1006,21 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.choose_receipt_popup = new module.ChooseReceiptPopupWidget(this, {});
this.choose_receipt_popup.appendTo(this.$el);
this.error_no_client_popup = new module.ErrorNoClientPopupWidget(this, {});
this.error_no_client_popup.appendTo(this.$el);
this.error_invoice_transfer_popup = new module.ErrorInvoiceTransferPopupWidget(this, {});
this.error_invoice_transfer_popup.appendTo(this.$el);
this.error_traceback_popup = new module.ErrorTracebackPopupWidget(this,{});
this.error_traceback_popup.appendTo(this.$el);
this.confirm_popup = new module.ConfirmPopupWidget(this,{});
this.confirm_popup.appendTo(this.$el);
this.fullscreen_popup = new module.FullscreenPopup(this,{});
this.fullscreen_popup.appendTo(this.$el);
this.unsent_orders_popup = new module.UnsentOrdersPopupWidget(this,{});
this.unsent_orders_popup.appendTo(this.$el);
// -------- Misc ---------
this.close_button = new module.HeaderButtonWidget(this,{
@@ -1104,11 +1040,8 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.username = new module.UsernameWidget(this,{});
this.username.replace(this.$('.placeholder-UsernameWidget'));
this.action_bar = new module.ActionBarWidget(this);
this.action_bar.replace(this.$(".placeholder-RightActionBar"));
this.paypad = new module.PaypadWidget(this, {});
this.paypad.replace(this.$('.placeholder-PaypadWidget'));
this.actionpad = new module.ActionpadWidget(this, {});
this.actionpad.replace(this.$('.placeholder-ActionpadWidget'));
this.numpad = new module.NumpadWidget(this);
this.numpad.replace(this.$('.placeholder-NumpadWidget'));
@@ -1137,9 +1070,11 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
'error-barcode': this.error_barcode_popup,
'error-session': this.error_session_popup,
'choose-receipt': this.choose_receipt_popup,
'error-no-client': this.error_no_client_popup,
'error-invoice-transfer': this.error_invoice_transfer_popup,
'error-traceback': this.error_traceback_popup,
'confirm': this.confirm_popup,
'fullscreen': this.fullscreen_popup,
'unsent-orders': this.unsent_orders_popup,
},
default_screen: 'products',
default_mode: 'cashier',
@@ -1164,10 +1099,10 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.numpad_visible = visible;
if(visible){
this.numpad.show();
this.paypad.show();
this.actionpad.show();
}else{
this.numpad.hide();
this.paypad.hide();
this.actionpad.hide();
}
}
},
Binary file not shown.
+262 -88
View File
@@ -36,19 +36,12 @@
<div class='subwindow-container'>
<div class='subwindow-container-fix pads'>
<div class="control-buttons oe_hidden"></div>
<div class="placeholder-PaypadWidget"></div>
<div class="placeholder-ActionpadWidget"></div>
<div class="placeholder-NumpadWidget"></div>
</div>
</div>
</div>
<div class='subwindow collapsed'>
<div class='subwindow-container'>
<div class='subwindow-container-fix'>
<div class='placeholder-LeftActionBar'></div>
</div>
</div>
</div>
</div>
</div>
@@ -60,14 +53,6 @@
</div>
</div>
</div>
<div class='subwindow collapsed'>
<div class='subwindow-container'>
<div class='subwindow-container-fix'>
<div class='placeholder-RightActionBar'></div>
</div>
</div>
</div>
</div>
</div>
@@ -144,8 +129,14 @@
<div>There are pending operations that could not be saved into the database, are you sure you want to exit?</div>
</t>
<t t-name="PaypadWidget">
<div class="paypad touch-scrollable">
<t t-name="ActionpadWidget">
<div class='actionpad'>
<button class='button set-customer'>
<i class='fa fa-user' /> Set Customer
</button>
<button class="button pay">
<i class='fa fa-chevron-right' /> Payment
</button>
</div>
</t>
@@ -383,41 +374,179 @@
</div>
</t>
<t t-name="PaymentScreenWidget">
<div class="payment-screen screen touch-scrollable">
<div class="pos-payment-container">
<div class='payment-due-total'></div>
<div class='payment-lines'></div>
<div class='payment-info'>
<div class="infoline">
<span class='left-block'>
Paid:
</span>
<span class="right-block payment-paid-total"></span>
</div>
<div class="infoline">
<span class='left-block'>
Remaining:
</span>
<span class="right-block payment-remaining"></span>
</div>
<div class="infoline bigger" >
<span class='left-block'>
Change:
</span>
<span class="right-block payment-change"></span>
</div>
<t t-name="PaymentScreen-Paymentlines">
<t t-if="!paymentlines.length">
<div class='paymentlines-empty'>
<div class='total'>
<t t-esc="widget.format_currency(order.getTotalTaxIncluded())"/>
</div>
<div class='message'>
Please select a payment method.
</div>
</div>
</t>
<t t-if="paymentlines.length">
<table class='paymentlines'>
<colgroup>
<col class='due' />
<col class='tendered' />
<col class='change' />
<col class='method' />
<col class='controls' />
</colgroup>
<thead>
<tr class='label'>
<th>Due</th>
<th>Tendered</th>
<th>Change</th>
<th>Method</th>
<th></th>
</tr>
</thead>
<tbody>
<t t-foreach='paymentlines' t-as='line'>
<t t-if='line.selected'>
<tr class='paymentline selected'>
<td class='col-due'> <t t-esc='widget.format_currency_no_symbol(order.getDueLeft(line))' /> </td>
<td class='col-tendered edit'>
<t t-esc='widget.inputbuffer' />
<!-- <t t-esc='line.get_amount()' /> -->
</td>
<t t-if='order.getChange(line)'>
<td class='col-change highlight' >
<t t-esc='widget.format_currency_no_symbol(order.getChange(line))' />
</td>
</t>
<t t-if='!order.getChange(line)'>
<td class='col-change' ></td>
</t>
<td class='col-name' > <t t-esc='line.name' /> </td>
<td class='delete-button' t-att-data-cid='line.cid'> <i class='fa fa-times-circle' /> </td>
</tr>
</t>
<t t-if='!line.selected'>
<tr class='paymentline' t-att-data-cid='line.cid'>
<td class='col-due'> <t t-esc='widget.format_currency_no_symbol(order.getDueLeft(line))' /> </td>
<td class='col-tendered'> <t t-esc='widget.format_currency_no_symbol(line.get_amount())' /> </td>
<td class='col-change'>
<t t-if='order.getChange(line)'>
<t t-esc='widget.format_currency_no_symbol(order.getChange(line))' />
</t>
</td>
<td class='col-name'> <t t-esc='line.name' /> </td>
<td class='delete-button' t-att-data-cid='line.cid'> <i class='fa fa-times-circle' /> </td>
</tr>
</t>
</t>
</tbody>
</table>
</t>
</t>
<t t-name="PaymentScreen-Numpad">
<div class="numpad">
<button class="input-button number-char" data-action='1'>1</button>
<button class="input-button number-char" data-action='2'>2</button>
<button class="input-button number-char" data-action='3'>3</button>
<button class="mode-button" data-action='+10'>+10</button>
<br />
<button class="input-button number-char" data-action='4'>4</button>
<button class="input-button number-char" data-action='5'>5</button>
<button class="input-button number-char" data-action='6'>6</button>
<button class="mode-button" data-action='+20'>+20</button>
<br />
<button class="input-button number-char" data-action='7'>7</button>
<button class="input-button number-char" data-action='8'>8</button>
<button class="input-button number-char" data-action='9'>9</button>
<button class="mode-button" data-action='+50'>+50</button>
<br />
<button class="input-button numpad-char" data-action='CLEAR' >C</button>
<button class="input-button number-char" data-action='0'>0</button>
<button class="input-button number-char" data-action='.'>.</button>
<button class="input-button numpad-backspace" data-action='BACKSPACE' >
<img src="/point_of_sale/static/src/img/backspace.png" width="24" height="21" />
</button>
<br />
</div>
</t>
<t t-name="PaymentScreen-Paymentmethods">
<div class='paymentmethods'>
<t t-foreach="widget.pos.cashregisters" t-as="cashregister">
<div class="paymentmethod" t-att-data-id="cashregister.journal_id[0]">
<t t-esc="cashregister.journal_id[1]" />
</div>
</t>
</div>
</t>
<t t-name="PaymentScreenWidget">
<div class='payment-screen screen'>
<div class='screen-content'>
<div class='top-content'>
<span class='button back'>
<i class='fa fa-angle-double-left'></i>
Back
</span>
<h1>Payment</h1>
<span class='button next'>
Validate
<i class='fa fa-angle-double-right'></i>
</span>
</div>
<div class='left-content pc40 touch-scrollable scrollable-y'>
<div class='paymentmethods-container'>
</div>
</div>
<div class='right-content pc60 touch-scrollable scrollable-y'>
<section class='paymentlines-container'>
</section>
<section class='payment-numpad'>
</section>
<div class='payment-buttons'>
<t t-if='widget.pos.config.iface_invoicing'>
<div t-attf-class='button js_invoice #{ widget.pos.get_order().is_to_invoice() ? "highlight" : ""} '>
<i class='fa fa-file-text-o' /> Invoice
</div>
</t>
<t t-if='widget.pos.config.iface_cashdrawer'>
<div class='button js_invoice'>
<i class='fa fa-archive' /> Open Cashbox
</div>
</t>
</div>
</div>
</div>
</div>
</t>
<t t-name="ReceiptScreenWidget">
<div class="receipt-screen screen touch-scrollable" >
<div class="pos-step-container">
<div class="pos-receipt-container">
<div class='receipt-screen screen'>
<div class='screen-content'>
<div class='top-content'>
<h1>Receipt</h1>
<span class='button next'>
Next Order
<i class='fa fa-angle-double-right'></i>
</span>
</div>
<div class="centered-content">
<div class="button print">
<i class='fa fa-print'></i> Print
</div>
<div class="pos-receipt-container">
</div>
</div>
</div>
</div>
@@ -586,23 +715,55 @@
</div>
</t>
<t t-name="ErrorSessionPopupWidget">
<t t-name="FullscreenPopupWidget">
<div class="modal-dialog">
<div class="popup popup-nosession">
<p class="message">Sorry, we could not create a session for this user.</p>
<div class="popup popup-fullscreen">
<p class="message">Fullscreen Setup</p>
<t t-if='widget.ismobile()'>
<p class="comment">
The best way to make the point of sale fullscreen on mobile
devices is to add the point of sale to your home screen. On
iPhone and iPad this is done by tapping <img src='/point_of_sale/static/src/img/ios-share-icon.png' />
and then <i>Add to Homescreen</i>
</p>
<p class='comment'>
This also works on Android with the Chrome Beta Browser, using the <i>Add to Homescreen</i> option
in the browser's menu.
</p>
<p class='comment'>
If you want to work in fullscreen just this time tap the <i>Go Fullscreen</i> button.
</p>
</t>
<t t-if='!widget.ismobile()'>
<p class="comment">
The best way to make the point of sale fullscreen on desktop
and laptops is to launch your browser in kiosk mode. Please
refer to your browser's documentation for the specific
instructions.
</p>
<p class="comment">
If you want to work in fullscreen just this time, click the <i> Go Fullscreen</i> button.
</p>
</t>
<div class="footer">
<div class="button fullscreen">
Go Fullscreen
</div>
<div class="button cancel">
Cancel
</div>
</div>
</div>
</div>
</t>
<t t-name="ErrorNoClientPopupWidget">
<t t-name="ErrorSessionPopupWidget">
<div class="modal-dialog">
<div class="popup popup-noclient">
<p class="message">An anonymous order cannot be invoiced</p>
<div class="footer">
<div class="button">
Ok
</div>
</div>
<div class="popup popup-nosession">
<p class="message">Sorry, we could not create a session for this user.</p>
</div>
</div>
</t>
@@ -635,6 +796,20 @@
</div>
</t>
<t t-name="ErrorTracebackPopupWidget">
<div class="modal-dialog">
<div class="popup popup-error">
<p class="message"><t t-esc=" widget.message || 'Error' " /></p>
<p class="comment traceback"><t t-esc=" widget.comment || '' "/></p>
<div class="footer">
<div class="button">
Ok
</div>
</div>
</div>
</div>
</t>
<t t-name="ErrorBarcodePopupWidget">
<div class="modal-dialog">
<div class="popup popup-barcode">
@@ -672,6 +847,29 @@
</div>
</t>
<t t-name="UnsentOrdersPopupWidget">
<div class="modal-dialog">
<div class="popup popup-unsent-orders">
<p class="message">Unsent Orders</p>
<t t-if='widget.pos.db.get_orders().length === 0'>
<p class='comment'>
There are no unsent orders
</p>
</t>
<t t-if='widget.pos.db.get_orders().length > 0'>
<p class='comment traceback'>
<t t-esc='JSON.stringify(widget.pos.db.get_orders(),null,2)' />
</p>
</t>
<div class="footer">
<div class="button confirm">
Ok
</div>
</div>
</div>
</div>
</t>
<t t-name="Product">
<span class='product' t-att-data-product-id="product.id">
<div class="product-img">
@@ -779,6 +977,12 @@
<li class="button reference">Reference</li>
</ul>
<p class="category">Unsent Orders</p>
<ul>
<li class="button show_orders">Show All Unsent Orders</li>
<li class="button delete_orders">Delete All Unsent Orders</li>
</ul>
<p class="category">Hardware Status</p>
<ul>
<li class="status weighting">Weighting</li>
@@ -818,12 +1022,6 @@
</tr>
</t>
<t t-name="PaypadButtonWidget">
<button class="paypad-button" t-att-cash-register-id="widget.cashregister.id">
<t t-esc="widget.cashregister.journal.name"/>
</button>
</t>
<t t-name="OrderButtonWidget">
<span class="order-button select-order">
<t t-if='widget.selected'>
@@ -944,30 +1142,6 @@
</div>
</t>
<t t-name="ActionBarWidget">
<div class="pos-actionbar">
<ul class="pos-actionbar-button-list">
</ul>
</div>
</t>
<t t-name="ActionButtonWidget">
<li t-att-class=" 'button '+ (widget.rightalign ? 'rightalign ' : '') + (widget.disabled ? 'disabled ' : '')">
<div class='label'>
<t t-esc="widget.label" />
</div>
</li>
</t>
<t t-name="ActionButtonWidgetWithIcon">
<li t-att-class=" 'button '+ (widget.rightalign ? 'rightalign ' : '') + (widget.disabled ? 'disabled ' : '')">
<div class='icon'>
<img t-att-src="widget.icon" />
<div class='iconlabel'><t t-esc="widget.label" /></div>
</div>
</li>
</t>
<!-- Onscreen Keyboard :
http://net.tutsplus.com/tutorials/javascript-ajax/creating-a-keyboard-with-css-and-jquery/ -->
<t t-name="OnscreenKeyboardFull">
@@ -0,0 +1,9 @@
import openerp.tests
@openerp.tests.common.at_install(False)
@openerp.tests.common.post_install(True)
class TestUi(openerp.tests.HttpCase):
def test_01_pos_basic_order(self):
self.phantom_js("/", "openerp.Tour.run('pos_basic_order', 'test')", "openerp.Tour.tours.pos_basic_order", login="admin")
+25
View File
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import discount
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+47
View File
@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'Point of Sale Discounts',
'version': '1.0',
'category': 'Point of Sale',
'sequence': 6,
'summary': 'Simple Discounts in the Point of Sale ',
'description': """
=======================
This module allows the cashier to quickly give a percentage
sale discount to a customer.
""",
'author': 'OpenERP SA',
'depends': ['point_of_sale'],
'data': [
'views/views.xml',
'views/templates.xml'
],
'installable': True,
'auto_install': False,
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+39
View File
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import logging
import openerp
from openerp import tools
from openerp.osv import fields, osv
from openerp.tools.translate import _
class pos_config(osv.osv):
_inherit = 'pos.config'
_columns = {
'discount_pc': fields.float('Discount Percentage', help='The discount percentage'),
'discount_product_id': fields.many2one('product.product','Discount Product', help='The product used to model the discount'),
}
_defaults = {
'discount_pc': 10,
}
@@ -0,0 +1,34 @@
openerp.pos_discount = function(instance){
var module = instance.point_of_sale;
var round_pr = instance.web.round_precision
var QWeb = instance.web.qweb;
QWeb.add_template('/pos_discount/static/src/xml/discount.xml');
module.PosWidget.include({
build_widgets: function(){
var self = this;
this._super();
if(!this.pos.config.discount_product_id){
return;
}
var discount = $(QWeb.render('DiscountButton'));
discount.click(function(){
var order = self.pos.get('selectedOrder');
var product = self.pos.db.get_product_by_id(self.pos.config.discount_product_id[0]);
var discount = - self.pos.config.discount_pc/ 100.0 * order.getTotalTaxIncluded();
if( discount < 0 ){
order.addProduct(product, { price: discount });
}
});
discount.appendTo(this.$('.control-buttons'));
this.$('.control-buttons').removeClass('oe_hidden');
},
});
};
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="DiscountButton">
<div class='control-button js_discount'>
<i class='fa fa-tag' /> Discount
</div>
</t>
</templates>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<template id="assets_frontend" inherit_id="web.assets_common">
<xpath expr="." position="inside">
<script type="text/javascript" src="/pos_discount/static/src/js/discount.js"></script>
</xpath>
</template>
</data>
</openerp>
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0"?>
<openerp>
<data>
<record model="ir.ui.view" id="view_pos_config_form">
<field name="name">pos.config.form.view</field>
<field name="model">pos.config</field>
<field name="inherit_id" ref="point_of_sale.view_pos_config_form" />
<field name="arch" type="xml">
<xpath expr="//group[@string='Receipt']" position="after">
<group string="Discounts" col="4" >
<field name='discount_pc' />
<field name="discount_product_id" />
</group>
</xpath>
</field>
</record>
</data>
</openerp>
+25
View File
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import restaurant
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+55
View File
@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'Restaurant',
'version': '1.0',
'category': 'Point of Sale',
'sequence': 6,
'summary': 'Restaurant extensions for the Point of Sale ',
'description': """
=======================
This module adds several restaurant features to the Point of Sale:
- Bill Printing: Allows you to print a receipt before the order is paid
- Bill Splitting: Allows you to split an order into different orders
- Kitchen Order Printing: allows you to print orders updates to kitchen or bar printers
""",
'author': 'OpenERP SA',
'depends': ['point_of_sale'],
'data': [
'restaurant_view.xml',
'security/ir.model.access.csv',
'views/templates.xml',
],
'qweb':[
'static/src/xml/multiprint.xml',
'static/src/xml/splitbill.xml',
'static/src/xml/printbill.xml',
],
'installable': True,
'auto_install': False,
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+55
View File
@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import logging
import openerp
from openerp import tools
from openerp.osv import fields, osv
from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
class restaurant_printer(osv.osv):
_name = 'restaurant.printer'
_columns = {
'name' : fields.char('Printer Name', size=32, required=True, help='An internal identification of the printer'),
'proxy_ip': fields.char('Proxy IP Address', size=32, help="The IP Address or hostname of the Printer's hardware proxy"),
'product_categories_ids': fields.many2many('pos.category','printer_category_rel', 'printer_id','category_id',string='Printed Product Categories'),
}
_defaults = {
'name' : 'Printer',
}
class pos_config(osv.osv):
_inherit = 'pos.config'
_columns = {
'iface_splitbill': fields.boolean('Bill Splitting', help='Enables Bill Splitting in the Point of Sale'),
'iface_printbill': fields.boolean('Bill Printing', help='Allows to print the Bill before payment'),
'printer_ids': fields.many2many('restaurant.printer','pos_config_printer_rel', 'config_id','printer_id',string='Order Printers'),
}
_defaults = {
'iface_splitbill': False,
'iface_printbill': False,
}
+74
View File
@@ -0,0 +1,74 @@
<?xml version="1.0"?>
<openerp>
<data>
<record model="ir.ui.view" id="view_restaurant_printer_form">
<field name="name">Order Printer</field>
<field name="model">restaurant.printer</field>
<field name="arch" type="xml">
<form string="POS Printer" version="7.0">
<group col="2">
<field name="name" />
<field name="proxy_ip" />
<field name="product_categories_ids" />
</group>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="action_restaurant_printer_form">
<field name="name">Order Printers</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">restaurant.printer</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to add a Restaurant Order Printer.
</p><p>
Order Printers are used by restaurants and bars to print the
order updates in the kitchen/bar when the waiter updates the order.
</p><p>
Each Order Printer has an IP Address that defines the PosBox/Hardware
Proxy where the printer can be found, and a list of product categories.
An Order Printer will only print updates for prodcuts belonging to one of
its categories.
</p>
</field>
</record>
<record model="ir.ui.view" id="view_restaurant_printer">
<field name="name">Order Printers</field>
<field name="model">restaurant.printer</field>
<field name="arch" type="xml">
<tree string="Restaurant Order Printers">
<field name="name" />
<field name="proxy_ip" />
<field name="product_categories_ids" />
</tree>
</field>
</record>
<menuitem
parent="point_of_sale.menu_point_config_product"
action="action_restaurant_printer_form"
id="menu_restaurant_printer_all"
sequence="30"
groups="point_of_sale.group_pos_manager"/>
<record model="ir.ui.view" id="view_pos_config_form">
<field name="name">pos.config.form.view.inherit</field>
<field name="model">pos.config</field>
<field name="inherit_id" ref="point_of_sale.view_pos_config_form"></field>
<field name="arch" type="xml">
<sheet position='inside'>
<group string="Bar &amp; Restaurant" >
<field name="iface_splitbill" />
<field name="iface_printbill" />
<field name="printer_ids" />
</group>
</sheet>
</field>
</record>
</data>
</openerp>
@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_restaurant_printer,restaurant.printer.user,model_restaurant_printer,point_of_sale.group_pos_user,1,0,0,0
access_restaurant_printer_manager,restaurant.printer.manager,model_restaurant_printer,point_of_sale.group_pos_manager,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_restaurant_printer restaurant.printer.user model_restaurant_printer point_of_sale.group_pos_user 1 0 0 0
3 access_restaurant_printer_manager restaurant.printer.manager model_restaurant_printer point_of_sale.group_pos_manager 1 0 0 0
@@ -0,0 +1,2 @@
/* --- Restaurant Specific CSS --- */
+11
View File
@@ -0,0 +1,11 @@
openerp.restaurant = function(instance){
var module = instance.point_of_sale;
openerp_restaurant_multiprint(instance,module);
openerp_restaurant_splitbill(instance,module);
openerp_restaurant_printbill(instance,module);
};
@@ -0,0 +1,216 @@
function openerp_restaurant_multiprint(instance,module){
var QWeb = instance.web.qweb;
var _t = instance.web._t;
module.Printer = instance.web.Class.extend(openerp.PropertiesMixin,{
init: function(parent,options){
openerp.PropertiesMixin.init.call(this,parent);
var self = this;
options = options || {};
var url = options.url || 'http://localhost:8069';
this.connection = new instance.web.Session(undefined,url, { use_cors: true});
this.host = url;
this.receipt_queue = [];
},
print: function(receipt){
var self = this;
if(receipt){
this.receipt_queue.push(receipt);
}
var aborted = false;
function send_printing_job(){
if(self.receipt_queue.length > 0){
var r = self.receipt_queue.shift();
self.connection.rpc('/hw_proxy/print_xml_receipt',{receipt: r},{timeout: 5000})
.then(function(){
send_printing_job();
},function(){
self.receipt_queue.unshift(r);
});
}
}
send_printing_job();
},
});
module.PosModel.prototype.models.push({
model: 'restaurant.printer',
fields: ['name','proxy_ip','product_categories_ids'],
domain: null,
loaded: function(self,printers){
var active_printers = {};
for (var i = 0; i < self.config.printer_ids.length; i++) {
active_printers[self.config.printer_ids[i]] = true;
}
self.printers = [];
for(var i = 0; i < printers.length; i++){
if(active_printers[printers[i].id]){
var printer = new module.Printer(self,{url:'http://'+printers[i].proxy_ip+':8069'});
printer.config = printers[i];
self.printers.push(printer);
}
}
},
});
module.Order = module.Order.extend({
lineResume: function(){
var resume = {};
this.get('orderLines').each(function(item){
var line = item.export_as_JSON();
if( typeof resume[line.product_id] === 'undefined'){
resume[line.product_id] = line.qty;
}else{
resume[line.product_id] += line.qty;
}
});
return resume;
},
saveChanges: function(){
this.old_resume = this.lineResume();
},
computeChanges: function(categories){
var current = this.lineResume();
var old = this.old_resume || {};
var json = this.export_as_JSON();
var add = [];
var rem = [];
for( product in current){
if (typeof old[product] === 'undefined'){
add.push({
'id': product,
'name': this.pos.db.get_product_by_id(product).name,
'quantity': current[product],
});
}else if( old[product] < current[product]){
add.push({
'id': product,
'name': this.pos.db.get_product_by_id(product).name,
'quantity': current[product] - old[product],
});
}else if( old[product] > current[product]){
rem.push({
'id': product,
'name': this.pos.db.get_product_by_id(product).name,
'quantity': old[product] - current[product],
});
}
}
for( product in old){
if(typeof current[product] === 'undefined'){
rem.push({
'id': product,
'name': this.pos.db.get_product_by_id(product).name,
'quantity': old[product],
});
}
}
if(categories && categories.length > 0){
// filter the added and removed orders to only contains
// products that belong to one of the categories supplied as a parameter
var self = this;
function product_in_category(product_id){
var cat = self.pos.db.get_product_by_id(product_id).pos_categ_id[0];
while(cat){
for(var i = 0; i < categories.length; i++){
if(cat === categories[i]){
return true;
}
}
cat = self.pos.db.get_category_parent_id(cat);
}
return false;
}
var _add = [];
var _rem = [];
for(var i = 0; i < add.length; i++){
if(product_in_category(add[i].id)){
_add.push(add[i]);
}
}
add = _add;
for(var i = 0; i < rem.length; i++){
if(product_in_category(rem[i].id)){
_rem.push(rem[i]);
}
}
rem = _rem;
}
return {
'new': add,
'cancelled': rem,
'table': json.table || 'unknown table',
'name': json.name || 'unknown order',
'sequence_number': json.sequence_number || 0,
};
},
printChanges: function(){
var printers = this.pos.printers;
for(var i = 0; i < printers.length; i++){
var changes = this.computeChanges(printers[i].config.product_categories_ids);
if ( changes['new'].length > 0 || changes['cancelled'].length > 0){
var receipt = QWeb.render('OrderChangeReceipt',{changes:changes, widget:this});
printers[i].print(receipt);
}
}
},
hasChangesToPrint: function(){
var printers = this.pos.printers;
for(var i = 0; i < printers.length; i++){
var changes = this.computeChanges(printers[i].config.product_categories_ids);
if ( changes['new'].length > 0 || changes['cancelled'].length > 0){
return true;
}
}
return false;
},
});
module.PosWidget.include({
build_widgets: function(){
var self = this;
this._super();
if(this.pos.printers.length){
var submitorder = $(QWeb.render('SubmitOrderButton'));
submitorder.click(function(){
var order = self.pos.get('selectedOrder');
if(order.hasChangesToPrint()){
order.printChanges();
order.saveChanges();
self.pos_widget.order_widget.update_summary();
}
});
submitorder.appendTo(this.$('.control-buttons'));
this.$('.control-buttons').removeClass('oe_hidden');
}
},
});
module.OrderWidget.include({
update_summary: function(){
this._super();
var order = this.pos.get('selectedOrder');
if(order.hasChangesToPrint()){
this.pos_widget.$('.order-submit').addClass('highlight');
}else{
this.pos_widget.$('.order-submit').removeClass('highlight');
}
},
});
}
@@ -0,0 +1,28 @@
function openerp_restaurant_printbill(instance,module){
var QWeb = instance.web.qweb;
var _t = instance.web._t;
module.PosWidget.include({
build_widgets: function(){
var self = this;
this._super();
if(this.pos.config.iface_printbill){
var printbill = $(QWeb.render('PrintBillButton'));
printbill.click(function(){
var order = self.pos.get('selectedOrder');
if(order.get('orderLines').models.length > 0){
var receipt = order.export_for_printing();
self.pos.proxy.print_receipt(QWeb.render('BillReceipt',{
receipt: receipt, widget: self,
}));
}
});
printbill.appendTo(this.$('.control-buttons'));
this.$('.control-buttons').removeClass('oe_hidden');
}
},
});
}
@@ -0,0 +1,193 @@
function openerp_restaurant_splitbill(instance, module){
var QWeb = instance.web.qweb;
var _t = instance.web._t;
module.SplitbillScreenWidget = module.ScreenWidget.extend({
template: 'SplitbillScreenWidget',
show_leftpane: false,
previous_screen: 'products',
renderElement: function(){
var self = this;
this._super();
var order = this.pos.get('selectedOrder');
if(!order){
return;
}
var orderlines = order.get('orderLines').models;
for(var i = 0; i < orderlines.length; i++){
var line = orderlines[i];
linewidget = $(QWeb.render('SplitOrderline',{
widget:this,
line:line,
selected: false,
quantity: 0,
id: line.id,
}));
linewidget.data('id',line.id);
this.$('.orderlines').append(linewidget);
}
this.$('.back').click(function(){
self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
});
},
lineselect: function($el,order,neworder,splitlines,line_id){
var split = splitlines[line_id] || {'quantity': 0, line: null};
var line = order.getOrderline(line_id);
if( !line.get_unit().groupable ){
if( split.quantity !== line.get_quantity()){
split.quantity = line.get_quantity();
}else{
split.quantity = 0;
}
}else{
if( split.quantity < line.get_quantity()){
split.quantity += line.get_unit().rounding;
if(split.quantity > line.get_quantity()){
split.quantity = line.get_quantity();
}
}else{
split.quantity = 0;
}
}
if( split.quantity ){
if ( !split.line ){
split.line = line.clone();
neworder.addOrderline(split.line);
}
split.line.set_quantity(split.quantity);
}else if( split.line ) {
neworder.removeOrderline(split.line);
split.line = null;
}
splitlines[line_id] = split;
$el.replaceWith($(QWeb.render('SplitOrderline',{
widget: this,
line: line,
selected: split.quantity !== 0,
quantity: split.quantity,
id: line_id,
})));
this.$('.order-info .subtotal').text(this.format_currency(neworder.getSubtotal()));
},
pay: function($el,order,neworder,splitlines,cashregister_id){
var orderlines = order.get('orderLines').models;
var empty = true;
var full = true;
for(var i = 0; i < orderlines.length; i++){
var id = orderlines[i].id;
var split = splitlines[id];
if(!split){
full = false;
}else{
if(split.quantity){
empty = false;
if(split.quantity !== orderlines[i].get_quantity()){
full = false;
}
}
}
}
if(empty){
return;
}
for(var i = 0; i < this.pos.cashregisters.length; i++){
if(this.pos.cashregisters[i].id === cashregister_id){
var cashregister = this.pos.cashregisters[i];
break;
}
}
if(full){
order.addPaymentline(cashregister);
this.pos_widget.screen_selector.set_current_screen('payment');
}else{
for(var id in splitlines){
var split = splitlines[id];
var line = order.getOrderline(parseInt(id));
line.set_quantity(line.get_quantity() - split.quantity);
if(Math.abs(line.get_quantity()) < 0.00001){
order.removeOrderline(line);
}
delete splitlines[id];
}
neworder.addPaymentline(cashregister);
neworder.set_screen_data('screen','payment');
// for the kitchen printer we assume that everything
// has already been sent to the kitchen before splitting
// the bill. So we save all changes both for the old
// order and for the new one. This is not entirely correct
// but avoids flooding the kitchen with unnecessary orders.
// Not sure what to do in this case.
if ( neworder.saveChanges ) {
order.saveChanges();
neworder.saveChanges();
}
this.pos.get('orders').add(neworder);
this.pos.set('selectedOrder',neworder);
}
},
show: function(){
var self = this;
this._super();
this.renderElement();
var order = this.pos.get('selectedOrder');
var neworder = new module.Order({
pos: this.pos,
temporary: true,
});
neworder.set('client',order.get('client'));
var splitlines = {};
this.$('.orderlines').on('click','.orderline',function(){
var id = parseInt($(this).data('id'));
var $el = $(this);
self.lineselect($el,order,neworder,splitlines,id);
});
this.$('.paymentmethod').click(function(){
var id = parseInt($(this).data('id'));
var $el = $(this);
self.pay($el,order,neworder,splitlines,id);
});
},
});
module.PosWidget.include({
build_widgets: function(){
var self = this;
this._super();
if(this.pos.config.iface_splitbill){
this.splitbill_screen = new module.SplitbillScreenWidget(this,{});
this.splitbill_screen.appendTo(this.$('.screens'));
this.screen_selector.add_screen('splitbill',this.splitbill_screen);
var splitbill = $(QWeb.render('SplitbillButton'));
splitbill.click(function(){
if(self.pos.get('selectedOrder').get('orderLines').models.length > 0){
self.pos_widget.screen_selector.set_current_screen('splitbill');
}
});
splitbill.appendTo(this.$('.control-buttons'));
this.$('.control-buttons').removeClass('oe_hidden');
}
},
});
}
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="SubmitOrderButton">
<span class="control-button order-submit">
<i class="fa fa-cutlery"></i>
Order
</span>
</t>
<t t-name="OrderChangeReceipt">
<receipt
align='center'
width='40'
size='double-height'
line-ratio='0.6'
value-decimals='3'
value-thousands-separator=''
value-autoint='on'
>
<div>#<t t-esc="changes.sequence_number" /></div>
<div size='normal'><t t-esc="changes.name" /></div>
<br />
<br />
<t t-if="changes.cancelled.length > 0">
<div color='red'>
<div bold='on' size='double'>CANCELLED</div>
<br />
<br />
<t t-foreach="changes.cancelled" t-as="change">
<line>
<left><t t-esc="change.name" /></left>
<right><value><t t-esc="change.quantity" /></value></right>
</line>
</t>
<br />
<br />
</div>
</t>
<t t-if="changes.new.length > 0">
<div bold='on' size='double'>NEW</div>
<br />
<br />
<t t-foreach="changes.new" t-as="change">
<line>
<left><t t-esc="change.name" /></left>
<right><value><t t-esc="change.quantity" /></value></right>
</line>
</t>
<br />
<br />
</t>
</receipt>
</t>
</templates>
@@ -0,0 +1,143 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="PrintBillButton">
<span class="control-button order-printbill">
<i class="fa fa-print"></i>
Bill
</span>
</t>
<t t-name="BillReceipt">
<receipt align='center' width='40' value-thousands-separator='' >
<t t-if='receipt.company.logo'>
<img t-att-src='receipt.company.logo' />
<br/>
</t>
<t t-if='!receipt.company.logo'>
<h1><t t-esc='receipt.company.name' /></h1>
<br/>
</t>
<div font='b'>
<t t-if='receipt.shop.name'>
<div><t t-esc='receipt.shop.name' /></div>
</t>
<t t-if='receipt.company.contact_address'>
<div><t t-esc='receipt.company.contact_address' /></div>
</t>
<t t-if='receipt.company.phone'>
<div>Tel:<t t-esc='receipt.company.phone' /></div>
</t>
<t t-if='receipt.company.vat'>
<div>VAT:<t t-esc='receipt.company.vat' /></div>
</t>
<t t-if='receipt.company.email'>
<div><t t-esc='receipt.company.email' /></div>
</t>
<t t-if='receipt.company.website'>
<div><t t-esc='receipt.company.website' /></div>
</t>
<t t-if='receipt.header'>
<div><t t-esc='receipt.header' /></div>
</t>
<t t-if='receipt.cashier'>
<div>--------------------------------</div>
<div>Served by <t t-esc='receipt.cashier' /></div>
</t>
</div>
<br /><br />
<!-- Orderlines -->
<div line-ratio='0.6'>
<t t-foreach='receipt.orderlines' t-as='line'>
<t t-set='simple' t-value='line.discount === 0 and line.unit_name === "Unit(s)" and line.quantity === 1' />
<t t-if='simple'>
<line>
<left><t t-esc='line.product_name' /></left>
<right><value><t t-esc='line.price_display' /></value></right>
</line>
</t>
<t t-if='!simple'>
<line><left><t t-esc='line.product_name' /></left></line>
<t t-if='line.discount !== 0'>
<line indent='1'><left>Discount: <t t-esc='line.discount' />%</left></line>
</t>
<line indent='1'>
<left>
<value value-decimals='3' value-autoint='on'>
<t t-esc='line.quantity' />
</value>
<t t-if='line.unit_name !== "Unit(s)"'>
<t t-esc='line.unit_name' />
</t>
x
<value value-decimals='2'>
<t t-esc='line.price' />
</value>
</left>
<right>
<value><t t-esc='line.price_display' /></value>
</right>
</line>
</t>
</t>
</div>
<!-- Subtotal -->
<t t-set='taxincluded' t-value='Math.abs(receipt.subtotal - receipt.total_with_tax) &lt;= 0.000001' />
<t t-if='!taxincluded'>
<line><right>--------</right></line>
<line><left>Subtotal</left><right> <value><t t-esc="receipt.subtotal" /></value></right></line>
<t t-foreach='receipt.tax_details' t-as='tax'>
<line>
<left><t t-esc='tax.name' /></left>
<right><value><t t-esc='tax.amount' /></value></right>
</line>
</t>
</t>
<!-- Total -->
<line><right>--------</right></line>
<line size='double-height'>
<left><pre> TOTAL</pre></left>
<right><value><t t-esc='receipt.total_with_tax' /></value></right>
</line>
<br/><br/>
<!-- Extra Payment Info -->
<t t-if='receipt.total_discount'>
<line>
<left>Discounts</left>
<right><value><t t-esc='receipt.total_discount'/></value></right>
</line>
</t>
<t t-if='taxincluded'>
<t t-foreach='receipt.tax_details' t-as='tax'>
<line>
<left><t t-esc='tax.name' /></left>
<right><value><t t-esc='tax.amount' /></value></right>
</line>
</t>
</t>
<!-- Footer -->
<t t-if='receipt.footer'>
<br/>
<pre><t t-esc='receipt.footer' /></pre>
<br/>
<br/>
</t>
<br/>
<div font='b'>
<div><t t-esc='receipt.name' /></div>
<div><t t-esc='receipt.date.localestring' /></div>
</div>
</receipt>
</t>
</templates>
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="SplitbillButton">
<span class="control-button order-split">
<i class="fa fa-copy"></i>
Split
</span>
</t>
<t t-name="SplitOrderline">
<li t-attf-class="orderline #{ selected ? 'selected' : ''} #{ quantity !== line.get_quantity() ? 'partially' : '' }"
t-att-data-id="id">
<span class="product-name">
<t t-esc="line.get_product().name"/>
</span>
<span class="price">
<t t-esc="widget.format_currency(line.get_display_price())"/>
</span>
<ul class="info-list">
<t t-if="line.get_quantity_str() !== '1'">
<li class="info">
<t t-if='selected and line.get_unit().groupable'>
<em class='big'>
<t t-esc='quantity' />
</em>
/
<t t-esc="line.get_quantity_str()" />
</t>
<t t-if='!(selected and line.get_unit().groupable)'>
<em>
<t t-esc="line.get_quantity_str()" />
</em>
</t>
<t t-esc="line.get_unit().name" />
at
<t t-esc="widget.format_currency(line.get_unit_price())" />
/
<t t-esc="line.get_unit().name" />
</li>
</t>
<t t-if="line.get_discount_str() !== '0'">
<li class="info">
With a
<em>
<t t-esc="line.get_discount_str()" />%
</em>
discount
</li>
</t>
</ul>
</li>
</t>
<t t-name="SplitbillScreenWidget">
<div class='splitbill-screen screen'>
<div class='screen-content'>
<div class='top-content'>
<span class='button back'>
<i class='fa fa-angle-double-left'></i>
Back
</span>
<h1>Bill Splitting</h1>
</div>
<div class='left-content touch-scrollable scrollable-y'>
<div class='order'>
<ul class='orderlines'>
</ul>
</div>
</div>
<div class='right-content touch-scrollable scrollable-y'>
<div class='order-info'>
<span class='subtotal'><t t-esc='widget.format_currency(0.0)'/></span>
</div>
<div class='paymentmethods'>
<t t-foreach="widget.pos.cashregisters" t-as="cashregister">
<div class='button paymentmethod' t-att-data-id="cashregister.id">
<t t-esc='cashregister.journal.name' />
</div>
</t>
</div>
</div>
</div>
</div>
</t>
</templates>
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- vim:fdn=3:
-->
<openerp>
<data>
<template id="index" inherit_id='point_of_sale.index' name="Restaurant Index">&lt;!DOCTYPE html&gt;
<xpath expr="//link[@id='pos-stylesheet']" position="after">
<link rel="stylesheet" href="/restaurant/static/src/css/restaurant.css" />
</xpath>
</template>
<template id="assets_frontend" inherit_id="web.assets_common">
<xpath expr="." position="inside">
<script type="text/javascript" src="/restaurant/static/src/js/multiprint.js"></script>
<script type="text/javascript" src="/restaurant/static/src/js/splitbill.js"></script>
<script type="text/javascript" src="/restaurant/static/src/js/printbill.js"></script>
<script type="text/javascript" src="/restaurant/static/src/js/main.js"></script>
</xpath>
</template>
</data>
</openerp>