summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMitali Patel <mpa@odoo.com>2016-05-04 11:44:32 +0530
committerJoren Van Onder <jov@odoo.com>2016-06-13 10:19:01 +0200
commit8f39c652c65c81d87503d64eeef0da3e0048ffc9 (patch)
treee31794524a27537c5a44a3371bf324fc235630cc
parent06e28ce2a483deeb40496ec167ba95fd12cd1e4c (diff)
[IMP] point_of_sale: manage lot/serial number from POS front-end
Frontend: - When product has lot/serial tracking open popup for accept lot/serial number when it selected. - Add icon in order line to show related lot/serial number and it indicate status of product lot - User can add or remove product by lot from current lot dialog box - Order line quantity don't match with lot line quantity show warning message Back End: - Added model `pos.pack.operation.lot` to manage product lot related with pos order - Added state button in pos session form view to show pickings which are not in 'Done' stage
-rw-r--r--addons/point_of_sale/models/pos_order.py43
-rw-r--r--addons/point_of_sale/models/pos_session.py16
-rw-r--r--addons/point_of_sale/static/src/css/pos.css13
-rw-r--r--addons/point_of_sale/static/src/js/gui.js8
-rw-r--r--addons/point_of_sale/static/src/js/models.js133
-rw-r--r--addons/point_of_sale/static/src/js/popups.js69
-rw-r--r--addons/point_of_sale/static/src/js/screens.js31
-rw-r--r--addons/point_of_sale/static/src/xml/pos.xml32
-rw-r--r--addons/point_of_sale/views/pos_session_view.xml3
9 files changed, 337 insertions, 11 deletions
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index e5269122798..0d0d69d3479 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -589,9 +589,7 @@ class PosOrder(models.Model):
if picking_id:
picking_id.action_confirm()
picking_id.force_assign()
- # Mark pack operations as done
- for pack in picking_id.pack_operation_ids:
- pack.write({'qty_done': pack.product_qty})
+ order.set_pack_operation_lot()
picking_id.action_done()
elif Move:
Move.action_confirm()
@@ -599,6 +597,34 @@ class PosOrder(models.Model):
Move.action_done()
return True
+ def set_pack_operation_lot(self):
+ """Set Serial/Lot number in pack operations to mark the pack operation done."""
+
+ StockProductionLot = self.env['stock.production.lot']
+ PosPackOperationLot = self.env['pos.pack.operation.lot']
+
+ for order in self:
+ for pack_operation in order.picking_id.pack_operation_ids:
+ qty = 0
+ qty_done = 0
+ pack_lots = []
+ pos_pack_lots = PosPackOperationLot.search([('order_id', '=', order.id), ('product_id', '=', pack_operation.product_id.id)])
+ pack_lot_names = [pos_pack.lot_name for pos_pack in pos_pack_lots]
+
+ if pack_lot_names:
+ for lot_name in list(set(pack_lot_names)):
+ stock_production_lot = StockProductionLot.search([('name', '=', lot_name), ('product_id', '=', pack_operation.product_id.id)])
+ if stock_production_lot:
+ if stock_production_lot.product_id.tracking == 'lot':
+ qty = pack_lot_names.count(lot_name)
+ else:
+ qty = 1.0
+ qty_done += qty
+ pack_lots.append({'lot_id': stock_production_lot.id, 'qty': qty})
+ else:
+ qty_done = pack_operation.product_qty
+ pack_operation.write({'pack_lot_ids': map(lambda x: (0, 0, x), pack_lots), 'qty_done': qty_done})
+
def add_payment(self, data):
"""Create a new payment for the order"""
args = {
@@ -704,6 +730,7 @@ class PosOrderLine(models.Model):
create_date = fields.Datetime(string='Creation Date', readonly=True)
tax_ids = fields.Many2many('account.tax', string='Taxes', readonly=True)
tax_ids_after_fiscal_position = fields.Many2many('account.tax', compute='_get_tax_ids_after_fiscal_position', string='Taxes')
+ pack_lot_ids = fields.One2many('pos.pack.operation.lot', 'pos_order_line_id', string='Lot/serial Number')
@api.depends('price_unit', 'tax_ids', 'qty', 'discount', 'product_id')
def _compute_amount_line_all(self):
@@ -752,3 +779,13 @@ class PosOrderLine(models.Model):
def _get_tax_ids_after_fiscal_position(self):
for line in self:
line.tax_ids_after_fiscal_position = line.order_id.fiscal_position_id.map_tax(line.tax_ids)
+
+
+class PosOrderLineLot(models.Model):
+ _name = "pos.pack.operation.lot"
+ _description = "Specify product lot/serial number in pos order line"
+
+ pos_order_line_id = fields.Many2one('pos.order.line')
+ order_id = fields.Many2one('pos.order', related="pos_order_line_id.order_id")
+ lot_name = fields.Char('Lot Name')
+ product_id = fields.Many2one('product.product', related='pos_order_line_id.product_id')
diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py
index 162f89b0462..2cbb19e71f1 100644
--- a/addons/point_of_sale/models/pos_session.py
+++ b/addons/point_of_sale/models/pos_session.py
@@ -88,9 +88,25 @@ class PosSession(models.Model):
string='Available Payment Methods')
order_ids = fields.One2many('pos.order', 'session_id', string='Orders')
statement_ids = fields.One2many('account.bank.statement', 'pos_session_id', string='Bank Statement', readonly=True)
+ picking_count = fields.Integer(compute='_compute_picking_count')
_sql_constraints = [('uniq_name', 'unique(name)', _("The name of this POS Session must be unique !"))]
+ @api.multi
+ def _compute_picking_count(self):
+ for pos in self:
+ pickings = pos.order_ids.mapped('picking_id').filtered(lambda x: x.state != 'done')
+ pos.picking_count = len(pickings.ids)
+
+ @api.multi
+ def action_stock_picking(self):
+ pickings = self.order_ids.mapped('picking_id').filtered(lambda x: x.state != 'done')
+ action_picking = self.env.ref('stock.action_picking_tree_ready')
+ action = action_picking.read()[0]
+ action['context'] = {}
+ action['domain'] = [('id', 'in', pickings.ids)]
+ return action
+
@api.depends('cash_control', 'cash_journal_id', 'config_id.cash_control')
def _compute_cash_all(self):
for session in self:
diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css
index 4904dde752d..939b3940b84 100644
--- a/addons/point_of_sale/static/src/css/pos.css
+++ b/addons/point_of_sale/static/src/css/pos.css
@@ -497,7 +497,9 @@ td {
.pos .oe_icon.oe_orange {
color: rgb(239, 153, 65);
}
-
+.pos .oe_link_icon{
+ cursor:pointer;
+}
/* ********* Contains everything below the bar ********* */
.pos .pos-content {
@@ -2223,6 +2225,15 @@ td {
box-sizing: border-box;
width: 80%;
}
+.pos .popup .packlot-lines{
+ overflow: auto;
+ height: 250px;
+ margin: 10px;
+}
+.pos .popup .packlot-line-input {
+ margin: 3px;
+}
+
.pos .popup-number .popup-input {
text-align: center;
}
diff --git a/addons/point_of_sale/static/src/js/gui.js b/addons/point_of_sale/static/src/js/gui.js
index 11da0b4441f..08fb2b57cc1 100644
--- a/addons/point_of_sale/static/src/js/gui.js
+++ b/addons/point_of_sale/static/src/js/gui.js
@@ -85,14 +85,14 @@ var Gui = core.Class.extend({
// 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) {
+ show_screen: function(screen_name,params,refresh,skip_close_popup) {
var screen = this.screen_instances[screen_name];
if (!screen) {
console.error("ERROR: show_screen("+screen_name+") : screen not found");
}
-
- this.close_popup();
-
+ if (!skip_close_popup){
+ this.close_popup();
+ }
var order = this.pos.get_order();
if (order) {
var old_screen_name = order.get_screen_data('screen');
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index cd0fe4fa9f4..d1d9f987c85 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -309,7 +309,7 @@ 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',
- 'product_tmpl_id'],
+ 'product_tmpl_id','tracking'],
order: ['sequence','default_code','name'],
domain: [['sale_ok','=',true],['available_in_pos','=',true]],
context: function(self){ return { pricelist: self.pricelist.id, display_default_code: false }; },
@@ -1063,6 +1063,7 @@ exports.Orderline = Backbone.Model.extend({
return;
}
this.product = options.product;
+ this.set_product_lot(this.product)
this.price = options.product.price;
this.set_quantity(1);
this.discount = 0;
@@ -1076,11 +1077,18 @@ exports.Orderline = Backbone.Model.extend({
if (!this.product) {
console.error('ERROR: attempting to recover product not available in the point of sale');
}
+ this.set_product_lot(this.product)
this.price = json.price_unit;
this.set_discount(json.discount);
this.set_quantity(json.qty);
this.id = json.id;
orderline_id = Math.max(this.id+1,orderline_id);
+ var pack_lot_lines = json.pack_lot_ids;
+ for (var i = 0; i < pack_lot_lines.length; i++) {
+ var packlotline = pack_lot_lines[i][2];
+ var pack_lot_line = new exports.Packlotline({}, {'json': _.extend(packlotline, {'order_line':this})});
+ this.pack_lot_lines.add(pack_lot_line);
+ }
},
clone: function(){
var orderline = new exports.Orderline({},{
@@ -1097,6 +1105,10 @@ exports.Orderline = Backbone.Model.extend({
orderline.selected = false;
return orderline;
},
+ set_product_lot: function(product){
+ this.has_product_lot = product.tracking !== 'none';
+ this.pack_lot_lines = this.has_product_lot && new PacklotlineCollection(null, {'order_line': this});
+ },
// sets a discount [0,100]%
set_discount: function(discount){
var disc = Math.min(Math.max(parseFloat(discount) || 0, 0),100);
@@ -1156,6 +1168,31 @@ exports.Orderline = Backbone.Model.extend({
return this.quantityStr;
}
},
+
+ compute_lot_lines: function(){
+ var pack_lot_lines = this.pack_lot_lines;
+ var lines = pack_lot_lines.length;
+ if(this.quantity > lines){
+ for(var i=0; i<this.quantity - lines; i++){
+ pack_lot_lines.add(new exports.Packlotline({}, {'order_line': this}));
+ }
+ }
+ if(this.quantity < lines){
+ var to_remove = lines - this.quantity;
+ var lot_lines = pack_lot_lines.sortBy('lot_name').slice(0, to_remove);
+ pack_lot_lines.remove(lot_lines);
+ }
+ return this.pack_lot_lines;
+ },
+
+ has_valid_product_lot: function(){
+ if(!this.has_product_lot){
+ return true;
+ }
+ var valid_product_lot = this.pack_lot_lines.get_valid_lots();
+ return this.quantity === valid_product_lot.length;
+ },
+
// return the unit of measure of the product
get_unit: function(){
var unit_id = this.product.uom_id;
@@ -1203,6 +1240,12 @@ exports.Orderline = Backbone.Model.extend({
this.set_quantity(this.get_quantity() + orderline.get_quantity());
},
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(),
@@ -1210,6 +1253,7 @@ exports.Orderline = Backbone.Model.extend({
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
};
},
//used to create a json of the ticket, to be sent to the printer
@@ -1438,6 +1482,76 @@ var OrderlineCollection = Backbone.Collection.extend({
model: exports.Orderline,
});
+exports.Packlotline = Backbone.Model.extend({
+ defaults: {
+ lot_name: null
+ },
+ initialize: function(attributes, options){
+ this.order_line = options.order_line;
+ if (options.json) {
+ this.init_from_JSON(options.json);
+ return;
+ }
+ },
+
+ init_from_JSON: function(json) {
+ this.order_line = json.order_line;
+ this.set_lot_name(json.lot_name);
+ },
+
+ set_lot_name: function(name){
+ this.set({lot_name : _.str.trim(name) || null});
+ },
+
+ get_lot_name: function(){
+ return this.get('lot_name');
+ },
+
+ export_as_JSON: function(){
+ return {
+ lot_name: this.get_lot_name(),
+ };
+ },
+
+ add: function(){
+ var order_line = this.order_line,
+ index = this.collection.indexOf(this);
+ var new_lot_model = new exports.Packlotline({}, {'order_line': this.order_line});
+ this.collection.add(new_lot_model, {at: index + 1});
+ return new_lot_model;
+ },
+
+ remove: function(){
+ this.collection.remove(this);
+ }
+});
+
+var PacklotlineCollection = Backbone.Collection.extend({
+ model: exports.Packlotline,
+ initialize: function(models, options) {
+ this.order_line = options.order_line;
+ },
+
+ get_empty_model: function(){
+ return this.findWhere({'lot_name': null});
+ },
+
+ remove_empty_model: function(){
+ this.remove(this.where({'lot_name': null}));
+ },
+
+ get_valid_lots: function(){
+ return this.filter(function(model){
+ return model.get('lot_name');
+ });
+ },
+
+ set_quantity_by_lot: function() {
+ var valid_lots = this.get_valid_lots();
+ this.order_line.set_quantity(valid_lots.length);
+ }
+});
+
// Every Paymentline contains a cashregister and an amount of money.
exports.Paymentline = Backbone.Model.extend({
initialize: function(attributes, options) {
@@ -1861,6 +1975,10 @@ exports.Order = Backbone.Model.extend({
this.orderlines.add(line);
}
this.select_orderline(this.get_last_orderline());
+
+ if(line.has_product_lot){
+ this.display_lot_popup();
+ }
},
get_selected_orderline: function(){
return this.selected_orderline;
@@ -1884,6 +2002,19 @@ exports.Order = Backbone.Model.extend({
this.selected_orderline = undefined;
}
},
+
+ display_lot_popup: function() {
+ var order_line = this.get_selected_orderline();
+ if (order_line){
+ var pack_lot_lines = order_line.compute_lot_lines();
+ this.pos.gui.show_popup('packlotline', {
+ 'title': _t('Lot/Serial Number(s) Required'),
+ 'pack_lot_lines': pack_lot_lines,
+ 'order': this
+ });
+ }
+ },
+
/* ---- Payment Lines --- */
add_paymentline: function(cashregister) {
this.assert_editable();
diff --git a/addons/point_of_sale/static/src/js/popups.js b/addons/point_of_sale/static/src/js/popups.js
index 17a3fd6ac7c..f4429840237 100644
--- a/addons/point_of_sale/static/src/js/popups.js
+++ b/addons/point_of_sale/static/src/js/popups.js
@@ -197,6 +197,75 @@ var TextAreaPopupWidget = TextInputPopupWidget.extend({
});
gui.define_popup({name:'textarea', widget: TextAreaPopupWidget});
+var PackLotLinePopupWidget = PopupWidget.extend({
+ template: 'PackLotLinePopupWidget',
+ events: _.extend({}, PopupWidget.prototype.events, {
+ 'click .remove-lot': 'remove_lot',
+ 'keydown': 'add_lot',
+ 'blur .packlot-line-input': 'lose_input_focus'
+ }),
+
+ show: function(options){
+ this._super(options);
+ this.focus();
+ },
+
+ click_confirm: function(){
+ var pack_lot_lines = this.options.pack_lot_lines;
+ this.$('.packlot-line-input').each(function(index, el){
+ var cid = $(el).attr('cid'),
+ lot_name = $(el).val();
+ var pack_line = pack_lot_lines.get({cid: cid});
+ pack_line.set_lot_name(lot_name);
+ });
+ pack_lot_lines.remove_empty_model();
+ pack_lot_lines.set_quantity_by_lot();
+ this.options.order.save_to_db();
+ this.gui.close_popup();
+ },
+
+ add_lot: function(ev) {
+ if (ev.keyCode === $.ui.keyCode.ENTER){
+ var pack_lot_lines = this.options.pack_lot_lines,
+ $input = $(ev.target),
+ cid = $input.attr('cid'),
+ lot_name = $input.val();
+
+ var lot_model = pack_lot_lines.get({cid: cid});
+ lot_model.set_lot_name(lot_name); // First set current model then add new one
+ if(!pack_lot_lines.get_empty_model()){
+ var new_lot_model = lot_model.add();
+ this.focus_model = new_lot_model;
+ }
+ pack_lot_lines.set_quantity_by_lot();
+ this.renderElement();
+ this.focus();
+ }
+ },
+
+ remove_lot: function(ev){
+ var pack_lot_lines = this.options.pack_lot_lines,
+ $input = $(ev.target).prev(),
+ cid = $input.attr('cid');
+ var lot_model = pack_lot_lines.get({cid: cid});
+ lot_model.remove();
+ pack_lot_lines.set_quantity_by_lot();
+ this.renderElement();
+ },
+
+ lose_input_focus: function(ev){
+ var $input = $(ev.target),
+ cid = $input.attr('cid');
+ var lot_model = this.options.pack_lot_lines.get({cid: cid});
+ lot_model.set_lot_name($input.val());
+ },
+
+ focus: function(){
+ this.$("input[autofocus]").focus();
+ this.focus_model = false; // after focus clear focus_model on widget
+ }
+});
+gui.define_popup({name:'packlotline', widget:PackLotLinePopupWidget});
var NumberPopupWidget = PopupWidget.extend({
template: 'NumberPopupWidget',
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index ee28d1a393e..978b0fb8a03 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -61,7 +61,7 @@ var ScreenWidget = PosBaseWidget.extend({
var self = this;
if (self.pos.scan_product(code)) {
if (self.barcode_product_screen) {
- self.gui.show_screen(self.barcode_product_screen);
+ self.gui.show_screen(self.barcode_product_screen, null, null, true);
}
} else {
this.barcode_error_action(code);
@@ -402,7 +402,21 @@ var ActionpadWidget = PosBaseWidget.extend({
var self = this;
this._super();
this.$('.pay').click(function(){
- self.gui.show_screen('payment');
+ var order = self.pos.get_order();
+ var has_valid_product_lot = _.every(order.orderlines.models, function(line){
+ return line.has_valid_product_lot();
+ });
+ if(!has_valid_product_lot){
+ self.gui.show_popup('confirm',{
+ 'title': _t('Empty Serial/Lot Number'),
+ 'body': _t('One or more product(s) required serial/lot number.'),
+ confirm: function(){
+ self.gui.show_screen('payment');
+ },
+ });
+ }else{
+ self.gui.show_screen('payment');
+ }
});
this.$('.set-customer').click(function(){
self.gui.show_screen('clientlist');
@@ -439,6 +453,8 @@ var OrderWidget = PosBaseWidget.extend({
this.pos.get_order().select_orderline(orderline);
this.numpad_state.reset();
},
+
+
set_value: function(val) {
var order = this.pos.get_order();
if (order.get_selected_orderline()) {
@@ -495,6 +511,12 @@ var OrderWidget = PosBaseWidget.extend({
el_node = el_node.childNodes[0];
el_node.orderline = orderline;
el_node.addEventListener('click',this.line_click_handler);
+ var el_lot_icon = el_node.querySelector('.line-lot-icon');
+ if(el_lot_icon){
+ el_lot_icon.addEventListener('click', (function() {
+ this.show_product_lot(orderline);
+ }.bind(this)));
+ }
orderline.node = el_node;
return el_node;
@@ -559,6 +581,11 @@ var OrderWidget = PosBaseWidget.extend({
this.el.querySelector('.summary .total > .value').textContent = this.format_currency(total);
this.el.querySelector('.summary .total .subentry .value').textContent = this.format_currency(taxes);
},
+ show_product_lot: function(orderline){
+ this.pos.get_order().select_orderline(orderline);
+ var order = this.pos.get_order();
+ order.display_lot_popup();
+ },
});
/* ------ The Product Categories ------ */
diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml
index c93eee36eda..6570961f28b 100644
--- a/addons/point_of_sale/static/src/xml/pos.xml
+++ b/addons/point_of_sale/static/src/xml/pos.xml
@@ -941,6 +941,35 @@
</div>
</t>
+ <t t-name="PackLotLinePopupWidget">
+ <div class="modal-dialog">
+ <div class="popup popup-text">
+ <p class="title"><t t-esc=" widget.options.title || '' " /></p>
+ <div class='packlot-lines'>
+ <t t-if="widget.options.pack_lot_lines">
+ <t t-set="focus_lot_line" t-value="widget.focus_model || widget.options.pack_lot_lines.get_empty_model()"/>
+ <t t-foreach='widget.options.pack_lot_lines.models' t-as='lot_line'>
+ <input type="text" t-att-cid="lot_line.cid"
+ t-att-value="lot_line.get('lot_name')"
+ t-att-autofocus="lot_line === focus_lot_line ? 'autofocus': undefined"
+ class="popup-input packlot-line-input"
+ placeholder="Serial/Lot Number"/>
+ <i class="oe_link_icon remove-lot fa fa-trash-o"></i>
+ </t>
+ </t>
+ </div>
+ <div class="footer">
+ <div class="button confirm">
+ Ok
+ </div>
+ <div class="button cancel">
+ Cancel
+ </div>
+ </div>
+ </div>
+ </div>
+ </t>
+
<t t-name="TextAreaPopupWidget">
<div class="modal-dialog">
<div class="popup popup-textinput">
@@ -1094,6 +1123,9 @@
<li t-attf-class="orderline #{ line.selected ? 'selected' : '' }">
<span class="product-name">
<t t-esc="line.get_product().display_name"/>
+ <t t-if="line.get_product().tracking!=='none'">
+ <i t-attf-class="oe_link_icon fa fa-list oe_icon line-lot-icon #{line.has_valid_product_lot() ? 'oe_green' : 'oe_red' }"/>
+ </t>
</span>
<span class="price">
<t t-esc="widget.format_currency(line.get_display_price())"/>
diff --git a/addons/point_of_sale/views/pos_session_view.xml b/addons/point_of_sale/views/pos_session_view.xml
index c5e070251f8..b4d14e023cd 100644
--- a/addons/point_of_sale/views/pos_session_view.xml
+++ b/addons/point_of_sale/views/pos_session_view.xml
@@ -62,6 +62,9 @@
type="action" context="{'balance': 'end', 'bank_statement_id': cash_register_id, 'default_pos_id': config_id}">
<span class="o_stat_text">Set Closing Balance</span>
</button>
+ <button class="oe_stat_button" name="action_stock_picking" type="object" icon="fa-arrows-v" attrs="{'invisible':[('picking_count', '=', 0)]}">
+ <field name="picking_count" widget="statinfo" string="Picking Errors"/>
+ </button>
</div>
<h1 class="oe_title">
Session: