diff --git a/addons/stock/__init__.py b/addons/stock/__init__.py index a3e04934caf..7903619646d 100644 --- a/addons/stock/__init__.py +++ b/addons/stock/__init__.py @@ -27,3 +27,5 @@ import report import wizard import res_config +import controllers + diff --git a/addons/stock/__openerp__.py b/addons/stock/__openerp__.py index cc4392b006d..5651913c9eb 100644 --- a/addons/stock/__openerp__.py +++ b/addons/stock/__openerp__.py @@ -104,7 +104,6 @@ Dashboard / Reports for Warehouse Management will include: 'application': True, 'auto_install': False, 'css': [ - 'static/src/css/picking.css', 'static/src/css/stock.css', ], 'js': [ diff --git a/addons/stock/controllers/__init__.py b/addons/stock/controllers/__init__.py new file mode 100644 index 00000000000..039d9715fab --- /dev/null +++ b/addons/stock/controllers/__init__.py @@ -0,0 +1 @@ +import main \ No newline at end of file diff --git a/addons/stock/controllers/main.py b/addons/stock/controllers/main.py new file mode 100644 index 00000000000..b877dd11614 --- /dev/null +++ b/addons/stock/controllers/main.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +import logging +import simplejson +import os +import openerp +import time +import random + +from openerp import http +from openerp.http import request +from openerp.addons.web.controllers.main import manifest_list, module_boot, html_template + +_logger = logging.getLogger(__name__) + +html_template = """ + + + Barcode Scanner + + + + + + + + + + + + + + + %(js)s + + + + + + +""" + +class BarcodeController(http.Controller): + + @http.route(['/barcode/web/'], type='http', auth='user') + def a(self, debug=False, **k): + if not request.session.uid: + return http.local_redirect('/web/login?redirect=/barcode/web') + + js_list = manifest_list('js',db=request.db, debug=debug) + css_list = manifest_list('css',db=request.db, debug=debug) + + js = "\n".join('' % i for i in js_list) + #css = "\n".join('' % i for i in css_list) + r = html_template % { + 'js': js, + # 'css': css, + 'modules': simplejson.dumps(module_boot(request.db)), + 'init': """ + var wc = new s.web.WebClient(); + wc.show_application = function(){ + wc.action_manager.do_action("stock.ui", {}); + }; + wc.do_push_state = function(state){}; + wc.appendTo($(document.body)); + """ + } + return r diff --git a/addons/stock/static/src/css/barcode.css b/addons/stock/static/src/css/barcode.css new file mode 100644 index 00000000000..1b47d662690 --- /dev/null +++ b/addons/stock/static/src/css/barcode.css @@ -0,0 +1,134 @@ +.in_container_hidden { + display: none; +} +.in_container { +} +.oe_pick_app_header{ + margin-top: 0; +} +.oe_picking { + cursor: pointer; +} +.oe_kanban.oe_picking { + min-height: 80px; + margin-bottom: 10px; + border: 1px solid rgba(0, 0, 0, 0.16); + border-bottom-color: rgba(0, 0, 0, 0.3); + -webkit-transition: -webkit-transform, -webkit-box-shadow, border 200ms linear; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; + padding: 5px; +} + +.oe_kanban_color_0 { + background-color: white; + color: #5a5a5a; +} +.oe_kanban_color_1 { + background-color: #cccccc; + color: #424242; +} +.oe_kanban_color_2 { + background-color: #ffc7c7; + color: #7a3737; +} +.oe_kanban_color_3 { + background-color: #fff1c7; + color: #756832; +} +.oe_kanban_color_4 { + background-color: #e3ffc7; + color: #5d6937; +} +.oe_kanban_color_5 { + background-color: #c7ffd5; + color: #1a7759; +} +.oe_kanban_color_6 { + background-color: #c7ffff; + color: #1a5d83; +} +.oe_kanban_color_7 { + background-color: #c7d5ff; + color: #3b3e75; +} +.oe_kanban_color_8 { + background-color: #e3c7ff; + color: #4c3668; +} +.oe_kanban_color_9 { + background-color: #ffc7f1; + color: #6d2c70; +} + +/*Blinking text*/ +.blink_me { + -webkit-animation-name: blinker; + -webkit-animation-duration: 1s; + -webkit-animation-timing-function: linear; + -webkit-animation-iteration-count: 2; + + -moz-animation-name: blinker; + -moz-animation-duration: 1s; + -moz-animation-timing-function: linear; + -moz-animation-iteration-count: 2; + + animation-name: blinker; + animation-duration: 1s; + animation-timing-function: linear; + animation-iteration-count: 2; +} + +@-moz-keyframes blinker { + 0% { opacity: 1.0; } + 50% { opacity: 0.0; } + 100% { opacity: 1.0; } +} + +@-webkit-keyframes blinker { + 0% { opacity: 1.0; } + 50% { opacity: 0.0; } + 100% { opacity: 1.0; } +} + +@keyframes blinker { + 0% { opacity: 1.0; } + 50% { opacity: 0.0; } + 100% { opacity: 1.0; } +} + +/*hide OpenERP leftbar, table should use all width by default and display vertical scrollbar if needed*/ +.oe_leftbar { + display: none; +} + +table.oe_webclient.oe_content_full_screen{ + width: 100%; +} + +body{ + overflow-y: visible !important; +} + +/* --- Styling of OpenERP Elements --- + Needed for loading and error box */ + +/* Increase z-index value to insure that loading goes above navbar of bootstrap*/ +.openerp .oe_loading { + display: none; + z-index: 1000; + position: fixed; + top: 0; + right: 50%; + padding: 4px 12px; + background: #a61300; + color: white; + text-align: center; + border: 1px solid #990000; + border-top: none; + -moz-border-radius-bottomright: 8px; + -moz-border-radius-bottomleft: 8px; + border-bottom-right-radius: 8px; + border-bottom-left-radius: 8px; +} \ No newline at end of file diff --git a/addons/stock/static/src/css/picking.css b/addons/stock/static/src/css/picking.css deleted file mode 100644 index 80a89abe9c5..00000000000 --- a/addons/stock/static/src/css/picking.css +++ /dev/null @@ -1,291 +0,0 @@ - -/* ----------------------- * - * PICKING WIDGET LAYOUT * - * ----------------------- */ - -.oe_pick_widget { - position: absolute; - top: 0px; right: 0px; bottom: 0px; left: 0px; - color: #444444; - background: #686868; - text-shadow: none; - font-family: 'Lato', 'Open Sans', Arial, Helvetica, sans-serif; -} -.oe_pick_widget .oe_pick_layout { - width: 100%; - height: 100%; -} -.oe_pick_widget .oe_hidden{ - display: none !important; -} - -.oe_pick_widget .oe_pick_header{ - background: #454343; - background-image: linear-gradient(#646060,#262626); -} -.oe_pick_widget .oe_pick_body_cont{ - height: 100%; -} -.oe_pick_widget .oe_pick_body{ - width: 100%; - height: 100%; - background-color: white; - background: url('/web/static/src/img/form_sheetbg.png'); - box-shadow: 0px 9px 5px -5px rgba(0, 0, 0, 0.16) inset; - overflow: hidden; - overflow-y: auto; -} -.oe_pick_widget .oe_pick_app{ - width: 600px; - background: white; - margin: 40px auto; - padding: 20px; - border: solid 1px rgb(228, 228, 255); - box-shadow: 0px 2px 3px rgba(114, 114, 175, 0.11); -} -.oe_pick_widget .oe_pick_toolbar{ - width: 600px; - margin: auto; - text-align: center; -} -.oe_pick_widget .oe_pick_button{ - display: inline-block; - min-width: 64px; - padding: 5px 10px; - font-size: 18px; - border-radius: 3px; - margin: 10px 5px; - text-align: center; - line-height: 30px; - background: #b2b3d7; - background: linear-gradient(#b2b3d7, #7f82ac); - color: #273072; - border: 1px solid #353A7E; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(255, 255, 255, 0.8) inset; - cursor: pointer; - text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5); -} -.oe_pick_widget .oe_pick_button.oe_disabled { - cursor: default; - background-color: #5A5A5A; - background: linear-gradient(#666666, #474747); - border: solid 1px rgb(44, 44, 44); - color: rgb(53, 53, 53); - box-shadow: 0px 1px 1px rgba(255, 255, 255, 0.32) inset; -} - -.oe_pick_widget .oe_pick_button:not(.oe_disabled):active, -.oe_pick_widget .oe_pick_button:not(.oe_disabled).oe_active{ - background: rgb(92, 84, 133); - box-shadow: 0px 1px 0px 1px rgba(0,0,0,0.3) inset; - margin: 11px 6px; - border: none; - color: white; - text-shadow: 0px 1px rgba(0,0,0, 0.3); -} - -.oe_pick_widget .oe_pick_button.oe_small{ - min-width: 0px; - width: 32px; -} -.oe_pick_widget .oe_pick_right_toolbar{ - float: right; - border-left: solid 1px rgba(0,0,0,0.3); - padding-left: 8px; - padding-right: 8px; - margin-left: 8px; -} -.oe_pick_widget .oe_pick_col_small{ width: 60px; } -.oe_pick_widget .oe_pick_col_medium{ width: 100px; } -.oe_pick_widget .oe_pick_col_big{ width: 200px; } -.oe_pick_widget .oe_pick_col_expand{ width: 100%; } -.oe_pick_widget .oe_centeralign{ text-align: center; } - -.oe_pick_widget .oe_invalid{ - background: rgb(255, 226, 226); - color: rgb(168, 6, 6); -} -.oe_pick_widget .oe_pick_app_header{ - font-size: 24px; - margin-left: 5px; - margin-bottom: 10px; -} -.oe_pick_widget .oe_pick_app_title{ - font-size: 20px; - margin-top: 0px; -} -.oe_pick_widget .oe_pick_app_subtitle{ - font-size: 16px; - font-weight: normal; -} -.oe_pick_widget .oe_pick_app_info{ - margin-left: 4px; - opacity: 0.5; -} - - - -/* ----------------------- * - * PICKING TABLES * - * ----------------------- */ - -.oe_pick_widget .oe_pick_list{ - margin: 0px 0px 20px 0px; - padding: 5px; -} - -.oe_pick_widget .oe_pick_list_header{ - font-size: 20px; - margin-bottom: 5px; -} -.oe_pick_widget .oe_pick_list_table{ - background: whitesmoke; - width: 100%; - table-layout: fixed; - font-size: 16px; - border-radius: 3px; - -} -.oe_pick_widget .oe_pick_list_table tbody tr:nth-child(odd){ - background: rgb(250,250,250); -} -.oe_pick_widget .oe_pick_list_table tbody tr.oe_selected{ - background: rgb(236, 236, 247); - outline: solid 1px rgba(79, 107, 255, 0.25); -} -.oe_pick_widget .oe_pick_list_table td, -.oe_pick_widget .oe_pick_list_table th { - padding: 5px; - overflow: hidden; - text-overflow: ellipsis; -} -.oe_pick_widget .oe_pick_list_table thead{ - background: rgb(224, 225, 235); - border-top-left-radius: 3px; - border-top-right-radius: 3px; -} -.oe_pick_widget .oe_pick_list_table th { - background: none; - border: none; - font-weight: bold; -} -.oe_pick_widget .oe_row_button{ - display: inline-block; - background: rgb(238, 237, 248); - padding: 2px 4px; - line-height: 14px; - color: rgb(137, 127, 173); - border-radius: 3px; - border: solid 1px rgba(0, 0, 0, 0.09); - cursor: pointer; -} -.oe_pick_widget .oe_expanded{ - text-align: center; - min-width: 32px; -} -.oe_pick_widget .js_pack_select, -.oe_pick_widget .js_pack_op{ - cursor: pointer; -} - - -/* ----------------------- * - * MAIN PICKING MENU * - * ----------------------- */ - -.oe_pick_widget .oe_picking{ - background: rgb(223, 226, 246); - padding: 10px; - margin-bottom: 3px; - border-radius: 3px; - border-bottom: solid 2px rgb(189, 189, 241); - cursor: pointer; -} -.oe_pick_widget .oe_picking.oe_empty { - border: solid 1px rgb(228, 226, 241); - background: white; - cursor: default; -} -.oe_pick_widget .oe_picking > .oe_picking_name{ - display: inline-block; - min-width: 100px; - margin-right: 10px; -} -.oe_pick_widget .oe_search_empty{ - padding: 10px; - border-radius: 3px; - background: rgb(228, 226, 241); - text-align: center; -} - -/* ----------------------- * - * SEARCH BOX * - * ----------------------- */ - -.oe_pick_widget .oe_searchbox{ - margin-top: 13px; -} -.oe_pick_widget .oe_searchbox input{ - padding: 8px; -} -.oe_pick_widget .oe_picking_not_found{ - padding: 8px; - margin: 24px 0px; - text-align: center; - background: rgb(239, 240, 247); - border-radius: 3px; - color: rgb(119, 77, 77); -} - -/* ----------------------- * - * RESPONSIVENESS * - * ----------------------- */ - -@media screen and (max-width:642px){ - .oe_pick_widget .oe_pick_body{ - background: white; - background-image: none; - box-shadow: none; - } - .oe_pick_widget .oe_pick_app{ - width: auto; - max-width: 600px; - margin: 0 auto; - border: none; - box-shadow: none; - padding-left: 5px; - padding-right: 5px; - } - .oe_pick_widget .oe_pick_toolbar{ - width: auto; - } - .oe_pick_widget .oe_pick_right_toolbar{ - float: none; - text-align: center; - border-left: none; - margin-left: 0px; - } - .oe_pick_widget .oe_pick_button{ - margin-top: 3px; - margin-bottom: 3px; - } -} - -@media screen and (max-width:450px){ - .oe_pick_widget .oe_pick_list_header{ - font-size: 16px; - } - .oe_pick_widget .oe_pick_list_table{ - font-size: 12px; - } - .oe_pick_widget .oe_pick_col_small{ - width: 50px; - } -} - -@media screen and (max-width:326px){ - .oe_pick_widget .oe_pick_toolbar{ - width: 326px; - } -} - diff --git a/addons/stock/static/src/js/widgets.js b/addons/stock/static/src/js/widgets.js index 6995eff8de1..756d3caf841 100644 --- a/addons/stock/static/src/js/widgets.js +++ b/addons/stock/static/src/js/widgets.js @@ -1,4 +1,3 @@ - function openerp_picking_widgets(instance){ var module = instance.stock; @@ -26,116 +25,341 @@ function openerp_picking_widgets(instance){ template: 'PickingEditorWidget', init: function(parent,options){ this._super(parent,options); - }, - get_rows: function(){ - var model = this.getParent(); - var rows = []; - - _.each( model.movelines, function(moveline){ - rows.push({ - cols: { product: moveline.product_id[1], - qty: moveline.product_uom_qty, - rem: moveline.remaining_qty, - uom: moveline.product_uom[1], - loc: moveline.location_id[1], - id: moveline.product_id[0], - }, - classes: (moveline.qty_remaining < 0 ? 'oe_invalid' : '') - }); - }); - - return rows; - }, - renderElement: function(){ var self = this; - this._super(); - this.$('.js_pack_scan').click(function(){ - var id = parseInt($(this).attr('op-id')); - console.log('Id:',id); - self.getParent().scan_product_id(id); - }); - - //remove navigtion bar from default openerp GUI - $('td.navbar').html('
'); - }, - }); - - module.PackageEditorWidget = instance.web.Widget.extend({ - template: 'PackageEditorWidget', - get_header: function(){ - var model = this.getParent(); - var current_package = model.get_selected_package(); - return current_package ? 'Operations for Package: ' + current_package.name : 'Current Operations'; - }, - get_rows: function(){ - var model = this.getParent(); - var rows = []; - var ops = model.get_current_operations(); - _.each( ops, function(op){ - rows.push({ - cols: { - product: (op.package_id ? op.package_id[1] : op.product_id[1]) + (op.lot_id ? ' Lot: ' + op.lot_id[1] : ''), - uom: op.product_uom ? product_uom[1] : '', - qty: op.product_qty, - }, - classes: 'js_pack_op '+ (op.id === model.get_selected_operation() ? 'oe_selected' : ''), - att_op_id: op.id, - }); - }); - - return rows; - }, - renderElement: function(){ - var self = this; - this._super(); - var model = this.getParent(); - this.$('.js_pack_op').click(function(){ - if (!this.classList.contains('oe_selected')){ - self.$('.js_pack_op').removeClass('oe_selected'); - $(this).addClass('oe_selected'); - model.set_selected_operation(parseInt($(this).attr('op-id'))); - } else { - $(this).removeClass('oe_selected'); - model.set_selected_operation(null); + this.rows = []; + this.search_filter = ""; + jQuery.expr[":"].Contains = jQuery.expr.createPseudo(function(arg) { + return function( elem ) { + return jQuery(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0; }; }); }, - }); - - module.PackageSelectorWidget = instance.web.Widget.extend({ - template: 'PackageSelectorWidget', get_header: function(){ - return this._header || 'Packages:'; + return this.getParent().get_header(); + }, + get_location: function(){ + var model = this.getParent(); + var locations = []; + var self = this; + _.each(model.locations, function(loc){ + locations.push({name: loc.complete_name, id: loc.id,}); + }); + return locations; }, get_rows: function(){ var model = this.getParent(); - var current_package = model.get_selected_package(); - var rows = []; - _.each( model.packages, function(pack){ - rows.push({ - cols:{ pack: pack.name}, - id: pack.id, - classes: pack === current_package ? ' oe_selected' : '' , - }); + this.rows = []; + var self = this; + var pack_created = []; + _.each( model.packoplines, function(packopline){ + var pack = undefined; + var color = ""; + if (packopline.product_id[1] !== undefined){ pack = packopline.package_id[1];} + if (packopline.product_qty == packopline.qty_done){ color = "success "; } + if (packopline.product_qty < packopline.qty_done){ color = "danger "; } + //also check that we don't have a line already existing for that package + if (packopline.result_package_id[1] !== undefined && $.inArray(packopline.result_package_id[0], pack_created) === -1){ + self.rows.push({ + cols: { product: packopline.result_package_id[1], + qty: '', + rem: '', + uom: undefined, + lot: undefined, + pack: undefined, + container: packopline.result_package_id[1], + container_id: undefined, + loc: packopline.location_id[1], + dest: packopline.location_dest_id[1], + id: packopline.result_package_id[0], + product_id: undefined, + can_scan: false, + head_container: true, + processed: packopline.processed, + }, + classes: ('success container_head ') + (packopline.processed === "true" ? 'processed hidden ':''), + }); + pack_created.push(packopline.result_package_id[0]); + } + self.rows.push({ + cols: { product: packopline.product_id[1] || packopline.package_id[1], + qty: packopline.product_qty, + rem: packopline.qty_done, + uom: packopline.product_uom_id[1], + lot: packopline.lot_id[1], + pack: pack, + container: packopline.result_package_id[1], + container_id: packopline.result_package_id[0], + loc: packopline.location_id[1], + dest: packopline.location_dest_id[1], + id: packopline.id, + product_id: packopline.product_id[0], + can_scan: packopline.result_package_id[1] === undefined ? true : false, + head_container: false, + processed: packopline.processed, + }, + classes: color + (packopline.result_package_id[1] !== undefined ? 'in_container_hidden ' : '') + (packopline.processed === "true" ? 'processed hidden ':''), + }); }); - return rows; + //sort element by things to do, then things done, then grouped by packages + group_by_container = _.groupBy(self.rows, function(row){ + return row.cols.container; + }); + var sorted_row = []; + if (group_by_container.undefined !== undefined){ + group_by_container.undefined.sort(function(a,b){return (b.classes === '') - (a.classes === '');}); + $.each(group_by_container.undefined, function(key, value){ + sorted_row.push(value); + }); + } + + $.each(group_by_container, function(key, value){ + if (key !== 'undefined'){ + $.each(value, function(k,v){ + sorted_row.push(v); + }); + } + }); + + return sorted_row; }, renderElement: function(){ + var self = this; this._super(); - var model = this.getParent(); - this.$('.js_pack_row').each(function(){ - var pack_id = parseInt($(this).attr('pack-id')); - $('.js_pack_print', this).click(function(){ model.print_package(pack_id); }); - $('.js_pack_plus', this).click(function(){ model.copy_package_op(pack_id); }); - $('.js_pack_minus', this).click(function(){ model.delete_package_op(pack_id); }); - $('.js_pack_select', this).click(function(){ - if(model.get_selected_package() && model.get_selected_package().id === pack_id){ - model.deselect_package(); - }else{ - model.select_package(pack_id); - } + this.check_content_screen(); + this.$('.js_pick_done').click(function(){ self.getParent().done(); }); + this.$('.js_pick_print').click(function(){ self.getParent().print_picking(); }); + this.$('.oe_pick_app_header').text(self.get_header()); + this.$('.oe_searchbox').keyup(function(event){ + self.on_searchbox($(this).val()); + }); + this.$('.js_pick_pack').click(function(){ self.getParent().pack(); }); + this.$('.js_drop_down').click(function(){ self.getParent().drop_down();}); + this.$('.js_clear_search').click(function(){ + self.on_searchbox(''); + self.$('.oe_searchbox').val(''); + }); + this.$('.oe_searchbox').focus(function(){ + self.getParent().barcode_scanner.disconnect(); + }); + this.$('.oe_searchbox').blur(function(){ + self.getParent().barcode_scanner.connect(function(ean){ + self.get_Parent().scan(ean); + }); + }) + this.$('#js_select').change(function(){ + var selection = self.$('#js_select option:selected').attr('value'); + if (selection === "ToDo"){ + self.getParent().$('.js_pick_pack').removeClass('hidden') + self.getParent().$('.js_drop_down').removeClass('hidden') + self.$('.js_pack_op_line.processed').addClass('hidden') + self.$('.js_pack_op_line:not(.processed)').removeClass('hidden') + } + else{ + self.getParent().$('.js_pick_pack').addClass('hidden') + self.getParent().$('.js_drop_down').addClass('hidden') + self.$('.js_pack_op_line.processed').removeClass('hidden') + self.$('.js_pack_op_line:not(.processed)').addClass('hidden') + } + self.on_searchbox(self.search_filter); + }); + this.$('.js_plus').click(function(){ + var id = $(this).data('product-id'); + var op_id = $(this).parents("[data-id]:first").data('id'); + self.getParent().scan_product_id(id,true,op_id); + }); + this.$('.js_minus').click(function(){ + var id = $(this).data('product-id'); + var op_id = $(this).parents("[data-id]:first").data('id'); + self.getParent().scan_product_id(id,false,op_id); + }); + this.$('.js_unfold').click(function(){ + var op_id = $(this).parent().data('id'); + var line = $(this).parent(); + //select all js_pack_op_line with class in_container_hidden and correct container-id + select = self.$('.js_pack_op_line.in_container_hidden[data-container-id='+op_id+']') + if (select.length > 0){ + //we unfold + line.addClass('warning'); + select.removeClass('in_container_hidden'); + select.addClass('in_container'); + } + else{ + //we fold + line.removeClass('warning'); + select = self.$('.js_pack_op_line.in_container[data-container-id='+op_id+']') + select.removeClass('in_container'); + select.addClass('in_container_hidden'); + } + }); + this.$('.js_create_lot').click(function(){ + var op_id = $(this).parents("[data-id]:first").data('id'); + self.getParent().create_lot(op_id); + }); + this.$('.js_delete_pack').click(function(){ + var pack_id = $(this).parents("[data-id]:first").data('id'); + self.getParent().delete_package_op(pack_id); + }); + this.$('.js_print_pack').click(function(){ + var pack_id = $(this).parents("[data-id]:first").data('id'); + // $(this).parents("[data-id]:first").data('id') + self.getParent().print_package(pack_id); + }); + this.$('.js_submit_value').submit(function(event){ + var op_id = $(this).parents("[data-id]:first").data('id'); + var value = parseInt($("input", this).val()); + if (value>=0){ + self.getParent().set_operation_quantity(value, op_id); + } + $("input", this).val(""); + return false; + }); + this.$('.js_qty').focus(function(){ + self.getParent().barcode_scanner.disconnect(); + }); + this.$('.js_qty').blur(function(){ + this.value = ""; + self.getParent().barcode_scanner.connect(function(ean){ + self.getParent().scan(ean); }); }); + this.$('.js_change_src').click(function(){ + var op_id = $(this).parents("[data-id]:first").data('id');//data('op_id'); + self.$('#js_loc_select').addClass('source'); + self.$('#js_loc_select').data('op-id',op_id); + self.$el.siblings('#js_LocationChooseModal').modal(); + }); + this.$('.js_change_dst').click(function(){ + var op_id = $(this).parents("[data-id]:first").data('id'); + self.$('#js_loc_select').data('op-id',op_id); + self.$el.siblings('#js_LocationChooseModal').modal(); + }); + this.$('.js_pack_change_dst').click(function(){ + var op_id = $(this).parents("[data-id]:first").data('id'); + self.$('#js_loc_select').addClass('pack'); + self.$('#js_loc_select').data('op-id',op_id); + self.$el.siblings('#js_LocationChooseModal').modal(); + }); + this.$('.js_validate_location').click(function(){ + //get current selection + var select_dom_element = self.$('#js_loc_select'); + var loc_id = self.$('#js_loc_select option:selected').data('loc-id'); + var src_dst = false; + var op_id = select_dom_element.data('op-id'); + if (select_dom_element.hasClass('pack')){ + select_dom_element.removeClass('source'); + op_ids = []; + self.$('.js_pack_op_line[data-container-id='+op_id+']').each(function(){ + op_ids.push($(this).data('id')); + }); + op_id = op_ids; + } + else if (select_dom_element.hasClass('source')){ + src_dst = true; + select_dom_element.removeClass('source'); + } + if (loc_id === false){ + //close window + self.$el.siblings('#js_LocationChooseModal').modal('hide'); + } + else{ + self.$el.siblings('#js_LocationChooseModal').modal('hide'); + self.getParent().change_location(op_id, parseInt(loc_id), src_dst); + + } + }); + //remove navigtion bar from default openerp GUI + $('td.navbar').html('
'); + }, + on_searchbox: function(query){ + //hide line that has no location matching the query and highlight location that match the query + this.search_filter = query; + var processed = ".processed"; + if (this.$('#js_select option:selected').attr('value') == "ToDo"){ + processed = ":not(.processed)"; + } + if (query !== '') { + this.$('.js_loc:not(.js_loc:Contains('+query+'))').removeClass('info'); + this.$('.js_loc:Contains('+query+')').addClass('info'); + this.$('.js_pack_op_line'+processed+':not(.js_pack_op_line:has(.js_loc:Contains('+query+')))').addClass('hidden'); + this.$('.js_pack_op_line'+processed+':has(.js_loc:Contains('+query+'))').removeClass('hidden'); + } + //if no query specified, then show everything + if (query === '') { + this.$('.js_loc').removeClass('info'); + this.$('.js_pack_op_line'+processed+'.hidden').removeClass('hidden'); + } + this.check_content_screen(); + }, + check_content_screen: function(){ + //get all visible element and if none has positive qty, disable put in pack and process button + var self = this; + var qties = this.$('.js_pack_op_line:not(.processed):not(.hidden) .js_qty').map(function(){return $(this).attr('placeholder')}); + var container = this.$('.js_pack_op_line.container_head:not(.processed):not(.hidden)') + var disabled = true; + $.each(qties,function(index, value){ + if (parseInt(value)>0){ + disabled = false; + } + }); + if (disabled){ + if (container.length===0){ + self.$('.js_drop_down').addClass('disabled'); + } + else { + self.$('.js_drop_down').removeClass('disabled'); + } + self.$('.js_pick_pack').addClass('disabled'); + } + else{ + self.$('.js_drop_down').removeClass('disabled'); + self.$('.js_pick_pack').removeClass('disabled'); + } + }, + get_current_op_selection: function(ignore_container){ + //get ids of visible on the screen + pack_op_ids = [] + this.$('.js_pack_op_line:not(.processed):not(.js_pack_op_line.hidden):not(.container_head)').each(function(){ + cur_id = $(this).data('id'); + pack_op_ids.push(parseInt(cur_id)); + }); + //get list of element in this.rows where rem > 0 and container is empty is specified + list = [] + _.each(this.rows, function(row){ + if (row.cols.rem > 0 && (ignore_container || row.cols.container === undefined)){ + list.push(row.cols.id); + } + }); + //return only those visible with rem qty > 0 and container empty + return _.intersection(pack_op_ids, list); + }, + remove_blink: function(){ + this.$('.js_pack_op_line.blink_me').removeClass('blink_me'); + }, + blink: function(op_id){ + this.$('.js_pack_op_line[data-id="'+op_id+'"]').addClass('blink_me'); + }, + check_done: function(){ + var model = this.getParent(); + var self = this; + var done = true; + _.each( model.packoplines, function(packopline){ + if (packopline.processed === "false"){ + done = false; + return done; + } + }); + return done; + }, + get_visible_ids: function(){ + var self = this; + var visible_op_ids = [] + var op_ids = this.$('.js_pack_op_line:not(.processed):not(.hidden):not(.container_head):not(.in_container):not(.in_container_hidden)').map(function(){ + return $(this).data('id'); + }); + $.each(op_ids, function(key, op_id){ + visible_op_ids.push(parseInt(op_id)); + }); + return visible_op_ids; }, }); @@ -144,7 +368,18 @@ function openerp_picking_widgets(instance){ init: function(parent, params){ this._super(parent,params); var self = this; - + $(window).bind('hashchange', function(){ + var states = $.bbq.getState(); + if (states.action === "stock.ui"){ + self.do_action({ + type: 'ir.actions.client', + tag: 'stock.ui', + target: 'current', + },{ + clear_breadcrumbs: true, + }); + } + }); this.picking_types = []; this.loaded = this.load(); this.scanning_type = 0; @@ -152,14 +387,12 @@ function openerp_picking_widgets(instance){ this.pickings_by_type = {}; this.pickings_by_id = {}; this.picking_search_string = ""; - }, load: function(){ var self = this; return new instance.web.Model('stock.picking.type').get_func('search_read')([],[]) .then(function(types){ self.picking_types = types; - for(var i = 0; i < types.length; i++){ self.pickings_by_type[types[i].id] = []; } @@ -169,7 +402,6 @@ function openerp_picking_widgets(instance){ }).then(function(pickings){ self.pickings = pickings; - for(var i = 0; i < pickings.length; i++){ var picking = pickings[i]; self.pickings_by_type[picking.picking_type_id[0]].push(picking); @@ -185,7 +417,7 @@ function openerp_picking_widgets(instance){ this.$('.js_pick_quit').click(function(){ self.quit(); }); this.$('.js_pick_scan').click(function(){ self.scan_picking($(this).data('id')); }); this.$('.js_pick_last').click(function(){ self.goto_last_picking_of_type($(this).data('id')); }); - this.$('.oe_searchbox input').keyup(function(event){ + this.$('.oe_searchbox').keyup(function(event){ self.on_searchbox($(this).val()); }); //remove navigtion bar from default openerp GUI @@ -194,6 +426,7 @@ function openerp_picking_widgets(instance){ start: function(){ this._super(); var self = this; + instance.webclient.set_content_full_screen(true); this.barcode_scanner.connect(function(barcode){ self.on_scan(barcode); }); @@ -202,24 +435,12 @@ function openerp_picking_widgets(instance){ }); }, goto_picking: function(picking_id){ - this.do_action({ - type: 'ir.actions.client', - tag: 'stock.ui', - target: 'current', - context: { picking_id: picking_id }, - },{ - clear_breadcrumbs: true, - }); + $.bbq.pushState('#action=stock.ui&picking_id='+picking_id); + $(window).trigger('hashchange'); }, goto_last_picking_of_type: function(type_id){ - this.do_action({ - type: 'ir.actions.client', - tag: 'stock.ui', - target: 'current', - context: { active_id: type_id }, - },{ - clear_breadcrumbs: true, - }); + $.bbq.pushState('#action=stock.ui&picking_type_id='+type_id); + $(window).trigger('hashchange'); }, search_picking: function(barcode){ //TODO don't crash if a not supported char is given @@ -240,7 +461,6 @@ function openerp_picking_widgets(instance){ }, on_scan: function(barcode){ var self = this; - for(var i = 0, len = this.pickings.length; i < len; i++){ var picking = this.pickings[i]; if(picking.name.toUpperCase() === $.trim(barcode.toUpperCase())){ @@ -248,11 +468,11 @@ function openerp_picking_widgets(instance){ break; } } - this.$('.oe_picking_not_found').removeClass('oe_hidden'); + this.$('.js_picking_not_found').removeClass('hidden'); clearTimeout(this.picking_not_found_timeout); this.picking_not_found_timeout = setTimeout(function(){ - self.$('.oe_picking_not_found').addClass('oe_hidden'); + self.$('.js_picking_not_found').addClass('hidden'); },2000); }, @@ -262,24 +482,26 @@ function openerp_picking_widgets(instance){ clearTimeout(this.searchbox_timeout); this.searchbox_timout = setTimeout(function(){ if(query){ - self.$('.oe_picking_not_found').addClass('oe_hidden'); - self.$('.oe_picking_categories').addClass('oe_hidden'); - self.$('.oe_picking_search_results').html( + self.$('.js_picking_not_found').addClass('hidden'); + self.$('.js_picking_categories').addClass('hidden'); + self.$('.js_picking_search_results').html( QWeb.render('PickingSearchResults',{results:self.search_picking(query)}) ); - self.$('.oe_picking_search_results .oe_picking').click(function(){ + self.$('.js_picking_search_results .oe_picking').click(function(){ self.goto_picking($(this).data('id')); }); - self.$('.oe_picking_search_results').removeClass('oe_hidden'); + self.$('.js_picking_search_results').removeClass('hidden'); }else{ - self.$('.oe_picking_categories').removeClass('oe_hidden'); - self.$('.oe_picking_search_results').addClass('oe_hidden'); + self.$('.js_title_label').removeClass('hidden'); + self.$('.js_picking_categories').removeClass('hidden'); + self.$('.js_picking_search_results').addClass('hidden'); } },100); }, quit: function(){ - instance.webclient.set_content_full_screen(false); - window.location = '/'; // FIXME Ask niv how to do it correctly + return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_picking_type_form']], ['res_id']).pipe(function(res) { + window.location = '/web#action=' + res[0]['res_id']; + }); }, destroy: function(){ this._super(); @@ -294,18 +516,31 @@ function openerp_picking_widgets(instance){ init: function(parent,params){ this._super(parent,params); var self = this; - + $(window).bind('hashchange', function(){ + var states = $.bbq.getState(); + if (states.action === "stock.menu"){ + self.do_action({ + type: 'ir.actions.client', + tag: 'stock.menu', + target: 'current', + },{ + clear_breadcrumbs: true, + }); + } + }); + init_hash = $.bbq.getState(); + this.picking_type_id = init_hash.picking_type_id ? init_hash.picking_type_id:0; + this.picking_id = init_hash.picking_id ? init_hash.picking_id:undefined; this.picking = null; this.pickings = []; - this.movelines = null; + this.packoplines = null; this.operations = null; this.selected_operation = { id: null, picking_id: null}; this.packages = null; this.barcode_scanner = new module.BarcodeScanner(); - this.picking_type_id = params.context.active_id || 0; - - if(params.context.picking_id){ - this.loaded = this.load(params.context.picking_id); + this.locations = []; + if(this.picking_id){ + this.loaded = this.load(this.picking_id); }else{ this.loaded = this.load(); } @@ -321,7 +556,7 @@ function openerp_picking_widgets(instance){ function load_picking_list(type_id){ var pickings = new $.Deferred(); new instance.web.Model('stock.picking') - .call('get_next_picking_for_ui',[{'default_picking_type_id':type_id}]) + .call('get_next_picking_for_ui',[{'default_picking_type_id':parseInt(type_id)}]) .then(function(picking_ids){ if(!picking_ids || picking_ids.length === 0){ (new instance.web.Dialog(self,{ @@ -369,10 +604,16 @@ function openerp_picking_widgets(instance){ } return loaded_picking.then(function(){ + new instance.web.Model('stock.location').call('search',[[['usage','=','internal']]]).then(function(locations_ids){ + return new instance.web.Model('stock.location').call('read',[locations_ids, []]).then(function(locations){ + self.locations = locations; + }); + }); + }).then(function(){ - return new instance.web.Model('stock.move').call('read',[self.picking.move_lines, [], new instance.web.CompoundContext()]); - }).then(function(movelines){ - self.movelines = movelines; + return new instance.web.Model('stock.pack.operation').call('read',[self.picking.pack_operation_ids, [], new instance.web.CompoundContext()]); + }).then(function(packoplines){ + self.packoplines = packoplines; return new instance.web.Model('stock.pack.operation').call('read',[self.picking.pack_operation_ids, [], new instance.web.CompoundContext()]); }).then(function(operations){ @@ -390,22 +631,16 @@ function openerp_picking_widgets(instance){ }).then(function(packages){ self.packages = packages; }); - }, start: function(){ this._super(); var self = this; instance.webclient.set_content_full_screen(true); - this.connect_numpad(); this.barcode_scanner.connect(function(ean){ self.scan(ean); }); - this.$('.js_pick_quit').click(function(){ self.quit(); }); - this.$('.js_pick_pack').click(function(){ self.pack(); }); - this.$('.js_pick_done').click(function(){ self.done(); }); - this.$('.js_pick_print').click(function(){ self.print_picking(); }); this.$('.js_pick_prev').click(function(){ self.picking_prev(); }); this.$('.js_pick_next').click(function(){ self.picking_next(); }); this.$('.js_pick_menu').click(function(){ self.menu(); }); @@ -423,52 +658,58 @@ function openerp_picking_widgets(instance){ $.when(this.loaded).done(function(){ self.picking_editor = new module.PickingEditorWidget(self); self.picking_editor.replace(self.$('.oe_placeholder_picking_editor')); - - self.package_editor = new module.PackageEditorWidget(self); - self.package_editor.replace(self.$('.oe_placeholder_package_editor')); - - self.package_selector = new module.PackageSelectorWidget(self); - self.package_selector.replace(self.$('.oe_placeholder_package_selector')); if( self.picking.id === self.pickings[0]){ - self.$('.js_pick_prev').addClass('oe_disabled'); + self.$('.js_pick_prev').addClass('disabled'); }else{ - self.$('.js_pick_prev').removeClass('oe_disabled'); + self.$('.js_pick_prev').removeClass('disabled'); } if( self.picking.id === self.pickings[self.pickings.length-1] ){ - self.$('.js_pick_next').addClass('oe_disabled'); + self.$('.js_pick_next').addClass('disabled'); }else{ - self.$('.js_pick_next').removeClass('oe_disabled'); + self.$('.js_pick_next').removeClass('disabled'); } - self.$('.oe_pick_app_header').text(self.get_header()); - }).fail(function(error) {console.log(error);}); }, + on_searchbox: function(query){ + var self = this; + self.picking_editor.on_searchbox(query.toUpperCase()); + }, // reloads the data from the provided picking and refresh the ui. // (if no picking_id is provided, gets the first picking in the db) refresh_ui: function(picking_id){ var self = this; + var remove_search_filter = ""; + if (self.picking.id === picking_id){ + remove_search_filter = self.$('.oe_searchbox').val(); + } return this.load(picking_id) - .then(function(){ + .then(function(){ + self.picking_editor.remove_blink(); self.picking_editor.renderElement(); - self.package_editor.renderElement(); - self.package_selector.renderElement(); if( self.picking.id === self.pickings[0]){ - self.$('.js_pick_prev').addClass('oe_disabled'); + self.$('.js_pick_prev').addClass('disabled'); }else{ - self.$('.js_pick_prev').removeClass('oe_disabled'); + self.$('.js_pick_prev').removeClass('disabled'); } if( self.picking.id === self.pickings[self.pickings.length-1] ){ - self.$('.js_pick_next').addClass('oe_disabled'); + self.$('.js_pick_next').addClass('disabled'); }else{ - self.$('.js_pick_next').removeClass('oe_disabled'); + self.$('.js_pick_next').removeClass('disabled'); + } + if (remove_search_filter === ""){ + self.$('.oe_searchbox').val(''); + self.on_searchbox(''); + } + else{ + self.$('.oe_searchbox').val(remove_search_filter); + self.on_searchbox(remove_search_filter); } - self.$('.oe_pick_app_header').text(self.get_header()); }); }, get_header: function(){ @@ -479,42 +720,69 @@ function openerp_picking_widgets(instance){ } }, menu: function(){ - this.do_action({ - type: 'ir.actions.client', - tag: 'stock.menu', - target: 'current', - },{ - clear_breadcrumbs: true, - }); - + $.bbq.pushState('#action=stock.menu'); + $(window).trigger('hashchange'); }, scan: function(ean){ //scans a barcode, sends it to the server, then reload the ui var self = this; + var product_visible_ids = this.picking_editor.get_visible_ids(); new instance.web.Model('stock.picking') - .call('process_barcode_from_ui', [self.picking.id, ean]) - .then(function(){ - self.reset_selected_operation(); - return self.refresh_ui(self.picking.id); + .call('process_barcode_from_ui', [self.picking.id, ean, product_visible_ids]) + .then(function(result){ + if (result.filter_loc !== false){ + //check if we have receive a location as answer + if (result.filter_loc !== undefined){ + var modal_loc_hidden = self.$('#js_LocationChooseModal').attr('aria-hidden'); + if (modal_loc_hidden === "false"){ + var line = self.$('#js_LocationChooseModal .js_loc_option[data-loc-id='+result.filter_loc_id+']').attr('selected','selected'); + } + else{ + self.$('.oe_searchbox').val(result.filter_loc); + self.on_searchbox(result.filter_loc); + } + } + } + if (result.operation_id !== false){ + self.refresh_ui(self.picking.id).then(function(){ + return self.picking_editor.blink(result.operation_id); + }); + } }); }, - scan_product_id: function(product_id){ //performs the same operation as a scan, but with product id instead + scan_product_id: function(product_id,increment,op_id){ //performs the same operation as a scan, but with product id instead var self = this; new instance.web.Model('stock.picking') - .call('process_product_id_from_ui', [self.picking.id, product_id]) - .then(function(){ - self.reset_selected_operation(); + .call('process_product_id_from_ui', [self.picking.id, product_id, op_id, increment]) + .then(function(result){ return self.refresh_ui(self.picking.id); }); }, pack: function(){ var self = this; - new instance.web.Model('stock.picking') - .call('action_pack',[[[self.picking.id]]]) - .then(function(){ - instance.session.user_context.current_package_id = false; - - return self.refresh_ui(self.picking.id); - }); + var pack_op_ids = self.picking_editor.get_current_op_selection(false); + if (pack_op_ids.length !== 0){ + new instance.web.Model('stock.picking') + .call('action_pack',[[[self.picking.id]], pack_op_ids]) + .then(function(){ + instance.session.user_context.current_package_id = false; + return self.refresh_ui(self.picking.id); + }); + } + }, + drop_down: function(){ + var self = this; + var pack_op_ids = self.picking_editor.get_current_op_selection(true); + if (pack_op_ids.length !== 0){ + new instance.web.Model('stock.pack.operation') + .call('action_drop_down', [pack_op_ids]) + .then(function(){ + return self.refresh_ui(self.picking.id).then(function(){ + if (self.picking_editor.check_done()){ + return self.done(); + } + }); + }); + } }, done: function(){ var self = this; @@ -529,6 +797,26 @@ function openerp_picking_widgets(instance){ } }); }, + create_lot: function(op_id){ + var self = this; + new instance.web.Model('stock.pack.operation') + .call('create_and_assign_lot',[parseInt(op_id)]) + .then(function(){ + return self.refresh_ui(self.picking.id); + }); + }, + change_location: function(op_id, loc_id, is_src_dst){ + var self = this; + var vals = {'location_dest_id': loc_id}; + if (is_src_dst){ + vals = {'location_id': loc_id}; + } + new instance.web.Model('stock.pack.operation') + .call('write',[op_id, vals]) + .then(function(){ + return self.refresh_ui(self.picking.id); + }); + }, print_package: function(package_id){ var self = this; new instance.web.Model('stock.quant.package') @@ -559,6 +847,7 @@ function openerp_picking_widgets(instance){ for(var i = 0; i < this.pickings.length; i++){ if(this.pickings[i] === this.picking.id){ if(i < this.pickings.length -1){ + $.bbq.pushState('picking_id='+this.pickings[i+1]); this.refresh_ui(this.pickings[i+1]); return; } @@ -569,137 +858,43 @@ function openerp_picking_widgets(instance){ for(var i = 0; i < this.pickings.length; i++){ if(this.pickings[i] === this.picking.id){ if(i > 0){ + $.bbq.pushState('picking_id='+this.pickings[i-1]); this.refresh_ui(this.pickings[i-1]); return; } } } }, - copy_package_op: function(pack_id){ - var self = this; - new instance.web.Model('stock.quant.package').call('copy_pack',[pack_id]) - .then(function(){ - return self.refresh_ui(self.picking.id); - }); - }, delete_package_op: function(pack_id){ var self = this; new instance.web.Model('stock.pack.operation').call('search', [[['result_package_id', '=', pack_id]]]) .then(function(op_ids) { - new instance.web.Model('stock.pack.operation').call('unlink', [op_ids]) + new instance.web.Model('stock.pack.operation').call('write', [op_ids, {'result_package_id':false}]) .then(function() { return self.refresh_ui(self.picking.id); }); }); }, - deselect_package: function(){ - instance.session.user_context.current_package_id = false; - this.package_editor.renderElement(); - this.package_selector.renderElement(); - }, - select_package: function(package_id){ - instance.session.user_context.current_package_id = package_id; - this.package_editor.renderElement(); - this.package_selector.renderElement(); - }, - get_selected_package: function(){ - var current_package; - - _.each( this.packages, function(pack){ - if(pack.id === instance.session.user_context.current_package_id){ - current_package = pack; - } - }); - - return current_package; - }, - get_current_operations: function(){ - var current_package_id = instance.session.user_context.current_package_id; - var ops = []; - _.each( this.operations, function(op){ - if(!current_package_id){ - if(op.result_package_id !== false){ - return; - } - }else if(op.result_package_id[0] !== current_package_id){ - return; - } - ops.push(op); - }); - console.log('Current Operations:',ops); - return ops; - }, - get_selected_operation: function(){ - if( this.selected_operation.picking_id === this.picking.id && this.selected_operation.id ){ - return this.selected_operation.id; - }else{ - return null; - } - }, - reset_selected_operation: function(){ - if(this.selected_operation.picking_id === this.picking.id){ - this.selected_operation.id = null; - } - }, - set_selected_operation: function(id){ - this.selected_operation.picking_id = this.picking.id; - this.selected_operation.id = id; - }, - set_operation_quantity: function(quantity){ + set_operation_quantity: function(quantity, op_id){ var self = this; - var op = this.get_selected_operation(); - if( !op ){ - //TODO typing the ean of a product manually ? - //(scanning the barcode is already handled somewhere else, and i don't know how to differenciate the 2 operations) - // and the result is that if i uncomment the next line, scanning a product counts it twice - //self.scan(quantity); - } - - else {if(typeof quantity === 'number' && quantity >= 0){ + if(quantity >= 0){ new instance.web.Model('stock.pack.operation') - .call('write',[[op],{'product_qty': quantity }]) + .call('write',[[op_id],{'qty_done': quantity }]) .then(function(){ self.refresh_ui(self.picking.id); }); - }} + } }, - connect_numpad: function(){ - var self = this; - var numpad = []; - var numpad_timestamp; - - this.numpad_handler = function(e){ - if(numpad_timestamp + 1500 < new Date().getTime()){ - numpad = []; - } - if(e.keyCode === 27 || e.keyCode === 8){ // ESC or BACKSPACE - numpad = []; - }else if(e.keyCode >= 48 && e.keyCode <= 57){ // NUMPAD NUMBERS - numpad.push(e.keyCode - 48); - }else if(e.keyCode === 13){ // ENTER - console.log('enter'); - if(numpad.length > 0){ - self.set_operation_quantity(parseInt(numpad.join(''))); - } - numpad = []; - }else{ - numpad = []; - } - numpad_timestamp = new Date().getTime(); - }; - $('body').on('keypress', this.numpad_handler); - }, - disconnect_numpad: function(){ - $('body').off('keypress', this.numpad_handler); - }, quit: function(){ this.destroy(); - window.location = '/'; // FIXME Ask niv how to do it correctly + return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_picking_type_form']], ['res_id']).pipe(function(res) { + window.location = '/web#action=' + res[0]['res_id']; + }); }, destroy: function(){ this._super(); - this.disconnect_numpad(); + // this.disconnect_numpad(); this.barcode_scanner.disconnect(); $('body').off('keyup',this.hotkey_handler); instance.webclient.set_content_full_screen(false); @@ -748,7 +943,4 @@ function openerp_picking_widgets(instance){ openerp.stock = function(openerp) { openerp.stock = openerp.stock || {}; openerp_picking_widgets(openerp); - - -} - +} \ No newline at end of file diff --git a/addons/stock/static/src/xml/picking.xml b/addons/stock/static/src/xml/picking.xml index 0dcca4be2e1..0e2e8653bce 100644 --- a/addons/stock/static/src/xml/picking.xml +++ b/addons/stock/static/src/xml/picking.xml @@ -1,7 +1,7 @@ - + -
-
- Operations To Process -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ProductQtyRemUoMLocationScan
-
-
- - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - -
ProductQtyUoM
-
-
- - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Package
Print Copy Delete
-
-
- - -
- No picking found. -
-
- - -
- + + + +
+
+
+

