[IMP] point_of_sale: full pricelist support
Before this the POS would load product.product records with pricelist_id defined on the pos.config in context. These unit prices were then used everywhere in the POS as-is. The POS contained no pricelist logic at all. This implements the full pricelist logic in the POS and provides a way for users to choose what pricelist to apply per order. This implementation closely follows the behavior defined in the product and sale modules. This includes unit tests for the get_price function and acceptance tests. The unit tests are done in PhantomJS and ensure that get_price returns the same as the backend returns when reading the 'price' field on product.product. Prices of products are displayed according to the selected pricelist. To accomodate this the product_cache DomCache of the ProductListWidget is now done per pricelist. Additionally, this feature should speed up the loading of product.product records. Now only stored product.product fields are loaded. Before the non-stored computed 'price' field had to be loaded for every product.product record.
This commit is contained in:
committed by
Pierre Masereel
parent
80f4230a4e
commit
ac89783d2f
@@ -127,7 +127,10 @@ class PosConfig(models.Model):
|
||||
pos_session_state = fields.Char(compute='_compute_current_session_user')
|
||||
group_by = fields.Boolean(string='Group Journal Items', default=True,
|
||||
help="Check this if you want to group the Journal Items by Product while closing a Session.")
|
||||
pricelist_id = fields.Many2one('product.pricelist', string='Pricelist', required=True, default=_default_pricelist)
|
||||
pricelist_id = fields.Many2one('product.pricelist', string='Default Pricelist', required=True, default=_default_pricelist,
|
||||
help="The pricelist used if no customer is selected or if the customer has no Sale Pricelist configured.")
|
||||
available_pricelist_ids = fields.Many2many('product.pricelist', string='Available Pricelists', default=_default_pricelist,
|
||||
help="Make several pricelists available in the Point of Sale. You can also apply a pricelist to specific customers from their contact form (in Sales tab). To be valid, this pricelist must be listed here as an available pricelist. Otherwise the default pricelist will apply.")
|
||||
company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.user.company_id)
|
||||
barcode_nomenclature_id = fields.Many2one('barcode.nomenclature', string='Barcode Nomenclature',
|
||||
help='Defines what kind of barcodes are available and how they are assigned to products, customers and cashiers.')
|
||||
@@ -143,6 +146,12 @@ class PosConfig(models.Model):
|
||||
default_cashbox_lines_ids = fields.One2many('account.cashbox.line', 'default_pos_id', string='Default Balance')
|
||||
customer_facing_display_html = fields.Html(string='Customer facing display content', translate=True, default=_compute_default_customer_html)
|
||||
use_pricelist = fields.Boolean("Use a pricelist.")
|
||||
group_sale_pricelist = fields.Boolean("Use pricelists to adapt your price per customers",
|
||||
implied_group='product.group_sale_pricelist',
|
||||
help="""Allows to manage different prices based on rules per category of customers.
|
||||
Example: 10% for retailers, promotion of 5 EUR on this product, etc.""")
|
||||
group_pricelist_item = fields.Boolean("Show pricelists to customers",
|
||||
implied_group='product.group_pricelist_item')
|
||||
tax_regime = fields.Boolean("Tax Regime")
|
||||
tax_regime_selection = fields.Boolean("Tax Regime Selection value")
|
||||
barcode_scanner = fields.Boolean("Barcode Scanner")
|
||||
@@ -220,6 +229,19 @@ class PosConfig(models.Model):
|
||||
if self.env['account.journal'].search_count([('id', 'in', self.journal_ids.ids), ('company_id', '!=', self.company_id.id)]):
|
||||
raise ValidationError(_("The company of a payment method is different than the one of point of sale"))
|
||||
|
||||
@api.constrains('pricelist_id', 'available_pricelist_ids', 'journal_id', 'invoice_journal_id', 'journal_ids')
|
||||
def _check_currencies(self):
|
||||
if self.pricelist_id not in self.available_pricelist_ids:
|
||||
raise ValidationError(_("The default pricelist must be included in the available pricelists."))
|
||||
if any(self.available_pricelist_ids.mapped(lambda pricelist: pricelist.currency_id != self.currency_id)):
|
||||
raise ValidationError(_("All available pricelists must be in the same currency as the company or"
|
||||
" as the Sales Journal set on this point of sale if you use"
|
||||
" the Accounting application."))
|
||||
if self.invoice_journal_id.currency_id and self.invoice_journal_id.currency_id != self.currency_id:
|
||||
raise ValidationError(_("The invoice journal must be in the same currency as the Sales Journal or the company currency if that is not set."))
|
||||
if any(self.journal_ids.mapped(lambda journal: journal.currency_id and journal.currency_id != self.currency_id)):
|
||||
raise ValidationError(_("All payment methods must be in the same currency as the Sales Journal or the company currency if that is not set."))
|
||||
|
||||
@api.onchange('iface_print_via_proxy')
|
||||
def _onchange_iface_print_via_proxy(self):
|
||||
self.iface_print_auto = self.iface_print_via_proxy
|
||||
@@ -237,6 +259,16 @@ class PosConfig(models.Model):
|
||||
"""
|
||||
if not self.use_pricelist:
|
||||
self.pricelist_id = self._default_pricelist()
|
||||
else:
|
||||
self.update({
|
||||
'group_sale_pricelist': True,
|
||||
'group_pricelist_item': True,
|
||||
})
|
||||
|
||||
@api.onchange('available_pricelist_ids')
|
||||
def _onchange_available_pricelist_ids(self):
|
||||
if self.pricelist_id not in self.available_pricelist_ids:
|
||||
self.pricelist_id = False
|
||||
|
||||
@api.onchange('iface_scan_via_proxy')
|
||||
def _onchange_iface_scan_via_proxy(self):
|
||||
@@ -310,6 +342,7 @@ class PosConfig(models.Model):
|
||||
values['sequence_line_id'] = IrSequence.create(val).id
|
||||
pos_config = super(PosConfig, self).create(values)
|
||||
pos_config.sudo()._check_modules_to_install()
|
||||
pos_config.sudo()._check_groups_implied()
|
||||
# If you plan to add something after this, use a new environment. The one above is no longer valid after the modules install.
|
||||
return pos_config
|
||||
|
||||
@@ -318,6 +351,7 @@ class PosConfig(models.Model):
|
||||
result = super(PosConfig, self).write(vals)
|
||||
self.sudo()._set_fiscal_position()
|
||||
self.sudo()._check_modules_to_install()
|
||||
self.sudo()._check_groups_implied()
|
||||
return result
|
||||
|
||||
@api.multi
|
||||
@@ -346,6 +380,16 @@ class PosConfig(models.Model):
|
||||
# just in case we want to do something if we install a module. (like a refresh ...)
|
||||
return module_installed
|
||||
|
||||
def _check_groups_implied(self):
|
||||
for pos_config in self:
|
||||
for field_name in [f for f in pos_config.fields_get_keys() if f.startswith('group_')]:
|
||||
field = pos_config._fields[field_name]
|
||||
if field.type in ('boolean', 'selection') and hasattr(field, 'implied_group'):
|
||||
field_group_xmlids = getattr(field, 'group', 'base.group_user').split(',')
|
||||
field_groups = self.env['res.groups'].concat(*(self.env.ref(it) for it in field_group_xmlids))
|
||||
field_groups.write({'implied_ids': [(4, self.env.ref(field.implied_group).id)]})
|
||||
|
||||
|
||||
def execute(self):
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
|
||||
@@ -41,7 +41,8 @@ class PosOrder(models.Model):
|
||||
'pos_reference': ui_order['name'],
|
||||
'partner_id': ui_order['partner_id'] or False,
|
||||
'date_order': ui_order['creation_date'],
|
||||
'fiscal_position_id': ui_order['fiscal_position_id']
|
||||
'fiscal_position_id': ui_order['fiscal_position_id'],
|
||||
'pricelist_id': ui_order['pricelist_id'],
|
||||
}
|
||||
|
||||
def _payment_fields(self, ui_paymentline):
|
||||
|
||||
@@ -62,7 +62,7 @@ exports.PosModel = Backbone.Model.extend({
|
||||
this.config = null;
|
||||
this.units = [];
|
||||
this.units_by_id = {};
|
||||
this.pricelist = null;
|
||||
this.default_pricelist = null;
|
||||
this.order_sequence = 1;
|
||||
window.posmodel = this;
|
||||
|
||||
@@ -184,7 +184,9 @@ exports.PosModel = Backbone.Model.extend({
|
||||
}
|
||||
},{
|
||||
model: 'res.partner',
|
||||
fields: ['name','street','city','state_id','country_id','vat','phone','zip','mobile','email','barcode','write_date','property_account_position_id'],
|
||||
fields: ['name','street','city','state_id','country_id','vat',
|
||||
'phone','zip','mobile','email','barcode','write_date',
|
||||
'property_account_position_id','property_product_pricelist'],
|
||||
domain: [['customer','=',true]],
|
||||
loaded: function(self,partners){
|
||||
self.partners = partners;
|
||||
@@ -285,13 +287,46 @@ exports.PosModel = Backbone.Model.extend({
|
||||
loaded: function(self, locations){ self.shop = locations[0]; },
|
||||
},{
|
||||
model: 'product.pricelist',
|
||||
fields: ['currency_id'],
|
||||
ids: function(self){ return [self.config.pricelist_id[0]]; },
|
||||
loaded: function(self, pricelists){ self.pricelist = pricelists[0]; },
|
||||
fields: ['name'],
|
||||
domain: function(self) { return [['id', 'in', self.config.available_pricelist_ids]]; },
|
||||
loaded: function(self, pricelists){
|
||||
_.map(pricelists, function (pricelist) { pricelist.items = []; });
|
||||
self.default_pricelist = _.findWhere(pricelists, {id: self.config.pricelist_id[0]});
|
||||
self.pricelists = pricelists;
|
||||
},
|
||||
},{
|
||||
model: 'product.pricelist.item',
|
||||
domain: function(self) { return [['pricelist_id', 'in', _.pluck(self.pricelists, 'id')]]; },
|
||||
loaded: function(self, pricelist_items){
|
||||
var pricelist_by_id = {};
|
||||
_.each(self.pricelists, function (pricelist) {
|
||||
pricelist_by_id[pricelist.id] = pricelist;
|
||||
});
|
||||
|
||||
_.each(pricelist_items, function (item) {
|
||||
var pricelist = pricelist_by_id[item.pricelist_id[0]];
|
||||
pricelist.items.push(item);
|
||||
item.base_pricelist = pricelist_by_id[item.base_pricelist_id[0]];
|
||||
});
|
||||
},
|
||||
},{
|
||||
model: 'product.category',
|
||||
fields: ['name', 'parent_id'],
|
||||
loaded: function(self, product_categories){
|
||||
var category_by_id = {};
|
||||
_.each(product_categories, function (category) {
|
||||
category_by_id[category.id] = category;
|
||||
});
|
||||
_.each(product_categories, function (category) {
|
||||
category.parent = category_by_id[category.parent_id[0]];
|
||||
});
|
||||
|
||||
self.product_categories = product_categories;
|
||||
},
|
||||
},{
|
||||
model: 'res.currency',
|
||||
fields: ['name','symbol','position','rounding'],
|
||||
ids: function(self){ return [self.pricelist.currency_id[0]]; },
|
||||
ids: function(self){ return [self.config.currency_id[0]]; },
|
||||
loaded: function(self, currencies){
|
||||
self.currency = currencies[0];
|
||||
if (self.currency.rounding > 0) {
|
||||
@@ -310,14 +345,17 @@ exports.PosModel = Backbone.Model.extend({
|
||||
},
|
||||
},{
|
||||
model: 'product.product',
|
||||
fields: ['display_name', 'list_price','price','pos_categ_id', 'taxes_id', 'barcode', 'default_code',
|
||||
'to_weight', 'uom_id', 'description_sale', 'description',
|
||||
fields: ['display_name', 'list_price', 'standard_price', 'categ_id', 'pos_categ_id', 'taxes_id',
|
||||
'barcode', 'default_code', 'to_weight', 'uom_id', 'description_sale', 'description',
|
||||
'product_tmpl_id','tracking'],
|
||||
order: _.map(['sequence','default_code','name'], function (name) { return {name: name}; }),
|
||||
domain: [['sale_ok','=',true],['available_in_pos','=',true]],
|
||||
context: function(self){ return { pricelist: self.pricelist.id, display_default_code: false }; },
|
||||
context: function(self){ return { display_default_code: false }; },
|
||||
loaded: function(self, products){
|
||||
self.db.add_products(products);
|
||||
self.db.add_products(_.map(products, function (product) {
|
||||
product.categ = _.findWhere(self.product_categories, {'id': product.categ_id[0]});
|
||||
return new exports.Product({}, product);
|
||||
}));
|
||||
},
|
||||
},{
|
||||
model: 'account.bank.statement',
|
||||
@@ -1160,6 +1198,87 @@ exports.load_models = function(models,options) {
|
||||
pmodels.splice.apply(pmodels,[index,0].concat(models));
|
||||
};
|
||||
|
||||
exports.Product = Backbone.Model.extend({
|
||||
initialize: function(attr, options){
|
||||
_.extend(this, options);
|
||||
},
|
||||
|
||||
// Port of get_product_price on product.pricelist.
|
||||
//
|
||||
// Anything related to UOM can be ignored, the POS will always use
|
||||
// the default UOM set on the product and the user cannot change
|
||||
// it.
|
||||
//
|
||||
// Pricelist items do not have to be sorted. All
|
||||
// product.pricelist.item records are loaded with a search_read
|
||||
// and were automatically sorted based on their _order by the
|
||||
// ORM. After that they are added in this order to the pricelists.
|
||||
get_price: function(pricelist, quantity){
|
||||
var self = this;
|
||||
var date = moment().startOf('day');
|
||||
|
||||
var category_ids = [];
|
||||
var category = this.categ;
|
||||
while (category) {
|
||||
category_ids.push(category.id);
|
||||
category = category.parent;
|
||||
}
|
||||
|
||||
var pricelist_items = _.filter(pricelist.items, function (item) {
|
||||
return (! item.product_tmpl_id || item.product_tmpl_id[0] === self.product_tmpl_id) &&
|
||||
(! item.product_id || item.product_id[0] === self.id) &&
|
||||
(! item.categ_id || _.contains(category_ids, item.categ_id[0])) &&
|
||||
(! item.date_start || moment(item.date_start).isSameOrBefore(date)) &&
|
||||
(! item.date_end || moment(item.date_end).isSameOrAfter(date));
|
||||
});
|
||||
|
||||
var price = self.list_price;
|
||||
_.find(pricelist_items, function (rule) {
|
||||
if (rule.min_quantity && quantity < rule.min_quantity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rule.base === 'pricelist') {
|
||||
price = self.get_price(rule.base_pricelist, quantity);
|
||||
} else if (rule.base === 'standard_price') {
|
||||
price = self.standard_price;
|
||||
}
|
||||
|
||||
if (rule.compute_price === 'fixed') {
|
||||
price = rule.fixed_price;
|
||||
return true;
|
||||
} else if (rule.compute_price === 'percentage') {
|
||||
price = price - (price * (rule.percent_price / 100));
|
||||
return true;
|
||||
} else {
|
||||
var price_limit = price;
|
||||
price = price - (price * (rule.price_discount / 100));
|
||||
if (rule.price_round) {
|
||||
price = round_pr(price, rule.price_round);
|
||||
}
|
||||
if (rule.price_surcharge) {
|
||||
price += rule.price_surcharge;
|
||||
}
|
||||
if (rule.price_min_margin) {
|
||||
price = Math.max(price, price_limit + rule.price_min_margin);
|
||||
}
|
||||
if (rule.price_max_margin) {
|
||||
price = Math.min(price, price_limit + rule.price_max_margin);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// This return value has to be rounded with round_di before
|
||||
// being used further. Note that this cannot happen here,
|
||||
// because it would cause inconsistencies with the backend for
|
||||
// pricelist that have base == 'pricelist'.
|
||||
return price;
|
||||
},
|
||||
});
|
||||
|
||||
var orderline_id = 1;
|
||||
|
||||
// An orderline represent one element of the content of a client's shopping cart.
|
||||
@@ -1174,14 +1293,19 @@ exports.Orderline = Backbone.Model.extend({
|
||||
return;
|
||||
}
|
||||
this.product = options.product;
|
||||
this.price = options.product.price;
|
||||
this.set_product_lot(this.product);
|
||||
this.set_quantity(1);
|
||||
this.discount = 0;
|
||||
this.discountStr = '0';
|
||||
this.type = 'unit';
|
||||
this.selected = false;
|
||||
this.id = orderline_id++;
|
||||
this.id = orderline_id++;
|
||||
|
||||
if (options.price) {
|
||||
this.set_unit_price(options.price);
|
||||
} else {
|
||||
this.set_unit_price(this.product.get_price(this.order.pricelist, this.get_quantity()));
|
||||
}
|
||||
},
|
||||
init_from_JSON: function(json) {
|
||||
this.product = this.pos.db.get_product_by_id(json.product_id);
|
||||
@@ -1192,7 +1316,7 @@ exports.Orderline = Backbone.Model.extend({
|
||||
this.set_product_lot(this.product);
|
||||
this.price = json.price_unit;
|
||||
this.set_discount(json.discount);
|
||||
this.set_quantity(json.qty);
|
||||
this.set_quantity(json.qty, 'do not recompute unit price');
|
||||
this.id = json.id;
|
||||
orderline_id = Math.max(this.id+1,orderline_id);
|
||||
var pack_lot_lines = json.pack_lot_ids;
|
||||
@@ -1242,7 +1366,7 @@ exports.Orderline = Backbone.Model.extend({
|
||||
// sets the quantity of the product. The quantity will be rounded according to the
|
||||
// product's unity of measure properties. Quantities greater than zero will not get
|
||||
// rounded to zero
|
||||
set_quantity: function(quantity){
|
||||
set_quantity: function(quantity, keep_price){
|
||||
this.order.assert_editable();
|
||||
if(quantity === 'remove'){
|
||||
this.order.remove_orderline(this);
|
||||
@@ -1264,7 +1388,11 @@ exports.Orderline = Backbone.Model.extend({
|
||||
this.quantityStr = '' + this.quantity;
|
||||
}
|
||||
}
|
||||
this.trigger('change',this);
|
||||
|
||||
// just like in sale.order changing the quantity will recompute the unit price
|
||||
if(! keep_price){
|
||||
this.set_unit_price(this.product.get_price(this.order.pricelist, this.get_quantity()));
|
||||
}
|
||||
},
|
||||
// return the quantity of product
|
||||
get_quantity: function(){
|
||||
@@ -1354,7 +1482,7 @@ exports.Orderline = Backbone.Model.extend({
|
||||
return false;
|
||||
}else if(this.get_discount() > 0){ // we don't merge discounted orderlines
|
||||
return false;
|
||||
}else if(this.price !== orderline.price){
|
||||
}else if(this.price !== orderline.get_product().get_price(orderline.order.pricelist, this.get_quantity())){
|
||||
return false;
|
||||
}else if(this.product.tracking == 'lot') {
|
||||
return false;
|
||||
@@ -1778,6 +1906,7 @@ exports.Order = Backbone.Model.extend({
|
||||
this.paymentlines = new PaymentlineCollection();
|
||||
this.pos_session_id = this.pos.pos_session.id;
|
||||
this.finalized = false; // if true, cannot be modified.
|
||||
this.set_pricelist(this.pos.default_pricelist);
|
||||
|
||||
this.set({ client: null });
|
||||
|
||||
@@ -1824,7 +1953,7 @@ exports.Order = Backbone.Model.extend({
|
||||
var client;
|
||||
this.sequence_number = json.sequence_number;
|
||||
this.pos.pos_session.sequence_number = Math.max(this.sequence_number+1,this.pos.pos_session.sequence_number);
|
||||
this.session_id = json.pos_session_id;
|
||||
this.session_id = json.pos_session_id;
|
||||
this.uid = json.uid;
|
||||
this.name = _t("Order ") + this.uid;
|
||||
this.validation_date = json.creation_date;
|
||||
@@ -1841,6 +1970,14 @@ exports.Order = Backbone.Model.extend({
|
||||
}
|
||||
}
|
||||
|
||||
if (json.pricelist_id) {
|
||||
this.pricelist = _.find(this.pos.pricelists, function (pricelist) {
|
||||
return pricelist.id === json.pricelist_id;
|
||||
});
|
||||
} else {
|
||||
this.pricelist = this.pos.default_pricelist;
|
||||
}
|
||||
|
||||
if (json.partner_id) {
|
||||
client = this.pos.db.get_partner_by_id(json.partner_id);
|
||||
if (!client) {
|
||||
@@ -1890,6 +2027,7 @@ exports.Order = Backbone.Model.extend({
|
||||
lines: orderLines,
|
||||
statement_ids: paymentLines,
|
||||
pos_session_id: this.pos_session_id,
|
||||
pricelist_id: this.pricelist ? this.pricelist.id : false,
|
||||
partner_id: this.get_client() ? this.get_client().id : false,
|
||||
user_id: this.pos.get_cashier().id,
|
||||
uid: this.uid,
|
||||
@@ -2080,6 +2218,14 @@ exports.Order = Backbone.Model.extend({
|
||||
this.add_product(tip_product, {quantity: 1, price: tip });
|
||||
}
|
||||
},
|
||||
set_pricelist: function (pricelist) {
|
||||
var self = this;
|
||||
this.pricelist = pricelist;
|
||||
_.each(this.get_orderlines(), function (line) {
|
||||
line.set_unit_price(line.product.get_price(self.pricelist, line.get_quantity()));
|
||||
});
|
||||
this.trigger('change');
|
||||
},
|
||||
remove_orderline: function( line ){
|
||||
this.assert_editable();
|
||||
this.orderlines.remove(line);
|
||||
|
||||
@@ -816,6 +816,14 @@ var ProductListWidget = PosBaseWidget.extend({
|
||||
|
||||
this.product_list = options.product_list || [];
|
||||
this.product_cache = new DomCache();
|
||||
|
||||
this.pos.get('orders').bind('add remove change', function () {
|
||||
self.renderElement();
|
||||
}, this);
|
||||
|
||||
this.pos.bind('change:selectedOrder', function () {
|
||||
this.renderElement();
|
||||
}, this);
|
||||
},
|
||||
set_product_list: function(product_list){
|
||||
this.product_list = product_list;
|
||||
@@ -829,20 +837,35 @@ var ProductListWidget = PosBaseWidget.extend({
|
||||
var target = $target[0];
|
||||
target.parentNode.replaceChild(this.el,target);
|
||||
},
|
||||
calculate_cache_key: function(product, pricelist){
|
||||
return product.id + ',' + pricelist.id;
|
||||
},
|
||||
_get_active_pricelist: function(){
|
||||
var current_order = this.pos.get_order();
|
||||
var current_pricelist = this.pos.default_pricelist;
|
||||
|
||||
if (current_order) {
|
||||
current_pricelist = current_order.pricelist;
|
||||
}
|
||||
|
||||
return current_pricelist;
|
||||
},
|
||||
render_product: function(product){
|
||||
var cached = this.product_cache.get_node(product.id);
|
||||
var current_pricelist = this._get_active_pricelist();
|
||||
var cache_key = this.calculate_cache_key(product, current_pricelist);
|
||||
var cached = this.product_cache.get_node(cache_key);
|
||||
if(!cached){
|
||||
var image_url = this.get_product_image_url(product);
|
||||
var product_html = QWeb.render('Product',{
|
||||
widget: this,
|
||||
product: product,
|
||||
product: product,
|
||||
pricelist: current_pricelist,
|
||||
image_url: this.get_product_image_url(product),
|
||||
});
|
||||
var product_node = document.createElement('div');
|
||||
product_node.innerHTML = product_html;
|
||||
product_node = product_node.childNodes[1];
|
||||
this.product_cache.cache_node(product.id,product_node);
|
||||
this.product_cache.cache_node(cache_key,product_node);
|
||||
return product_node;
|
||||
}
|
||||
return cached;
|
||||
@@ -1121,15 +1144,14 @@ var ClientListScreenWidget = ScreenWidget.extend({
|
||||
}
|
||||
},
|
||||
save_changes: function(){
|
||||
var self = this;
|
||||
var order = this.pos.get_order();
|
||||
if( this.has_client_changed() ){
|
||||
if ( this.new_client ) {
|
||||
order.fiscal_position = _.find(this.pos.fiscal_positions, function (fp) {
|
||||
return fp.id === self.new_client.property_account_position_id[0];
|
||||
});
|
||||
order.fiscal_position = _.findWhere(this.pos.fiscal_positions, {'id': this.new_client.property_account_position_id[0]});
|
||||
order.set_pricelist(_.findWhere(this.pos.pricelists, {'id': this.new_client.property_product_pricelist[0]}) || this.pos.default_pricelist);
|
||||
} else {
|
||||
order.fiscal_position = undefined;
|
||||
order.set_pricelist(this.pos.default_pricelist);
|
||||
}
|
||||
|
||||
order.set_client(this.new_client);
|
||||
@@ -2071,6 +2093,64 @@ define_action_button({
|
||||
},
|
||||
});
|
||||
|
||||
var set_pricelist_button = ActionButtonWidget.extend({
|
||||
template: 'SetPricelistButton',
|
||||
init: function (parent, options) {
|
||||
this._super(parent, options);
|
||||
|
||||
this.pos.get('orders').bind('add remove change', function () {
|
||||
this.renderElement();
|
||||
}, this);
|
||||
|
||||
this.pos.bind('change:selectedOrder', function () {
|
||||
this.renderElement();
|
||||
}, this);
|
||||
},
|
||||
button_click: function () {
|
||||
var self = this;
|
||||
|
||||
var pricelists = _.map(self.pos.pricelists, function (pricelist) {
|
||||
return {
|
||||
label: pricelist.name,
|
||||
item: pricelist
|
||||
};
|
||||
});
|
||||
|
||||
self.gui.show_popup('selection',{
|
||||
title: _t('Select pricelist'),
|
||||
list: pricelists,
|
||||
confirm: function (pricelist) {
|
||||
var order = self.pos.get_order();
|
||||
order.set_pricelist(pricelist);
|
||||
},
|
||||
is_selected: function (pricelist) {
|
||||
return pricelist.id === self.pos.get_order().pricelist.id;
|
||||
}
|
||||
});
|
||||
},
|
||||
get_current_pricelist_name: function () {
|
||||
var name = _t('Pricelist');
|
||||
var order = this.pos.get_order();
|
||||
|
||||
if (order) {
|
||||
var pricelist = order.pricelist;
|
||||
|
||||
if (pricelist) {
|
||||
name = pricelist.display_name;
|
||||
}
|
||||
}
|
||||
return name;
|
||||
},
|
||||
});
|
||||
|
||||
define_action_button({
|
||||
'name': 'set_pricelist',
|
||||
'widget': set_pricelist_button,
|
||||
'condition': function(){
|
||||
return this.pos.pricelists.length > 1;
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
ReceiptScreenWidget: ReceiptScreenWidget,
|
||||
ActionButtonWidget: ActionButtonWidget,
|
||||
@@ -2087,6 +2167,7 @@ return {
|
||||
ProductCategoriesWidget: ProductCategoriesWidget,
|
||||
ScaleScreenWidget: ScaleScreenWidget,
|
||||
set_fiscal_position_button: set_fiscal_position_button,
|
||||
set_pricelist_button: set_pricelist_button,
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
@@ -1,7 +1,212 @@
|
||||
odoo.define('point_of_sale.Tour', function (require) {
|
||||
odoo.define('point_of_sale.tour.pricelist', function (require) {
|
||||
"use strict";
|
||||
|
||||
var tour = require("web_tour.tour");
|
||||
var Tour = require('web_tour.tour');
|
||||
var rpc = require('web.rpc');
|
||||
var utils = require('web.utils');
|
||||
var round_di = utils.round_decimals;
|
||||
|
||||
function assert (condition, message) {
|
||||
if (! condition) {
|
||||
throw message || "Assertion failed";
|
||||
}
|
||||
}
|
||||
|
||||
function _build_pricelist_context (pricelist, quantity, date) {
|
||||
return {
|
||||
pricelist: pricelist.id,
|
||||
quantity: quantity,
|
||||
};
|
||||
}
|
||||
|
||||
function compare_backend_frontend (product, pricelist_name, quantity) {
|
||||
return function () {
|
||||
var pricelist = _.findWhere(posmodel.pricelists, {name: pricelist_name});
|
||||
var frontend_price = product.get_price(pricelist, quantity);
|
||||
// ORM applies digits= on non-stored computed field when
|
||||
// reading. It does not however truncate like it does when
|
||||
// storing the field.
|
||||
frontend_price = round_di(frontend_price, posmodel.dp['Product Price']);
|
||||
|
||||
var context = _build_pricelist_context(pricelist, quantity);
|
||||
return rpc.query({model: 'product.product', method: 'read', args: [[product.id], ['price']], context: context})
|
||||
.then(function (backend_result) {
|
||||
var debug_info = _.extend(context, {
|
||||
product: product.id,
|
||||
product_display_name: product.display_name,
|
||||
pricelist_name: pricelist.name,
|
||||
});
|
||||
var backend_price = backend_result[0].price;
|
||||
assert(frontend_price === backend_price,
|
||||
JSON.stringify(debug_info) + ' DOESN\'T MATCH -> ' + backend_price + ' (backend) != ' + frontend_price + ' (frontend)');
|
||||
return (new $.Deferred()).resolve();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
var steps = [{
|
||||
content: 'waiting for loading to finish',
|
||||
trigger: '.o_main_content:has(.loader:hidden)',
|
||||
run: function () {
|
||||
var product_boni_orange = posmodel.db.search_product_in_category(0, 'Boni Oranges')[0];
|
||||
var product_papillon_orange = posmodel.db.search_product_in_category(0, 'Orange Butterfly')[0];
|
||||
var product_citron = posmodel.db.search_product_in_category(0, 'Lemon')[0];
|
||||
var product_limon = posmodel.db.search_product_in_category(0, 'Stringers')[0];
|
||||
var product_pamplemousse = posmodel.db.search_product_in_category(0, 'Red grapefruit')[0];
|
||||
var product_grapes = posmodel.db.search_product_in_category(0, 'Black Grapes')[0];
|
||||
var product_external_audit = posmodel.db.search_product_in_category(0, 'External Audit')[0];
|
||||
var product_miscellaneous = posmodel.db.search_product_in_category(0, 'Miscellaneous')[0];
|
||||
|
||||
compare_backend_frontend(product_grapes, 'Public Pricelist', 0, undefined)()
|
||||
.then(compare_backend_frontend(product_grapes, 'Public Pricelist', 1, undefined))
|
||||
.then(compare_backend_frontend(product_grapes, 'Fixed', 1, undefined))
|
||||
.then(compare_backend_frontend(product_boni_orange, 'Fixed', 1, undefined))
|
||||
.then(compare_backend_frontend(product_papillon_orange, 'Fixed', 1, undefined))
|
||||
.then(compare_backend_frontend(product_boni_orange, 'Percentage', 1, undefined))
|
||||
.then(compare_backend_frontend(product_papillon_orange, 'Percentage', 1, undefined))
|
||||
.then(compare_backend_frontend(product_citron, 'Percentage', 1, undefined))
|
||||
.then(compare_backend_frontend(product_boni_orange, 'Formula', 1, undefined))
|
||||
.then(compare_backend_frontend(product_papillon_orange, 'Formula', 1, undefined))
|
||||
.then(compare_backend_frontend(product_citron, 'Formula', 1, undefined))
|
||||
.then(compare_backend_frontend(product_limon, 'Formula', 1, undefined))
|
||||
.then(compare_backend_frontend(product_pamplemousse, 'Formula', 1, undefined))
|
||||
.then(compare_backend_frontend(product_boni_orange, 'min_quantity ordering', 1, undefined))
|
||||
.then(compare_backend_frontend(product_boni_orange, 'min_quantity ordering', 2, undefined))
|
||||
.then(compare_backend_frontend(product_grapes, 'Category vs no category', 1, undefined))
|
||||
.then(compare_backend_frontend(product_external_audit, 'Category vs no category', 1, undefined))
|
||||
.then(compare_backend_frontend(product_grapes, 'Category', 1, undefined))
|
||||
.then(compare_backend_frontend(product_external_audit, 'Category', 1, undefined))
|
||||
.then(compare_backend_frontend(product_boni_orange, 'Product template', 1, undefined))
|
||||
.then(compare_backend_frontend(product_external_audit, 'Product template', 1, undefined))
|
||||
.then(compare_backend_frontend(product_boni_orange, 'Dates', 1, undefined))
|
||||
.then(compare_backend_frontend(product_miscellaneous, 'Cost base', 1, undefined))
|
||||
.then(compare_backend_frontend(product_miscellaneous, 'Pricelist base', 1, undefined))
|
||||
.then(compare_backend_frontend(product_miscellaneous, 'Pricelist base 2', 1, undefined))
|
||||
.then(compare_backend_frontend(product_papillon_orange, 'Pricelist base rounding', 1, undefined))
|
||||
.then(function () {
|
||||
$('.pos').addClass('done-testing');
|
||||
});
|
||||
},
|
||||
}];
|
||||
|
||||
steps = steps.concat([{
|
||||
content: "wait for unit tests to finish",
|
||||
trigger: ".pos.done-testing",
|
||||
run: function () {}, // it's a check
|
||||
}, {
|
||||
content: "click pricelist button",
|
||||
trigger: ".control-button.o_pricelist_button",
|
||||
}, {
|
||||
content: "verify default pricelist is set",
|
||||
trigger: ".selection-item.selected:contains('Public Pricelist')",
|
||||
run: function () {}, // it's a check
|
||||
}, {
|
||||
content: "select fixed pricelist",
|
||||
trigger: ".selection-item:contains('Fixed')",
|
||||
}, {
|
||||
content: "prices should be updated in the product screen",
|
||||
trigger: ".product:contains('Miscellaneous'):contains('$ 1.00')",
|
||||
run: function () {}, // it's a check
|
||||
}, {
|
||||
content: "open customer list",
|
||||
trigger: "button.set-customer",
|
||||
}, {
|
||||
content: "select agrolait",
|
||||
trigger: ".client-line:contains('Agrolait')",
|
||||
}, {
|
||||
content: "confirm selection",
|
||||
trigger: ".clientlist-screen .next",
|
||||
}, {
|
||||
content: "click pricelist button",
|
||||
trigger: ".control-button.o_pricelist_button",
|
||||
}, {
|
||||
content: "verify pricelist changed",
|
||||
trigger: ".selection-item.selected:contains('Public Pricelist')",
|
||||
run: function () {}, // it's a check
|
||||
}, {
|
||||
content: "cancel pricelist dialog",
|
||||
trigger: ".button.cancel:visible",
|
||||
}, {
|
||||
content: "prices should be updated in the product screen",
|
||||
trigger: ".product:contains('Miscellaneous'):contains('$ 18.00')",
|
||||
run: function () {}, // it's a check
|
||||
}, {
|
||||
content: "open customer list",
|
||||
trigger: "button.set-customer",
|
||||
}, {
|
||||
content: "select think big systems",
|
||||
trigger: ".client-line:contains('Think Big Systems')",
|
||||
}, {
|
||||
content: "confirm selection",
|
||||
trigger: ".clientlist-screen .next",
|
||||
}, {
|
||||
content: "click pricelist button",
|
||||
trigger: ".control-button.o_pricelist_button",
|
||||
}, {
|
||||
content: "verify pricelist remained public pricelist ('Not loaded' is not available)",
|
||||
trigger: ".selection-item.selected:contains('Public Pricelist')",
|
||||
run: function () {}, // it's a check
|
||||
}, {
|
||||
content: "cancel pricelist dialog",
|
||||
trigger: ".button.cancel:visible",
|
||||
}, {
|
||||
content: "click pricelist button",
|
||||
trigger: ".control-button.o_pricelist_button",
|
||||
}, {
|
||||
content: "select fixed pricelist",
|
||||
trigger: ".selection-item:contains('min_quantity ordering')",
|
||||
}, {
|
||||
content: "order 1 kg oranges",
|
||||
trigger: ".product:contains('Boni Oranges')",
|
||||
}, {
|
||||
content: "change qty to 2 kg",
|
||||
trigger: ".numpad button.input-button:visible:contains('2')",
|
||||
}, {
|
||||
content: "verify that unit price of oranges changed to $1",
|
||||
trigger: ".total > .value:contains('$ 2.00')",
|
||||
}, {
|
||||
content: "click pricelist button",
|
||||
trigger: ".control-button.o_pricelist_button",
|
||||
}, {
|
||||
content: "select public pricelist",
|
||||
trigger: ".selection-item:contains('Public Pricelist')",
|
||||
}, {
|
||||
content: "verify that order has been recomputed",
|
||||
trigger: ".total > .value:contains('$ 3.96')",
|
||||
}, {
|
||||
content: "click pricelist button",
|
||||
trigger: ".control-button.o_pricelist_button",
|
||||
}, {
|
||||
content: "select fixed pricelist",
|
||||
trigger: ".selection-item:contains('min_quantity ordering')",
|
||||
}, {
|
||||
content: "order 1 miscellaneous product",
|
||||
trigger: ".product:contains('Miscellaneous')",
|
||||
}, {
|
||||
content: "order 1 miscellaneous product",
|
||||
trigger: ".product:contains('Miscellaneous')",
|
||||
}, {
|
||||
content: "order 1 miscellaneous product",
|
||||
trigger: ".product:contains('Miscellaneous')",
|
||||
}, {
|
||||
content: "verify there is one line with 3 miscellaneous products",
|
||||
trigger: ".orderline:contains('Miscellaneous') em:contains('3.000')",
|
||||
run: function () {}, // it's a check
|
||||
}, {
|
||||
content: "close the Point of Sale frontend",
|
||||
trigger: ".header-button",
|
||||
}, {
|
||||
content: "confirm closing the frontend",
|
||||
trigger: ".header-button",
|
||||
}]);
|
||||
|
||||
Tour.register('pos_pricelist', { test: true, url: '/pos/web' }, steps);
|
||||
});
|
||||
|
||||
odoo.define('point_of_sale.tour.acceptance', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Tour = require("web_tour.tour");
|
||||
|
||||
function add_product_to_order(product_name) {
|
||||
return [{
|
||||
@@ -113,6 +318,6 @@ odoo.define('point_of_sale.Tour', function (require) {
|
||||
trigger: ".header-button",
|
||||
}]);
|
||||
|
||||
tour.register('pos_basic_order', { test: true, url: '/pos/web' }, steps);
|
||||
Tour.register('pos_basic_order', { test: true, url: '/pos/web' }, steps);
|
||||
|
||||
});
|
||||
|
||||
@@ -129,6 +129,12 @@
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="SetPricelistButton">
|
||||
<div class='control-button o_pricelist_button'>
|
||||
<i class='fa fa-th-list' /> <t t-esc='widget.get_current_pricelist_name()'/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="ActionpadWidget">
|
||||
<div class="actionpad">
|
||||
<button t-attf-class='button set-customer #{ ( widget.pos.get_client() and widget.pos.get_client().name.length > 10) ? "decentered" : "" }' >
|
||||
@@ -440,6 +446,15 @@
|
||||
<span class='detail vat empty'>N/A</span>
|
||||
</t>
|
||||
</div>
|
||||
<div class='client-detail'>
|
||||
<span class='label'>Pricelist</span>
|
||||
<t t-if='partner.property_product_pricelist'>
|
||||
<span class='detail property_product_pricelist'><t t-esc='partner.property_product_pricelist[1]'/></span>
|
||||
</t>
|
||||
<t t-if='!partner.property_product_pricelist'>
|
||||
<span class='detail property_product_pricelist empty'>N/A</span>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1221,12 +1236,12 @@
|
||||
<img t-att-src='image_url' />
|
||||
<t t-if="!product.to_weight">
|
||||
<span class="price-tag">
|
||||
<t t-esc="widget.format_currency(product.price,'Product Price')"/>
|
||||
<t t-esc="widget.format_currency(product.get_price(pricelist, 1),'Product Price')"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-if="product.to_weight">
|
||||
<span class="price-tag">
|
||||
<t t-esc="widget.format_currency(product.price,'Product Price')+'/'+widget.pos.units_by_id[product.uom_id[0]].name"/>
|
||||
<t t-esc="widget.format_currency(product.get_price(pricelist, 1),'Product Price')+'/'+widget.pos.units_by_id[product.uom_id[0]].name"/>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.api import Environment
|
||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
||||
from datetime import date, timedelta
|
||||
|
||||
import odoo.tests
|
||||
|
||||
@@ -12,6 +14,11 @@ class TestUi(odoo.tests.HttpCase):
|
||||
assert cr == self.registry.test_cr
|
||||
env = Environment(cr, self.uid, {})
|
||||
|
||||
# By default parent_store computation is deferred until end of
|
||||
# tests. Pricelist items however are sorted based on these
|
||||
# fields, so they need to be computed.
|
||||
env['product.category']._parent_store_compute()
|
||||
|
||||
journal_obj = env['account.journal']
|
||||
account_obj = env['account.account']
|
||||
main_company = env.ref('base.main_company')
|
||||
@@ -29,6 +36,216 @@ class TestUi(odoo.tests.HttpCase):
|
||||
'fields_id': field.id,
|
||||
'value': 'account.account,' + str(account_receivable.id)})
|
||||
|
||||
fixed_pricelist = env['product.pricelist'].create({
|
||||
'name': 'Fixed',
|
||||
'item_ids': [(0, 0, {
|
||||
'compute_price': 'fixed',
|
||||
'fixed_price': 1,
|
||||
}), (0, 0, {
|
||||
'compute_price': 'fixed',
|
||||
'fixed_price': 2,
|
||||
'applied_on': '0_product_variant',
|
||||
'product_id': env.ref('point_of_sale.boni_orange').id,
|
||||
}), (0, 0, {
|
||||
'compute_price': 'fixed',
|
||||
'fixed_price': 13.95, # test for issues like in 7f260ab517ebde634fc274e928eb062463f0d88f
|
||||
'applied_on': '0_product_variant',
|
||||
'product_id': env.ref('point_of_sale.papillon_orange').id,
|
||||
})],
|
||||
})
|
||||
|
||||
env['product.pricelist'].create({
|
||||
'name': 'Percentage',
|
||||
'item_ids': [(0, 0, {
|
||||
'compute_price': 'percentage',
|
||||
'percent_price': 100,
|
||||
'applied_on': '0_product_variant',
|
||||
'product_id': env.ref('point_of_sale.boni_orange').id,
|
||||
}), (0, 0, {
|
||||
'compute_price': 'percentage',
|
||||
'percent_price': 99,
|
||||
'applied_on': '0_product_variant',
|
||||
'product_id': env.ref('point_of_sale.papillon_orange').id,
|
||||
}), (0, 0, {
|
||||
'compute_price': 'percentage',
|
||||
'percent_price': 0,
|
||||
'applied_on': '0_product_variant',
|
||||
'product_id': env.ref('point_of_sale.citron').id,
|
||||
})],
|
||||
})
|
||||
|
||||
env['product.pricelist'].create({
|
||||
'name': 'Formula',
|
||||
'item_ids': [(0, 0, {
|
||||
'compute_price': 'formula',
|
||||
'price_discount': 6,
|
||||
'price_surcharge': 5,
|
||||
'applied_on': '0_product_variant',
|
||||
'product_id': env.ref('point_of_sale.boni_orange').id,
|
||||
}), (0, 0, {
|
||||
# .99 prices
|
||||
'compute_price': 'formula',
|
||||
'price_surcharge': -0.01,
|
||||
'price_round': 1,
|
||||
'applied_on': '0_product_variant',
|
||||
'product_id': env.ref('point_of_sale.papillon_orange').id,
|
||||
}), (0, 0, {
|
||||
'compute_price': 'formula',
|
||||
'price_min_margin': 10,
|
||||
'price_max_margin': 100,
|
||||
'applied_on': '0_product_variant',
|
||||
'product_id': env.ref('point_of_sale.citron').id,
|
||||
}), (0, 0, {
|
||||
'compute_price': 'formula',
|
||||
'price_surcharge': 10,
|
||||
'price_max_margin': 5,
|
||||
'applied_on': '0_product_variant',
|
||||
'product_id': env.ref('point_of_sale.limon').id,
|
||||
}), (0, 0, {
|
||||
'compute_price': 'formula',
|
||||
'price_discount': -100,
|
||||
'price_min_margin': 5,
|
||||
'price_max_margin': 20,
|
||||
'applied_on': '0_product_variant',
|
||||
'product_id': env.ref('point_of_sale.pamplemousse_rouge_pamplemousse').id,
|
||||
})],
|
||||
})
|
||||
|
||||
env['product.pricelist'].create({
|
||||
'name': 'min_quantity ordering',
|
||||
'item_ids': [(0, 0, {
|
||||
'compute_price': 'fixed',
|
||||
'fixed_price': 1,
|
||||
'applied_on': '0_product_variant',
|
||||
'min_quantity': 2,
|
||||
'product_id': env.ref('point_of_sale.boni_orange').id,
|
||||
}), (0, 0, {
|
||||
'compute_price': 'fixed',
|
||||
'fixed_price': 2,
|
||||
'applied_on': '0_product_variant',
|
||||
'min_quantity': 1,
|
||||
'product_id': env.ref('point_of_sale.boni_orange').id,
|
||||
}), (0, 0, {
|
||||
'compute_price': 'fixed',
|
||||
'fixed_price': 2,
|
||||
'applied_on': '0_product_variant',
|
||||
'min_quantity': 2,
|
||||
'product_id': env.ref('point_of_sale.product_product_consumable').id,
|
||||
})],
|
||||
})
|
||||
|
||||
env['product.pricelist'].create({
|
||||
'name': 'Product template',
|
||||
'item_ids': [(0, 0, {
|
||||
'compute_price': 'fixed',
|
||||
'fixed_price': 1,
|
||||
'applied_on': '1_product',
|
||||
'product_tmpl_id': env.ref('point_of_sale.boni_orange_product_template').id,
|
||||
}), (0, 0, {
|
||||
'compute_price': 'fixed',
|
||||
'fixed_price': 2,
|
||||
})],
|
||||
})
|
||||
|
||||
env['product.pricelist'].create({
|
||||
# no category has precedence over category
|
||||
'name': 'Category vs no category',
|
||||
'item_ids': [(0, 0, {
|
||||
'compute_price': 'fixed',
|
||||
'fixed_price': 1,
|
||||
'applied_on': '2_product_category',
|
||||
'categ_id': env.ref('product.product_category_3').id, # All / Saleable / Services
|
||||
}), (0, 0, {
|
||||
'compute_price': 'fixed',
|
||||
'fixed_price': 2,
|
||||
})],
|
||||
})
|
||||
|
||||
p = env['product.pricelist'].create({
|
||||
'name': 'Category',
|
||||
'item_ids': [(0, 0, {
|
||||
'compute_price': 'fixed',
|
||||
'fixed_price': 2,
|
||||
'applied_on': '2_product_category',
|
||||
'categ_id': env.ref('product.product_category_all').id,
|
||||
}), (0, 0, {
|
||||
'compute_price': 'fixed',
|
||||
'fixed_price': 1,
|
||||
'applied_on': '2_product_category',
|
||||
'categ_id': env.ref('product.product_category_3').id, # All / Saleable / Services
|
||||
})],
|
||||
})
|
||||
|
||||
today = date.today()
|
||||
one_week_ago = today - timedelta(weeks=1)
|
||||
two_weeks_ago = today - timedelta(weeks=2)
|
||||
one_week_from_now = today + timedelta(weeks=1)
|
||||
two_weeks_from_now = today + timedelta(weeks=2)
|
||||
|
||||
env['product.pricelist'].create({
|
||||
'name': 'Dates',
|
||||
'item_ids': [(0, 0, {
|
||||
'compute_price': 'fixed',
|
||||
'fixed_price': 1,
|
||||
'date_start': two_weeks_ago.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||
'date_end': one_week_ago.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||
}), (0, 0, {
|
||||
'compute_price': 'fixed',
|
||||
'fixed_price': 2,
|
||||
'date_start': today.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||
'date_end': one_week_from_now.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||
}), (0, 0, {
|
||||
'compute_price': 'fixed',
|
||||
'fixed_price': 3,
|
||||
'date_start': one_week_from_now.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||
'date_end': two_weeks_from_now.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||
})],
|
||||
})
|
||||
|
||||
cost_base_pricelist = env['product.pricelist'].create({
|
||||
'name': 'Cost base',
|
||||
'item_ids': [(0, 0, {
|
||||
'base': 'standard_price',
|
||||
'compute_price': 'percentage',
|
||||
'percent_price': 55,
|
||||
})],
|
||||
})
|
||||
|
||||
pricelist_base_pricelist = env['product.pricelist'].create({
|
||||
'name': 'Pricelist base',
|
||||
'item_ids': [(0, 0, {
|
||||
'base': 'pricelist',
|
||||
'base_pricelist_id': cost_base_pricelist.id,
|
||||
'compute_price': 'percentage',
|
||||
'percent_price': 15,
|
||||
})],
|
||||
})
|
||||
|
||||
env['product.pricelist'].create({
|
||||
'name': 'Pricelist base 2',
|
||||
'item_ids': [(0, 0, {
|
||||
'base': 'pricelist',
|
||||
'base_pricelist_id': pricelist_base_pricelist.id,
|
||||
'compute_price': 'percentage',
|
||||
'percent_price': 3,
|
||||
})],
|
||||
})
|
||||
|
||||
env['product.pricelist'].create({
|
||||
'name': 'Pricelist base rounding',
|
||||
'item_ids': [(0, 0, {
|
||||
'base': 'pricelist',
|
||||
'base_pricelist_id': fixed_pricelist.id,
|
||||
'compute_price': 'percentage',
|
||||
'percent_price': 0.01,
|
||||
})],
|
||||
})
|
||||
|
||||
excluded_pricelist = env['product.pricelist'].create({
|
||||
'name': 'Not loaded'
|
||||
})
|
||||
env.ref('base.res_partner_18').property_product_pricelist = excluded_pricelist
|
||||
|
||||
# set the company currency to USD, otherwise it will assume
|
||||
# euro's. this will cause issues as the sales journal is in
|
||||
# USD, because of this all products would have a different
|
||||
@@ -40,7 +257,8 @@ class TestUi(odoo.tests.HttpCase):
|
||||
'type': 'sale',
|
||||
'company_id': main_company.id})
|
||||
|
||||
env['product.pricelist'].search([]).write(dict(currency_id=main_company.currency_id.id))
|
||||
all_pricelists = env['product.pricelist'].search([('id', '!=', excluded_pricelist.id)])
|
||||
all_pricelists.write(dict(currency_id=main_company.currency_id.id))
|
||||
|
||||
main_pos_config.write({
|
||||
'journal_id': test_sale_journal.id,
|
||||
@@ -49,7 +267,9 @@ class TestUi(odoo.tests.HttpCase):
|
||||
'code': 'TSC',
|
||||
'type': 'cash',
|
||||
'company_id': main_company.id,
|
||||
'journal_user': True})]})
|
||||
'journal_user': True})],
|
||||
'available_pricelist_ids': [(4, pricelist.id) for pricelist in all_pricelists],
|
||||
})
|
||||
|
||||
# open a session, the /pos/web controller will redirect to it
|
||||
main_pos_config.open_session_cb()
|
||||
@@ -61,6 +281,11 @@ class TestUi(odoo.tests.HttpCase):
|
||||
env['ir.module.module'].search([('name', '=', 'point_of_sale')], limit=1).state = 'installed'
|
||||
cr.release()
|
||||
|
||||
self.phantom_js("/pos/web",
|
||||
"odoo.__DEBUG__.services['web_tour.tour'].run('pos_pricelist')",
|
||||
"odoo.__DEBUG__.services['web_tour.tour'].tours.pos_pricelist.ready",
|
||||
login="admin")
|
||||
|
||||
self.phantom_js("/pos/web",
|
||||
"odoo.__DEBUG__.services['web_tour.tour'].run('pos_basic_order')",
|
||||
"odoo.__DEBUG__.services['web_tour.tour'].tours.pos_basic_order.ready",
|
||||
|
||||
@@ -481,6 +481,7 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
|
||||
'amount_total': untax + atax,
|
||||
'creation_date': fields.Datetime.now(),
|
||||
'fiscal_position_id': False,
|
||||
'pricelist_id': self.pos_config.available_pricelist_ids[0].id,
|
||||
'lines': [[0,
|
||||
0,
|
||||
{'discount': 0,
|
||||
@@ -514,6 +515,7 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
|
||||
'amount_total': untax + atax,
|
||||
'creation_date': fields.Datetime.now(),
|
||||
'fiscal_position_id': False,
|
||||
'pricelist_id': self.pos_config.available_pricelist_ids[0].id,
|
||||
'lines': [[0,
|
||||
0,
|
||||
{'discount': 0,
|
||||
@@ -547,6 +549,7 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
|
||||
'amount_total': untax + atax,
|
||||
'creation_date': fields.Datetime.now(),
|
||||
'fiscal_position_id': False,
|
||||
'pricelist_id': self.pos_config.available_pricelist_ids[0].id,
|
||||
'lines': [[0,
|
||||
0,
|
||||
{'discount': 0,
|
||||
|
||||
@@ -249,8 +249,13 @@
|
||||
Set shop-specific prices, seasonal discounts, etc.
|
||||
</div>
|
||||
<div class="content-group mt16" attrs="{'invisible': [('use_pricelist','=',False)], 'required' : [('use_pricelist', '=', True)]}">
|
||||
<div class="mt16">
|
||||
<field name="pricelist_id"/>
|
||||
<div class="row mt16">
|
||||
<label string="Available Pricelists" for="available_pricelist_ids" class="col-md-3 o_light_label"/>
|
||||
<field name="available_pricelist_ids" widget="many2many_tags"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label string="Default Pricelist" for="pricelist_id" class="col-md-3 o_light_label"/>
|
||||
<field name="pricelist_id" domain="[('id', 'in', available_pricelist_ids)]" options="{'no_create': True}"/>
|
||||
</div>
|
||||
<div>
|
||||
<button name="%(product.product_pricelist_action2)d" icon="fa-arrow-right" type="action" string="Pricelists" class="btn-link"/>
|
||||
|
||||
@@ -66,7 +66,7 @@ var SplitbillScreenWidget = screens.ScreenWidget.extend({
|
||||
split.line = line.clone();
|
||||
neworder.add_orderline(split.line);
|
||||
}
|
||||
split.line.set_quantity(split.quantity);
|
||||
split.line.set_quantity(split.quantity, 'do not recompute unit price');
|
||||
}else if( split.line ) {
|
||||
neworder.remove_orderline(split.line);
|
||||
split.line = null;
|
||||
@@ -115,7 +115,7 @@ var SplitbillScreenWidget = screens.ScreenWidget.extend({
|
||||
for(var id in splitlines){
|
||||
var split = splitlines[id];
|
||||
var line = order.get_orderline(parseInt(id));
|
||||
line.set_quantity(line.get_quantity() - split.quantity);
|
||||
line.set_quantity(line.get_quantity() - split.quantity, 'do not recompute unit price');
|
||||
if(Math.abs(line.get_quantity()) < 0.00001){
|
||||
order.remove_orderline(line);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user