Odoo Javascript
Odoo Javascript
// [Link] 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 = [Link] = {};
}
// Require Underscore, if we're on the server, and it's not already present.
var _ = root._;
if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
// [Link]
// ---------------
// 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() {
[Link](name, once);
[Link](this, arguments);
});
once._callback = callback;
return [Link](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 = [Link](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);
// [Link]
// --------------
// 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([Link]);
[Link] = {};
}
current = [Link], 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;
[Link]('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([Link]);
return _.has([Link], 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 ([Link] === void 0) [Link] = true;
var model = this;
var success = [Link];
[Link] = function(resp) {
if (, options)) return false;
if (success) success(model, resp, options);
[Link]('sync', model, resp, options);
};
wrapError(this, options);
return [Link]('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 = [Link];
// Restore attributes.
if (attrs && [Link]) [Link] = attributes;
return xhr;
},
[Link] = function(resp) {
if ([Link] || [Link]()) destroy();
if (success) success(model, resp, options);
if (![Link]()) [Link]('sync', model, resp, options);
};
if ([Link]()) {
[Link]();
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 [Link] == null;
},
});
// [Link]
// -------------------
// Turn bare objects into model references, and prevent invalid models
// from being added.
for (i = 0, l = [Link]; i < l; i++) {
attrs = models[i];
if (attrs instanceof Model) {
id = model = attrs;
} else {
id = attrs[[Link]];
}
// 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 = [Link]; i < l; i++) {
this._removeReference([Link][i]);
}
[Link] = [Link];
this._reset();
models = [Link](models, _.extend({silent: true}, options));
if (![Link]) [Link]('reset', this, options);
return models;
},
// Return the first model with matching attributes. Useful for simple cases
// of `find`.
findWhere: function(attrs) {
return [Link](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 (![Link]) 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 ([Link] === void 0) [Link] = true;
var success = [Link];
var collection = this;
[Link] = function(resp) {
var method = [Link] ? 'reset' : 'set';
collection[method](resp, options);
if (success) success(collection, resp, options);
[Link]('sync', collection, resp, options);
};
wrapError(this, options);
return [Link]('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 (![Link]) [Link](model, options);
var collection = this;
var success = [Link];
[Link] = function(model, resp, options) {
if ([Link]) [Link](model, options);
if (success) success(model, resp, options);
};
[Link](null, options);
return model;
},
// Private method to reset all internal state. Called when the collection
// is first initialized or reset.
_reset: function() {
[Link] = 0;
[Link] = [];
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') [Link](model, options);
if (model && event === 'change:' + [Link]) {
delete this._byId[[Link]([Link])];
if ([Link] != null) this._byId[[Link]] = model;
}
[Link](this, arguments);
}
});
// [Link]
// -------------
// 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.$[Link](selector);
},
// **render** is the core function that your view should override, in order
// to populate its element (`[Link]`), 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 [Link] listeners.
remove: function() {
this.$[Link]();
[Link]();
return this;
},
});
// [Link]
// -------------
// For older servers, emulate JSON by encoding the request into an HTML-form.
if ([Link]) {
[Link] = 'application/x-www-form-urlencoded';
[Link] = [Link] ? {model: [Link]} : {};
}
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
// And an `X-HTTP-Method-Override` header.
if ([Link] && (type === 'PUT' || type === 'DELETE' || type ===
'PATCH')) {
[Link] = 'POST';
if ([Link]) [Link]._method = type;
var beforeSend = [Link];
[Link] = function(xhr) {
[Link]('X-HTTP-Method-Override', type);
if (beforeSend) return [Link](this, arguments);
};
}
// Make the request, allowing the user to override any Ajax options.
var xhr = [Link] = [Link](_.extend(params, options));
[Link]('request', model, xhr, options);
return xhr;
};
var noXhrPatch = typeof window !== 'undefined' && !![Link] &&
!([Link] && (new XMLHttpRequest).dispatchEvent);
// [Link]
// ---------------
// 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 = [Link] = function(options) {
options || (options = {});
if ([Link]) [Link] = [Link];
this._bindRoutes();
[Link](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 = [Link](fragment).slice(1);
return _.map(params, function(param) {
return param ? decodeURIComponent(param) : null;
});
}
});
// [Link]
// ----------------
// Gets the true hash value. Cannot use [Link] directly due to bug
// in Firefox where [Link] will always be decoded.
getHash: function(window) {
var match = (window || this).[Link](/#(.*)$/);
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 = [Link];
var root = [Link](trailingSlash, '');
if () fragment = [Link]([Link]);
} else {
fragment = [Link]();
}
}
return [Link](routeStripper, '');
},
// Start the hash change handling, returning `true` if the current URL matches
// an existing route, and `false` otherwise.
start: function(options) {
if ([Link]) throw new Error("[Link] has already been
started");
[Link] = true;
// Add a route to be tested when the fragment changes. Routes added later
// may override previous routes.
route: function(route, callback) {
[Link]({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 (![Link]) 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 = [Link](/(javascript:|#).*$/, '');
[Link](href + '#' + fragment);
} else {
// Some browsers require that `hash` contains a leading #.
[Link] = '#' + 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 = [Link];
} else {
child = function(){ return [Link](this, arguments); };
}
return child;
};
// Set up inheritance for the model, collection, router, view and history.
[Link] = [Link] = [Link] = [Link] = [Link] =
extend;
}).call(this);
POS Models JS
[Link]('point_of_sale.models', function (require) {
"use strict";
[Link] = [Link]({
initialize: function(session, attributes) {
[Link](this, attributes);
var self = this;
this.flush_mutex = new Mutex(); // used to make sure the
orders are sent to the server once at time
[Link] = [Link];
[Link] = [Link];
[Link]('orders').bind('remove', function(order,_unused_,options){
self.on_removed_order(order,[Link],[Link]);
});
// 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.
[Link] = 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([Link].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
// [Link]();
[Link]();
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();
[Link].loading_message(_t('Connecting to the PosBox'),0);
[Link].loading_skip(function(){
[Link].stop_searching();
});
[Link]({
force_ip: [Link].proxy_ip || undefined,
progress: function(prog){
[Link].loading_progress(prog);
},
}).then(function(){
if([Link].iface_scan_via_proxy){
self.barcode_reader.connect_to_proxy();
}
}).always(function(){
[Link]();
});
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
[Link]('/web/webclient/version_info',{}).done(function(version) {
[Link] = version;
});
},
},{
model: '[Link]',
fields: ['name','company_id'],
ids: function(self){ return [[Link]]; },
loaded: function(self,users){ [Link] = users[0]; },
},{
model: '[Link]',
fields: [ 'currency_id', 'email', 'website', 'company_registry', 'vat',
'name', 'phone', 'partner_id' , 'country_id', 'tax_calculation_rounding_method'],
ids: function(self){ return [[Link].company_id[0]]; },
loaded: function(self,companies){ [Link] = companies[0]; },
},{
model: '[Link]',
fields: ['name','digits'],
loaded: function(self,dps){
[Link] = {};
for (var i = 0; i < [Link]; i++) {
[Link][dps[i].name] = dps[i].digits;
}
},
},{
model: '[Link]',
fields: [],
domain: null,
context: function(self){ return { active_test: false }; },
loaded: function(self,units){
[Link] = units;
var units_by_id = {};
for(var i = 0, len = [Link]; 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: '[Link]',
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){
[Link] = partners;
[Link].add_partners(partners);
},
},{
model: '[Link]',
fields: ['name'],
loaded: function(self,countries){
[Link] = countries;
[Link] = null;
for (var i = 0; i < [Link]; i++) {
if (countries[i].id === [Link].country_id[0]){
[Link] = countries[i];
}
}
},
},{
model: '[Link]',
fields: ['name','amount', 'price_include', 'include_base_amount',
'amount_type', 'children_tax_ids'],
domain: null,
loaded: function(self, taxes){
[Link] = taxes;
self.taxes_by_id = {};
_.each(taxes, function(tax){
self.taxes_by_id[[Link]] = 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: '[Link]',
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','=',[Link]]]; },
loaded: function(self,pos_sessions){
self.pos_session = pos_sessions[0];
},
},{
model: '[Link]',
fields: [],
domain: function(self){ return [['id','=', self.pos_session.config_id[0]]]; },
loaded: function(self,configs){
[Link] = configs[0];
[Link].use_proxy = [Link].iface_payment_terminal ||
[Link].iface_electronic_scale ||
[Link].iface_print_via_proxy ||
[Link].iface_scan_via_proxy ||
[Link].iface_cashdrawer;
[Link].set_uuid([Link]);
},
},{
model: '[Link]',
fields: ['id','name','parent_id','child_id','image'],
domain: null,
loaded: function(self, categories){
[Link].add_categories(categories);
},
},{
model: '[Link]',
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: [Link],
display_default_code: false }; },
loaded: function(self, products){
[Link].add_products(products);
},
},{
model: '[Link]',
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){
[Link] = cashregisters;
[Link] = [];
_.each(cashregisters,function(statement){
[Link](statement.journal_id[0]);
});
},
},{
model: '[Link]',
fields: ['type', 'sequence'],
domain: function(self,tmp){ return [['id','in',[Link]]]; },
loaded: function(self, journals){
var i;
[Link] = journals;
self.cashregisters_by_id = {};
for (i = 0; i < [Link]; i++) {
self.cashregisters_by_id[[Link][i].id] =
[Link][i];
}
[Link] = [Link](function(a,b){
// prefer cashregisters to be first in the list
if ([Link] == "cash" && [Link] != "cash") {
return -1;
} else if ([Link] != "cash" && [Link] == "cash") {
return 1;
} else {
return [Link] - [Link];
}
});
},
}, {
model: '[Link]',
fields: [],
domain: function(self){ return [['id','in',[Link].fiscal_position_ids]];
},
loaded: function(self, fiscal_positions){
self.fiscal_positions = fiscal_positions;
}
}, {
model: '[Link]',
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( [Link] !== targetwidth ){
ratio = targetwidth / [Link];
}
if( [Link] * ratio > maxheight ){
ratio = maxheight / [Link];
}
var width = [Link]([Link] * ratio);
var height = [Link]([Link] * ratio);
var c = [Link]('canvas');
[Link] = width;
[Link] = height;
var ctx = [Link]('2d');
[Link](self.company_logo,0,0, width, height);
self.company_logo_base64 = [Link]();
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=' +
[Link] + '&_'+[Link]();
return logo_loaded;
},
}, {
label: 'barcodes',
loaded: function(self) {
var barcode_parser = new BarcodeParser({'nomenclature_id':
[Link].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 / [Link];
var tmp = {}; // this is used to share a temporary state between models
loaders
function load_model(index){
if(index >= [Link]){
[Link]();
}else{
var model = [Link][index];
[Link].loading_message(_t('Loading')+' '+([Link] ||
[Link] || ''), progress);
var records;
if( [Link] ){
if ([Link]) {
records = new
Model([Link]).call('read',[ids,fields],context);
} else {
records = new Model([Link])
.query(fields)
.filter(domain)
.order_by(order)
.context(context)
.all();
}
[Link](function(result){
try{ // catching exceptions in [Link](...)
$.when([Link](self,result,tmp))
.then(function(){ load_model(index + 1); },
function(err){ [Link](err); });
}catch(err){
[Link]([Link]);
[Link](err);
}
},function(err){
[Link](err);
});
}else if( [Link] ){
try{ // catching exceptions in [Link](...)
$.when([Link](self,tmp))
.then( function(){ load_model(index +1); },
function(err){ [Link](err); });
}catch(err){
[Link](err);
}
}else{
load_model(index + 1);
}
}
}
try{
load_model(0);
}catch(err){
[Link](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([Link],function(model){ return [Link] ===
'[Link]'; }).fields;
new Model('[Link]')
.query(fields)
.filter([['customer','=',true],['write_date','>',[Link].get_partner_write_date()]])
.all({'timeout':3000, 'shadow': true})
.then(function(partners){
if ([Link].add_partners(partners)) { // check if the partners we
got were real updates
[Link]();
} else {
[Link]();
}
}, function(err,event){ [Link](); [Link](); });
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 [Link] || [Link];
},
// changes the current cashier
set_cashier: function(user){
[Link] = user;
},
//creates a new empty order and sets it as the current order
add_new_order: function(){
var order = new [Link]({},{pos:this});
[Link]('orders').add(order);
[Link]('selectedOrder', order);
return order;
},
// load the locally saved unpaid orders for this session.
load_orders: function(){
var jsons = [Link].get_unpaid_orders();
var orders = [];
var not_loaded_count = 0;
if (not_loaded_count) {
[Link]('There are '+not_loaded_count+' locally saved unpaid orders
belonging to another session');
}
orders = [Link](function(a,b){
return a.sequence_number - b.sequence_number;
});
if ([Link]) {
[Link]('orders').add(orders);
}
},
set_start_order: function(){
var orders = [Link]('orders').models;
get_client: function() {
var order = this.get_order();
if (order) {
return order.get_client();
}
return null;
},
if(order){
[Link].add_order(order.export_as_JSON());
}
this.flush_mutex.exec(function(){
var flushed = self._flush_orders([Link].get_orders(), opts);
[Link](function(ids){
[Link]();
});
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()){
[Link]({code:400, message:'Missing Customer', data:{}});
return invoiced;
}
this.flush_mutex.exec(function(){
var done = new $.Deferred(); // holds the mutex
[Link](function(error){
[Link](error);
[Link]();
});
[Link].do_action('point_of_sale.pos_invoice_report',{additional_context:{
active_ids:order_server_id,
}});
[Link]();
[Link]();
});
return done;
});
return invoiced;
},
// wrapper around the _save_to_server that updates the synch status widget
_flush_orders: function(orders, options) {
var self = this;
[Link]('synch',{ state: 'connecting', pending: [Link]});
[Link]('synch', {
state: pending ? 'connecting' : 'connected',
pending: pending
});
return server_ids;
}).fail(function(error, event){
var pending = [Link].get_orders().length;
if ([Link]('failed')) {
[Link]('synch', { state: 'error', pending: pending });
} else {
[Link]('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('[Link]');
return [Link]('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) {
[Link].remove_order(order_id);
});
[Link]('failed',false);
return server_ids;
}).fail(function (error, event){
if([Link] === 200 ){ // Business Logic Error, not a connection
problem
//if warning do not need to display traceback!!
if ([Link].exception_type == 'warning') {
delete [Link];
}
// Hide error if already shown before ...
if (( || options.show_error) &&
!options.to_invoice) {
[Link].show_popup('error-traceback',{
'title': [Link],
'body': [Link]
});
}
[Link]('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
[Link]();
[Link]('Failed to send orders:', orders);
});
},
scan_product: function(parsed_code){
var selectedOrder = this.get_order();
var product = [Link].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 [Link]({
'paid_orders': [Link].get_orders(),
'session': this.pos_session.name,
'session_id': this.pos_session.id,
'date': (new Date()).toUTCString(),
'version': [Link].server_version_info,
},null,2);
},
if (json.paid_orders) {
for (var i = 0; i < json.paid_orders.length; i++) {
[Link].add_order(json.paid_orders[i].data);
}
[Link] = json.paid_orders.length;
this.push_order();
}
if (json.unpaid_orders) {
orders = [Link](function(a,b){
return a.sequence_number - b.sequence_number;
});
if ([Link]) {
[Link] = [Link];
[Link]('orders').add(orders);
}
report.unpaid_skipped_sessions = _.keys(skipped_sessions);
}
return report;
},
_load_orders: function(){
var jsons = [Link].get_unpaid_orders();
var orders = [];
var not_loaded_count = 0;
if (not_loaded_count) {
[Link]('There are '+not_loaded_count+' locally saved unpaid orders
belonging to another session');
}
orders = [Link](function(a,b){
return a.sequence_number - b.sequence_number;
});
if ([Link]) {
[Link]('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 [Link] === valid_product_lot.length;
},
if (current_line.length) {
[Link](current_line);
}
return wrapped;
},
// changes the base price of the product for this orderline
set_unit_price: function(price){
[Link].assert_editable();
[Link] = round_di(parseFloat(price) || 0, [Link]['Product Price']);
[Link]('change',this);
},
get_unit_price: function(){
var digits = [Link]['Product Price'];
// round and truncate to mimic _sybmbol_set behavior
return parseFloat(round_di([Link] || 0, digits).toFixed(digits));
},
get_unit_display_price: function(){
if ([Link].iface_tax_included) {
var quantity = [Link];
[Link] = 1.0;
var price = this.get_all_prices().priceWithTax;
[Link] = quantity;
return price;
} else {
return this.get_unit_price();
}
},
get_base_price: function(){
var rounding = [Link];
return round_pr(this.get_unit_price() * this.get_quantity() * (1 -
this.get_discount()/100), rounding);
},
get_display_price: function(){
if ([Link].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 < [Link]; i++) {
if (ptaxes_set[[Link][i].id]) {
[Link]([Link][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++) {
[Link]([Link].taxes_by_id[taxes_ids[i]]);
}
return taxes;
},
_map_tax_fiscal_position: function(tax) {
var current_order = [Link].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] === [Link];
});
if (mapped_tax) {
tax = [Link].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 ([Link]([Link]) * sign_base_amount) * quantity;
}
if ((tax.amount_type === 'percent' && !tax.price_include) || (tax.amount_type
=== 'division' && tax.price_include)){
return base_amount * [Link] / 100;
}
if (tax.amount_type === 'percent' && tax.price_include){
return base_amount - (base_amount / (1 + [Link] / 100));
}
if (tax.amount_type === 'division' && !tax.price_include) {
return base_amount / (1 - [Link] / 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 ([Link].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([Link]);
}
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: [Link],
amount: tax_amount,
name: [Link],
};
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 [Link] === el;
}));
});
return {
"priceWithTax": all_taxes.total_included,
"priceWithoutTax": all_taxes.total_excluded,
"tax": taxtotal,
"taxDetails": taxdetail,
};
},
});
[Link] = [Link]({
defaults: {
lot_name: null
},
initialize: function(attributes, options){
this.order_line = options.order_line;
if ([Link]) {
this.init_from_JSON([Link]);
return;
}
},
init_from_JSON: function(json) {
this.order_line = json.order_line;
this.set_lot_name(json.lot_name);
},
set_lot_name: function(name){
[Link]({lot_name : _.[Link](name) || null});
},
get_lot_name: function(){
return [Link]('lot_name');
},
export_as_JSON: function(){
return {
lot_name: this.get_lot_name(),
};
},
add: function(){
var order_line = this.order_line,
index = [Link](this);
var new_lot_model = new [Link]({}, {'order_line':
this.order_line});
[Link](new_lot_model, {at: index + 1});
return new_lot_model;
},
remove: function(){
[Link](this);
}
});
var PacklotlineCollection = [Link]({
model: [Link],
initialize: function(models, options) {
this.order_line = options.order_line;
},
get_empty_model: function(){
return [Link]({'lot_name': null});
},
remove_empty_model: function(){
[Link]([Link]({'lot_name': null}));
},
get_valid_lots: function(){
return [Link](function(model){
return [Link]('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.
[Link] = [Link]({
initialize: function(attributes,options){
[Link](this, arguments);
var self = this;
options = options || {};
this.init_locked = true;
[Link] = [Link];
this.selected_orderline = undefined;
this.selected_paymentline = undefined;
this.screen_data = {}; // see Gui
[Link] = [Link] || false;
this.creation_date = new Date();
this.to_invoice = false;
[Link] = new OrderlineCollection();
[Link] = new PaymentlineCollection();
this.pos_session_id = [Link].pos_session.id;
[Link] = false; // if true, cannot be modified.
if ([Link]) {
this.init_from_JSON([Link]);
} else {
this.sequence_number = [Link].pos_session.sequence_number++;
[Link] = this.generate_unique_id();
[Link] = _t("Order ") + [Link];
this.validation_date = undefined;
this.fiscal_position = _.find([Link].fiscal_positions, function(fp) {
return [Link] === [Link].default_fiscal_position_id[0];
});
}
this.init_locked = false;
this.save_to_db();
return this;
},
save_to_db: function(){
if (![Link] && !this.init_locked) {
[Link].save_unpaid_order(this);
}
},
init_from_JSON: function(json) {
var client;
this.sequence_number = json.sequence_number;
[Link].pos_session.sequence_number =
[Link](this.sequence_number+1,[Link].pos_session.sequence_number);
this.session_id = json.pos_session_id;
[Link] = [Link];
[Link] = _t("Order ") + [Link];
this.validation_date = json.creation_date;
if (json.fiscal_position_id) {
var fiscal_position = _.find([Link].fiscal_positions, function (fp) {
return [Link] === json.fiscal_position_id;
});
if (fiscal_position) {
this.fiscal_position = fiscal_position;
} else {
[Link]('ERROR: trying to load a fiscal position not available
in the pos');
}
}
if (json.partner_id) {
client = [Link].get_partner_by_id(json.partner_id);
if (!client) {
[Link]('ERROR: trying to load a parner not available in the
pos');
}
} else {
client = null;
}
this.set_client(client);
if (i === [Link] - 1) {
this.select_paymentline(newpaymentline);
}
}
},
export_as_JSON: function() {
var orderLines, paymentLines;
orderLines = [];
[Link](_.bind( function(item) {
return [Link]([0, 0, item.export_as_JSON()]);
}, this));
paymentLines = [];
[Link](_.bind( function(item) {
return [Link]([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: [Link] ? [Link] : [Link],
uid: [Link],
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;
[Link](function(orderline){
[Link](orderline.export_for_printing());
});
function is_xml(subreceipt){
return subreceipt ? ([Link]('\n')[0].indexOf('<!DOCTYPE QWEB')
>= 0) : false;
}
function render_xml(subreceipt){
if (!is_xml(subreceipt)) {
return subreceipt;
} else {
subreceipt = [Link]('\n').slice(1).join('\n');
var qweb = new [Link]();
[Link] = [Link];
qweb.default_dict = _.clone(QWeb.default_dict);
qweb.add_template('<templates><t t-
name="subreceipt">'+subreceipt+'</t></templates>');
return
[Link]('subreceipt',{'pos':[Link],'widget':[Link],'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 ? [Link] : null ,
invoice_id: null, //TODO
cashier: cashier ? [Link] : null,
precision: {
price: 2,
money: 2,
quantity: 3,
},
date: {
year: [Link](),
month: [Link](),
date: [Link](), // day of the month
day: [Link](), // day of the week
hour: [Link](),
minute: [Link]() ,
isostring: [Link](),
localestring: [Link](),
},
company:{
email: [Link],
website: [Link],
company_registry: company.company_registry,
contact_address: company.partner_id[1],
vat: [Link],
name: [Link],
phone: [Link],
logo: [Link].company_logo_base64,
},
shop:{
name: [Link],
},
currency: [Link],
};
if (is_xml([Link].receipt_header)){
[Link] = '';
receipt.header_xml = render_xml([Link].receipt_header);
} else {
[Link] = [Link].receipt_header || '';
}
if (is_xml([Link].receipt_footer)){
[Link] = '';
receipt.footer_xml = render_xml([Link].receipt_footer);
} else {
[Link] = [Link].receipt_footer || '';
}
return receipt;
},
is_empty: function(){
return [Link] === 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 ([Link] < size) {
s = "0" + s;
}
return s;
}
return zero_pad([Link].pos_session.id,5) +'-'+
zero_pad([Link].pos_session.login_number,3) +'-'+
zero_pad(this.sequence_number,4);
},
get_name: function() {
return [Link];
},
assert_editable: function() {
if ([Link]) {
throw new Error('Finalized Order cannot be modified');
}
},
/* ---- Order Lines --- */
add_orderline: function(line){
this.assert_editable();
if([Link]){
[Link].remove_orderline(line);
}
[Link] = this;
[Link](line);
this.select_orderline(this.get_last_orderline());
},
get_orderline: function(id){
var orderlines = [Link];
for(var i = 0; i < [Link]; i++){
if(orderlines[i].id === id){
return orderlines[i];
}
}
return null;
},
get_orderlines: function(){
return [Link];
},
get_last_orderline: function(){
return [Link]([Link] -1);
},
get_tip: function() {
var tip_product =
[Link].get_product_by_id([Link].tip_product_id[0]);
var lines = this.get_orderlines();
if (!tip_product) {
return 0;
} else {
for (var i = 0; i < [Link]; 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 =
[Link].get_product_by_id([Link].tip_product_id[0]);
var lines = this.get_orderlines();
if (tip_product) {
for (var i = 0; i < [Link]; 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();
[Link](line);
this.select_orderline(this.get_last_orderline());
},
fix_tax_included_price: function(line){
if(this.fiscal_position){
var unit_price = [Link];
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 && [Link] != 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();
[Link].show_popup('packlotline', {
'title': _t('Lot/Serial Number(s) Required'),
'pack_lot_lines': pack_lot_lines,
'order': this
});
}
},
},
get_paymentlines: function(){
return [Link];
},
remove_paymentline: function(line){
this.assert_editable();
if(this.selected_paymentline === line){
this.select_paymentline(undefined);
}
[Link](line);
},
clean_empty_paymentlines: function() {
var lines = [Link];
var empty = [];
for ( var i = 0; i < [Link]; i++) {
if (!lines[i].get_amount()) {
[Link](lines[i]);
}
}
for ( var i = 0; i < [Link]; 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);
}
[Link]('change:selected_paymentline',this.selected_paymentline);
}
},
/* ---- Payment Status --- */
get_subtotal : function(){
return round_pr([Link]((function(sum, orderLine){
return sum + orderLine.get_display_price();
}), 0), [Link]);
},
get_total_with_tax: function() {
return this.get_total_without_tax() + this.get_total_tax();
},
get_total_without_tax: function() {
return round_pr([Link]((function(sum, orderLine) {
return sum + orderLine.get_price_without_tax();
}), 0), [Link]);
},
get_total_discount: function() {
return round_pr([Link]((function(sum, orderLine) {
return sum + (orderLine.get_unit_price() * (orderLine.get_discount()/100)
* orderLine.get_quantity());
}), 0), [Link]);
},
get_total_tax: function() {
return round_pr([Link]((function(sum, orderLine) {
return sum + orderLine.get_tax();
}), 0), [Link]);
},
get_total_paid: function() {
return round_pr([Link]((function(sum, paymentLine) {
return sum + paymentLine.get_amount();
}), 0), [Link]);
},
get_tax_details: function(){
var details = {};
var fulldetails = [];
[Link](function(line){
var ldetails = line.get_tax_details();
for(var id in ldetails){
if([Link](id)){
details[id] = (details[id] || 0) + ldetails[id];
}
}
});
for(var id in details){
if([Link](id)){
[Link]({amount: details[id], tax: [Link].taxes_by_id[id],
name: [Link].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;
[Link](function(line){
if ( [Link].category_contains(categ_id,[Link]) ) {
total += line.get_price_with_tax();
}
});
return total;
},
get_total_for_taxes: function(tax_id){
var total = 0;
[Link](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 = [Link];
for (var i = 0; i < [Link]; i++) {
change += lines[i].get_amount();
if (lines[i] === paymentline) {
break;
}
}
}
return round_pr([Link](0,change), [Link]);
},
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 = [Link];
for (var i = 0; i < [Link]; i++) {
if (lines[i] === paymentline) {
break;
} else {
due -= lines[i].get_amount();
}
}
}
return round_pr([Link](0,due), [Link]);
},
is_paid: function(){
return this.get_due() === 0;
},
is_paid_with_cash: function(){
return !{
return [Link] === 'cash';
});
},
finalize: function(){
[Link]();
},
destroy: function(){
[Link](this,arguments);
[Link].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();
[Link]('client',client);
},
get_client: function(){
return [Link]('client');
},
get_client_name: function(){
var client = [Link]('client');
return client ? [Link] : "";
},
/* ---- 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([Link] === 2){
this.screen_data[key] = value;
}else if([Link] === 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.
*/
[Link] = [Link]({
defaults: {
buffer: "0",
mode: "quantity"
},
appendNewChar: function(newChar) {
var oldBuffer;
oldBuffer = [Link]('buffer');
if (oldBuffer === '0') {
[Link]({
buffer: newChar
});
} else if (oldBuffer === '-0') {
[Link]({
buffer: "-" + newChar
});
} else {
[Link]({
buffer: ([Link]('buffer')) + newChar
});
}
[Link]('set_value',[Link]('buffer'));
},
deleteLastChar: function() {
if([Link]('buffer') === ""){
if([Link]('mode') === 'quantity'){
[Link]('set_value','remove');
}else{
[Link]('set_value',[Link]('buffer'));
}
}else{
var newBuffer = [Link]('buffer').slice(0,-1) || "";
[Link]({ buffer: newBuffer });
[Link]('set_value',[Link]('buffer'));
}
},
switchSign: function() {
var oldBuffer;
oldBuffer = [Link]('buffer');
[Link]({
buffer: oldBuffer[0] === '-' ? [Link](1) : "-" + oldBuffer
});
[Link]('set_value',[Link]('buffer'));
},
changeMode: function(newMode) {
[Link]({
buffer: "0",
mode: newMode
});
},
reset: function() {
[Link]({
buffer: "0",
mode: "quantity"
});
},
resetValue: function(){
[Link]({buffer:'0'});
},
});
// exports = {
// PosModel: PosModel,
// NumpadState: NumpadState,
// load_fields: load_fields,
// load_models: load_models,
// Orderline: Orderline,
// Order: Order,
// };
return exports;
});
POS Screens JS
[Link]('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 [Link] 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
// [Link]() 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);
[Link] = false;
},
[Link] = false;
if(this.$el){
this.$[Link]('oe_hidden');
}
[Link].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([Link].barcode_reader){
[Link].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(){
[Link] = true;
if(this.$el){
this.$[Link]('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([Link]){
if(this.$el){
this.$[Link]('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.
[Link] = {};
this.access_time = {};
[Link] = 0;
},
cache_node: function(key,node){
var cached = [Link][key];
[Link][key] = node;
this.access_time[key] = new Date().getTime();
if(!cached){
[Link]++;
while([Link] >= this.max_size){
var oldest_key = null;
var oldest_time = new Date().getTime();
for(key in [Link]){
var time = this.access_time[key];
if(time <= oldest_time){
oldest_time = time;
oldest_key = key;
}
}
if(oldest_key){
delete [Link][oldest_key];
delete this.access_time[oldest_key];
}
[Link]--;
}
}
return node;
},
clear_node: function(key) {
var cached = [Link][key];
if (cached) {
delete [Link][key];
delete this.access_time[key];
[Link] --;
}
},
get_node: function(key){
var cached = [Link][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 = [Link].proxy_queue;
this.set_weight(0);
[Link]();
this.hotkey_handler = function(event){
if([Link] === 13){
self.order_product();
[Link].show_screen(self.next_screen);
}else if([Link] === 27){
[Link].show_screen(self.previous_screen);
}
};
$('body').on('keypress',this.hotkey_handler);
this.$('.back').click(function(){
[Link].show_screen(self.previous_screen);
});
this.$('.next,.buy-product').click(function(){
[Link].show_screen(self.next_screen);
// add product *after* switching screen to scroll properly
self.order_product();
});
[Link](function(){
return [Link].scale_read().then(function(weight){
self.set_weight([Link]);
});
},{duration:150, repeat: true});
},
get_product: function(){
return [Link].get_current_screen_param('product');
},
order_product: function(){
[Link].get_order().add_product(this.get_product(),{ quantity: [Link]
});
},
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 ? [Link] : 0) || 0;
},
get_product_uom: function(){
var product = this.get_product();
if(product){
return [Link].units_by_id[product.uom_id[0]].name;
}else{
return '';
}
},
set_weight: function(weight){
[Link] = 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 = ([Link] || 0).toFixed(3) + ' Kg';
if(!product || ![Link]){
return defaultstr;
}
var unit_id = product.uom_id;
if(!unit_id){
return defaultstr;
}
var unit = [Link].units_by_id[unit_id[0]];
var weight = round_pr([Link] || 0, [Link]);
var weightstr = [Link]([Link]([Link](1.0/[Link]) /
[Link](10) ));
weightstr += ' ' + [Link];
return weightstr;
},
get_computed_price_string: function(){
return this.format_currency(this.get_product_price() * [Link]);
},
close: function(){
this._super();
$('body').off('keypress',this.hotkey_handler);
[Link].proxy_queue.clear();
},
});
gui.define_screen({name: 'scale', widget: ScaleScreenWidget});
/*--------------------------------------*\
| THE PRODUCT SCREEN |
\*======================================*/
[Link]('change:selectedClient', function() {
[Link]();
});
},
renderElement: function() {
var self = this;
this._super();
this.$('.pay').click(function(){
var order = [Link].get_order();
var has_valid_product_lot = _.every([Link],
function(line){
return line.has_valid_product_lot();
});
if(!has_valid_product_lot){
[Link].show_popup('confirm',{
'title': _t('Empty Serial/Lot Number'),
'body': _t('One or more product(s) required serial/lot number.'),
confirm: function(){
[Link].show_screen('payment');
},
});
}else{
[Link].show_screen('payment');
}
});
this.$('.set-customer').click(function(){
[Link].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([Link], event);
};
if ([Link].get_order()) {
this.bind_order_events();
}
},
click_line: function(orderline, event) {
[Link].get_order().select_orderline(orderline);
this.numpad_state.reset();
},
set_value: function(val) {
var order = [Link].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 ([Link].get_order()) {
this.bind_order_events();
this.numpad_state.reset();
[Link]();
}
},
orderline_add: function(){
this.numpad_state.reset();
[Link]('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 = [Link].get_order();
[Link]('change:client', this.update_summary, this);
[Link]('change:client', this.update_summary, this);
[Link]('change', this.update_summary, this);
[Link]('change', this.update_summary, this);
},
render_orderline: function(orderline){
var el_str = [Link]('Orderline',{widget:this, line:orderline});
var el_node = [Link]('div');
el_node.innerHTML = _.[Link](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)));
}
[Link] = el_node;
return el_node;
},
remove_orderline: function(order_line){
if([Link].get_order().get_orderlines().length === 0){
[Link]();
}else{
order_line.[Link](order_line.node);
}
},
rerender_orderline: function(order_line){
var node = order_line.node;
var replacement_line = this.render_orderline(order_line);
[Link](replacement_line,node);
},
// overriding the openerp framework replace method for performance reasons
replace: function($target){
[Link]();
var target = $target[0];
[Link]([Link],target);
},
renderElement: function(scrollbottom){
var order = [Link].get_order();
if (!order) {
return;
}
var orderlines = order.get_orderlines();
if(scrollbottom){
[Link]('.order-scroller').scrollTop = 100 *
[Link];
}
},
update_summary: function(){
var order = [Link].get_order();
if (!order.get_orderlines().length) {
return;
}
this.switch_category_handler = function(event){
self.set_category([Link].get_category_by_id(Number([Link])));
[Link]();
};
this.clear_search_handler = function(event){
self.clear_search();
};
search_timeout = setTimeout(function(){
self.perform_search([Link], [Link], [Link]
=== 13);
},70);
}
};
},
replace: function($target){
[Link]();
var target = $target[0];
[Link]([Link],target);
},
renderElement: function(){
el_node.innerHTML = el_str;
el_node = el_node.childNodes[1];
[Link] = el_node;
list_container.appendChild(this.render_category([Link][i],withpics));
}
}
[Link]('.searchbox
input').addEventListener('keypress',this.search_handler);
[Link]('.searchbox
input').addEventListener('keydown',this.search_handler);
[Link]('.search-
clear').addEventListener('click',this.clear_search_handler);
});
/* --------- The Product List --------- */
this.click_product_handler = function(){
var product = [Link].get_product_by_id([Link]);
options.click_product_action(product);
};
render_product: function(product){
var cached = this.product_cache.get_node([Link]);
if(!cached){
var image_url = this.get_product_image_url(product);
var product_html = [Link]('Product',{
widget: this,
product: product,
image_url: this.get_product_image_url(product),
});
var product_node = [Link]('div');
product_node.innerHTML = product_html;
product_node = product_node.childNodes[1];
this.product_cache.cache_node([Link],product_node);
return product_node;
}
return cached;
},
renderElement: function() {
var el_str = [Link]([Link], {widget: this});
var el_node = [Link]('div');
el_node.innerHTML = el_str;
el_node = el_node.childNodes[1];
if([Link] && [Link]){
[Link](el_node,[Link]);
}
[Link] = el_node;
if ([Link]) {
for (i = 0; i < [Link]; i++) {
if (classes[i].name === [Link]) {
index = i + 1;
}
}
} else if ([Link]) {
for (i = 0; i < [Link]; i++) {
if (classes[i].name === [Link]) {
index = i;
break;
}
}
}
[Link](i,0,classe);
};
start: function(){
this.action_buttons = {};
var classes = action_button_classes;
for (var i = 0; i < [Link]; i++) {
var classe = classes[i];
if ( ![Link] || [Link](this) ) {
var widget = new [Link](this,{});
[Link](this.$('.control-buttons'));
this.action_buttons[[Link]] = widget;
}
}
if (_.size(this.action_buttons)) {
this.$('.control-buttons').removeClass('oe_hidden');
}
},
click_product: function(product) {
if(product.to_weight && [Link].iface_electronic_scale){
[Link].show_screen('scale',{product: product});
}else{
[Link].get_order().add_product(product);
}
},
show: function(reset){
this._super();
if (reset) {
this.product_categories_widget.reset_category();
[Link]();
}
},
close: function(){
this._super();
if([Link].iface_vkeyboard && [Link]){
[Link]();
}
},
});
gui.define_screen({name:'products', widget: ProductScreenWidget});
/*--------------------------------------*\
| THE CLIENT LIST |
\*======================================*/
auto_back: true,
show: function(){
var self = this;
this._super();
[Link]();
this.details_visible = false;
this.old_client = [Link].get_order().get_client();
this.$('.back').click(function(){
[Link]();
});
this.$('.next').click(function(){
self.save_changes();
[Link](); // FIXME HUH ?
});
this.$('.new-customer').click(function(){
self.display_client_details('edit',{
'country_id': [Link].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,[Link] === 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.$('.[Link]').val([Link]);
} else if ([Link].get_partner_by_barcode([Link])) {
var partner = [Link].get_partner_by_barcode([Link]);
this.new_client = partner;
this.display_client_details('show', partner);
}
},
perform_search: function(query, associate_result){
var customers;
if(query){
customers = [Link].search_partner(query);
this.display_client_details('hide');
if ( associate_result && [Link] === 1){
this.new_client = customers[0];
this.save_changes();
[Link]();
}
this.render_list(customers);
}else{
customers = [Link].get_partners_sorted();
this.render_list(customers);
}
},
clear_search: function(){
var customers = [Link].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');
[Link] = "";
for(var i = 0, len = [Link]([Link],1000); i < len; i++){
var partner = partners[i];
var clientline = this.partner_cache.get_node([Link]);
if(!clientline){
var clientline_html = [Link]('ClientLine',{widget: this,
partner:partners[i]});
var clientline = [Link]('tbody');
[Link] = clientline_html;
clientline = [Link][1];
this.partner_cache.cache_node([Link],clientline);
}
if( partner === this.old_client ){
[Link]('highlight');
}else{
[Link]('highlight');
}
[Link](clientline);
}
},
save_changes: function(){
var self = this;
var order = [Link].get_order();
if( this.has_client_changed() ){
if ( this.new_client ) {
order.fiscal_position = _.find([Link].fiscal_positions, function
(fp) {
return [Link] === 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.$('.[Link]');
if (this.editing_client) {
$[Link]('oe_hidden');
return;
} else if( this.new_client ){
if( !this.old_client){
$[Link](_t('Set Customer'));
}else{
$[Link](_t('Change Customer'));
}
}else{
$[Link](_t('Deselect Customer'));
}
$[Link]('oe_hidden',!this.has_client_changed());
},
line_select: function(event,$line,id){
var partner = [Link].get_partner_by_id(id);
this.$('.client-list .lowlight').removeClass('lowlight');
if ( $[Link]('highlight') ){
$[Link]('highlight');
$[Link]('lowlight');
this.display_client_details('hide',partner);
this.new_client = null;
this.toggle_save_button();
}else{
this.$('.client-list .highlight').removeClass('highlight');
$[Link]('highlight');
var y = [Link] - $[Link]().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=[Link]&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 (![Link]) {
[Link].show_popup('error',_t('A Customer Name Is Required'));
return;
}
if (this.uploaded_picture) {
[Link] = this.uploaded_picture;
}
new
Model('[Link]').call('create_from_ui',[fields]).then(function(partner_id){
self.saved_client_details(partner_id);
},function(err,event){
[Link]();
[Link].show_popup('error',{
'title': _t('Error: Could not Save Changes'),
'body': _t('Your Internet connection is probably down.'),
});
});
},
[Link] = width;
[Link] = height;
[Link](img,0,0,width,height);
[Link].get_order().set_client([Link].get_partner_by_id(curr_client.id));
}
});
},
[Link]('click','.[Link]');
[Link]('click','.[Link]');
[Link]('click','.[Link]');
[Link]('click','.[Link]',function(){
self.edit_client_details(partner); });
[Link]('click','.[Link]',function(){
self.save_client_details(partner); });
[Link]('click','.[Link]',function(){
self.undo_client_details(partner); });
this.editing_client = false;
this.uploaded_picture = null;
[Link]($([Link]('ClientDetails',{widget:this,partner:partner})));
var new_height = [Link]();
if(!this.details_visible){
// resize client list to take into account client details
[Link]('-=' + new_height);
this.details_visible = true;
this.toggle_save_button();
} else if (visibility === 'edit') {
this.editing_client = true;
[Link]();
[Link]($([Link]('ClientDetailsEdit',{widget:this,partner:partner})));
this.toggle_save_button();
[Link]('.image-uploader').on('change',function(event){
self.load_image_file([Link][0],function(res){
if (res) {
[Link]('.client-picture img, .client-picture
.fa').remove();
[Link]('.client-picture').append("<img
src='"+res+"'>");
[Link]('.[Link]').remove();
self.uploaded_picture = res;
}
});
});
} else if (visibility === 'hide') {
[Link]();
[Link]('100%');
if( height > scroll ){
[Link]({height:height+'px'});
[Link]({height:0},400,function(){
[Link]({height:''});
});
}else{
[Link]( [Link]() - 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()) {
[Link]();
if (this.should_close_immediately()){
this.click_next();
}
} else {
this.lock_screen(false);
}
},
should_auto_print: function() {
return [Link].iface_print_auto && ![Link].get_order()._printed;
},
should_close_immediately: function() {
return [Link].iface_print_via_proxy &&
[Link].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() {
[Link]();
[Link].get_order()._printed = true;
},
print_xml: function() {
var env = {
widget: this,
order: [Link].get_order(),
receipt: [Link].get_order().export_for_printing(),
paymentlines: [Link].get_order().get_paymentlines()
};
var receipt = [Link]('XmlReceipt',env);
[Link].print_receipt(receipt);
[Link].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() {
[Link].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.$('.[Link]').click(function(){
if (!self._locked) {
[Link]();
}
});
},
render_change: function() {
this.$('.change-
value').html(this.format_currency([Link].get_order().get_change()));
},
render_receipt: function() {
var order = [Link].get_order();
this.$('.pos-receipt-container').html([Link]('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 |
\*======================================*/
[Link]('change:selectedOrder',function(){
[Link]();
this.watch_order_changes();
},this);
this.watch_order_changes();
[Link] = "";
[Link] = true;
this.decimal_point = _t.[Link].decimal_point;
self.payment_input(key);
[Link]();
};
[Link]('change:selectedClient', function() {
self.customer_changed();
}, this);
},
// resets the current input buffer
reset_input: function(){
var line = [Link].get_order().selected_paymentline;
[Link] = true;
if (line) {
[Link] = this.format_currency_no_symbol(line.get_amount());
} else {
[Link] = "";
}
},
// handle both keyboard and numpad input. Accepts
// a string that represents the key pressed.
payment_input: function(input) {
var newbuf = [Link].numpad_input([Link], input, {'firstinput':
[Link]});
order.selected_paymentline.set_amount(amount);
this.order_changes();
this.render_paymentlines();
this.$('.[Link]
.edit').text(this.format_currency_no_symbol(amount));
}
}
},
click_numpad: function(button) {
var paymentlines = [Link].get_order().get_paymentlines();
var open_paymentline = false;
if (! open_paymentline) {
[Link].get_order().add_paymentline( [Link][0]);
this.render_paymentlines();
}
this.payment_input([Link]('action'));
},
render_numpad: function() {
var self = this;
var numpad = $([Link]('PaymentScreen-Numpad', { widget:this }));
[Link]('click','button',function(){
self.click_numpad($(this));
});
return numpad;
},
click_delete_paymentline: function(cid){
var lines = [Link].get_order().get_paymentlines();
for ( var i = 0; i < [Link]; i++ ) {
if (lines[i].cid === cid) {
[Link].get_order().remove_paymentline(lines[i]);
this.reset_input();
this.render_paymentlines();
return;
}
}
},
click_paymentline: function(cid){
var lines = [Link].get_order().get_paymentlines();
for ( var i = 0; i < [Link]; i++ ) {
if (lines[i].cid === cid) {
[Link].get_order().select_paymentline(lines[i]);
this.reset_input();
this.render_paymentlines();
return;
}
}
},
render_paymentlines: function() {
var self = this;
var order = [Link].get_order();
if (!order) {
return;
}
this.$('.paymentlines-container').empty();
var lines = $([Link]('PaymentScreen-Paymentlines', {
widget: this,
order: order,
paymentlines: lines,
extradue: extradue,
}));
[Link]('click','.delete-button',function(){
self.click_delete_paymentline($(this).data('cid'));
});
[Link]('click','.paymentline',function(){
self.click_paymentline($(this).data('cid'));
});
[Link](this.$('.paymentlines-container'));
},
click_paymentmethods: function(id) {
var cashregister = null;
for ( var i = 0; i < [Link]; i++ ) {
if ( [Link][i].journal_id[0] === id ){
cashregister = [Link][i];
break;
}
}
[Link].get_order().add_paymentline( cashregister );
this.reset_input();
this.render_paymentlines();
},
render_paymentmethods: function() {
var self = this;
var methods = $([Link]('PaymentScreen-Paymentmethods', { widget:this }));
[Link]('click','.paymentmethod',function(){
self.click_paymentmethods($(this).data('id'));
});
return methods;
},
click_invoice: function(){
var order = [Link].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 = [Link].get_order();
var tip = order.get_tip();
var change = order.get_change();
var value = tip;
[Link].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 = [Link].get_client();
this.$('.js_customer_name').text( client ? [Link] : _t('Customer') );
},
click_set_customer: function(){
[Link].show_screen('clientlist');
},
click_back: function(){
[Link].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(){
[Link].open_cashbox();
});
},
show: function(){
[Link].get_order().clean_empty_paymentlines();
this.reset_input();
this.render_paymentlines();
this.order_changes();
[Link]('keypress',this.keyboard_handler);
[Link]('keydown',this.keyboard_keydown_handler);
this._super();
},
hide: function(){
[Link]('keypress',this.keyboard_handler);
[Link]('keydown',this.keyboard_keydown_handler);
this._super();
},
// sets up listeners to watch for order changes
watch_order_changes: function() {
var self = this;
var order = [Link].get_order();
if (!order) {
return;
}
if(this.old_order){
this.old_order.unbind(null,null,this);
}
[Link]('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 = [Link].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 = [Link].get_order();
if (!order.is_paid() || [Link]) {
return false;
}
// The exact amount must be paid if there is no cash payment method defined.
if ([Link](order.get_total_with_tax() - order.get_total_paid()) > 0.00001) {
var cash = false;
for (var i = 0; i < [Link]; i++) {
cash = cash || ([Link][i].[Link] === 'cash');
}
if (!cash) {
[Link].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())) {
[Link].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 = [Link].get_order();
order.initialize_validation_date();
if (order.is_to_invoice()) {
var invoiced = [Link].push_and_invoice_order(order);
[Link] = true;
[Link](function(error){
[Link] = false;
if ([Link] === 'Missing Customer') {
[Link].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(){
[Link].show_screen('clientlist');
},
});
} else if ([Link] < 0) { // XmlHttpRequest Errors
[Link].show_popup('error',{
'title': _t('The order could not be sent'),
'body': _t('Check your internet connection and try again.'),
});
} else if ([Link] === 200) { // OpenERP Server Errors
[Link].show_popup('error-traceback',{
'title': [Link] || _t("Server Error"),
'body': [Link] || _t('The server encountered an
error while receiving your order.'),
});
} else { // ???
[Link].show_popup('error',{
'title': _t("Unknown Error"),
'body': _t("The order could not be sent to the server due to
an unknown error"),
});
}
});
[Link](function(){
[Link] = false;
[Link].show_screen('receipt');
});
} else {
[Link].push_order(order);
[Link].show_screen('receipt');
}
},
[Link]('change:selectedOrder', function () {
[Link]();
}, this);
},
button_click: function () {
var self = this;
var no_fiscal_position = [{
label: _t("None"),
}];
var fiscal_positions = _.map([Link].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 [Link].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
[Link]('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;
[Link](function(){
self.close_other_tabs();
var order = [Link].get_order();
if (order) {
self.show_saved_screen(order);
} else {
self.show_screen(self.startup_screen);
}
[Link]('change:selectedOrder', function(){
self.show_saved_screen([Link].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) {
[Link]("ERROR: show_screen("+screen_name+") : screen not found");
}
if (!skip_close_popup){
this.close_popup();
}
var order = [Link].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 = [Link].get_order().get_screen_data('previous-screen');
if (previous) {
this.show_screen(previous);
}
},
localStorage['message'] = '';
localStorage['message'] = [Link]({
'message':'close_tabs',
'session': [Link].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 [Link]
event-fired-in-source-window
// Use window_uid parameter to exclude the current window
[Link]("storage", function(event) {
var msg = [Link];
}, false);
},
return [Link](function(user){
if ([Link] && 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 || [Link].get_cashier();
close: function() {
var self = this;
var pending = [Link].get_orders().length;
if (!pending) {
this._close();
} else {
[Link].push_order().always(function() {
var pending = [Link].get_orders().length;
if (!pending) {
self._close();
} else {
var reason = [Link]('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;
[Link].loading_show();
[Link].loading_message(_t('Closing ...'));
[Link].push_order().then(function(){
var url = "/web#action=point_of_sale.action_client_pos_menu";
[Link] = [Link] ? $.[Link](url, {debug:
[Link]}) : url;
});
},
play_sound: function(sound) {
var src = '';
if (sound === 'error') {
src = "/point_of_sale/static/src/sounds/[Link]";
} else if (sound === 'bell') {
src = "/point_of_sale/static/src/sounds/[Link]";
} else {
[Link]('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
[Link]('pos_snips_updates.hair_stylist', function (require) {
"use strict";
var Class = require('[Link]');
var Model = require('[Link]');
var session = require('[Link]');
var core = require('[Link]');
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('[Link]');
var _t = core._t;
models.load_models({
model: '[Link]',
fields: ['name', 'id',],
loaded: function (self, employees) {
[Link] = employees;
self.employees_by_id = {};
for (var i = 0; i < [Link]; i++) {
employees[i].tables = [];
self.employees_by_id[employees[i].id] = employees[i];
}
[Link] = [Link]({
initialize: function (attr, options) {
_super_orderline.[Link](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 = [Link].hair_stylist_id;
}
if (!this.hair_stylist_name) {
this.hair_stylist_name = [Link].hair_stylist_name;
}
},
set_hair_stylist_id: function (hair_stylist_id) {
this.hair_stylist_id = hair_stylist_id;
[Link]('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;
[Link]('change', this);
},
get_hair_stylist_name: function () {
return this.hair_stylist_name;
},
clone: function () {
var orderline = _super_orderline.[Link](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;
},
});
[Link]("OrderlineHairStylistButton");
var hair_stylists=[Link];
var hair_stylists_length=hair_stylists.length;
[Link]({
'label': hair_stylist.name,
'item': hair_stylist,
});
}
//
//
var the_seleted=line.get_hair_stylist_name();
[Link].show_popup('selection',{
'title':_t('Select Hair Stylist'),
list: list,
confirm: function (item) {
[Link]("Item");
[Link](item);
line.set_hair_stylist_id([Link]);
line.set_hair_stylist_name([Link]);
},
cancel: function () { },
});
}
},
});
screens.define_action_button({
'name': 'orderline_note',
'widget': OrderlineHairStylistButton,
});
});
INTERNAL REFERENCE JS
[Link]('pos_snips_updates.internal_reference', function (require) {
"use strict";
// New orders are now associated with the current table, if any.
var _super_order = [Link];
[Link] = [Link]({
initialize: function () {
[Link]("internal_reference start")
_super_order.[Link](this, arguments);
if (!this.internal_reference) {
this.internal_reference = [Link].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;
[Link]('change');
},
});
button_click: function () {
var self = this;
if (session_id) {
} else {
var session_id = utils.get_cookie("session_id");
}
[Link]("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], } });
}
},
});
[Link]({
update_summary: function () {
this._super();
if ([Link]().action_buttons &&
[Link]().action_buttons.internal_reference) {
[Link]().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
[Link]('pos_customer_history.customer_history', function (require) {
"use strict";
},
show: function () {
var self = this;
this._super();
this.$('.customer-history').click(function () {
// alert("customer");
[Link]("Action
pos_customer_history.action_report_customer_history");
if (self.new_client) {
[Link]("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;
[Link]('change');
},
get_last_visit: function () {
return this.last_visit;
},
set_last_visit: function (last_visit) {
this.last_visit = last_visit;
[Link]('change');
},
get_average: function () {
return [Link];
},
set_average: function (average) {
[Link] = average;
[Link]('change');
},
get_total_orders: function () {
return this.total_orders;
},
set_total_orders: function (total_orders) {
this.total_orders = total_orders;
[Link]('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 = [Link]();
var height = [Link]();
[Link]('click', '.[Link]');
[Link]('click', '.[Link]');
[Link]('click', '.[Link]');
[Link]('click', '.[Link]', function () {
self.edit_client_details(partner);
});
[Link]('click', '.[Link]', function () {
self.save_client_details(partner);
});
[Link]('click', '.[Link]', function () {
self.undo_client_details(partner);
});
this.editing_client = false;
this.uploaded_picture = null;
[Link]('get_partner_history',[[Link]]).then(function(history_lst) {
[Link]();
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]);
[Link]($([Link]('ClientDetails',{widget:temp,partner:partner})));
[Link](temp);
});
if (!this.details_visible) {
// resize client list to take into account client details
[Link]('-=' + new_height);
this.details_visible = true;
this.toggle_save_button();
} else if (visibility === 'edit') {
this.editing_client = true;
[Link]();
[Link]($([Link]('ClientDetailsEdit', {widget: this,
partner: partner})));
this.toggle_save_button();
});
});
The 'create' method in a Backbone Collection creates a new instance of a model and immediately adds it to the collection, unless the 'wait' option is specified to delay addition until a server acknowledgment. The method saves the model to the server using 'model.save', allowing for persistence and retrieval of data. It also handles server responses to update the collection appropriately if 'wait' is used .
The PaymentScreenWidget in a POS system validates payments by checking that the total payment matches the order total, prohibiting negative bank payments, and confirming transactions if unusually large payments are entered. By enforcing these checks, it ensures that transactions comply with financial protocols and alerts users with popups in case of discrepancies .
In the Backbone framework, a Collection serves the purpose of organizing models, analogous to a table of data, and it maintains indexes of models for ordered access and lookup by 'id'. It manages model sorting via a 'comparator' function that determines the order in which models are stored within the collection .
The POS system employs strategies such as displaying error popups when a zero-total order is attempted for validation, ensuring that at least one product line is present before completing a sale. This measure prevents unintended processing of incomplete transactions, maintaining sales accuracy and integrity .
The Backbone.Collection's 'sort' method uses a comparator to determine the order of models in the collection. If a string or single-attribute comparator is specified, the models are sorted using 'sortBy'; otherwise, the native 'sort' function is used with the comparator function. This ensures that the models maintain the correct order according to the given logic .
Backbone.Model handles attribute validation by using a 'validate' function that checks the model attributes before setting them. If the validate function returns an error, the model is considered invalid, and an 'invalid' event is triggered with the error details . The method effectively prevents setting attributes that do not meet validation criteria by returning false in such cases.
In the POS system interface, customer selection is integrated with order processing by allowing users to assign customers to orders, impacting tax calculations and potential invoicing options. The system dynamically updates order details and validates that customer information is essential for invoicing tasks, promoting an interconnected workflow for efficient sales processing .
The Backbone framework addresses printing processes in a point of sale system by utilizing synchronous and asynchronous methods based on the configuration settings. Browser printing involves asynchronous 'print' calls, while proxy settings streamline xml-based synchronous printing to avoid conflicts with background tasks. To mitigate asynchronous timing issues, UI elements like 'setTimeout' ensure that screen states are adequately controlled during printing operations .
In the POS system, fiscal positions can be selected to adjust tax regulations for an order. When a fiscal position is chosen, it modifies the tax rules applied, potentially changing order totals and tax calculations. This selection system ensures that taxation complies with jurisdictional requirements, affecting how orders are logged and finalized .
The 'reset' method in a Backbone Collection is used to replace the entire set of models with new data without triggering individual 'add' or 'remove' events for each model. This minimizes the overhead of event handling during bulk updates, thereby enhancing performance. A 'reset' event is triggered after the operation to indicate the completion of changes .