+

+
+ + + + +
+
+
+ +
+
+
+

+ + +

+
+
+

+
+ + + + +
+

+
+
+

+ + +

+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
ProductScannedTodoFromTo
+ + + + +
+ +
+ + +
+ +
+
+ + + +
+ + + + : + : + + +
+
+ + + +
+

Search Results

+
+
+ + No picking found. - + + +
+
+
+ +
+
+
+
+
+
-
- - - - - - - -
-
-
Quit
+ +
+ +

Select your operation

+ + + + +
+ +
+
+ + + + +
+ picking(s) +
-
- -
-
-
-
- -

Pickings

- -
- Scanned picking could not be found -
- -
-
- -
-

- Select the type of picking you want to process. -

- -
- - - - Nothing to do - - - 1 picking - - - pickings - -
-
-
-
-
-
+
+
+
- -
- - - - - - - -
-
-
Menu
-
-
-
< Previous
-
Next >
-
+ -
-
-
-
Done
-
Print
-
- -
- -
Put in Pack
-
-
-
-
- -
-
-
+
+
diff --git a/addons/stock/stock.py b/addons/stock/stock.py index de70e21bb23..bd8f7be7974 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -131,6 +131,7 @@ class stock_location(osv.osv): 'scrap_location': fields.boolean('Is a Scrap Location?', help='Check this box to allow using this location to put scrapped/damaged goods.'), 'removal_strategy_ids': fields.one2many('product.removal', 'location_id', 'Removal Strategies'), 'putaway_strategy_ids': fields.one2many('product.putaway', 'location_id', 'Put Away Strategies'), + 'loc_barcode': fields.char('Location barcode'), } _defaults = { 'active': True, @@ -141,6 +142,12 @@ class stock_location(osv.osv): 'posz': 0, 'scrap_location': False, } + _sql_constraints = [('loc_barcode_company_uniq', 'unique (loc_barcode,company_id)', 'The barcode for a location must be unique per company !')] + + def create(self, cr, uid, default, context=None): + if not default.get('loc_barcode', False): + default.update({'loc_barcode': default.get('complete_name', False)}) + return super(stock_location,self).create(cr, uid, default, context=context) def get_putaway_strategy(self, cr, uid, location, product, context=None): pa = self.pool.get('product.putaway') @@ -1062,6 +1069,14 @@ class stock_picking(osv.osv): 'product_uom_id': self.pool.get("product.product").browse(cr, uid, key[0], context=context).uom_id.id, }) return vals + + def open_barcode_interface(self, cr, uid, picking_ids, context=None): + final_url="/barcode/web/#action=stock.ui&picking_id="+str(picking_ids[0]) + return {'type': 'ir.actions.act_url', 'url':final_url, 'target': 'self',} + + def do_partial_open_barcode(self, cr, uid, picking_ids, context=None): + self.do_prepare_partial(cr, uid, picking_ids, context=context) + return self.open_barcode_interface(cr, uid, picking_ids, context=context) def do_prepare_partial(self, cr, uid, picking_ids, context=None): context = context or {} @@ -1325,50 +1340,83 @@ class stock_picking(osv.osv): def action_done_from_ui(self, cr, uid, picking_id, context=None): """ called when button 'done' is pushed in the barcode scanner UI """ + #write qty_done into field product_qty for every package_operation before doing the transfer + pack_op_obj = self.pool.get('stock.pack.operation') + for operation in self.browse(cr, uid, picking_id, context=context).pack_operation_ids: + pack_op_obj.write(cr, uid, operation.id, {'product_qty': operation.qty_done}, context=context) self.do_transfer(cr, uid, [picking_id], context=context) #return id of next picking to work on return self.get_next_picking_for_ui(cr, uid, context=context) - def action_pack(self, cr, uid, picking_ids, context=None): + def action_pack(self, cr, uid, picking_ids, operation_filter_ids=None, context=None): """ Create a package with the current pack_operation_ids of the picking that aren't yet in a pack. - Used in the barcode scanner UI and the normal interface as well. """ + Used in the barcode scanner UI and the normal interface as well. + operation_filter_ids is used by barcode scanner interface to specify a subset of operation to pack""" + if operation_filter_ids == None: + operation_filter_ids = [] stock_operation_obj = self.pool.get('stock.pack.operation') package_obj = self.pool.get('stock.quant.package') stock_move_obj = self.pool.get('stock.move') for picking_id in picking_ids: - operation_ids = stock_operation_obj.search(cr, uid, [('picking_id', '=', picking_id), ('result_package_id', '=', False)], context=context) + operation_search_domain = [('picking_id', '=', picking_id), ('result_package_id', '=', False)] + if operation_filter_ids != []: + operation_search_domain.append(('id', 'in', operation_filter_ids)) + operation_ids = stock_operation_obj.search(cr, uid, operation_search_domain, context=context) + pack_operation_ids = [] if operation_ids: for operation in stock_operation_obj.browse(cr, uid, operation_ids, context=context): - for record in operation.linked_move_operation_ids: - stock_move_obj.check_tracking(cr, uid, record.move_id, operation.package_id.id or operation.lot_id.id, context=context) + #If we haven't done all qty in operation, we have to split into 2 operation + op = operation + if (operation.qty_done < operation.product_qty): + new_operation = stock_operation_obj.copy(cr, uid, operation.id, {'product_qty': operation.qty_done,'qty_done': operation.qty_done}, context=context) + stock_operation_obj.write(cr, uid, operation.id, {'product_qty': operation.product_qty - operation.qty_done,'qty_done': 0}, context=context) + op = stock_operation_obj.browse(cr, uid, new_operation, context=context) + pack_operation_ids.append(op.id) + for record in op.linked_move_operation_ids: + stock_move_obj.check_tracking(cr, uid, record.move_id, op.package_id.id or op.lot_id.id, context=context) package_id = package_obj.create(cr, uid, {}, context=context) - stock_operation_obj.write(cr, uid, operation_ids, {'result_package_id': package_id}, context=context) + stock_operation_obj.write(cr, uid, pack_operation_ids, {'result_package_id': package_id}, context=context) return True - def process_product_id_from_ui(self, cr, uid, picking_id, product_id, context=None): - return self.pool.get('stock.pack.operation')._search_and_increment(cr, uid, picking_id, [('product_id', '=', product_id)], context=context) + def process_product_id_from_ui(self, cr, uid, picking_id, product_id, op_id, increment=True, context=None): + return self.pool.get('stock.pack.operation')._search_and_increment(cr, uid, picking_id, [('product_id', '=', product_id),('id', '=', op_id)], increment=increment, context=context) - def process_barcode_from_ui(self, cr, uid, picking_id, barcode_str, context=None): + def process_barcode_from_ui(self, cr, uid, picking_id, barcode_str, visible_op_ids, context=None): '''This function is called each time there barcode scanner reads an input''' lot_obj = self.pool.get('stock.production.lot') package_obj = self.pool.get('stock.quant.package') product_obj = self.pool.get('product.product') stock_operation_obj = self.pool.get('stock.pack.operation') + stock_location_obj = self.pool.get('stock.location') + answer = {'filter_loc': False, 'operation_id': False} + #check if the barcode correspond to a location + matching_location_ids = stock_location_obj.search(cr, uid, [('loc_barcode', '=', barcode_str)], context=context) + if matching_location_ids: + #if we have a location, return immediatly with the location name + location = stock_location_obj.browse(cr, uid, matching_location_ids[0], context=None) + answer['filter_loc'] = stock_location_obj._name_get(cr, uid, location, context=None) + answer['filter_loc_id'] = matching_location_ids[0] + return answer #check if the barcode correspond to a product matching_product_ids = product_obj.search(cr, uid, ['|', ('ean13', '=', barcode_str), ('default_code', '=', barcode_str)], context=context) if matching_product_ids: - self.process_product_id_from_ui(cr, uid, picking_id, matching_product_ids[0], context=context) - + op_id = stock_operation_obj._search_and_increment(cr, uid, picking_id, [('product_id', '=', matching_product_ids[0])], filter_visible=True, visible_op_ids=visible_op_ids, increment=True, context=context) + answer['operation_id'] = op_id + return answer #check if the barcode correspond to a lot matching_lot_ids = lot_obj.search(cr, uid, [('name', '=', barcode_str)], context=context) if matching_lot_ids: lot = lot_obj.browse(cr, uid, matching_lot_ids[0], context=context) - stock_operation_obj._search_and_increment(cr, uid, picking_id, [('product_id', '=', lot.product_id.id), ('lot_id', '=', lot.id)], context=context) - + op_id = stock_operation_obj._search_and_increment(cr, uid, picking_id, [('product_id', '=', lot.product_id.id), ('lot_id', '=', lot.id)], filter_visible=True, visible_op_ids=visible_op_ids, increment=True, context=context) + answer['operation_id'] = op_id + return answer #check if the barcode correspond to a package matching_package_ids = package_obj.search(cr, uid, [('name', '=', barcode_str)], context=context) if matching_package_ids: - stock_operation_obj._search_and_increment(cr, uid, picking_id, [('package_id', '=', matching_package_ids[0])], context=context) + op_id = stock_operation_obj._search_and_increment(cr, uid, picking_id, [('package_id', '=', matching_package_ids[0])], filter_visible=True, visible_op_ids=visible_op_ids, increment=True, context=context) + answer['operation_id'] = op_id + return answer + return answer class stock_production_lot(osv.osv): @@ -3616,6 +3664,7 @@ class stock_pack_operation(osv.osv): 'product_id': fields.many2one('product.product', 'Product', ondelete="CASCADE"), # 1 'product_uom_id': fields.many2one('product.uom', 'Product Unit of Measure'), 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True), + 'qty_done': fields.float('Quantity Processed', digits_compute=dp.get_precision('Product Unit of Measure')), 'package_id': fields.many2one('stock.quant.package', 'Package'), # 2 'lot_id': fields.many2one('stock.production.lot', 'Lot/Serial Number'), 'result_package_id': fields.many2one('stock.quant.package', 'Container Package', help="If set, the operations are packed into this package", required=False, ondelete='cascade'), @@ -3628,10 +3677,13 @@ class stock_pack_operation(osv.osv): 'remaining_qty': fields.function(_get_remaining_qty, type='float', string='Remaining Qty'), 'location_id': fields.many2one('stock.location', 'Location From', required=True), 'location_dest_id': fields.many2one('stock.location', 'Location To', required=True), + 'processed': fields.selection([('true','Yes'), ('false','No')],'Has been processed?', required=True), } _defaults = { 'date': fields.date.context_today, + 'qty_done': 0, + 'processed': lambda *a: 'false', } def write(self, cr, uid, ids, vals, context=None): @@ -3651,8 +3703,32 @@ class stock_pack_operation(osv.osv): self.pool.get("stock.picking").do_recompute_remaining_quantities(cr, uid, [vals['picking_id']], context=context) return res_id + def action_drop_down(self, cr, uid, ids, context=None): + ''' Used by barcode interface to say that pack_operation has been moved from src location + to destination location, if qty_done is less than product_qty than we have to split the + operation in two to process the one with the qty moved + ''' + processed_ids = [] + for pack_op in self.browse(cr, uid, ids, context=None): + op = pack_op.id + if pack_op.qty_done < pack_op.product_qty: + # we split the operation in two + op = self.copy(cr, uid, pack_op.id, {'product_qty': pack_op.qty_done, 'qty_done': pack_op.qty_done}, context=context) + self.write(cr, uid, ids, {'product_qty': pack_op.product_qty - pack_op.qty_done, 'qty_done': 0}, context=context) + processed_ids.append(op) + self.write(cr, uid, processed_ids, {'processed': 'true'}, context=context) + + def create_and_assign_lot(self, cr, uid, id, context=None): + ''' Used by barcode interface to create a new lot and assign it to the operation + ''' + obj = self.browse(cr,uid,id,context) + product_id = obj.product_id.id + if not obj.lot_id: + new_lot_id = self.pool.get('stock.production.lot').create(cr, uid, {'product_id': product_id}, context=context) + self.write(cr, uid, id, {'lot_id': new_lot_id}, context=context) + #TODO: this function can be refactored - def _search_and_increment(self, cr, uid, picking_id, domain, context=None): + def _search_and_increment(self, cr, uid, picking_id, domain, filter_visible=False ,visible_op_ids=False, increment=True, context=None): '''Search for an operation with given 'domain' in a picking, if it exists increment the qty (+1) otherwise create it :param domain: list of tuple directly reusable as a domain @@ -3670,16 +3746,32 @@ class stock_pack_operation(osv.osv): #if current_package_id is given in the context, we increase the number of items in this package package_clause = [('result_package_id', '=', context.get('current_package_id', False))] existing_operation_ids = self.search(cr, uid, [('picking_id', '=', picking_id)] + domain + package_clause, context=context) + todo_operation_ids = [] if existing_operation_ids: + if filter_visible: + todo_operation_ids = [val for val in existing_operation_ids if val in visible_op_ids] + else: + todo_operation_ids = existing_operation_ids + if todo_operation_ids: #existing operation found for the given domain and picking => increment its quantity - operation_id = existing_operation_ids[0] - qty = self.browse(cr, uid, operation_id, context=context).product_qty + 1 - self.write(cr, uid, [operation_id], {'product_qty': qty}, context=context) + operation_id = todo_operation_ids[0] + op_obj = self.browse(cr, uid, operation_id, context=context) + qty = op_obj.qty_done + if increment: + qty += 1 + else: + qty -= 1 if qty >= 1 else 0 + if qty == 0 and op_obj.product_qty == 0: + #we have a line with 0 qty set, so delete it + self.unlink(cr, uid, [operation_id], context=context) + return False + self.write(cr, uid, [operation_id], {'qty_done': qty}, context=context) else: #no existing operation found for the given domain and picking => create a new one values = { 'picking_id': picking_id, - 'product_qty': 1, + 'product_qty': 0, + 'qty_done': 1, } for key in domain: var_name, dummy, value = key @@ -3691,7 +3783,7 @@ class stock_pack_operation(osv.osv): update_dict['product_uom_id'] = uom_id values.update(update_dict) operation_id = self.create(cr, uid, values, context=context) - return True + return operation_id class stock_move_operation_link(osv.osv): @@ -3857,6 +3949,11 @@ class stock_picking_type(osv.osv): _description = "The picking type determines the picking view" _order = 'sequence' + def open_barcode_interface(self, cr, uid, ids, context=None): + final_url="/barcode/web/#action=stock.ui&picking_type_id="+str(ids[0]) if len(ids) else '0' + return {'type': 'ir.actions.act_url', 'url':final_url, 'target': 'self',} + + def _get_tristate_values(self, cr, uid, ids, field_name, arg, context=None): picking_obj = self.pool.get('stock.picking') res = dict.fromkeys(ids, []) diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml index de5e568afd5..2a7673e8a67 100644 --- a/addons/stock/stock_view.xml +++ b/addons/stock/stock_view.xml @@ -375,6 +375,7 @@ + @@ -721,9 +722,9 @@