diff options
| author | Mitali Patel <mpa@odoo.com> | 2016-05-04 11:44:32 +0530 |
|---|---|---|
| committer | Joren Van Onder <jov@odoo.com> | 2016-06-13 10:19:01 +0200 |
| commit | 8f39c652c65c81d87503d64eeef0da3e0048ffc9 (patch) | |
| tree | e31794524a27537c5a44a3371bf324fc235630cc /addons | |
| parent | 06e28ce2a483deeb40496ec167ba95fd12cd1e4c (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
Diffstat (limited to 'addons')
| -rw-r--r-- | addons/point_of_sale/models/pos_order.py | 43 | ||||
| -rw-r--r-- | addons/point_of_sale/models/pos_session.py | 16 | ||||
| -rw-r--r-- | addons/point_of_sale/static/src/css/pos.css | 13 | ||||
| -rw-r--r-- | addons/point_of_sale/static/src/js/gui.js | 8 | ||||
| -rw-r--r-- | addons/point_of_sale/static/src/js/models.js | 133 | ||||
| -rw-r--r-- | addons/point_of_sale/static/src/js/popups.js | 69 | ||||
| -rw-r--r-- | addons/point_of_sale/static/src/js/screens.js | 31 | ||||
| -rw-r--r-- | addons/point_of_sale/static/src/xml/pos.xml | 32 | ||||
| -rw-r--r-- | addons/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<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: |
