Odoo Javascript
Odoo Javascript
// Backbone.js 1.1.0
(function(){
// Initial Setup
// -------------
// The top-level namespace. All public Backbone classes and modules will
// be attached to this. Exported for both the browser and the server.
var Backbone;
if (typeof exports !== 'undefined') {
Backbone = exports;
} else {
Backbone = root.Backbone = {};
}
// Require Underscore, if we're on the server, and it's not already present.
var _ = root._;
if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
// Backbone.Events
// ---------------
// Bind an event to only be triggered a single time. After the first time
// the callback is invoked, it will be removed.
once: function(name, callback, context) {
if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return
this;
var self = this;
var once = _.once(function() {
self.off(name, once);
callback.apply(this, arguments);
});
once._callback = callback;
return this.on(name, once, context);
},
return this;
},
// Trigger one or many events, firing all bound callbacks. Callbacks are
// passed the same arguments as `trigger` is, apart from the event name
// (unless you're listening on `"all"`, which will cause your callback to
// receive the true name of the event as the first argument).
trigger: function(name) {
if (!this._events) return this;
var args = slice.call(arguments, 1);
if (!eventsApi(this, 'trigger', name, args)) return this;
var events = this._events[name];
var allEvents = this._events.all;
if (events) triggerEvents(events, args);
if (allEvents) triggerEvents(allEvents, arguments);
return this;
},
};
return true;
};
// Allow the `Backbone` object to serve as a global event bus, for folks who
// want global "pubsub" in a convenient place.
_.extend(Backbone, Events);
// Backbone.Model
// --------------
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
// CouchDB users may want to set this to `"_id"`.
idAttribute: 'id',
// Run validation.
if (!this._validate(attrs, options)) return false;
if (!changing) {
this._previousAttributes = _.clone(this.attributes);
this.changed = {};
}
current = this.attributes, prev = this._previousAttributes;
// You might be wondering why there's a `while` loop here. Changes can
// be recursively nested within `"change"` events.
if (changing) return this;
if (!silent) {
while (this._pending) {
this._pending = false;
this.trigger('change', this, options);
}
}
this._pending = false;
this._changing = false;
return this;
},
// Determine if the model has changed since the last `"change"` event.
// If you specify an attribute name, determine if that attribute has changed.
hasChanged: function(attr) {
if (attr == null) return !_.isEmpty(this.changed);
return _.has(this.changed, attr);
},
// Get the previous value of an attribute, recorded at the time the last
// `"change"` event was fired.
previous: function(attr) {
if (attr == null || !this._previousAttributes) return null;
return this._previousAttributes[attr];
},
// Get all of the attributes of the model at the time of the previous
// `"change"` event.
previousAttributes: function() {
return _.clone(this._previousAttributes);
},
// Fetch the model from the server. If the server's representation of the
// model differs from its current attributes, they will be overridden,
// triggering a `"change"` event.
fetch: function(options) {
options = options ? _.clone(options) : {};
if (options.parse === void 0) options.parse = true;
var model = this;
var success = options.success;
options.success = function(resp) {
if (!model.set(model.parse(resp, options), options)) return false;
if (success) success(model, resp, options);
model.trigger('sync', model, resp, options);
};
wrapError(this, options);
return this.sync('read', this, options);
},
// Set a hash of model attributes, and sync the model to the server.
// If the server returns an attributes hash that differs, the model's
// state will be `set` again.
save: function(key, val, options) {
var attrs, method, xhr, attributes = this.attributes;
// Restore attributes.
if (attrs && options.wait) this.attributes = attributes;
return xhr;
},
options.success = function(resp) {
if (options.wait || model.isNew()) destroy();
if (success) success(model, resp, options);
if (!model.isNew()) model.trigger('sync', model, resp, options);
};
if (this.isNew()) {
options.success();
return false;
}
wrapError(this, options);
// A model is new if it has never been saved to the server, and lacks an id.
isNew: function() {
return this.id == null;
},
});
// Backbone.Collection
// -------------------
// Turn bare objects into model references, and prevent invalid models
// from being added.
for (i = 0, l = models.length; i < l; i++) {
attrs = models[i];
if (attrs instanceof Model) {
id = model = attrs;
} else {
id = attrs[targetModel.prototype.idAttribute];
}
// When you have more items than you want to add or remove individually,
// you can reset the entire set with a new list of models, without firing
// any granular `add` or `remove` events. Fires `reset` when finished.
// Useful for bulk operations and optimizations.
reset: function(models, options) {
options || (options = {});
for (var i = 0, l = this.models.length; i < l; i++) {
this._removeReference(this.models[i]);
}
options.previousModels = this.models;
this._reset();
models = this.add(models, _.extend({silent: true}, options));
if (!options.silent) this.trigger('reset', this, options);
return models;
},
// Return the first model with matching attributes. Useful for simple cases
// of `find`.
findWhere: function(attrs) {
return this.where(attrs, true);
},
// Force the collection to re-sort itself. You don't need to call this under
// normal circumstances, as the set will maintain sort order as each item
// is added.
sort: function(options) {
if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
options || (options = {});
// Fetch the default set of models for this collection, resetting the
// collection when they arrive. If `reset: true` is passed, the response
// data will be passed through the `reset` method instead of `set`.
fetch: function(options) {
options = options ? _.clone(options) : {};
if (options.parse === void 0) options.parse = true;
var success = options.success;
var collection = this;
options.success = function(resp) {
var method = options.reset ? 'reset' : 'set';
collection[method](resp, options);
if (success) success(collection, resp, options);
collection.trigger('sync', collection, resp, options);
};
wrapError(this, options);
return this.sync('read', this, options);
},
// Create a new instance of a model in this collection. Add the model to the
// collection immediately, unless `wait: true` is passed, in which case we
// wait for the server to agree.
create: function(model, options) {
options = options ? _.clone(options) : {};
if (!(model = this._prepareModel(model, options))) return false;
if (!options.wait) this.add(model, options);
var collection = this;
var success = options.success;
options.success = function(model, resp, options) {
if (options.wait) collection.add(model, options);
if (success) success(model, resp, options);
};
model.save(null, options);
return model;
},
// Private method to reset all internal state. Called when the collection
// is first initialized or reset.
_reset: function() {
this.length = 0;
this.models = [];
this._byId = {};
},
// Internal method called every time a model in the set fires an event.
// Sets need to update their indexes when models change ids. All other
// events simply proxy through. "add" and "remove" events that originate
// in other collections are ignored.
_onModelEvent: function(event, model, collection, options) {
if ((event === 'add' || event === 'remove') && collection !== this) return;
if (event === 'destroy') this.remove(model, options);
if (model && event === 'change:' + model.idAttribute) {
delete this._byId[model.previous(model.idAttribute)];
if (model.id != null) this._byId[model.id] = model;
}
this.trigger.apply(this, arguments);
}
});
// Backbone.View
// -------------
// Backbone Views are almost more convention than they are actual code. A View
// is simply a JavaScript object that represents a logical chunk of UI in the
// DOM. This might be a single item, an entire list, a sidebar or panel, or
// even the surrounding frame which wraps your whole app. Defining a chunk of
// UI as a **View** allows you to define your DOM events declaratively, without
// having to worry about render order ... and makes it easy for the view to
// react to specific changes in the state of your models.
// jQuery delegate for element lookup, scoped to DOM elements within the
// current view. This should be preferred to global lookups where possible.
$: function(selector) {
return this.$el.find(selector);
},
// **render** is the core function that your view should override, in order
// to populate its element (`this.el`), with the appropriate HTML. The
// convention is for **render** to always return `this`.
render: function() {
return this;
},
// Remove this view by taking the element out of the DOM, and removing any
// applicable Backbone.Events listeners.
remove: function() {
this.$el.remove();
this.stopListening();
return this;
},
});
// Backbone.sync
// -------------
// For older servers, emulate JSON by encoding the request into an HTML-form.
if (options.emulateJSON) {
params.contentType = 'application/x-www-form-urlencoded';
params.data = params.data ? {model: params.data} : {};
}
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
// And an `X-HTTP-Method-Override` header.
if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type ===
'PATCH')) {
params.type = 'POST';
if (options.emulateJSON) params.data._method = type;
var beforeSend = options.beforeSend;
options.beforeSend = function(xhr) {
xhr.setRequestHeader('X-HTTP-Method-Override', type);
if (beforeSend) return beforeSend.apply(this, arguments);
};
}
// Make the request, allowing the user to override any Ajax options.
var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
model.trigger('request', model, xhr, options);
return xhr;
};
var noXhrPatch = typeof window !== 'undefined' && !!window.ActiveXObject &&
!(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent);
// Backbone.Router
// ---------------
// Routers map faux-URLs to actions, and fire events when routes are
// matched. Creating a new one sets its `routes` hash, if not set statically.
var Router = Backbone.Router = function(options) {
options || (options = {});
if (options.routes) this.routes = options.routes;
this._bindRoutes();
this.initialize.apply(this, arguments);
};
// Cached regular expressions for matching named param parts and splatted
// parts of route strings.
var optionalParam = /\((.*?)\)/g;
var namedParam = /(\(\?)?:\w+/g;
var splatParam = /\*\w+/g;
var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
// Given a route, and a URL fragment that it matches, return the array of
// extracted decoded parameters. Empty or unmatched parameters will be
// treated as `null` to normalize cross-browser behavior.
_extractParameters: function(route, fragment) {
var params = route.exec(fragment).slice(1);
return _.map(params, function(param) {
return param ? decodeURIComponent(param) : null;
});
}
});
// Backbone.History
// ----------------
// Gets the true hash value. Cannot use location.hash directly due to bug
// in Firefox where location.hash will always be decoded.
getHash: function(window) {
var match = (window || this).location.href.match(/#(.*)$/);
return match ? match[1] : '';
},
// Get the cross-browser normalized URL fragment, either from the URL,
// the hash, or the override.
getFragment: function(fragment, forcePushState) {
if (fragment == null) {
if (this._hasPushState || !this._wantsHashChange || forcePushState) {
fragment = this.location.pathname;
var root = this.root.replace(trailingSlash, '');
if (!fragment.indexOf(root)) fragment = fragment.slice(root.length);
} else {
fragment = this.getHash();
}
}
return fragment.replace(routeStripper, '');
},
// Start the hash change handling, returning `true` if the current URL matches
// an existing route, and `false` otherwise.
start: function(options) {
if (History.started) throw new Error("Backbone.history has already been
started");
History.started = true;
// Add a route to be tested when the fragment changes. Routes added later
// may override previous routes.
route: function(route, callback) {
this.handlers.unshift({route: route, callback: callback});
},
// Save a fragment into the hash history, or replace the URL state if the
// 'replace' option is passed. You are responsible for properly URL-encoding
// the fragment in advance.
//
// The options object can contain `trigger: true` if you wish to have the
// route callback be fired (not usually desirable), or `replace: true`, if
// you wish to modify the current URL without adding an entry to the history.
navigate: function(fragment, options) {
if (!History.started) return false;
if (!options || options === true) options = {trigger: !!options};
// Update the hash location, either replacing the current entry, or adding
// a new one to the browser history.
_updateHash: function(location, fragment, replace) {
if (replace) {
var href = location.href.replace(/(javascript:|#).*$/, '');
location.replace(href + '#' + fragment);
} else {
// Some browsers require that `hash` contains a leading #.
location.hash = '#' + fragment;
}
}
});
// Helpers
// -------
// The constructor function for the new subclass is either defined by you
// (the "constructor" property in your `extend` definition), or defaulted
// by us to simply call the parent's constructor.
if (protoProps && _.has(protoProps, 'constructor')) {
child = protoProps.constructor;
} else {
child = function(){ return parent.apply(this, arguments); };
}
return child;
};
// Set up inheritance for the model, collection, router, view and history.
Model.extend = Collection.extend = Router.extend = View.extend = History.extend =
extend;
}).call(this);
POS Models JS
odoo.define('point_of_sale.models', function (require) {
"use strict";
exports.PosModel = Backbone.Model.extend({
initialize: function(session, attributes) {
Backbone.Model.prototype.initialize.call(this, attributes);
var self = this;
this.flush_mutex = new Mutex(); // used to make sure the
orders are sent to the server once at time
this.chrome = attributes.chrome;
this.gui = attributes.gui;
this.get('orders').bind('remove', function(order,_unused_,options){
self.on_removed_order(order,options.index,options.reason);
});
// We fetch the backend data on the server asynchronously. this is done only
when the pos user interface is launched,
// Any change on this data made on the server is thus not reflected on the
point of sale until it is relaunched.
// when all the data has loaded, we compute some stuff, and declare the Pos
ready to be used.
this.ready = this.load_server_data().then(function(){
return self.after_load_server_data();
});
},
after_load_server_data: function(){
this.load_orders();
this.set_start_order();
if(this.config.use_proxy){
return this.connect_to_proxy();
}
},
// releases ressources holds by the model at the end of life of the posmodel
destroy: function(){
// FIXME, should wait for flushing, return a deferred to indicate successfull
destruction
// this.flush();
this.proxy.close();
this.barcode_reader.disconnect();
this.barcode_reader.disconnect_from_proxy();
},
connect_to_proxy: function(){
var self = this;
var done = new $.Deferred();
this.barcode_reader.disconnect_from_proxy();
this.chrome.loading_message(_t('Connecting to the PosBox'),0);
this.chrome.loading_skip(function(){
self.proxy.stop_searching();
});
this.proxy.autoconnect({
force_ip: self.config.proxy_ip || undefined,
progress: function(prog){
self.chrome.loading_progress(prog);
},
}).then(function(){
if(self.config.iface_scan_via_proxy){
self.barcode_reader.connect_to_proxy();
}
}).always(function(){
done.resolve();
});
return done;
},
// 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: [
{
label: 'version',
loaded: function(self){
return
session.rpc('/web/webclient/version_info',{}).done(function(version) {
self.version = version;
});
},
},{
model: 'res.users',
fields: ['name','company_id'],
ids: function(self){ return [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' , 'country_id', 'tax_calculation_rounding_method'],
ids: function(self){ return [self.user.company_id[0]]; },
loaded: function(self,companies){ self.company = companies[0]; },
},{
model: 'decimal.precision',
fields: ['name','digits'],
loaded: function(self,dps){
self.dp = {};
for (var i = 0; i < dps.length; i++) {
self.dp[dps[i].name] = dps[i].digits;
}
},
},{
model: 'product.uom',
fields: [],
domain: null,
context: function(self){ return { active_test: false }; },
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.partner',
fields:
['name','street','city','state_id','country_id','vat','phone','zip','mobile','email','
barcode','write_date','property_account_position_id'],
domain: [['customer','=',true]],
loaded: function(self,partners){
self.partners = partners;
self.db.add_partners(partners);
},
},{
model: 'res.country',
fields: ['name'],
loaded: function(self,countries){
self.countries = countries;
self.company.country = null;
for (var i = 0; i < countries.length; i++) {
if (countries[i].id === self.company.country_id[0]){
self.company.country = countries[i];
}
}
},
},{
model: 'account.tax',
fields: ['name','amount', 'price_include', 'include_base_amount',
'amount_type', 'children_tax_ids'],
domain: null,
loaded: function(self, taxes){
self.taxes = taxes;
self.taxes_by_id = {};
_.each(taxes, function(tax){
self.taxes_by_id[tax.id] = tax;
});
_.each(self.taxes_by_id, function(tax) {
tax.children_tax_ids = _.map(tax.children_tax_ids, function
(child_tax_id) {
return self.taxes_by_id[child_tax_id];
});
});
},
},{
model: 'pos.session',
fields: ['id',
'journal_ids','name','user_id','config_id','start_at','stop_at','sequence_number','log
in_number'],
domain: function(self){ return
[['state','=','opened'],['user_id','=',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.db.set_uuid(self.config.uuid);
},
},{
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: ['display_name', 'list_price','price','pos_categ_id', 'taxes_id',
'barcode', 'default_code',
'to_weight', 'uom_id', 'description_sale', 'description',
'product_tmpl_id','tracking'],
order: ['sequence','default_code','name'],
domain: [['sale_ok','=',true],['available_in_pos','=',true]],
context: function(self){ return { pricelist: self.pricelist.id,
display_default_code: false }; },
loaded: function(self, products){
self.db.add_products(products);
},
},{
model: 'account.bank.statement',
fields:
['account_id','currency_id','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, cashregisters, tmp){
self.cashregisters = cashregisters;
tmp.journals = [];
_.each(cashregisters,function(statement){
tmp.journals.push(statement.journal_id[0]);
});
},
},{
model: 'account.journal',
fields: ['type', 'sequence'],
domain: function(self,tmp){ return [['id','in',tmp.journals]]; },
loaded: function(self, journals){
var i;
self.journals = journals;
self.cashregisters_by_id = {};
for (i = 0; i < self.cashregisters.length; i++) {
self.cashregisters_by_id[self.cashregisters[i].id] =
self.cashregisters[i];
}
self.cashregisters = self.cashregisters.sort(function(a,b){
// prefer cashregisters to be first in the list
if (a.journal.type == "cash" && b.journal.type != "cash") {
return -1;
} else if (a.journal.type != "cash" && b.journal.type == "cash") {
return 1;
} else {
return a.journal.sequence - b.journal.sequence;
}
});
},
}, {
model: 'account.fiscal.position',
fields: [],
domain: function(self){ return [['id','in',self.config.fiscal_position_ids]];
},
loaded: function(self, fiscal_positions){
self.fiscal_positions = fiscal_positions;
}
}, {
model: 'account.fiscal.position.tax',
fields: [],
domain: function(self){
var fiscal_position_tax_ids = [];
self.fiscal_positions.forEach(function (fiscal_position) {
fiscal_position.tax_ids.forEach(function (tax_id) {
fiscal_position_tax_ids.push(tax_id);
});
});
return [['id','in',fiscal_position_tax_ids]];
},
loaded: function(self, fiscal_position_taxes){
self.fiscal_position_taxes = fiscal_position_taxes;
self.fiscal_positions.forEach(function (fiscal_position) {
fiscal_position.fiscal_position_taxes_by_id = {};
fiscal_position.tax_ids.forEach(function (tax_id) {
var fiscal_position_tax = _.find(fiscal_position_taxes, function
(fiscal_position_tax) {
return fiscal_position_tax.id === tax_id;
});
fiscal_position.fiscal_position_taxes_by_id[fiscal_position_tax.id] =
fiscal_position_tax;
});
});
}
}, {
label: 'fonts',
loaded: function(){
var fonts_loaded = new $.Deferred();
// Waiting for fonts to be loaded to prevent receipt printing
// from printing empty receipt while loading Inconsolata
// ( The font used for the receipt )
waitForWebfonts(['Lato','Inconsolata'], function(){
fonts_loaded.resolve();
});
// The JS used to detect font loading is not 100% robust, so
// do not wait more than 5sec
setTimeout(function(){
fonts_loaded.resolve();
},5000);
return fonts_loaded;
},
},{
label: 'pictures',
loaded: function(self){
self.company_logo = new Image();
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();
logo_loaded.resolve();
};
self.company_logo.onerror = function(){
logo_loaded.reject();
};
self.company_logo.crossOrigin = "anonymous";
self.company_logo.src = '/web/binary/company_logo' +'?dbname=' +
session.db + '&_'+Math.random();
return logo_loaded;
},
}, {
label: 'barcodes',
loaded: function(self) {
var barcode_parser = new BarcodeParser({'nomenclature_id':
self.config.barcode_nomenclature_id});
self.barcode_reader.set_barcode_parser(barcode_parser);
return barcode_parser.is_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
function load_model(index){
if(index >= self.models.length){
loaded.resolve();
}else{
var model = self.models[index];
self.chrome.loading_message(_t('Loading')+' '+(model.label ||
model.model || ''), progress);
var records;
if( model.model ){
if (model.ids) {
records = new
Model(model.model).call('read',[ids,fields],context);
} else {
records = new Model(model.model)
.query(fields)
.filter(domain)
.order_by(order)
.context(context)
.all();
}
records.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){
console.error(err.stack);
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);
}
}
}
try{
load_model(0);
}catch(err){
loaded.reject(err);
}
return loaded;
},
// reload the list of partner, returns as a deferred that resolves if there were
// updated partners, and fails if not
load_new_partners: function(){
var self = this;
var def = new $.Deferred();
var fields = _.find(this.models,function(model){ return model.model ===
'res.partner'; }).fields;
new Model('res.partner')
.query(fields)
.filter([['customer','=',true],['write_date','>',this.db.get_partner_write_date()]])
.all({'timeout':3000, 'shadow': true})
.then(function(partners){
if (self.db.add_partners(partners)) { // check if the partners we
got were real updates
def.resolve();
} else {
def.reject();
}
}, function(err,event){ event.preventDefault(); def.reject(); });
return def;
},
// this is called when an order is removed from the order collection. It ensures
that there is always an existing
// order and a valid selected order
on_removed_order: function(removed_order,index,reason){
var order_list = this.get_order_list();
if( (reason === 'abandon' || removed_order.temporary) && order_list.length >
0){
// when we intentionally remove an unfinished order, and there is another
existing one
this.set_order(order_list[index] || order_list[order_list.length -1]);
}else{
// when the order was automatically removed after completion,
// or when we intentionally delete the only concurrent order
this.add_new_order();
}
},
// returns the user who is currently the cashier for this point of sale
get_cashier: function(){
return this.cashier || this.user;
},
// changes the current cashier
set_cashier: function(user){
this.cashier = user;
},
//creates a new empty order and sets it as the current order
add_new_order: function(){
var order = new exports.Order({},{pos:this});
this.get('orders').add(order);
this.set('selectedOrder', order);
return order;
},
// load the locally saved unpaid orders for this session.
load_orders: function(){
var jsons = this.db.get_unpaid_orders();
var orders = [];
var not_loaded_count = 0;
if (not_loaded_count) {
console.info('There are '+not_loaded_count+' locally saved unpaid orders
belonging to another session');
}
orders = orders.sort(function(a,b){
return a.sequence_number - b.sequence_number;
});
if (orders.length) {
this.get('orders').add(orders);
}
},
set_start_order: function(){
var orders = this.get('orders').models;
get_client: function() {
var order = this.get_order();
if (order) {
return order.get_client();
}
return null;
},
if(order){
this.db.add_order(order.export_as_JSON());
}
this.flush_mutex.exec(function(){
var flushed = self._flush_orders(self.db.get_orders(), opts);
flushed.always(function(ids){
pushed.resolve();
});
return flushed;
});
return pushed;
},
// saves the order locally and try to send it to the backend and make an invoice
// returns a deferred that succeeds when the order has been posted and
successfully generated
// an invoice. This method can fail in various ways:
// error-no-client: the order must have an associated partner_id. You can retry to
make an invoice once
// this error is solved
// error-transfer: there was a connection error during the transfer. You can retry
to make the invoice once
// the network connection is up
push_and_invoice_order: function(order){
var self = this;
var invoiced = new $.Deferred();
if(!order.get_client()){
invoiced.reject({code:400, message:'Missing Customer', data:{}});
return invoiced;
}
this.flush_mutex.exec(function(){
var done = new $.Deferred(); // holds the mutex
transfer.fail(function(error){
invoiced.reject(error);
done.reject();
});
self.chrome.do_action('point_of_sale.pos_invoice_report',{additional_context:{
active_ids:order_server_id,
}});
invoiced.resolve();
done.resolve();
});
return done;
});
return invoiced;
},
// wrapper around the _save_to_server that updates the synch status widget
_flush_orders: function(orders, options) {
var self = this;
this.set('synch',{ state: 'connecting', pending: orders.length});
self.set('synch', {
state: pending ? 'connecting' : 'connected',
pending: pending
});
return server_ids;
}).fail(function(error, event){
var pending = self.db.get_orders().length;
if (self.get('failed')) {
self.set('synch', { state: 'error', pending: pending });
} else {
self.set('synch', { state: 'disconnected', pending: pending });
}
});
},
// we try to send the order. shadow prevents a spinner if it takes too long.
(unless we are sending an invoice,
// then we want to notify the user that we are waiting on something )
var posOrderModel = new Model('pos.order');
return posOrderModel.call('create_from_ui',
[_.map(orders, function (order) {
order.to_invoice = options.to_invoice || false;
return order;
})],
undefined,
{
shadow: !options.to_invoice,
timeout: timeout
}
).then(function (server_ids) {
_.each(order_ids_to_sync, function (order_id) {
self.db.remove_order(order_id);
});
self.set('failed',false);
return server_ids;
}).fail(function (error, event){
if(error.code === 200 ){ // Business Logic Error, not a connection
problem
//if warning do not need to display traceback!!
if (error.data.exception_type == 'warning') {
delete error.data.debug;
}
// Hide error if already shown before ...
if ((!self.get('failed') || options.show_error) &&
!options.to_invoice) {
self.gui.show_popup('error-traceback',{
'title': error.data.message,
'body': error.data.debug
});
}
self.set('failed',error)
}
// 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();
console.error('Failed to send orders:', orders);
});
},
scan_product: function(parsed_code){
var selectedOrder = this.get_order();
var product = this.db.get_product_by_barcode(parsed_code.base_code);
if(!product){
return false;
}
// Exports the paid orders (the ones waiting for internet connection)
export_paid_orders: function() {
return JSON.stringify({
'paid_orders': this.db.get_orders(),
'session': this.pos_session.name,
'session_id': this.pos_session.id,
'date': (new Date()).toUTCString(),
'version': this.version.server_version_info,
},null,2);
},
if (json.paid_orders) {
for (var i = 0; i < json.paid_orders.length; i++) {
this.db.add_order(json.paid_orders[i].data);
}
report.paid = json.paid_orders.length;
this.push_order();
}
if (json.unpaid_orders) {
orders = orders.sort(function(a,b){
return a.sequence_number - b.sequence_number;
});
if (orders.length) {
report.unpaid = orders.length;
this.get('orders').add(orders);
}
report.unpaid_skipped_sessions = _.keys(skipped_sessions);
}
return report;
},
_load_orders: function(){
var jsons = this.db.get_unpaid_orders();
var orders = [];
var not_loaded_count = 0;
if (not_loaded_count) {
console.info('There are '+not_loaded_count+' locally saved unpaid orders
belonging to another session');
}
orders = orders.sort(function(a,b){
return a.sequence_number - b.sequence_number;
});
if (orders.length) {
this.get('orders').add(orders);
}
},
});
has_valid_product_lot: function(){
if(!this.has_product_lot){
return true;
}
var valid_product_lot = this.pack_lot_lines.get_valid_lots();
return this.quantity === valid_product_lot.length;
},
if (current_line.length) {
wrapped.push(current_line);
}
return wrapped;
},
// changes the base price of the product for this orderline
set_unit_price: function(price){
this.order.assert_editable();
this.price = round_di(parseFloat(price) || 0, this.pos.dp['Product Price']);
this.trigger('change',this);
},
get_unit_price: function(){
var digits = this.pos.dp['Product Price'];
// round and truncate to mimic _sybmbol_set behavior
return parseFloat(round_di(this.price || 0, digits).toFixed(digits));
},
get_unit_display_price: function(){
if (this.pos.config.iface_tax_included) {
var quantity = this.quantity;
this.quantity = 1.0;
var price = this.get_all_prices().priceWithTax;
this.quantity = quantity;
return price;
} else {
return this.get_unit_price();
}
},
get_base_price: function(){
var rounding = this.pos.currency.rounding;
return round_pr(this.get_unit_price() * this.get_quantity() * (1 -
this.get_discount()/100), rounding);
},
get_display_price: function(){
if (this.pos.config.iface_tax_included) {
return this.get_price_with_tax();
} else {
return this.get_base_price();
}
},
get_price_without_tax: function(){
return this.get_all_prices().priceWithoutTax;
},
get_price_with_tax: function(){
return this.get_all_prices().priceWithTax;
},
get_tax: function(){
return this.get_all_prices().tax;
},
get_applicable_taxes: function(){
var i;
// Shenaningans because we need
// to keep the taxes ordering.
var ptaxes_ids = this.get_product().taxes_id;
var ptaxes_set = {};
for (i = 0; i < ptaxes_ids.length; i++) {
ptaxes_set[ptaxes_ids[i]] = true;
}
var taxes = [];
for (i = 0; i < this.pos.taxes.length; i++) {
if (ptaxes_set[this.pos.taxes[i].id]) {
taxes.push(this.pos.taxes[i]);
}
}
return taxes;
},
get_tax_details: function(){
return this.get_all_prices().taxDetails;
},
get_taxes: function(){
var taxes_ids = this.get_product().taxes_id;
var taxes = [];
for (var i = 0; i < taxes_ids.length; i++) {
taxes.push(this.pos.taxes_by_id[taxes_ids[i]]);
}
return taxes;
},
_map_tax_fiscal_position: function(tax) {
var current_order = this.pos.get_order();
var order_fiscal_position = current_order && current_order.fiscal_position;
if (order_fiscal_position) {
var mapped_tax = _.find(order_fiscal_position.fiscal_position_taxes_by_id,
function (fiscal_position_tax) {
return fiscal_position_tax.tax_src_id[0] === tax.id;
});
if (mapped_tax) {
tax = this.pos.taxes_by_id[mapped_tax.tax_dest_id[0]];
}
}
return tax;
},
_compute_all: function(tax, base_amount, quantity) {
if (tax.amount_type === 'fixed') {
var sign_base_amount = base_amount >= 0 ? 1 : -1;
return (Math.abs(tax.amount) * sign_base_amount) * quantity;
}
if ((tax.amount_type === 'percent' && !tax.price_include) || (tax.amount_type
=== 'division' && tax.price_include)){
return base_amount * tax.amount / 100;
}
if (tax.amount_type === 'percent' && tax.price_include){
return base_amount - (base_amount / (1 + tax.amount / 100));
}
if (tax.amount_type === 'division' && !tax.price_include) {
return base_amount / (1 - tax.amount / 100) - base_amount;
}
return false;
},
compute_all: function(taxes, price_unit, quantity, currency_rounding, no_map_tax)
{
var self = this;
var list_taxes = [];
var currency_rounding_bak = currency_rounding;
if (this.pos.company.tax_calculation_rounding_method == "round_globally"){
currency_rounding = currency_rounding * 0.00001;
}
var total_excluded = round_pr(price_unit * quantity, currency_rounding);
var total_included = total_excluded;
var base = total_excluded;
_(taxes).each(function(tax) {
if (!no_map_tax){
tax = self._map_tax_fiscal_position(tax);
}
if (tax.amount_type === 'group'){
var ret = self.compute_all(tax.children_tax_ids, price_unit, quantity,
currency_rounding);
total_excluded = ret.total_excluded;
base = ret.total_excluded;
total_included = ret.total_included;
list_taxes = list_taxes.concat(ret.taxes);
}
else {
var tax_amount = self._compute_all(tax, base, quantity);
tax_amount = round_pr(tax_amount, currency_rounding);
if (tax_amount){
if (tax.price_include) {
total_excluded -= tax_amount;
base -= tax_amount;
}
else {
total_included += tax_amount;
}
if (tax.include_base_amount) {
base += tax_amount;
}
var data = {
id: tax.id,
amount: tax_amount,
name: tax.name,
};
list_taxes.push(data);
}
}
});
return {
taxes: list_taxes,
total_excluded: round_pr(total_excluded, currency_rounding_bak),
total_included: round_pr(total_included, currency_rounding_bak)
};
},
get_all_prices: function(){
var price_unit = this.get_unit_price() * (1.0 - (this.get_discount() /
100.0));
var taxtotal = 0;
_(taxes_ids).each(function(el){
product_taxes.push(_.detect(taxes, function(t){
return t.id === el;
}));
});
return {
"priceWithTax": all_taxes.total_included,
"priceWithoutTax": all_taxes.total_excluded,
"tax": taxtotal,
"taxDetails": taxdetail,
};
},
});
exports.Packlotline = Backbone.Model.extend({
defaults: {
lot_name: null
},
initialize: function(attributes, options){
this.order_line = options.order_line;
if (options.json) {
this.init_from_JSON(options.json);
return;
}
},
init_from_JSON: function(json) {
this.order_line = json.order_line;
this.set_lot_name(json.lot_name);
},
set_lot_name: function(name){
this.set({lot_name : _.str.trim(name) || null});
},
get_lot_name: function(){
return this.get('lot_name');
},
export_as_JSON: function(){
return {
lot_name: this.get_lot_name(),
};
},
add: function(){
var order_line = this.order_line,
index = this.collection.indexOf(this);
var new_lot_model = new exports.Packlotline({}, {'order_line':
this.order_line});
this.collection.add(new_lot_model, {at: index + 1});
return new_lot_model;
},
remove: function(){
this.collection.remove(this);
}
});
var PacklotlineCollection = Backbone.Collection.extend({
model: exports.Packlotline,
initialize: function(models, options) {
this.order_line = options.order_line;
},
get_empty_model: function(){
return this.findWhere({'lot_name': null});
},
remove_empty_model: function(){
this.remove(this.where({'lot_name': null}));
},
get_valid_lots: function(){
return this.filter(function(model){
return model.get('lot_name');
});
},
set_quantity_by_lot: function() {
var valid_lots = this.get_valid_lots();
this.order_line.set_quantity(valid_lots.length);
}
});
// An order more or less represents the content of a client's shopping cart (the
OrderLines)
// plus the associated payment information (the Paymentlines)
// there is always an active ('selected') order in the Pos, a new one is created
// automaticaly once an order is completed and sent to the server.
exports.Order = Backbone.Model.extend({
initialize: function(attributes,options){
Backbone.Model.prototype.initialize.apply(this, arguments);
var self = this;
options = options || {};
this.init_locked = true;
this.pos = options.pos;
this.selected_orderline = undefined;
this.selected_paymentline = undefined;
this.screen_data = {}; // see Gui
this.temporary = options.temporary || false;
this.creation_date = new Date();
this.to_invoice = false;
this.orderlines = new OrderlineCollection();
this.paymentlines = new PaymentlineCollection();
this.pos_session_id = this.pos.pos_session.id;
this.finalized = false; // if true, cannot be modified.
if (options.json) {
this.init_from_JSON(options.json);
} else {
this.sequence_number = this.pos.pos_session.sequence_number++;
this.uid = this.generate_unique_id();
this.name = _t("Order ") + this.uid;
this.validation_date = undefined;
this.fiscal_position = _.find(this.pos.fiscal_positions, function(fp) {
return fp.id === self.pos.config.default_fiscal_position_id[0];
});
}
this.init_locked = false;
this.save_to_db();
return this;
},
save_to_db: function(){
if (!this.temporary && !this.init_locked) {
this.pos.db.save_unpaid_order(this);
}
},
init_from_JSON: function(json) {
var client;
this.sequence_number = json.sequence_number;
this.pos.pos_session.sequence_number =
Math.max(this.sequence_number+1,this.pos.pos_session.sequence_number);
this.session_id = json.pos_session_id;
this.uid = json.uid;
this.name = _t("Order ") + this.uid;
this.validation_date = json.creation_date;
if (json.fiscal_position_id) {
var fiscal_position = _.find(this.pos.fiscal_positions, function (fp) {
return fp.id === json.fiscal_position_id;
});
if (fiscal_position) {
this.fiscal_position = fiscal_position;
} else {
console.error('ERROR: trying to load a fiscal position not available
in the pos');
}
}
if (json.partner_id) {
client = this.pos.db.get_partner_by_id(json.partner_id);
if (!client) {
console.error('ERROR: trying to load a parner not available in the
pos');
}
} else {
client = null;
}
this.set_client(client);
if (i === paymentlines.length - 1) {
this.select_paymentline(newpaymentline);
}
}
},
export_as_JSON: function() {
var orderLines, paymentLines;
orderLines = [];
this.orderlines.each(_.bind( function(item) {
return orderLines.push([0, 0, item.export_as_JSON()]);
}, this));
paymentLines = [];
this.paymentlines.each(_.bind( function(item) {
return paymentLines.push([0, 0, item.export_as_JSON()]);
}, this));
return {
name: this.get_name(),
amount_paid: this.get_total_paid(),
amount_total: this.get_total_with_tax(),
amount_tax: this.get_total_tax(),
amount_return: this.get_change(),
lines: orderLines,
statement_ids: paymentLines,
pos_session_id: this.pos_session_id,
partner_id: this.get_client() ? this.get_client().id : false,
user_id: this.pos.cashier ? this.pos.cashier.id : this.pos.user.id,
uid: this.uid,
sequence_number: this.sequence_number,
creation_date: this.validation_date || this.creation_date, // todo: rename
creation_date in master
fiscal_position_id: this.fiscal_position ? this.fiscal_position.id : false
};
},
export_for_printing: function(){
var orderlines = [];
var self = this;
this.orderlines.each(function(orderline){
orderlines.push(orderline.export_for_printing());
});
function is_xml(subreceipt){
return subreceipt ? (subreceipt.split('\n')[0].indexOf('<!DOCTYPE QWEB')
>= 0) : false;
}
function render_xml(subreceipt){
if (!is_xml(subreceipt)) {
return subreceipt;
} else {
subreceipt = subreceipt.split('\n').slice(1).join('\n');
var qweb = new QWeb2.Engine();
qweb.debug = core.debug;
qweb.default_dict = _.clone(QWeb.default_dict);
qweb.add_template('<templates><t t-
name="subreceipt">'+subreceipt+'</t></templates>');
return
qweb.render('subreceipt',{'pos':self.pos,'widget':self.pos.chrome,'order':self,
'receipt': receipt}) ;
}
}
var receipt = {
orderlines: orderlines,
paymentlines: paymentlines,
subtotal: this.get_subtotal(),
total_with_tax: this.get_total_with_tax(),
total_without_tax: this.get_total_without_tax(),
total_tax: this.get_total_tax(),
total_paid: this.get_total_paid(),
total_discount: this.get_total_discount(),
tax_details: this.get_tax_details(),
change: this.get_change(),
name : this.get_name(),
client: client ? client.name : null ,
invoice_id: null, //TODO
cashier: cashier ? cashier.name : null,
precision: {
price: 2,
money: 2,
quantity: 3,
},
date: {
year: date.getFullYear(),
month: date.getMonth(),
date: date.getDate(), // day of the month
day: date.getDay(), // day of the week
hour: date.getHours(),
minute: date.getMinutes() ,
isostring: date.toISOString(),
localestring: date.toLocaleString(),
},
company:{
email: company.email,
website: company.website,
company_registry: company.company_registry,
contact_address: company.partner_id[1],
vat: company.vat,
name: company.name,
phone: company.phone,
logo: this.pos.company_logo_base64,
},
shop:{
name: shop.name,
},
currency: this.pos.currency,
};
if (is_xml(this.pos.config.receipt_header)){
receipt.header = '';
receipt.header_xml = render_xml(this.pos.config.receipt_header);
} else {
receipt.header = this.pos.config.receipt_header || '';
}
if (is_xml(this.pos.config.receipt_footer)){
receipt.footer = '';
receipt.footer_xml = render_xml(this.pos.config.receipt_footer);
} else {
receipt.footer = this.pos.config.receipt_footer || '';
}
return receipt;
},
is_empty: function(){
return this.orderlines.models.length === 0;
},
generate_unique_id: function() {
// Generates a public identification number for the order.
// The generated number must be unique and sequential. They are made 12 digit
long
// to fit into EAN-13 barcodes, should it be needed
function zero_pad(num,size){
var s = ""+num;
while (s.length < size) {
s = "0" + s;
}
return s;
}
return zero_pad(this.pos.pos_session.id,5) +'-'+
zero_pad(this.pos.pos_session.login_number,3) +'-'+
zero_pad(this.sequence_number,4);
},
get_name: function() {
return this.name;
},
assert_editable: function() {
if (this.finalized) {
throw new Error('Finalized Order cannot be modified');
}
},
/* ---- Order Lines --- */
add_orderline: function(line){
this.assert_editable();
if(line.order){
line.order.remove_orderline(line);
}
line.order = this;
this.orderlines.add(line);
this.select_orderline(this.get_last_orderline());
},
get_orderline: function(id){
var orderlines = this.orderlines.models;
for(var i = 0; i < orderlines.length; i++){
if(orderlines[i].id === id){
return orderlines[i];
}
}
return null;
},
get_orderlines: function(){
return this.orderlines.models;
},
get_last_orderline: function(){
return this.orderlines.at(this.orderlines.length -1);
},
get_tip: function() {
var tip_product =
this.pos.db.get_product_by_id(this.pos.config.tip_product_id[0]);
var lines = this.get_orderlines();
if (!tip_product) {
return 0;
} else {
for (var i = 0; i < lines.length; i++) {
if (lines[i].get_product() === tip_product) {
return lines[i].get_unit_price();
}
}
return 0;
}
},
initialize_validation_date: function () {
this.validation_date = new Date();
},
set_tip: function(tip) {
var tip_product =
this.pos.db.get_product_by_id(this.pos.config.tip_product_id[0]);
var lines = this.get_orderlines();
if (tip_product) {
for (var i = 0; i < lines.length; i++) {
if (lines[i].get_product() === tip_product) {
lines[i].set_unit_price(tip);
return;
}
}
this.add_product(tip_product, {quantity: 1, price: tip });
}
},
remove_orderline: function( line ){
this.assert_editable();
this.orderlines.remove(line);
this.select_orderline(this.get_last_orderline());
},
fix_tax_included_price: function(line){
if(this.fiscal_position){
var unit_price = line.price;
var taxes = line.get_taxes();
var mapped_included_taxes = [];
_(taxes).each(function(tax) {
var line_tax = line._map_tax_fiscal_position(tax);
if(tax.price_include && tax.id != line_tax.id){
mapped_included_taxes.push(tax);
}
})
line.set_unit_price(unit_price);
}
},
//To substract from the unit price the included taxes mapped by the fiscal
position
this.fix_tax_included_price(line);
if(line.has_product_lot){
this.display_lot_popup();
}
},
get_selected_orderline: function(){
return this.selected_orderline;
},
select_orderline: function(line){
if(line){
if(line !== this.selected_orderline){
if(this.selected_orderline){
this.selected_orderline.set_selected(false);
}
this.selected_orderline = line;
this.selected_orderline.set_selected(true);
}
}else{
this.selected_orderline = undefined;
}
},
deselect_orderline: function(){
if(this.selected_orderline){
this.selected_orderline.set_selected(false);
this.selected_orderline = undefined;
}
},
display_lot_popup: function() {
var order_line = this.get_selected_orderline();
if (order_line){
var pack_lot_lines = order_line.compute_lot_lines();
this.pos.gui.show_popup('packlotline', {
'title': _t('Lot/Serial Number(s) Required'),
'pack_lot_lines': pack_lot_lines,
'order': this
});
}
},
},
get_paymentlines: function(){
return this.paymentlines.models;
},
remove_paymentline: function(line){
this.assert_editable();
if(this.selected_paymentline === line){
this.select_paymentline(undefined);
}
this.paymentlines.remove(line);
},
clean_empty_paymentlines: function() {
var lines = this.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.remove_paymentline(empty[i]);
}
},
select_paymentline: function(line){
if(line !== this.selected_paymentline){
if(this.selected_paymentline){
this.selected_paymentline.set_selected(false);
}
this.selected_paymentline = line;
if(this.selected_paymentline){
this.selected_paymentline.set_selected(true);
}
this.trigger('change:selected_paymentline',this.selected_paymentline);
}
},
/* ---- Payment Status --- */
get_subtotal : function(){
return round_pr(this.orderlines.reduce((function(sum, orderLine){
return sum + orderLine.get_display_price();
}), 0), this.pos.currency.rounding);
},
get_total_with_tax: function() {
return this.get_total_without_tax() + this.get_total_tax();
},
get_total_without_tax: function() {
return round_pr(this.orderlines.reduce((function(sum, orderLine) {
return sum + orderLine.get_price_without_tax();
}), 0), this.pos.currency.rounding);
},
get_total_discount: function() {
return round_pr(this.orderlines.reduce((function(sum, orderLine) {
return sum + (orderLine.get_unit_price() * (orderLine.get_discount()/100)
* orderLine.get_quantity());
}), 0), this.pos.currency.rounding);
},
get_total_tax: function() {
return round_pr(this.orderlines.reduce((function(sum, orderLine) {
return sum + orderLine.get_tax();
}), 0), this.pos.currency.rounding);
},
get_total_paid: function() {
return round_pr(this.paymentlines.reduce((function(sum, paymentLine) {
return sum + paymentLine.get_amount();
}), 0), this.pos.currency.rounding);
},
get_tax_details: function(){
var details = {};
var fulldetails = [];
this.orderlines.each(function(line){
var ldetails = line.get_tax_details();
for(var id in ldetails){
if(ldetails.hasOwnProperty(id)){
details[id] = (details[id] || 0) + ldetails[id];
}
}
});
for(var id in details){
if(details.hasOwnProperty(id)){
fulldetails.push({amount: details[id], tax: this.pos.taxes_by_id[id],
name: this.pos.taxes_by_id[id].name});
}
}
return fulldetails;
},
// Returns a total only for the orderlines with products belonging to the category
get_total_for_category_with_tax: function(categ_id){
var total = 0;
var self = this;
this.orderlines.each(function(line){
if ( self.pos.db.category_contains(categ_id,line.product.id) ) {
total += line.get_price_with_tax();
}
});
return total;
},
get_total_for_taxes: function(tax_id){
var total = 0;
this.orderlines.each(function(line){
var taxes_ids = line.get_product().taxes_id;
for (var i = 0; i < taxes_ids.length; i++) {
if (tax_set[taxes_ids[i]]) {
total += line.get_price_with_tax();
return;
}
}
});
return total;
},
get_change: function(paymentline) {
if (!paymentline) {
var change = this.get_total_paid() - this.get_total_with_tax();
} else {
var change = -this.get_total_with_tax();
var lines = this.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);
},
get_due: function(paymentline) {
if (!paymentline) {
var due = this.get_total_with_tax() - this.get_total_paid();
} else {
var due = this.get_total_with_tax();
var lines = this.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);
},
is_paid: function(){
return this.get_due() === 0;
},
is_paid_with_cash: function(){
return !!this.paymentlines.find( function(pl){
return pl.cashregister.journal.type === 'cash';
});
},
finalize: function(){
this.destroy();
},
destroy: function(){
Backbone.Model.prototype.destroy.apply(this,arguments);
this.pos.db.remove_unpaid_order(this);
},
/* ---- Invoice --- */
set_to_invoice: function(to_invoice) {
this.assert_editable();
this.to_invoice = to_invoice;
},
is_to_invoice: function(){
return this.to_invoice;
},
/* ---- Client / Customer --- */
// the client related to the current order.
set_client: function(client){
this.assert_editable();
this.set('client',client);
},
get_client: function(){
return this.get('client');
},
get_client_name: function(){
var client = this.get('client');
return client ? client.name : "";
},
/* ---- Screen Status --- */
// the order also stores the screen status, as the PoS supports
// different active screens per order. This method is used to
// store the screen status.
set_screen_data: function(key,value){
if(arguments.length === 2){
this.screen_data[key] = value;
}else if(arguments.length === 1){
for(var key in arguments[0]){
this.screen_data[key] = arguments[0][key];
}
}
},
//see set_screen_data
get_screen_data: function(key){
return this.screen_data[key];
},
});
/*
The numpad handles both the choice of the property currently being modified
(quantity, price or discount) and the edition of the corresponding numeric value.
*/
exports.NumpadState = Backbone.Model.extend({
defaults: {
buffer: "0",
mode: "quantity"
},
appendNewChar: function(newChar) {
var oldBuffer;
oldBuffer = this.get('buffer');
if (oldBuffer === '0') {
this.set({
buffer: newChar
});
} else if (oldBuffer === '-0') {
this.set({
buffer: "-" + newChar
});
} else {
this.set({
buffer: (this.get('buffer')) + newChar
});
}
this.trigger('set_value',this.get('buffer'));
},
deleteLastChar: function() {
if(this.get('buffer') === ""){
if(this.get('mode') === 'quantity'){
this.trigger('set_value','remove');
}else{
this.trigger('set_value',this.get('buffer'));
}
}else{
var newBuffer = this.get('buffer').slice(0,-1) || "";
this.set({ buffer: newBuffer });
this.trigger('set_value',this.get('buffer'));
}
},
switchSign: function() {
var oldBuffer;
oldBuffer = this.get('buffer');
this.set({
buffer: oldBuffer[0] === '-' ? oldBuffer.substr(1) : "-" + oldBuffer
});
this.trigger('set_value',this.get('buffer'));
},
changeMode: function(newMode) {
this.set({
buffer: "0",
mode: newMode
});
},
reset: function() {
this.set({
buffer: "0",
mode: "quantity"
});
},
resetValue: function(){
this.set({buffer:'0'});
},
});
// exports = {
// PosModel: PosModel,
// NumpadState: NumpadState,
// load_fields: load_fields,
// load_models: load_models,
// Orderline: Orderline,
// Order: Order,
// };
return exports;
});
POS Screens JS
odoo.define('point_of_sale.screens', function (require) {
"use strict";
// This file contains the Screens definitions. Screens are the
// content of the right pane of the pos, containing the main functionalities.
//
// Screens must be defined and named in chrome.js before use.
//
// Screens transitions are controlled by the Gui.
// gui.set_startup_screen() sets the screen displayed at startup
// gui.set_default_screen() sets the screen displayed for new orders
// gui.show_screen() shows a screen
// gui.back() goes to the previous screen
//
// Screen state is saved in the order. When a new order is selected,
// a screen is displayed based on the state previously saved in the order.
// this is also done in the Gui with:
// gui.show_saved_screen()
//
// All screens inherit from ScreenWidget. The only addition from the base widgets
// are show() and hide() which shows and hides the screen but are also used to
// bind and unbind actions on widgets and devices. The gui guarantees
// that only one screen is shown at the same time and that show() is called after all
// hide()s
//
// Each Screens must be independant from each other, and should have no
// persistent state outside the models. Screen state variables are reset at
// each screen display. A screen can be called with parameters, which are
// to be used for the duration of the screen only.
/*--------------------------------------*\
| THE SCREEN WIDGET |
\*======================================*/
init: function(parent,options){
this._super(parent,options);
this.hidden = false;
},
this.hidden = false;
if(this.$el){
this.$el.removeClass('oe_hidden');
}
this.pos.barcode_reader.set_action_callback({
'cashier': _.bind(self.barcode_cashier_action, self),
'product': _.bind(self.barcode_product_action, self),
'weight': _.bind(self.barcode_product_action, self),
'price': _.bind(self.barcode_product_action, self),
'client' : _.bind(self.barcode_client_action, self),
'discount': _.bind(self.barcode_discount_action, self),
'error' : _.bind(self.barcode_error_action, self),
});
},
// this method is called when the screen is closed to make place for a new screen.
this is a good place
// to put your cleanup stuff as it is guaranteed that for each show() there is one
and only one close()
close: function(){
if(this.pos.barcode_reader){
this.pos.barcode_reader.reset_action_callbacks();
}
},
// this methods hides the screen. It's not a good place to put your cleanup stuff
as it is called on the
// POS initialization.
hide: function(){
this.hidden = true;
if(this.$el){
this.$el.addClass('oe_hidden');
}
},
// we need this because some screens re-render themselves when they are hidden
// (due to some events, or magic, or both...) we must make sure they remain
hidden.
// the good solution would probably be to make them not re-render themselves when
they
// are hidden.
renderElement: function(){
this._super();
if(this.hidden){
if(this.$el){
this.$el.addClass('oe_hidden');
}
}
},
});
/*--------------------------------------*\
| THE DOM CACHE |
\*======================================*/
// The Dom Cache is used by various screens to improve
// their performances when displaying many time the
// same piece of DOM.
//
// It is a simple map from string 'keys' to DOM Nodes.
//
// The cache empties itself based on usage frequency
// stats, so you may not always get back what
// you put in.
this.cache = {};
this.access_time = {};
this.size = 0;
},
cache_node: function(key,node){
var cached = this.cache[key];
this.cache[key] = node;
this.access_time[key] = new Date().getTime();
if(!cached){
this.size++;
while(this.size >= this.max_size){
var oldest_key = null;
var oldest_time = new Date().getTime();
for(key in this.cache){
var time = this.access_time[key];
if(time <= oldest_time){
oldest_time = time;
oldest_key = key;
}
}
if(oldest_key){
delete this.cache[oldest_key];
delete this.access_time[oldest_key];
}
this.size--;
}
}
return node;
},
clear_node: function(key) {
var cached = this.cache[key];
if (cached) {
delete this.cache[key];
delete this.access_time[key];
this.size --;
}
},
get_node: function(key){
var cached = this.cache[key];
if(cached){
this.access_time[key] = new Date().getTime();
}
return cached;
},
});
/*--------------------------------------*\
| THE SCALE SCREEN |
\*======================================*/
next_screen: 'products',
previous_screen: 'products',
show: function(){
this._super();
var self = this;
var queue = this.pos.proxy_queue;
this.set_weight(0);
this.renderElement();
this.hotkey_handler = function(event){
if(event.which === 13){
self.order_product();
self.gui.show_screen(self.next_screen);
}else if(event.which === 27){
self.gui.show_screen(self.previous_screen);
}
};
$('body').on('keypress',this.hotkey_handler);
this.$('.back').click(function(){
self.gui.show_screen(self.previous_screen);
});
this.$('.next,.buy-product').click(function(){
self.gui.show_screen(self.next_screen);
// add product *after* switching screen to scroll properly
self.order_product();
});
queue.schedule(function(){
return self.pos.proxy.scale_read().then(function(weight){
self.set_weight(weight.weight);
});
},{duration:150, repeat: true});
},
get_product: function(){
return this.gui.get_current_screen_param('product');
},
order_product: function(){
this.pos.get_order().add_product(this.get_product(),{ quantity: this.weight
});
},
get_product_name: function(){
var product = this.get_product();
return (product ? product.display_name : undefined) || 'Unnamed Product';
},
get_product_price: function(){
var product = this.get_product();
return (product ? product.price : 0) || 0;
},
get_product_uom: function(){
var product = this.get_product();
if(product){
return this.pos.units_by_id[product.uom_id[0]].name;
}else{
return '';
}
},
set_weight: function(weight){
this.weight = weight;
this.$('.weight').text(this.get_product_weight_string());
this.$('.computed-price').text(this.get_computed_price_string());
},
get_product_weight_string: function(){
var product = this.get_product();
var defaultstr = (this.weight || 0).toFixed(3) + ' Kg';
if(!product || !this.pos){
return defaultstr;
}
var unit_id = product.uom_id;
if(!unit_id){
return defaultstr;
}
var unit = this.pos.units_by_id[unit_id[0]];
var weight = round_pr(this.weight || 0, unit.rounding);
var weightstr = weight.toFixed(Math.ceil(Math.log(1.0/unit.rounding) /
Math.log(10) ));
weightstr += ' ' + unit.name;
return weightstr;
},
get_computed_price_string: function(){
return this.format_currency(this.get_product_price() * this.weight);
},
close: function(){
this._super();
$('body').off('keypress',this.hotkey_handler);
this.pos.proxy_queue.clear();
},
});
gui.define_screen({name: 'scale', widget: ScaleScreenWidget});
/*--------------------------------------*\
| THE PRODUCT SCREEN |
\*======================================*/
this.pos.bind('change:selectedClient', function() {
self.renderElement();
});
},
renderElement: function() {
var self = this;
this._super();
this.$('.pay').click(function(){
var order = self.pos.get_order();
var has_valid_product_lot = _.every(order.orderlines.models,
function(line){
return line.has_valid_product_lot();
});
if(!has_valid_product_lot){
self.gui.show_popup('confirm',{
'title': _t('Empty Serial/Lot Number'),
'body': _t('One or more product(s) required serial/lot number.'),
confirm: function(){
self.gui.show_screen('payment');
},
});
}else{
self.gui.show_screen('payment');
}
});
this.$('.set-customer').click(function(){
self.gui.show_screen('clientlist');
});
}
});
this.numpad_state = options.numpad_state;
this.numpad_state.reset();
this.numpad_state.bind('set_value', this.set_value, this);
this.line_click_handler = function(event){
self.click_line(this.orderline, event);
};
if (this.pos.get_order()) {
this.bind_order_events();
}
},
click_line: function(orderline, event) {
this.pos.get_order().select_orderline(orderline);
this.numpad_state.reset();
},
set_value: function(val) {
var order = this.pos.get_order();
if (order.get_selected_orderline()) {
var mode = this.numpad_state.get('mode');
if( mode === 'quantity'){
order.get_selected_orderline().set_quantity(val);
}else if( mode === 'discount'){
order.get_selected_orderline().set_discount(val);
}else if( mode === 'price'){
order.get_selected_orderline().set_unit_price(val);
}
}
},
change_selected_order: function() {
if (this.pos.get_order()) {
this.bind_order_events();
this.numpad_state.reset();
this.renderElement();
}
},
orderline_add: function(){
this.numpad_state.reset();
this.renderElement('and_scroll_to_bottom');
},
orderline_remove: function(line){
this.remove_orderline(line);
this.numpad_state.reset();
this.update_summary();
},
orderline_change: function(line){
this.rerender_orderline(line);
this.update_summary();
},
bind_order_events: function() {
var order = this.pos.get_order();
order.unbind('change:client', this.update_summary, this);
order.bind('change:client', this.update_summary, this);
order.unbind('change', this.update_summary, this);
order.bind('change', this.update_summary, this);
},
render_orderline: function(orderline){
var el_str = QWeb.render('Orderline',{widget:this, line:orderline});
var el_node = document.createElement('div');
el_node.innerHTML = _.str.trim(el_str);
el_node = el_node.childNodes[0];
el_node.orderline = orderline;
el_node.addEventListener('click',this.line_click_handler);
var el_lot_icon = el_node.querySelector('.line-lot-icon');
if(el_lot_icon){
el_lot_icon.addEventListener('click', (function() {
this.show_product_lot(orderline);
}.bind(this)));
}
orderline.node = el_node;
return el_node;
},
remove_orderline: function(order_line){
if(this.pos.get_order().get_orderlines().length === 0){
this.renderElement();
}else{
order_line.node.parentNode.removeChild(order_line.node);
}
},
rerender_orderline: function(order_line){
var node = order_line.node;
var replacement_line = this.render_orderline(order_line);
node.parentNode.replaceChild(replacement_line,node);
},
// overriding the openerp framework replace method for performance reasons
replace: function($target){
this.renderElement();
var target = $target[0];
target.parentNode.replaceChild(this.el,target);
},
renderElement: function(scrollbottom){
var order = this.pos.get_order();
if (!order) {
return;
}
var orderlines = order.get_orderlines();
if(scrollbottom){
this.el.querySelector('.order-scroller').scrollTop = 100 *
orderlines.length;
}
},
update_summary: function(){
var order = this.pos.get_order();
if (!order.get_orderlines().length) {
return;
}
this.switch_category_handler = function(event){
self.set_category(self.pos.db.get_category_by_id(Number(this.dataset.categoryId)));
self.renderElement();
};
this.clear_search_handler = function(event){
self.clear_search();
};
search_timeout = setTimeout(function(){
self.perform_search(self.category, searchbox.value, event.which
=== 13);
},70);
}
};
},
replace: function($target){
this.renderElement();
var target = $target[0];
target.parentNode.replaceChild(this.el,target);
},
renderElement: function(){
el_node.innerHTML = el_str;
el_node = el_node.childNodes[1];
this.el = el_node;
list_container.appendChild(this.render_category(this.subcategories[i],withpics));
}
}
this.el.querySelector('.searchbox
input').addEventListener('keypress',this.search_handler);
this.el.querySelector('.searchbox
input').addEventListener('keydown',this.search_handler);
this.el.querySelector('.search-
clear').addEventListener('click',this.clear_search_handler);
});
/* --------- The Product List --------- */
this.click_product_handler = function(){
var product = self.pos.db.get_product_by_id(this.dataset.productId);
options.click_product_action(product);
};
render_product: function(product){
var cached = this.product_cache.get_node(product.id);
if(!cached){
var image_url = this.get_product_image_url(product);
var product_html = QWeb.render('Product',{
widget: this,
product: product,
image_url: this.get_product_image_url(product),
});
var product_node = document.createElement('div');
product_node.innerHTML = product_html;
product_node = product_node.childNodes[1];
this.product_cache.cache_node(product.id,product_node);
return product_node;
}
return cached;
},
renderElement: function() {
var el_str = QWeb.render(this.template, {widget: this});
var el_node = document.createElement('div');
el_node.innerHTML = el_str;
el_node = el_node.childNodes[1];
if(this.el && this.el.parentNode){
this.el.parentNode.replaceChild(el_node,this.el);
}
this.el = el_node;
if (options.after) {
for (i = 0; i < classes.length; i++) {
if (classes[i].name === options.after) {
index = i + 1;
}
}
} else if (options.before) {
for (i = 0; i < classes.length; i++) {
if (classes[i].name === options.after) {
index = i;
break;
}
}
}
classes.splice(i,0,classe);
};
start: function(){
this.action_buttons = {};
var classes = action_button_classes;
for (var i = 0; i < classes.length; i++) {
var classe = classes[i];
if ( !classe.condition || classe.condition.call(this) ) {
var widget = new classe.widget(this,{});
widget.appendTo(this.$('.control-buttons'));
this.action_buttons[classe.name] = widget;
}
}
if (_.size(this.action_buttons)) {
this.$('.control-buttons').removeClass('oe_hidden');
}
},
click_product: function(product) {
if(product.to_weight && this.pos.config.iface_electronic_scale){
this.gui.show_screen('scale',{product: product});
}else{
this.pos.get_order().add_product(product);
}
},
show: function(reset){
this._super();
if (reset) {
this.product_categories_widget.reset_category();
this.numpad.state.reset();
}
},
close: function(){
this._super();
if(this.pos.config.iface_vkeyboard && this.chrome.widget.keyboard){
this.chrome.widget.keyboard.hide();
}
},
});
gui.define_screen({name:'products', widget: ProductScreenWidget});
/*--------------------------------------*\
| THE CLIENT LIST |
\*======================================*/
auto_back: true,
show: function(){
var self = this;
this._super();
this.renderElement();
this.details_visible = false;
this.old_client = this.pos.get_order().get_client();
this.$('.back').click(function(){
self.gui.back();
});
this.$('.next').click(function(){
self.save_changes();
self.gui.back(); // FIXME HUH ?
});
this.$('.new-customer').click(function(){
self.display_client_details('edit',{
'country_id': self.pos.company.country_id,
});
});
this.reload_partners();
if( this.old_client ){
this.display_client_details('show',this.old_client,0);
}
this.$('.client-list-contents').delegate('.client-
line','click',function(event){
self.line_select(event,$(this),parseInt($(this).data('id')));
});
this.$('.searchbox input').on('keypress',function(event){
clearTimeout(search_timeout);
search_timeout = setTimeout(function(){
self.perform_search(query,event.which === 13);
},70);
});
this.$('.searchbox .search-clear').click(function(){
self.clear_search();
});
},
hide: function () {
this._super();
this.new_client = null;
},
barcode_client_action: function(code){
if (this.editing_client) {
this.$('.detail.barcode').val(code.code);
} else if (this.pos.db.get_partner_by_barcode(code.code)) {
var partner = this.pos.db.get_partner_by_barcode(code.code);
this.new_client = partner;
this.display_client_details('show', partner);
}
},
perform_search: function(query, associate_result){
var customers;
if(query){
customers = this.pos.db.search_partner(query);
this.display_client_details('hide');
if ( associate_result && customers.length === 1){
this.new_client = customers[0];
this.save_changes();
this.gui.back();
}
this.render_list(customers);
}else{
customers = this.pos.db.get_partners_sorted();
this.render_list(customers);
}
},
clear_search: function(){
var customers = this.pos.db.get_partners_sorted(1000);
this.render_list(customers);
this.$('.searchbox input')[0].value = '';
this.$('.searchbox input').focus();
},
render_list: function(partners){
var contents = this.$el[0].querySelector('.client-list-contents');
contents.innerHTML = "";
for(var i = 0, len = Math.min(partners.length,1000); i < len; i++){
var partner = partners[i];
var clientline = this.partner_cache.get_node(partner.id);
if(!clientline){
var clientline_html = QWeb.render('ClientLine',{widget: this,
partner:partners[i]});
var clientline = document.createElement('tbody');
clientline.innerHTML = clientline_html;
clientline = clientline.childNodes[1];
this.partner_cache.cache_node(partner.id,clientline);
}
if( partner === this.old_client ){
clientline.classList.add('highlight');
}else{
clientline.classList.remove('highlight');
}
contents.appendChild(clientline);
}
},
save_changes: function(){
var self = this;
var order = this.pos.get_order();
if( this.has_client_changed() ){
if ( this.new_client ) {
order.fiscal_position = _.find(this.pos.fiscal_positions, function
(fp) {
return fp.id === self.new_client.property_account_position_id[0];
});
} else {
order.fiscal_position = undefined;
}
order.set_client(this.new_client);
}
},
has_client_changed: function(){
if( this.old_client && this.new_client ){
return this.old_client.id !== this.new_client.id;
}else{
return !!this.old_client !== !!this.new_client;
}
},
toggle_save_button: function(){
var $button = this.$('.button.next');
if (this.editing_client) {
$button.addClass('oe_hidden');
return;
} else if( this.new_client ){
if( !this.old_client){
$button.text(_t('Set Customer'));
}else{
$button.text(_t('Change Customer'));
}
}else{
$button.text(_t('Deselect Customer'));
}
$button.toggleClass('oe_hidden',!this.has_client_changed());
},
line_select: function(event,$line,id){
var partner = this.pos.db.get_partner_by_id(id);
this.$('.client-list .lowlight').removeClass('lowlight');
if ( $line.hasClass('highlight') ){
$line.removeClass('highlight');
$line.addClass('lowlight');
this.display_client_details('hide',partner);
this.new_client = null;
this.toggle_save_button();
}else{
this.$('.client-list .highlight').removeClass('highlight');
$line.addClass('highlight');
var y = event.pageY - $line.parent().offset().top;
this.display_client_details('show',partner,y);
this.new_client = partner;
this.toggle_save_button();
}
},
partner_icon_url: function(id){
return '/web/image?model=res.partner&id='+id+'&field=image_small';
},
// what happens when we save the changes on the client edit form -> we fetch the
fields, sanitize them,
// send them to the backend for update, and call saved_client_details() when the
server tells us the
// save was successfull.
save_client_details: function(partner) {
var self = this;
if (!fields.name) {
this.gui.show_popup('error',_t('A Customer Name Is Required'));
return;
}
if (this.uploaded_picture) {
fields.image = this.uploaded_picture;
}
new
Model('res.partner').call('create_from_ui',[fields]).then(function(partner_id){
self.saved_client_details(partner_id);
},function(err,event){
event.preventDefault();
self.gui.show_popup('error',{
'title': _t('Error: Could not Save Changes'),
'body': _t('Your Internet connection is probably down.'),
});
});
},
canvas.width = width;
canvas.height = height;
ctx.drawImage(img,0,0,width,height);
self.pos.get_order().set_client(self.pos.db.get_partner_by_id(curr_client.id));
}
});
},
contents.off('click','.button.edit');
contents.off('click','.button.save');
contents.off('click','.button.undo');
contents.on('click','.button.edit',function(){
self.edit_client_details(partner); });
contents.on('click','.button.save',function(){
self.save_client_details(partner); });
contents.on('click','.button.undo',function(){
self.undo_client_details(partner); });
this.editing_client = false;
this.uploaded_picture = null;
contents.append($(QWeb.render('ClientDetails',{widget:this,partner:partner})));
var new_height = contents.height();
if(!this.details_visible){
// resize client list to take into account client details
parent.height('-=' + new_height);
this.details_visible = true;
this.toggle_save_button();
} else if (visibility === 'edit') {
this.editing_client = true;
contents.empty();
contents.append($(QWeb.render('ClientDetailsEdit',{widget:this,partner:partner})));
this.toggle_save_button();
contents.find('.image-uploader').on('change',function(event){
self.load_image_file(event.target.files[0],function(res){
if (res) {
contents.find('.client-picture img, .client-picture
.fa').remove();
contents.find('.client-picture').append("<img
src='"+res+"'>");
contents.find('.detail.picture').remove();
self.uploaded_picture = res;
}
});
});
} else if (visibility === 'hide') {
contents.empty();
parent.height('100%');
if( height > scroll ){
contents.css({height:height+'px'});
contents.animate({height:0},400,function(){
contents.css({height:''});
});
}else{
parent.scrollTop( parent.scrollTop() - height);
}
this.details_visible = false;
this.toggle_save_button();
}
},
close: function(){
this._super();
},
});
gui.define_screen({name:'clientlist', widget: ClientListScreenWidget});
/*--------------------------------------*\
| THE RECEIPT SCREEN |
\*======================================*/
this.render_change();
this.render_receipt();
this.handle_auto_print();
},
handle_auto_print: function() {
if (this.should_auto_print()) {
this.print();
if (this.should_close_immediately()){
this.click_next();
}
} else {
this.lock_screen(false);
}
},
should_auto_print: function() {
return this.pos.config.iface_print_auto && !this.pos.get_order()._printed;
},
should_close_immediately: function() {
return this.pos.config.iface_print_via_proxy &&
this.pos.config.iface_print_skip_screen;
},
lock_screen: function(locked) {
this._locked = locked;
if (locked) {
this.$('.next').removeClass('highlight');
} else {
this.$('.next').addClass('highlight');
}
},
print_web: function() {
window.print();
this.pos.get_order()._printed = true;
},
print_xml: function() {
var env = {
widget: this,
order: this.pos.get_order(),
receipt: this.pos.get_order().export_for_printing(),
paymentlines: this.pos.get_order().get_paymentlines()
};
var receipt = QWeb.render('XmlReceipt',env);
this.pos.proxy.print_receipt(receipt);
this.pos.get_order()._printed = true;
},
print: function() {
var self = this;
this.lock_screen(true);
setTimeout(function(){
self.lock_screen(false);
}, 1000);
this.print_web();
} else { // proxy (xml) printing
this.print_xml();
this.lock_screen(false);
}
},
click_next: function() {
this.pos.get_order().finalize();
},
click_back: function() {
// Placeholder method for ReceiptScreen extensions that
// can go back ...
},
renderElement: function() {
var self = this;
this._super();
this.$('.next').click(function(){
if (!self._locked) {
self.click_next();
}
});
this.$('.back').click(function(){
if (!self._locked) {
self.click_back();
}
});
this.$('.button.print').click(function(){
if (!self._locked) {
self.print();
}
});
},
render_change: function() {
this.$('.change-
value').html(this.format_currency(this.pos.get_order().get_change()));
},
render_receipt: function() {
var order = this.pos.get_order();
this.$('.pos-receipt-container').html(QWeb.render('PosTicket',{
widget:this,
order: order,
receipt: order.export_for_printing(),
orderlines: order.get_orderlines(),
paymentlines: order.get_paymentlines(),
}));
},
});
gui.define_screen({name:'receipt', widget: ReceiptScreenWidget});
/*--------------------------------------*\
| THE PAYMENT SCREEN |
\*======================================*/
this.pos.bind('change:selectedOrder',function(){
this.renderElement();
this.watch_order_changes();
},this);
this.watch_order_changes();
this.inputbuffer = "";
this.firstinput = true;
this.decimal_point = _t.database.parameters.decimal_point;
self.payment_input(key);
event.preventDefault();
};
this.pos.bind('change:selectedClient', function() {
self.customer_changed();
}, this);
},
// 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 newbuf = this.gui.numpad_input(this.inputbuffer, input, {'firstinput':
this.firstinput});
order.selected_paymentline.set_amount(amount);
this.order_changes();
this.render_paymentlines();
this.$('.paymentline.selected
.edit').text(this.format_currency_no_symbol(amount));
}
}
},
click_numpad: function(button) {
var paymentlines = this.pos.get_order().get_paymentlines();
var open_paymentline = false;
if (! open_paymentline) {
this.pos.get_order().add_paymentline( this.pos.cashregisters[0]);
this.render_paymentlines();
}
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();
for ( var i = 0; i < lines.length; i++ ) {
if (lines[i].cid === cid) {
this.pos.get_order().remove_paymentline(lines[i]);
this.reset_input();
this.render_paymentlines();
return;
}
}
},
click_paymentline: function(cid){
var lines = this.pos.get_order().get_paymentlines();
for ( var i = 0; i < lines.length; i++ ) {
if (lines[i].cid === cid) {
this.pos.get_order().select_paymentline(lines[i]);
this.reset_input();
this.render_paymentlines();
return;
}
}
},
render_paymentlines: function() {
var self = this;
var order = this.pos.get_order();
if (!order) {
return;
}
this.$('.paymentlines-container').empty();
var lines = $(QWeb.render('PaymentScreen-Paymentlines', {
widget: this,
order: order,
paymentlines: lines,
extradue: extradue,
}));
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().add_paymentline( 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');
}
},
click_tip: function(){
var self = this;
var order = this.pos.get_order();
var tip = order.get_tip();
var change = order.get_change();
var value = tip;
this.gui.show_popup('number',{
'title': tip ? _t('Change Tip') : _t('Add Tip'),
'value': self.format_currency_no_symbol(value),
'confirm': function(value) {
order.set_tip(formats.parse_value(value, {type: "float"}, 0));
self.order_changes();
self.render_paymentlines();
}
});
},
customer_changed: function() {
var client = this.pos.get_client();
this.$('.js_customer_name').text( client ? client.name : _t('Customer') );
},
click_set_customer: function(){
this.gui.show_screen('clientlist');
},
click_back: function(){
this.gui.show_screen('products');
},
renderElement: function() {
var self = this;
this._super();
this.render_paymentlines();
this.$('.back').click(function(){
self.click_back();
});
this.$('.next').click(function(){
self.validate_order();
});
this.$('.js_set_customer').click(function(){
self.click_set_customer();
});
this.$('.js_tip').click(function(){
self.click_tip();
});
this.$('.js_invoice').click(function(){
self.click_invoice();
});
this.$('.js_cashdrawer').click(function(){
self.pos.proxy.open_cashbox();
});
},
show: function(){
this.pos.get_order().clean_empty_paymentlines();
this.reset_input();
this.render_paymentlines();
this.order_changes();
window.document.body.addEventListener('keypress',this.keyboard_handler);
window.document.body.addEventListener('keydown',this.keyboard_keydown_handler);
this._super();
},
hide: function(){
window.document.body.removeEventListener('keypress',this.keyboard_handler);
window.document.body.removeEventListener('keydown',this.keyboard_keydown_handler);
this._super();
},
// sets up listeners to watch for order changes
watch_order_changes: function() {
var self = this;
var order = this.pos.get_order();
if (!order) {
return;
}
if(this.old_order){
this.old_order.unbind(null,null,this);
}
order.bind('all',function(){
self.order_changes();
});
this.old_order = order;
},
// 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) {
return;
} else if (order.is_paid()) {
self.$('.next').addClass('highlight');
}else{
self.$('.next').removeClass('highlight');
}
},
order_is_valid: function(force_validation) {
var self = this;
var order = this.pos.get_order();
if (!order.is_paid() || this.invoicing) {
return false;
}
// The exact amount must be paid if there is no cash payment method defined.
if (Math.abs(order.get_total_with_tax() - order.get_total_paid()) > 0.00001) {
var cash = false;
for (var i = 0; i < this.pos.cashregisters.length; i++) {
cash = cash || (this.pos.cashregisters[i].journal.type === 'cash');
}
if (!cash) {
this.gui.show_popup('error',{
title: _t('Cannot return change without a cash payment method'),
body: _t('There is no cash payment method available in this point
of sale to handle the change.\n\n Please pay the exact amount or add a cash payment
method in the point of sale configuration'),
});
return false;
}
}
// if the change is too large, it's probably an input error, make the user
confirm.
if (!force_validation && order.get_total_with_tax() > 0 &&
(order.get_total_with_tax() * 1000 < order.get_total_paid())) {
this.gui.show_popup('confirm',{
title: _t('Please Confirm Large Amount'),
body: _t('Are you sure that the customer wants to pay') +
' ' +
this.format_currency(order.get_total_paid()) +
' ' +
_t('for an order of') +
' ' +
this.format_currency(order.get_total_with_tax()) +
' ' +
_t('? Clicking "Confirm" will validate the payment.'),
confirm: function() {
self.validate_order('confirm');
},
});
return false;
}
return true;
},
finalize_validation: function() {
var self = this;
var order = this.pos.get_order();
order.initialize_validation_date();
if (order.is_to_invoice()) {
var invoiced = this.pos.push_and_invoice_order(order);
this.invoicing = true;
invoiced.fail(function(error){
self.invoicing = false;
if (error.message === 'Missing Customer') {
self.gui.show_popup('confirm',{
'title': _t('Please select the Customer'),
'body': _t('You need to select the customer before you can
invoice an order.'),
confirm: function(){
self.gui.show_screen('clientlist');
},
});
} else if (error.code < 0) { // XmlHttpRequest Errors
self.gui.show_popup('error',{
'title': _t('The order could not be sent'),
'body': _t('Check your internet connection and try again.'),
});
} else if (error.code === 200) { // OpenERP Server Errors
self.gui.show_popup('error-traceback',{
'title': error.data.message || _t("Server Error"),
'body': error.data.debug || _t('The server encountered an
error while receiving your order.'),
});
} else { // ???
self.gui.show_popup('error',{
'title': _t("Unknown Error"),
'body': _t("The order could not be sent to the server due to
an unknown error"),
});
}
});
invoiced.done(function(){
self.invoicing = false;
self.gui.show_screen('receipt');
});
} else {
this.pos.push_order(order);
this.gui.show_screen('receipt');
}
},
this.pos.bind('change:selectedOrder', function () {
this.renderElement();
}, this);
},
button_click: function () {
var self = this;
var no_fiscal_position = [{
label: _t("None"),
}];
var fiscal_positions = _.map(self.pos.fiscal_positions, function
(fiscal_position) {
return {
label: fiscal_position.name,
item: fiscal_position
};
});
if (order) {
var fiscal_position = order.fiscal_position;
if (fiscal_position) {
name = fiscal_position.display_name;
}
}
return name;
}
});
define_action_button({
'name': 'set_fiscal_position',
'widget': set_fiscal_position_button,
'condition': function(){
return this.pos.fiscal_positions.length > 0;
},
});
return {
ReceiptScreenWidget: ReceiptScreenWidget,
ActionButtonWidget: ActionButtonWidget,
define_action_button: define_action_button,
ScreenWidget: ScreenWidget,
PaymentScreenWidget: PaymentScreenWidget,
OrderWidget: OrderWidget,
NumpadWidget: NumpadWidget,
ProductScreenWidget: ProductScreenWidget,
ProductListWidget: ProductListWidget,
ClientListScreenWidget: ClientListScreenWidget,
ActionpadWidget: ActionpadWidget,
DomCache: DomCache,
ProductCategoriesWidget: ProductCategoriesWidget,
ScaleScreenWidget: ScaleScreenWidget,
set_fiscal_position_button: set_fiscal_position_button,
};
});
POS GUI JS
odoo.define('point_of_sale.gui', function (require) {
"use strict";
// this file contains the Gui, which is the pos 'controller'.
// It contains high level methods to manipulate the interface
// such as changing between screens, creating popups, etc.
//
// it is available to all pos objects trough the '.gui' field.
var _t = core._t;
this.chrome.ready.then(function(){
self.close_other_tabs();
var order = self.pos.get_order();
if (order) {
self.show_saved_screen(order);
} else {
self.show_screen(self.startup_screen);
}
self.pos.bind('change:selectedOrder', function(){
self.show_saved_screen(self.pos.get_order());
});
});
},
// display a screen.
// If there is an order, the screen will be saved in the order
// - params: used to load a screen with parameters, for
// example loading a 'product_details' screen for a specific product.
// - refresh: if you want the screen to cycle trough show / hide even
// if you are already on the same screen.
show_screen: function(screen_name,params,refresh,skip_close_popup) {
var screen = this.screen_instances[screen_name];
if (!screen) {
console.error("ERROR: show_screen("+screen_name+") : screen not found");
}
if (!skip_close_popup){
this.close_popup();
}
var order = this.pos.get_order();
if (order) {
var old_screen_name = order.get_screen_data('screen');
order.set_screen_data('screen',screen_name);
if(params){
order.set_screen_data('params',params);
}
// goes to the previous screen (as specified in the order). The history only
// goes 1 deep ...
back: function() {
var previous = this.pos.get_order().get_screen_data('previous-screen');
if (previous) {
this.show_screen(previous);
}
},
localStorage['message'] = '';
localStorage['message'] = JSON.stringify({
'message':'close_tabs',
'session': this.pos.pos_session.id,
'window_uid': now,
});
// storage events are (most of the time) triggered only when the
// localstorage is updated in a different tab.
// some browsers (e.g. IE) does trigger an event in the same tab
// This may be a browser bug or a different interpretation of the HTML spec
// cf https://connect.microsoft.com/IE/feedback/details/774798/localstorage-
event-fired-in-source-window
// Use window_uid parameter to exclude the current window
window.addEventListener("storage", function(event) {
var msg = event.data;
}, false);
},
return def.then(function(user){
if (options.security && user !== options.current_user &&
user.pos_security_pin) {
return self.ask_password(user.pos_security_pin).then(function(){
return user;
});
} else {
return user;
}
});
},
// checks if the current user (or the user provided) has manager
// access rights. If not, a popup is shown allowing the user to
// temporarily login as an administrator.
// This method returns a deferred, that succeeds with the
// manager user when the login is successfull.
sudo: function(user){
user = user || this.pos.get_cashier();
close: function() {
var self = this;
var pending = this.pos.db.get_orders().length;
if (!pending) {
this._close();
} else {
this.pos.push_order().always(function() {
var pending = self.pos.db.get_orders().length;
if (!pending) {
self._close();
} else {
var reason = self.pos.get('failed') ?
'configuration errors' :
'internet connection issues';
self.show_popup('confirm', {
'title': _t('Offline Orders'),
'body': _t(['Some orders could not be submitted to',
'the server due to ' + reason + '.',
'You can exit the Point of Sale, but do',
'not close the session before the issue',
'has been resolved.'].join(' ')),
'confirm': function() {
self._close();
},
});
}
});
}
},
_close: function() {
var self = this;
this.chrome.loading_show();
this.chrome.loading_message(_t('Closing ...'));
this.pos.push_order().then(function(){
var url = "/web#action=point_of_sale.action_client_pos_menu";
window.location = session.debug ? $.param.querystring(url, {debug:
session.debug}) : url;
});
},
play_sound: function(sound) {
var src = '';
if (sound === 'error') {
src = "/point_of_sale/static/src/sounds/error.wav";
} else if (sound === 'bell') {
src = "/point_of_sale/static/src/sounds/bell.wav";
} else {
console.error('Unknown sound: ',sound);
return;
}
$('body').append('<audio src="'+src+'" autoplay="true"></audio>');
},
$("<a>",href_params).get(0).dispatchEvent(evt);
},
$(target).parent().attr(href_params);
$(src).addClass('oe_hidden');
$(target).removeClass('oe_hidden');
return newbuf;
},
});
return {
Gui: Gui,
define_screen: define_screen,
define_popup: define_popup,
};
});
HAIRSTYLIST JS
odoo.define('pos_snips_updates.hair_stylist', function (require) {
"use strict";
var Class = require('web.Class');
var Model = require('web.Model');
var session = require('web.session');
var core = require('web.core');
var screens = require('point_of_sale.screens');
var gui = require('point_of_sale.gui');
var pos_model = require('point_of_sale.models');
var utils = require('web.utils');
var _t = core._t;
models.load_models({
model: 'hr.employee',
fields: ['name', 'id',],
loaded: function (self, employees) {
self.employees = employees;
self.employees_by_id = {};
for (var i = 0; i < employees.length; i++) {
employees[i].tables = [];
self.employees_by_id[employees[i].id] = employees[i];
}
models.Orderline = models.Orderline.extend({
initialize: function (attr, options) {
_super_orderline.initialize.call(this, attr, options);
// this.hair_stylist_id = this.hair_stylist_id || false;
// this.hair_stylist_name = this.hair_stylist_name || "";
if (!this.hair_stylist_id) {
this.hair_stylist_id = this.pos.hair_stylist_id;
}
if (!this.hair_stylist_name) {
this.hair_stylist_name = this.pos.hair_stylist_name;
}
},
set_hair_stylist_id: function (hair_stylist_id) {
this.hair_stylist_id = hair_stylist_id;
this.trigger('change', this);
},
get_hair_stylist_id: function () {
return this.hair_stylist_id;
},
set_hair_stylist_name: function (hair_stylist_name) {
this.hair_stylist_name = hair_stylist_name;
this.trigger('change', this);
},
get_hair_stylist_name: function () {
return this.hair_stylist_name;
},
clone: function () {
var orderline = _super_orderline.clone.call(this);
orderline.hair_stylist_id = this.hair_stylist_id;
orderline.hair_stylist_name = this.hair_stylist_name;
return orderline;
},
export_as_JSON: function () {
var json = _super_orderline.export_as_JSON.call(this);
json.hair_stylist_id = this.hair_stylist_id;
json.hair_stylist_name = this.hair_stylist_name;
return json;
},
init_from_JSON: function (json) {
_super_orderline.init_from_JSON.apply(this, arguments);
this.hair_stylist_id = json.hair_stylist_id;
this.hair_stylist_name = json.hair_stylist_name;
},
});
console.log("OrderlineHairStylistButton");
var hair_stylists=this.pos.employees;
var hair_stylists_length=hair_stylists.length;
list.push({
'label': hair_stylist.name,
'item': hair_stylist,
});
}
//
//
var the_seleted=line.get_hair_stylist_name();
this.gui.show_popup('selection',{
'title':_t('Select Hair Stylist'),
list: list,
confirm: function (item) {
console.log("Item");
console.log(item);
line.set_hair_stylist_id(item.id);
line.set_hair_stylist_name(item.name);
},
cancel: function () { },
});
}
},
});
screens.define_action_button({
'name': 'orderline_note',
'widget': OrderlineHairStylistButton,
});
});
INTERNAL REFERENCE JS
odoo.define('pos_snips_updates.internal_reference', function (require) {
"use strict";
// New orders are now associated with the current table, if any.
var _super_order = models.Order.prototype;
models.Order = models.Order.extend({
initialize: function () {
console.log("internal_reference start")
_super_order.initialize.apply(this, arguments);
if (!this.internal_reference) {
this.internal_reference = this.pos.internal_reference;
}
this.save_to_db();
},
export_as_JSON: function () {
var json = _super_order.export_as_JSON.apply(this, arguments);
json.internal_reference = this.internal_reference;
return json;
},
init_from_JSON: function (json) {
_super_order.init_from_JSON.apply(this, arguments);
this.internal_reference = json.internal_reference || '';
},
export_for_printing: function () {
var json = _super_order.export_for_printing.apply(this, arguments);
json.internal_reference = this.get_internal_reference();
return json;
},
get_internal_reference: function () {
return this.internal_reference;
},
set_internal_reference: function (internal_reference) {
this.internal_reference = internal_reference;
this.trigger('change');
},
});
button_click: function () {
var self = this;
if (session_id) {
} else {
var session_id = utils.get_cookie("session_id");
}
console.log("session_id=" + session_id);
if (session_id) {
//
self.do_action('pos_customer_history.action_report_customer_history', {
// additional_context: { active_ids: [self.so_id.id], } });
}
},
});
screens.OrderWidget.include({
update_summary: function () {
this._super();
if (this.getParent().action_buttons &&
this.getParent().action_buttons.internal_reference) {
this.getParent().action_buttons.internal_reference.renderElement();
}
},
});
screens.define_action_button({
'name': 'internal_reference',
'widget': InternalReferenceButton,
});
screens.define_action_button({
'name': 'PrintSessionButton',
'widget': PrintSessionButton,
});
});
POS CUSTOMER HISTORY JS
odoo.define('pos_customer_history.customer_history', function (require) {
"use strict";
},
show: function () {
var self = this;
this._super();
this.$('.customer-history').click(function () {
// alert("customer");
console.log("Action
pos_customer_history.action_report_customer_history");
if (self.new_client) {
console.log("Client id=" + self.new_client.id)
self.do_action('pos_customer_history.action_report_customer_history', {
additional_context: { active_ids: [self.new_client.id], }
});
}
});
},
get_count_orders: function () {
return this.count_orders;
},
set_count_orders: function (count_orders) {
this.count_orders = count_orders;
this.trigger('change');
},
get_last_visit: function () {
return this.last_visit;
},
set_last_visit: function (last_visit) {
this.last_visit = last_visit;
this.trigger('change');
},
get_average: function () {
return this.average;
},
set_average: function (average) {
this.average = average;
this.trigger('change');
},
get_total_orders: function () {
return this.total_orders;
},
set_total_orders: function (total_orders) {
this.total_orders = total_orders;
this.trigger('change');
},
display_client_details: function (visibility, partner, clickpos) {
var self = this;
var contents = this.$('.client-details-contents');
var parent = this.$('.client-list').parent();
var scroll = parent.scrollTop();
var height = contents.height();
contents.off('click', '.button.edit');
contents.off('click', '.button.save');
contents.off('click', '.button.undo');
contents.on('click', '.button.edit', function () {
self.edit_client_details(partner);
});
contents.on('click', '.button.save', function () {
self.save_client_details(partner);
});
contents.on('click', '.button.undo', function () {
self.undo_client_details(partner);
});
this.editing_client = false;
this.uploaded_picture = null;
posOrderModel.call('get_partner_history',[partner.id]).then(function(history_lst) {
contents.empty();
temp.set_last_visit(history_lst[3]);
temp.set_average(history_lst[2]);
temp.set_count_orders(history_lst[1]);
temp.set_total_orders(history_lst[0]);
contents.append($(QWeb.render('ClientDetails',{widget:temp,partner:partner})));
console.log(temp);
});
if (!this.details_visible) {
// resize client list to take into account client details
parent.height('-=' + new_height);
this.details_visible = true;
this.toggle_save_button();
} else if (visibility === 'edit') {
this.editing_client = true;
contents.empty();
contents.append($(QWeb.render('ClientDetailsEdit', {widget: this,
partner: partner})));
this.toggle_save_button();
});
});