[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
This commit is contained in:
Mitali Patel
2016-06-13 10:19:01 +02:00
committed by Joren Van Onder
parent 06e28ce2a4
commit 8f39c652c6
9 changed files with 337 additions and 11 deletions
+40 -3
View File
@@ -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')
@@ -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:
+12 -1
View File
@@ -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;
}
+4 -4
View File
@@ -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');
+132 -1
View File
@@ -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();
@@ -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',
+29 -2
View File
@@ -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 ------ */
@@ -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())"/>
@@ -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: