From 8f39c652c65c81d87503d64eeef0da3e0048ffc9 Mon Sep 17 00:00:00 2001 From: Mitali Patel Date: Wed, 4 May 2016 11:44:32 +0530 Subject: [PATCH] [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 --- addons/point_of_sale/models/pos_order.py | 43 +++++- addons/point_of_sale/models/pos_session.py | 16 +++ addons/point_of_sale/static/src/css/pos.css | 13 +- addons/point_of_sale/static/src/js/gui.js | 8 +- addons/point_of_sale/static/src/js/models.js | 133 +++++++++++++++++- addons/point_of_sale/static/src/js/popups.js | 69 +++++++++ addons/point_of_sale/static/src/js/screens.js | 31 +++- addons/point_of_sale/static/src/xml/pos.xml | 32 +++++ .../point_of_sale/views/pos_session_view.xml | 3 + 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 .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 @@ + + + +