You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
619 lines
24 KiB
619 lines
24 KiB
/*
|
|
Copyright 2019 Coop IT Easy SCRLfs
|
|
Pierrick Brun <pierrick.brun@akretion.com>
|
|
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
*/
|
|
|
|
|
|
odoo.define('pos_container.models_and_db', function (require) {
|
|
"use strict";
|
|
|
|
var PosDB = require('point_of_sale.DB');
|
|
var models = require('point_of_sale.models');
|
|
var rpc = require('web.rpc');
|
|
var config = require('web.config');
|
|
|
|
var core = require('web.core');
|
|
var QWeb = core.qweb;
|
|
|
|
var utils = require('web.utils');
|
|
var field_utils = require('web.field_utils');
|
|
var round_di = utils.round_decimals;
|
|
var round_pr = utils.round_precision;
|
|
|
|
// include not available => extend
|
|
models.PosModel = models.PosModel.extend({
|
|
get_container_product: function(){
|
|
// assign value if not already assigned.
|
|
// Avoids rewriting init function
|
|
if (!this.container_product){
|
|
this.container_product = this.db.get_product_by_barcode(
|
|
'CONTAINER');
|
|
}
|
|
return this.container_product
|
|
},
|
|
scan_container: function(parsed_code){
|
|
var selected_order = this.get_order();
|
|
var container = this.db.get_container_by_barcode(
|
|
parsed_code.base_code);
|
|
|
|
if(!container){
|
|
return false;
|
|
}
|
|
|
|
selected_order.add_container(container);
|
|
return true;
|
|
},
|
|
// reload the list of container, returns as a deferred that resolves if there were
|
|
// updated containers, and fails if not
|
|
load_new_containers: function(){
|
|
var self = this;
|
|
var def = new $.Deferred();
|
|
var fields = _.find(this.models,function(model){
|
|
return model.model === 'pos.container';
|
|
}).fields;
|
|
var domain = [];
|
|
rpc.query({
|
|
model: 'pos.container',
|
|
method: 'search_read',
|
|
args: [domain, fields],
|
|
}, {
|
|
timeout: 3000,
|
|
shadow: true,
|
|
})
|
|
.then(function(containers){
|
|
if (self.db.add_containers(containers)) {
|
|
// check if the partners we got were real updates
|
|
def.resolve();
|
|
} else {
|
|
def.reject();
|
|
}
|
|
}, function(type,err){ def.reject(); });
|
|
return def;
|
|
},
|
|
// load placeholder product for containers.
|
|
// it is done here to load it even if inactivated.
|
|
load_placeholder_product: function(){
|
|
var self = this;
|
|
var fields = _.find(this.models,function(model){
|
|
return model.model === 'product.product';
|
|
}).fields;
|
|
var domain = [['barcode', '=', 'CONTAINER'], ['active', '=', false]];
|
|
// no need to load it when active because it is already done in standard
|
|
return rpc.query({
|
|
model: 'product.product',
|
|
method: 'search_read',
|
|
args: [domain, fields],
|
|
}).then(function(products){
|
|
self.db.add_products(_.map(products, function (product) {
|
|
return new models.Product({}, product);
|
|
}));
|
|
});
|
|
},
|
|
|
|
// saves the container locally and try to send it to the backend.
|
|
// it returns a deferred that succeeds after having tried to send the container and all the other pending containers.
|
|
push_container: function(container, opts) {
|
|
opts = opts || {};
|
|
var self = this;
|
|
|
|
if(container){
|
|
this.db.add_containers([container]);
|
|
}
|
|
|
|
var pushed = new $.Deferred();
|
|
|
|
this.flush_mutex.exec(function(){
|
|
var flushed = self._save_containers_to_server(self.db.get_containers_sorted(), opts);
|
|
|
|
flushed.always(function(ids){
|
|
pushed.resolve();
|
|
});
|
|
|
|
return flushed;
|
|
});
|
|
return pushed;
|
|
},
|
|
|
|
// send an array of containers to the server
|
|
// available options:
|
|
// - timeout: timeout for the rpc call in ms
|
|
// returns a deferred that resolves with the list of
|
|
// server generated ids for the sent containers
|
|
_save_containers_to_server: function (containers, options) {
|
|
var self = this;
|
|
var containers= containers.filter(container => !( "id" in container))
|
|
if (!containers || !containers.length) {
|
|
var result = $.Deferred();
|
|
result.resolve([]);
|
|
return result;
|
|
}
|
|
|
|
options = options || {};
|
|
var timeout = typeof options.timeout === 'number' ? options.timeout : 7500 * containers.length;
|
|
|
|
return rpc.query({
|
|
model: 'pos.container',
|
|
method: 'create_from_ui',
|
|
args: containers,
|
|
}, {
|
|
timeout: timeout,
|
|
})
|
|
.then(function (server_ids) {
|
|
//self.db.remove_containers(containers);
|
|
_.each(containers, function(container, key){
|
|
container["id"] = server_ids[key]
|
|
});
|
|
//self.db.add_containers(containers)
|
|
self.set('failed',false);
|
|
return server_ids;
|
|
}).fail(function (type, error){
|
|
if(error.code === 200 ){ // Business Logic Error, not a connection problem
|
|
//if warning do not need to display traceback!!
|
|
if (error.data.exception_type == 'warning') {
|
|
delete error.data.debug;
|
|
}
|
|
|
|
// Hide error if already shown before ...
|
|
if ((!self.get('failed') || options.show_error) && !options.to_invoice) {
|
|
self.gui.show_popup('error-traceback',{
|
|
'title': error.data.message,
|
|
'body': error.data.debug
|
|
});
|
|
}
|
|
self.set('failed',error);
|
|
}
|
|
console.error('Failed to send containers:', containers);
|
|
});
|
|
},
|
|
|
|
// wrapper around the _save_to_server that updates the synch status widget
|
|
// it is modified to send containers before orders
|
|
_flush_orders: function(orders, options) {
|
|
var self = this;
|
|
this.set('synch',{ state: 'connecting', pending: orders.length});
|
|
|
|
return self._save_containers_to_server(self.db.get_containers_sorted())
|
|
.then(function(container_ids) {
|
|
for (var i=0; i < orders.length; i++){
|
|
if (orders[i].data.lines) {
|
|
for (var j=0; j < orders[i].data.lines[0].length; j++){
|
|
var orderline = orders[i].data.lines[0][j]
|
|
if ( !orderline.container_id && orderline.container_barcode) {
|
|
orderline.container_id = self.db.get_container_by_barcode(orderline.container_barcode).id;
|
|
delete orderline["container_barcode"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return self._save_to_server(orders, options)
|
|
}).done(function (server_ids) {
|
|
var pending = self.db.get_orders().length;
|
|
|
|
self.set('synch', {
|
|
state: pending ? 'connecting' : 'connected',
|
|
pending: pending
|
|
});
|
|
|
|
return server_ids;
|
|
}).fail(function(error, event){
|
|
var pending = self.db.get_orders().length;
|
|
if (self.get('failed')) {
|
|
self.set('synch', { state: 'error', pending: pending });
|
|
} else {
|
|
self.set('synch', { state: 'disconnected', pending: pending });
|
|
}
|
|
});
|
|
},
|
|
});
|
|
|
|
models.Order = models.Order.extend({
|
|
add_container: function(container, options){
|
|
if(this._printed){
|
|
this.destroy();
|
|
return this.pos.get_order().add_container(container, options);
|
|
}
|
|
options = options || {};
|
|
var attr = JSON.parse(JSON.stringify(container));
|
|
attr.pos = this.pos;
|
|
attr.order = this;
|
|
var product = this.pos.get_container_product();
|
|
var line = new models.Orderline({}, {
|
|
pos: this.pos, order: this, product: product});
|
|
|
|
line.set_container(container);
|
|
line.set_quantity(0);
|
|
this.orderlines.add(line);
|
|
|
|
this.select_orderline(this.get_last_orderline());
|
|
},
|
|
has_tare_line: function(mode){
|
|
var orderlines = this.orderlines.models
|
|
for(var i=0; i < orderlines.length; i++){
|
|
var line = orderlines[i];
|
|
if(line && line.get_tare_mode() === mode){
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
export_for_printing: function(){
|
|
var orderlines = [];
|
|
var self = this;
|
|
|
|
this.orderlines.each(function(orderline){
|
|
orderlines.push(orderline.export_for_printing());
|
|
});
|
|
|
|
var paymentlines = [];
|
|
this.paymentlines.each(function(paymentline){
|
|
paymentlines.push(paymentline.export_for_printing());
|
|
});
|
|
var client = this.get('client');
|
|
var cashier = this.pos.get_cashier();
|
|
var company = this.pos.company;
|
|
var shop = this.pos.shop;
|
|
var date = new Date();
|
|
|
|
function is_xml(subreceipt){
|
|
return subreceipt ? (subreceipt.split('\n')[0].indexOf('<!DOCTYPE QWEB') >= 0) : false;
|
|
}
|
|
|
|
function render_xml(subreceipt){
|
|
if (!is_xml(subreceipt)) {
|
|
return subreceipt;
|
|
} else {
|
|
subreceipt = subreceipt.split('\n').slice(1).join('\n');
|
|
var qweb = new QWeb2.Engine();
|
|
qweb.debug = config.debug;
|
|
qweb.default_dict = _.clone(QWeb.default_dict);
|
|
qweb.add_template('<templates><t t-name="subreceipt">'+subreceipt+'</t></templates>');
|
|
|
|
return qweb.render('subreceipt',{'pos':self.pos,'widget':self.pos.chrome,'order':self, 'receipt': receipt}) ;
|
|
}
|
|
}
|
|
|
|
var receipt = {
|
|
orderlines: orderlines,
|
|
paymentlines: paymentlines,
|
|
subtotal: this.get_subtotal(),
|
|
total_with_tax: this.get_total_with_tax(),
|
|
total_without_tax: this.get_total_without_tax(),
|
|
total_tax: this.get_total_tax(),
|
|
total_paid: this.get_total_paid(),
|
|
total_discount: this.get_total_discount(),
|
|
tax_details: this.get_tax_details(),
|
|
change: this.get_change(),
|
|
name : this.get_name(),
|
|
client: client ? client.name : null ,
|
|
invoice_id: null, //TODO
|
|
cashier: cashier ? cashier.name : null,
|
|
precision: {
|
|
price: 2,
|
|
money: 2,
|
|
quantity: 3,
|
|
},
|
|
date: {
|
|
year: date.getFullYear(),
|
|
month: date.getMonth(),
|
|
date: date.getDate(), // day of the month
|
|
day: date.getDay(), // day of the week
|
|
hour: date.getHours(),
|
|
minute: date.getMinutes() ,
|
|
isostring: date.toISOString(),
|
|
localestring: date.toLocaleString(),
|
|
},
|
|
company:{
|
|
email: company.email,
|
|
website: company.website,
|
|
company_registry: company.company_registry,
|
|
contact_address: company.partner_id[1],
|
|
vat: company.vat,
|
|
vat_label: company.country && company.country.vat_label || '',
|
|
name: company.name,
|
|
phone: company.phone,
|
|
logo: this.pos.company_logo_base64,
|
|
},
|
|
shop:{
|
|
name: shop.name,
|
|
},
|
|
currency: this.pos.currency,
|
|
//custom here
|
|
has_tare_mode: {
|
|
auto: this.has_tare_line('AUTO'),
|
|
manual: this.has_tare_line('MAN'),
|
|
}
|
|
//custom end
|
|
};
|
|
|
|
if (is_xml(this.pos.config.receipt_header)){
|
|
receipt.header = '';
|
|
receipt.header_xml = render_xml(this.pos.config.receipt_header);
|
|
} else {
|
|
receipt.header = this.pos.config.receipt_header || '';
|
|
}
|
|
|
|
if (is_xml(this.pos.config.receipt_footer)){
|
|
receipt.footer = '';
|
|
receipt.footer_xml = render_xml(this.pos.config.receipt_footer);
|
|
} else {
|
|
receipt.footer = this.pos.config.receipt_footer || '';
|
|
}
|
|
|
|
return receipt;
|
|
},
|
|
});
|
|
|
|
// Add container to order line
|
|
models.Orderline = models.Orderline.extend({
|
|
get_container: function(){
|
|
return this.container;
|
|
},
|
|
set_container: function(container){
|
|
this.container = container;
|
|
},
|
|
set_tare_mode: function(mode){
|
|
if (['MAN', 'AUTO'].indexOf(mode) != -1){
|
|
this.tare_mode = mode;
|
|
this.trigger('change', this);
|
|
}
|
|
},
|
|
get_tare_mode: function() {
|
|
return this.tare_mode;
|
|
},
|
|
set_tare: function(tare){
|
|
this.tare = this.get_value_rounded(tare).toFixed(3);
|
|
this.container = null;
|
|
if (this.gross_weight && this.gross_weight != 'NaN'){
|
|
this.set_quantity(this.gross_weight - parseFloat(this.tare));
|
|
}
|
|
else{
|
|
this.set_gross_weight(this.quantity);
|
|
this.set_quantity(this.quantity - parseFloat(this.tare));
|
|
}
|
|
this.trigger('change', this);
|
|
},
|
|
get_tare: function(){
|
|
return this.tare || 0;
|
|
},
|
|
get_gross_weight: function(){
|
|
return this.gross_weight;
|
|
},
|
|
set_gross_weight: function(weight){
|
|
this.gross_weight = this.get_value_rounded(weight).toFixed(3);
|
|
this.trigger('change', this);
|
|
},
|
|
set_quantity: function(quantity, keep_price){
|
|
// copied from odoo core
|
|
this.order.assert_editable();
|
|
if(quantity === 'remove'){
|
|
this.order.remove_orderline(this);
|
|
return;
|
|
}else{
|
|
var quant = parseFloat(quantity) || 0;
|
|
var unit = this.get_unit();
|
|
if(unit){
|
|
if (unit.rounding) {
|
|
this.quantity = round_pr(quant, unit.rounding);
|
|
var decimals = this.pos.dp['Product Unit of Measure'];
|
|
this.quantity = round_di(this.quantity, decimals)
|
|
this.quantityStr = field_utils.format.float(this.quantity, {digits: [69, decimals]});
|
|
} else {
|
|
this.quantity = round_pr(quant, 1);
|
|
this.quantityStr = this.quantity.toFixed(0);
|
|
}
|
|
}else{
|
|
this.quantity = quant;
|
|
this.quantityStr = '' + this.quantity;
|
|
}
|
|
}
|
|
// just like in sale.order changing the quantity will recompute the unit price
|
|
if(! keep_price && ! this.price_manually_set){
|
|
this.set_unit_price(this.product.get_price(this.order.pricelist, this.get_quantity()));
|
|
this.order.fix_tax_included_price(this);
|
|
}
|
|
|
|
// surcharge starts here
|
|
if (this.tare){
|
|
this.set_gross_weight(this.quantity + parseFloat(this.tare));
|
|
}
|
|
this.trigger('change', this);
|
|
},
|
|
get_value_rounded: function(value){
|
|
var value = parseFloat(value) || 0;
|
|
var unit = this.get_unit();
|
|
if(unit){
|
|
if (unit.rounding) {
|
|
value = round_pr(value, unit.rounding);
|
|
var decimals = this.pos.dp['Product Unit of Measure'];
|
|
value = round_di(value, decimals)
|
|
} else {
|
|
value = round_pr(value, 1);
|
|
}
|
|
}
|
|
return value;
|
|
},
|
|
export_as_JSON: function(){
|
|
var pack_lot_ids = [];
|
|
if (this.has_product_lot){
|
|
this.pack_lot_lines.each(_.bind( function(item) {
|
|
return pack_lot_ids.push([0, 0, item.export_as_JSON()]);
|
|
}, this));
|
|
}
|
|
return {
|
|
qty: this.get_quantity(),
|
|
price_unit: this.get_unit_price(),
|
|
price_subtotal: this.get_price_without_tax(),
|
|
price_subtotal_incl: this.get_price_with_tax(),
|
|
discount: this.get_discount(),
|
|
product_id: this.get_product().id,
|
|
tax_ids: [[6, false, _.map(this.get_applicable_taxes(), function(tax){ return tax.id; })]],
|
|
id: this.id,
|
|
pack_lot_ids: pack_lot_ids,
|
|
//custom starts here
|
|
tare: this.get_tare() ? this.get_tare() : null,
|
|
container_id: this.get_container() ? this.get_container().id : null,
|
|
container_barcode: this.get_container() ? this.get_container().barcode : null,
|
|
container_weight: this.get_container() ? this.get_container().weight : null,
|
|
};
|
|
},
|
|
//used to create a json of the ticket, to be sent to the printer
|
|
export_for_printing: function(){
|
|
return {
|
|
quantity: this.get_quantity(),
|
|
unit_name: this.get_unit().name,
|
|
price: this.get_unit_price(),
|
|
discount: this.get_discount(),
|
|
product_name: this.get_product().display_name,
|
|
product_name_wrapped: this.generate_wrapped_product_name(),
|
|
price_display : this.get_display_price(),
|
|
price_with_tax : this.get_price_with_tax(),
|
|
price_without_tax: this.get_price_without_tax(),
|
|
tax: this.get_tax(),
|
|
product_description: this.get_product().description,
|
|
product_description_sale: this.get_product().description_sale,
|
|
// extension starts here
|
|
container: this.get_container(),
|
|
tare: this.get_tare(),
|
|
tare_mode: this.get_tare_mode(),
|
|
gross_weight: this.get_gross_weight(),
|
|
product_barcode: this.get_product().barcode,
|
|
};
|
|
},
|
|
});
|
|
|
|
PosDB.include({
|
|
init: function(parent, options) {
|
|
this._super(parent, options);
|
|
|
|
this.container_sorted = [];
|
|
this.container_by_id = {};
|
|
this.container_by_barcode = {};
|
|
this.container_search_string = "";
|
|
this.container_write_date = null;
|
|
},
|
|
_container_search_string: function(container){
|
|
|
|
var str = '';
|
|
|
|
if(container.barcode){
|
|
str += '|' + container.barcode;
|
|
}
|
|
if(container.name) {
|
|
str += '|' + container.name;
|
|
}
|
|
var id = container.id || 0;
|
|
str = '' + id + ':' + str.replace(':','') + '\n';
|
|
|
|
return str;
|
|
},
|
|
add_containers: function(containers) {
|
|
var updated_count = 0;
|
|
var new_write_date = '';
|
|
for(var i = 0, len = containers.length; i < len; i++) {
|
|
var container = containers[i];
|
|
|
|
if (this.container_write_date &&
|
|
this.container_by_barcode[container.barcode] &&
|
|
new Date(this.container_write_date).getTime() + 1000 >=
|
|
new Date(container.write_date).getTime() ) {
|
|
// FIXME: The write_date is stored with milisec precision in the database
|
|
// but the dates we get back are only precise to the second. This means when
|
|
// you read containers modified strictly after time X, you get back containers that were
|
|
// modified X - 1 sec ago.
|
|
continue;
|
|
} else if ( new_write_date < container.write_date ) {
|
|
new_write_date = container.write_date;
|
|
}
|
|
if (!this.container_by_barcode[container.barcode]) {
|
|
this.container_sorted.push(container.barcode);
|
|
}
|
|
this.container_by_barcode[container.barcode] = container;
|
|
|
|
updated_count += 1;
|
|
}
|
|
|
|
this.container_write_date = new_write_date || this.container_write_date;
|
|
|
|
if (updated_count) {
|
|
// If there were updates, we need to completely
|
|
// rebuild the search string and the id indexing
|
|
|
|
this.container_search_string = "";
|
|
this.container_by_id = {};
|
|
|
|
for (var barcode in this.container_by_barcode) {
|
|
var container = this.container_by_barcode[barcode];
|
|
|
|
if(container.id){
|
|
this.container_by_id[container.id] = container;
|
|
}
|
|
this.container_search_string += this._container_search_string(container);
|
|
}
|
|
}
|
|
return updated_count;
|
|
},
|
|
remove_containers: function(barcodes){
|
|
for(var i = 0; i < barcodes.length; i++) {
|
|
var container = this.container_by_barcode[barcodes[i]];
|
|
if (container){
|
|
var index_s = this.container_sorted.indexOf(container.barcode);
|
|
this.container_sorted.splice(index_s, 1);
|
|
delete this.container_by_id[container.id];
|
|
delete this.container_by_barcode[container.barcode];
|
|
}
|
|
}
|
|
},
|
|
get_container_write_date: function(){
|
|
return this.container_write_date;
|
|
},
|
|
get_container_by_id: function(id){
|
|
return this.container_by_id[id];
|
|
},
|
|
get_container_by_barcode: function(barcode){
|
|
return this.container_by_barcode[barcode];
|
|
},
|
|
get_containers_sorted: function(max_count){
|
|
max_count = max_count ? Math.min(this.container_sorted.length, max_count) : this.container_sorted.length;
|
|
var containers = [];
|
|
for (var i = 0; i < max_count; i++) {
|
|
containers.push(this.container_by_barcode[this.container_sorted[i]]);
|
|
}
|
|
|
|
return containers;
|
|
},
|
|
search_container: function(query) {
|
|
try {
|
|
query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g,'.');
|
|
query = query.replace(' ','.+');
|
|
var re = RegExp("([0-9]+):\\|([0-9]*"+query+"[0-9]*\\|\|[0-9]*\\|.*?"+query+")","gi");
|
|
} catch(e) {
|
|
return [];
|
|
}
|
|
var results = [];
|
|
for(var i = 0; i < this.limit; i++) {
|
|
var r = re.exec(this.container_search_string);
|
|
if(r) {
|
|
// r[1] = id, r[2] = barcode
|
|
var barcode = r[2].substring(0, r[2].indexOf("\|"));
|
|
results.push(this.get_container_by_barcode(barcode));
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return results;
|
|
},
|
|
|
|
});
|
|
|
|
models.load_models({
|
|
model: 'pos.container',
|
|
fields: ['name','barcode', 'weight'],
|
|
loaded: function(self, containers){
|
|
self.db.add_containers(containers);
|
|
return self.load_placeholder_product();
|
|
},
|
|
});
|
|
|
|
});
|